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}