-
Notifications
You must be signed in to change notification settings - Fork 69
Add support for HTTP_PROXY, HTTPS_PROXY, and NO_PROXY #1260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: peterguy/clean-up-config
Are you sure you want to change the base?
Changes from all commits
3ac2124
b2f1e4e
a7a2f59
e929425
9993da2
4c2de90
bae69a7
fb16c50
b3895b3
69763d9
6031e19
8853c80
bd3b210
7016f06
8a8032a
b4724eb
50ea28b
5dae354
651d4e9
b82148f
024fba3
e29397b
8d78585
0c25a83
d7bbb21
210862b
75d0aee
f810d2a
a13b998
f6c4e55
681f498
dff0820
039c39f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,33 +90,36 @@ type ClientOpts struct { | |
| } | ||
|
|
||
| func buildTransport(opts ClientOpts, flags *Flags) http.RoundTripper { | ||
| var transport http.RoundTripper | ||
| { | ||
| tp := http.DefaultTransport.(*http.Transport).Clone() | ||
| transport := http.DefaultTransport.(*http.Transport).Clone() | ||
|
|
||
| if flags.insecureSkipVerify != nil && *flags.insecureSkipVerify { | ||
| tp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} | ||
| } | ||
|
|
||
| if tp.TLSClientConfig == nil { | ||
| tp.TLSClientConfig = &tls.Config{} | ||
| } | ||
| if flags.insecureSkipVerify != nil && *flags.insecureSkipVerify { | ||
| transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} | ||
| } | ||
|
|
||
| if opts.ProxyURL != nil || opts.ProxyPath != "" { | ||
| tp = withProxyTransport(tp, opts.ProxyURL, opts.ProxyPath) | ||
| } | ||
| if transport.TLSClientConfig == nil { | ||
| transport.TLSClientConfig = &tls.Config{} | ||
| } | ||
|
|
||
| transport = tp | ||
| if opts.ProxyPath != "" || opts.ProxyURL != nil { | ||
| // Use our custom dialer for proxied connections. | ||
| // A custom dialer is not always needed - the connection libraries will handle HTTP(S)_PROXY-defined proxies | ||
| // (Go supports http, https, socks5, and socks5h proxies via HTTP(S)_PROXY), | ||
| // but we're also supporting proxies defined via SRC_PROXY, which can include UDS proxies, | ||
| // and connecting to TLS-enabled proxies adds an additional wrinkle when using HTTP/2. | ||
| transport = withProxyTransport(transport, opts.ProxyURL, opts.ProxyPath) | ||
| } | ||
|
|
||
| // For http:// and socks5:// proxies, the cloned | ||
| // transport's default Proxy handles them correctly without intervention. | ||
|
Comment on lines
+112
to
+113
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if I set SRC_PROXY to a http:// or socks5:// though, won't the default transport ignore it?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I re-worked this section |
||
|
|
||
| var rt http.RoundTripper = transport | ||
| if opts.AccessToken == "" && opts.OAuthToken != nil { | ||
| transport = &oauth.Transport{ | ||
| rt = &oauth.Transport{ | ||
| Base: transport, | ||
| Token: opts.OAuthToken, | ||
| } | ||
| } | ||
|
|
||
| return transport | ||
| return rt | ||
| } | ||
|
|
||
| // NewClient creates a new API client. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,32 +5,66 @@ import ( | |
| "context" | ||
| "crypto/tls" | ||
| "encoding/base64" | ||
| "fmt" | ||
| "io" | ||
| "net" | ||
| "net/http" | ||
| "net/url" | ||
| "sync" | ||
|
|
||
| "github.com/sourcegraph/sourcegraph/lib/errors" | ||
| ) | ||
|
|
||
| type connWithBufferedReader struct { | ||
| net.Conn | ||
| r *bufio.Reader | ||
| mu sync.Mutex | ||
| } | ||
|
|
||
| func (c *connWithBufferedReader) Read(p []byte) (int, error) { | ||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
| return c.r.Read(p) | ||
| } | ||
|
Comment on lines
+23
to
+27
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. net.Conn documents all methods to be concurrency safe. However, bufio.Reader is not. You need to add in a mutex here. > Multiple goroutines may invoke methods on a Conn simultaneously.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, thanks! |
||
|
|
||
| // proxyDialAddr returns proxyURL.Host with a default port appended if one is | ||
| // not already present (443 for https, 80 for http). | ||
| func proxyDialAddr(proxyURL *url.URL) string { | ||
| // net.SplitHostPort returns an error when the input doesn't contain a port | ||
| if _, _, err := net.SplitHostPort(proxyURL.Host); err == nil { | ||
| return proxyURL.Host | ||
| } | ||
| if proxyURL.Scheme == "https" { | ||
| return net.JoinHostPort(proxyURL.Hostname(), "443") | ||
| } | ||
| return net.JoinHostPort(proxyURL.Hostname(), "80") | ||
| } | ||
|
|
||
| // withProxyTransport modifies the given transport to handle proxying of unix, socks5 and http connections. | ||
| // | ||
| // Note: baseTransport is considered to be a clone created with transport.Clone() | ||
| // | ||
| // - If a the proxyPath is not empty, a unix socket proxy is created. | ||
| // - Otherwise, the proxyURL is used to determine if we should proxy socks5 / http connections | ||
| // - If proxyPath is not empty, a unix socket proxy is created. | ||
| // - Otherwise, proxyURL is used to determine if we should proxy socks5 / http connections | ||
| func withProxyTransport(baseTransport *http.Transport, proxyURL *url.URL, proxyPath string) *http.Transport { | ||
| handshakeTLS := func(ctx context.Context, conn net.Conn, addr string) (net.Conn, error) { | ||
| // Extract the hostname (without the port) for TLS SNI | ||
| host, _, err := net.SplitHostPort(addr) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| tlsConn := tls.Client(conn, &tls.Config{ | ||
| ServerName: host, | ||
| // Pull InsecureSkipVerify from the target host transport | ||
| // so that insecure-skip-verify flag settings are honored for the proxy server | ||
| InsecureSkipVerify: baseTransport.TLSClientConfig.InsecureSkipVerify, | ||
| }) | ||
| cfg := baseTransport.TLSClientConfig.Clone() | ||
| if cfg.ServerName == "" { | ||
| cfg.ServerName = host | ||
| } | ||
| // Preserve HTTP/2 negotiation to the origin when ForceAttemptHTTP2 | ||
| // is enabled. Without this, the manual TLS handshake would not | ||
| // advertise h2 via ALPN, silently forcing HTTP/1.1. | ||
| if baseTransport.ForceAttemptHTTP2 && len(cfg.NextProtos) == 0 { | ||
| cfg.NextProtos = []string{"h2", "http/1.1"} | ||
| } | ||
| tlsConn := tls.Client(conn, cfg) | ||
| if err := tlsConn.HandshakeContext(ctx); err != nil { | ||
| tlsConn.Close() | ||
| return nil, err | ||
peterguy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| return tlsConn, nil | ||
|
|
@@ -54,67 +88,79 @@ func withProxyTransport(baseTransport *http.Transport, proxyURL *url.URL, proxyP | |
| baseTransport.Proxy = nil | ||
| } else if proxyURL != nil { | ||
| switch proxyURL.Scheme { | ||
| case "socks5", "socks5h": | ||
| // SOCKS proxies work out of the box - no need to manually dial | ||
| case "http", "socks5", "socks5h": | ||
| // HTTP and SOCKS proxies work out of the box - no need to manually dial | ||
| baseTransport.Proxy = http.ProxyURL(proxyURL) | ||
| case "http", "https": | ||
| case "https": | ||
| dial := func(ctx context.Context, network, addr string) (net.Conn, error) { | ||
| // Dial the proxy | ||
| d := net.Dialer{} | ||
| conn, err := d.DialContext(ctx, "tcp", proxyURL.Host) | ||
| // Dial the proxy. For https:// proxies, we TLS-connect to the | ||
| // proxy itself and force ALPN to HTTP/1.1 to prevent Go from | ||
| // negotiating HTTP/2 for the CONNECT tunnel. Many proxy servers | ||
| // don't support HTTP/2 CONNECT, and Go's default Transport.Proxy | ||
| // would negotiate h2 via ALPN when TLS-connecting to an https:// | ||
| // proxy, causing "bogus greeting" errors. For http:// proxies, | ||
| // CONNECT is always HTTP/1.1 over plain TCP so this isn't needed. | ||
| // The target connection (e.g. to sourcegraph.com) still negotiates | ||
| // HTTP/2 normally through the established tunnel. | ||
| proxyAddr := proxyDialAddr(proxyURL) | ||
keegancsmith marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| var conn net.Conn | ||
| var err error | ||
| if proxyURL.Scheme == "https" { | ||
| raw, dialErr := (&net.Dialer{}).DialContext(ctx, "tcp", proxyAddr) | ||
| if dialErr != nil { | ||
| return nil, dialErr | ||
| } | ||
| cfg := baseTransport.TLSClientConfig.Clone() | ||
| cfg.NextProtos = []string{"http/1.1"} | ||
| if cfg.ServerName == "" { | ||
| cfg.ServerName = proxyURL.Hostname() | ||
| } | ||
| tlsConn := tls.Client(raw, cfg) | ||
| if err := tlsConn.HandshakeContext(ctx); err != nil { | ||
| raw.Close() | ||
| return nil, err | ||
| } | ||
| conn = tlsConn | ||
| } else { | ||
| conn, err = (&net.Dialer{}).DialContext(ctx, "tcp", proxyAddr) | ||
| } | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // this is the whole point of manually dialing the HTTP(S) proxy: | ||
| // being able to force HTTP/1. | ||
| // When relying on Transport.Proxy, the protocol is always HTTP/2, | ||
| // but many proxy servers don't support HTTP/2. | ||
| // We don't want to disable HTTP/2 in general because we want to use it when | ||
| // connecting to the Sourcegraph API, using HTTP/1 for the proxy connection only. | ||
| protocol := "HTTP/1.1" | ||
|
|
||
| // CONNECT is the HTTP method used to set up a tunneling connection with a proxy | ||
| method := "CONNECT" | ||
|
|
||
| // Manually writing out the HTTP commands because it's not complicated, | ||
| // and http.Request has some janky behavior: | ||
| // - ignores the Proto field and hard-codes the protocol to HTTP/1.1 | ||
| // - ignores the Host Header (Header.Set("Host", host)) and uses URL.Host instead. | ||
| // - When the Host field is set, overrides the URL field | ||
| connectReq := fmt.Sprintf("%s %s %s\r\n", method, addr, protocol) | ||
|
|
||
| // A Host header is required per RFC 2616, section 14.23 | ||
| connectReq += fmt.Sprintf("Host: %s\r\n", addr) | ||
|
|
||
| // use authentication if proxy credentials are present | ||
| connectReq := &http.Request{ | ||
| Method: "CONNECT", | ||
| URL: &url.URL{Opaque: addr}, | ||
| Host: addr, | ||
| Header: make(http.Header), | ||
| } | ||
| if proxyURL.User != nil { | ||
| password, _ := proxyURL.User.Password() | ||
| auth := base64.StdEncoding.EncodeToString([]byte(proxyURL.User.Username() + ":" + password)) | ||
| connectReq += fmt.Sprintf("Proxy-Authorization: Basic %s\r\n", auth) | ||
| connectReq.Header.Set("Proxy-Authorization", "Basic "+auth) | ||
| } | ||
|
|
||
| // finish up with an extra carriage return + newline, as per RFC 7230, section 3 | ||
| connectReq += "\r\n" | ||
|
|
||
| // Send the CONNECT request to the proxy to establish the tunnel | ||
| if _, err := conn.Write([]byte(connectReq)); err != nil { | ||
| if err := connectReq.Write(conn); err != nil { | ||
| conn.Close() | ||
| return nil, err | ||
| } | ||
|
|
||
| // Read and check the response from the proxy | ||
| resp, err := http.ReadResponse(bufio.NewReader(conn), nil) | ||
| br := bufio.NewReader(conn) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It isn't clear to me why you introduced the buffered reader?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because I didn't want to lose any bytes after the headers that the reader may have buffered, which bytes could be the start of the tunneled connection data. Granted, it's probably not really a problem because the SG endpoint will in nearly every scenario also be TLS, and "TLS server will not speak until spoken to.", so there will not be any bytes beyond the header. But if the SG endpoint is plain http, we could lose bytes if we discard the buffered reader. |
||
| resp, err := http.ReadResponse(br, nil) | ||
| if err != nil { | ||
| conn.Close() | ||
| return nil, err | ||
| } | ||
| if resp.StatusCode != http.StatusOK { | ||
| // For non-200, it's safe/appropriate to close the body (it’s a real response body here). | ||
| // Try to read a bit (4k bytes) to include in the error message. | ||
| b, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10)) | ||
| resp.Body.Close() | ||
| conn.Close() | ||
| return nil, fmt.Errorf("failed to connect to proxy %v: %v", proxyURL, resp.Status) | ||
| return nil, errors.Newf("failed to connect to proxy %s: %s: %q", proxyURL.Redacted(), resp.Status, b) | ||
| } | ||
| resp.Body.Close() | ||
| return conn, nil | ||
| // 200 CONNECT: do NOT resp.Body.Close(); it would interfere with the tunnel. | ||
| return &connWithBufferedReader{Conn: conn, r: br}, nil | ||
| } | ||
| dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) { | ||
| // Dial the underlying connection through the proxy | ||
|
|
@@ -126,7 +172,7 @@ func withProxyTransport(baseTransport *http.Transport, proxyURL *url.URL, proxyP | |
| } | ||
| baseTransport.DialContext = dial | ||
| baseTransport.DialTLSContext = dialTLS | ||
| // clear out any system proxy settings | ||
| // clear out the system proxy because we're defining our own dialers | ||
| baseTransport.Proxy = nil | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you changed this is some other places as well. go convention is lowercase on error messages since they get combined. eg
My outer error: My inner errordoesn't read as nicely asmy outer error: my inner error