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 grpc 20 21import ( 22 "bufio" 23 "errors" 24 "fmt" 25 "io" 26 "net" 27 "net/http" 28 "net/http/httputil" 29 "net/url" 30 31 "golang.org/x/net/context" 32) 33 34var ( 35 // errDisabled indicates that proxy is disabled for the address. 36 errDisabled = errors.New("proxy is disabled for the address") 37 // The following variable will be overwritten in the tests. 38 httpProxyFromEnvironment = http.ProxyFromEnvironment 39) 40 41func mapAddress(ctx context.Context, address string) (string, error) { 42 req := &http.Request{ 43 URL: &url.URL{ 44 Scheme: "https", 45 Host: address, 46 }, 47 } 48 url, err := httpProxyFromEnvironment(req) 49 if err != nil { 50 return "", err 51 } 52 if url == nil { 53 return "", errDisabled 54 } 55 return url.Host, nil 56} 57 58// To read a response from a net.Conn, http.ReadResponse() takes a bufio.Reader. 59// It's possible that this reader reads more than what's need for the response and stores 60// those bytes in the buffer. 61// bufConn wraps the original net.Conn and the bufio.Reader to make sure we don't lose the 62// bytes in the buffer. 63type bufConn struct { 64 net.Conn 65 r io.Reader 66} 67 68func (c *bufConn) Read(b []byte) (int, error) { 69 return c.r.Read(b) 70} 71 72func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr string) (_ net.Conn, err error) { 73 defer func() { 74 if err != nil { 75 conn.Close() 76 } 77 }() 78 79 req := (&http.Request{ 80 Method: http.MethodConnect, 81 URL: &url.URL{Host: addr}, 82 Header: map[string][]string{"User-Agent": {grpcUA}}, 83 }) 84 85 if err := sendHTTPRequest(ctx, req, conn); err != nil { 86 return nil, fmt.Errorf("failed to write the HTTP request: %v", err) 87 } 88 89 r := bufio.NewReader(conn) 90 resp, err := http.ReadResponse(r, req) 91 if err != nil { 92 return nil, fmt.Errorf("reading server HTTP response: %v", err) 93 } 94 defer resp.Body.Close() 95 if resp.StatusCode != http.StatusOK { 96 dump, err := httputil.DumpResponse(resp, true) 97 if err != nil { 98 return nil, fmt.Errorf("failed to do connect handshake, status code: %s", resp.Status) 99 } 100 return nil, fmt.Errorf("failed to do connect handshake, response: %q", dump) 101 } 102 103 return &bufConn{Conn: conn, r: r}, nil 104} 105 106// newProxyDialer returns a dialer that connects to proxy first if necessary. 107// The returned dialer checks if a proxy is necessary, dial to the proxy with the 108// provided dialer, does HTTP CONNECT handshake and returns the connection. 109func newProxyDialer(dialer func(context.Context, string) (net.Conn, error)) func(context.Context, string) (net.Conn, error) { 110 return func(ctx context.Context, addr string) (conn net.Conn, err error) { 111 var skipHandshake bool 112 newAddr, err := mapAddress(ctx, addr) 113 if err != nil { 114 if err != errDisabled { 115 return nil, err 116 } 117 skipHandshake = true 118 newAddr = addr 119 } 120 121 conn, err = dialer(ctx, newAddr) 122 if err != nil { 123 return 124 } 125 if !skipHandshake { 126 conn, err = doHTTPConnectHandshake(ctx, conn, addr) 127 } 128 return 129 } 130} 131