proxy.go

  1/*
  2 *
  3 * Copyright 2017 gRPC authors.
  4 *
  5 * Licensed under the Apache License, Version 2.0 (the "License");
  6 * you may not use this file except in compliance with the License.
  7 * You may obtain a copy of the License at
  8 *
  9 *     http://www.apache.org/licenses/LICENSE-2.0
 10 *
 11 * Unless required by applicable law or agreed to in writing, software
 12 * distributed under the License is distributed on an "AS IS" BASIS,
 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14 * See the License for the specific language governing permissions and
 15 * limitations under the License.
 16 *
 17 */
 18
 19package transport
 20
 21import (
 22	"bufio"
 23	"context"
 24	"encoding/base64"
 25	"fmt"
 26	"io"
 27	"net"
 28	"net/http"
 29	"net/http/httputil"
 30	"net/url"
 31
 32	"google.golang.org/grpc/internal"
 33	"google.golang.org/grpc/internal/proxyattributes"
 34	"google.golang.org/grpc/resolver"
 35)
 36
 37const proxyAuthHeaderKey = "Proxy-Authorization"
 38
 39// To read a response from a net.Conn, http.ReadResponse() takes a bufio.Reader.
 40// It's possible that this reader reads more than what's need for the response
 41// and stores those bytes in the buffer. bufConn wraps the original net.Conn
 42// and the bufio.Reader to make sure we don't lose the bytes in the buffer.
 43type bufConn struct {
 44	net.Conn
 45	r io.Reader
 46}
 47
 48func (c *bufConn) Read(b []byte) (int, error) {
 49	return c.r.Read(b)
 50}
 51
 52func basicAuth(username, password string) string {
 53	auth := username + ":" + password
 54	return base64.StdEncoding.EncodeToString([]byte(auth))
 55}
 56
 57func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, grpcUA string, opts proxyattributes.Options) (_ net.Conn, err error) {
 58	defer func() {
 59		if err != nil {
 60			conn.Close()
 61		}
 62	}()
 63
 64	req := &http.Request{
 65		Method: http.MethodConnect,
 66		URL:    &url.URL{Host: opts.ConnectAddr},
 67		Header: map[string][]string{"User-Agent": {grpcUA}},
 68	}
 69	if user := opts.User; user != nil {
 70		u := user.Username()
 71		p, _ := user.Password()
 72		req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p))
 73	}
 74	if err := sendHTTPRequest(ctx, req, conn); err != nil {
 75		return nil, fmt.Errorf("failed to write the HTTP request: %v", err)
 76	}
 77
 78	r := bufio.NewReader(conn)
 79	resp, err := http.ReadResponse(r, req)
 80	if err != nil {
 81		return nil, fmt.Errorf("reading server HTTP response: %v", err)
 82	}
 83	defer resp.Body.Close()
 84	if resp.StatusCode != http.StatusOK {
 85		dump, err := httputil.DumpResponse(resp, true)
 86		if err != nil {
 87			return nil, fmt.Errorf("failed to do connect handshake, status code: %s", resp.Status)
 88		}
 89		return nil, fmt.Errorf("failed to do connect handshake, response: %q", dump)
 90	}
 91	// The buffer could contain extra bytes from the target server, so we can't
 92	// discard it. However, in many cases where the server waits for the client
 93	// to send the first message (e.g. when TLS is being used), the buffer will
 94	// be empty, so we can avoid the overhead of reading through this buffer.
 95	if r.Buffered() != 0 {
 96		return &bufConn{Conn: conn, r: r}, nil
 97	}
 98	return conn, nil
 99}
100
101// proxyDial establishes a TCP connection to the specified address and performs an HTTP CONNECT handshake.
102func proxyDial(ctx context.Context, addr resolver.Address, grpcUA string, opts proxyattributes.Options) (net.Conn, error) {
103	conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", addr.Addr)
104	if err != nil {
105		return nil, err
106	}
107	return doHTTPConnectHandshake(ctx, conn, grpcUA, opts)
108}
109
110func sendHTTPRequest(ctx context.Context, req *http.Request, conn net.Conn) error {
111	req = req.WithContext(ctx)
112	if err := req.Write(conn); err != nil {
113		return fmt.Errorf("failed to write the HTTP request: %v", err)
114	}
115	return nil
116}