Add support for HTTP_PROXY, HTTPS_PROXY, and NO_PROXY#1260
Add support for HTTP_PROXY, HTTPS_PROXY, and NO_PROXY#1260
Conversation
internal/api/api.go
Outdated
| func envProxyURL(endpoint string) *url.URL { | ||
| u, err := url.Parse(endpoint) | ||
| if err != nil || u.Scheme == "" || u.Host == "" { | ||
| return nil |
There was a problem hiding this comment.
What do you think about logging the error here?
There was a problem hiding this comment.
Great idea. I re-did the URL parsing for both the endpoint and the proxy - pushing that out to main.go when the program loads, so that users get notified immediately if there are parsing problems for either of them.
internal/api/api.go
Outdated
| } | ||
| proxyURL, err := http.ProxyFromEnvironment(&http.Request{URL: u}) | ||
| if err != nil || proxyURL == nil { | ||
| return nil |
There was a problem hiding this comment.
What do you think about logging the error here?
There was a problem hiding this comment.
Great idea. I re-did the URL parsing for both the endpoint and the proxy - pushing that out to main.go when the program loads, so that users get notified immediately if there are parsing problems for either of them.
keegancsmith
left a comment
There was a problem hiding this comment.
Nice! Inline comments, but otherwise LGTM
internal/api/api.go
Outdated
| if opts.ProxyURL != nil || opts.ProxyPath != "" { | ||
| // Explicit SRC_PROXY configuration takes precedence. | ||
| transport = withProxyTransport(transport, opts.ProxyURL, opts.ProxyPath) | ||
| } else if proxyURL := envProxyURL(opts.Endpoint); proxyURL != nil && proxyURL.Scheme == "https" { |
There was a problem hiding this comment.
why does https:// not work without intervention? (as documented below for http and socks5)
There was a problem hiding this comment.
Since we're connecting to the SG API using HTTP/2, the standard transport tries to connect to the TLS-enabled proxy using HTTP/2 also, which fails if the proxy doesn't support HTTP/2. I haven't found a proxy for local testing that does support HTTP/2; I suspect that most/all proxies that are TLS-enabled don't support HTTP/2.
internal/api/proxy.go
Outdated
| // not already present (443 for https, 80 for http). | ||
| func proxyDialAddr(proxyURL *url.URL) string { | ||
| addr := proxyURL.Host | ||
| if _, _, err := net.SplitHostPort(addr); err != nil { |
There was a problem hiding this comment.
why are you ignoring the host here and instead assuming addr is just the host? I think this could be a bug right? IE if your proxy url has a port specified won't this break?
There was a problem hiding this comment.
Yeah, that's confusing. I wonder if I goofed and deleted a line or something. All the tests were passing; guess I'd better revisit those also! 😊
internal/api/proxy.go
Outdated
| // 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 { | ||
| addr := proxyURL.Host |
There was a problem hiding this comment.
I think you should be using the Hostname and Port methods instead of net.SplitHostPort. For example, I think ipv6 addresses would break since the way they are represented in a URL are different to net.SplitHostPort expects.
There was a problem hiding this comment.
Hmmmm, the docs for net.SplitHostPort mention IPV6; I'll add some test cases to see if it does work.
| ln, err := net.Listen("tcp", "127.0.0.1:0") | ||
| if err != nil { | ||
| t.Fatalf("proxy listen: %v", err) | ||
| } | ||
|
|
||
| srv := &http.Server{Handler: handler} | ||
|
|
||
| if useTLS { | ||
| cert := generateTestCert(t, "127.0.0.1") | ||
| srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}} | ||
| go srv.ServeTLS(ln, "", "") | ||
| } else { | ||
| go srv.Serve(ln) | ||
| } | ||
| t.Cleanup(func() { srv.Close() }) | ||
|
|
||
| // Wait for the server to be ready | ||
| waitForServerReady(t, ln.Addr().String(), useTLS, 5*time.Second) | ||
|
|
||
| scheme := "http" | ||
| if useTLS { | ||
| scheme = "https" | ||
| } | ||
| pURL, _ := url.Parse(fmt.Sprintf("%s://%s", scheme, ln.Addr().String())) | ||
| return pURL, ch |
There was a problem hiding this comment.
I think you can greatly simplify code here (and remove helpers like waitForServerReady and generateTestCert) by using the httptest package. It already provides functions to listen on TLS/etc and does things like wait for the server to be listening.
| {"https with port", "https://proxy.example.com:8443", "proxy.example.com:8443"}, | ||
| {"https without port", "https://proxy.example.com", "proxy.example.com:443"}, | ||
| {"http with port", "http://proxy.example.com:8080", "proxy.example.com:8080"}, | ||
| {"http without port", "http://proxy.example.com", "proxy.example.com:80"}, |
There was a problem hiding this comment.
get our favourite AI's to generate some example with ipv4, ipv6 and localhost.
|
You might wanna rebase, I think william just did some CI updates which include things like go-lint/etc. |
|
Thanks for all of your commentary; I forgot to open this PR in draft mode! Been trying to address what looks like flakey tests in the Ubuntu CI runner; I'll go through your very helpful comments soon. |
|
This change is part of the following stack: Change managed by git-spice. |
b1d6176 to
05f0fbb
Compare
|
@peterguy alright just re-request review when ready. |
…SRC_PROXY if present
…quest.Write to ensure correctness
…y edge cases of silently dropped/ignored pieces of the response from the proxy
…e sure it'll try HTTP/2 by setting NextProtos
…trim a trailing slash
… env up front so that typos and malformed urls will surface immediately
…e proxy in the config is now the one gathered from either SRC_PROXY or the standard env variables
… that proxies are used
622bd60 to
3536377
Compare
keegancsmith
left a comment
There was a problem hiding this comment.
Requesting changes since I just wanna re-review once you have addressed the comments. FYI I pushed some commits to resolve the CI issues.
| } | ||
| if !isValidUDS { | ||
| return nil, errors.Newf("invalid proxy socket: %s", path) | ||
| return nil, errors.Newf("Invalid proxy socket: %s", path) |
There was a problem hiding this comment.
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 error doesn't read as nicely as my outer error: my inner error
| // For http:// and socks5:// proxies, the cloned | ||
| // transport's default Proxy handles them correctly without intervention. |
There was a problem hiding this comment.
if I set SRC_PROXY to a http:// or socks5:// though, won't the default transport ignore it?
| ln, err := net.Listen("tcp", "127.0.0.1:0") | ||
| if err != nil { | ||
| t.Fatalf("proxy listen: %v", err) | ||
| } | ||
|
|
||
| srv := &http.Server{Handler: handler} | ||
|
|
||
| if useTLS { | ||
| cert := generateTestCert(t, "127.0.0.1") | ||
| srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}} | ||
| go srv.ServeTLS(ln, "", "") | ||
| } else { | ||
| go srv.Serve(ln) | ||
| } | ||
| t.Cleanup(func() { srv.Close() }) | ||
|
|
||
| // Wait for the server to be ready | ||
| waitForServerReady(t, ln.Addr().String(), useTLS, 5*time.Second) | ||
|
|
||
| scheme := "http" | ||
| if useTLS { | ||
| scheme = "https" | ||
| } | ||
| pURL, _ := url.Parse(fmt.Sprintf("%s://%s", scheme, ln.Addr().String())) | ||
| return pURL, ch |
| } | ||
| s += fmt.Sprintf(" %s \\\n", shellquote.Join("-d", string(data))) | ||
| s += fmt.Sprintf(" %s", shellquote.Join(r.client.opts.Endpoint+"/.api/graphql")) | ||
| s += fmt.Sprintf(" %s", shellquote.Join(r.client.opts.EndpointURL.String()+"/.api/graphql")) |
There was a problem hiding this comment.
given you are doing TrimRight before you should probably do it here as well.
| func (c *connWithBufferedReader) Read(p []byte) (int, error) { | ||
| return c.r.Read(p) | ||
| } |
There was a problem hiding this comment.
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.
|
|
||
| // Read and check the response from the proxy | ||
| resp, err := http.ReadResponse(bufio.NewReader(conn), nil) | ||
| br := bufio.NewReader(conn) |
There was a problem hiding this comment.
It isn't clear to me why you introduced the buffered reader?
| ProxyURL *url.URL | ||
| ProxyPath string | ||
| ConfigFilePath string | ||
| EndpointURL *url.URL |
There was a problem hiding this comment.
minor: but these fields should probably all not be exported. Looks like we don't want to marshal them / they are internally read/set.
| EndpointURL: &url.URL{ | ||
| Scheme: "https", | ||
| Host: "override.com", | ||
| }, |
There was a problem hiding this comment.
is it worth updating all these test cases for this? I'd consider just not comparing this field in the comparsion in the test.
|
|
||
| var cfg *config | ||
|
|
||
| // config represents the config format. |
There was a problem hiding this comment.
[Re: line +130]
I think you should marshal into a struct with just the json fields here. Then from your readConfig function return something which just contains the fields you want to read. Then you don't have both Endpoint and EndpointURL in this struct which is a bit weird.
See this comment inline on Graphite.
| } | ||
| tlsConn := tls.Client(conn, cfg) | ||
| if err := tlsConn.HandshakeContext(ctx); err != nil { | ||
| return nil, err |
There was a problem hiding this comment.
I think you need to call tlsConn.Close here to cleanup.
Adds support for the standard environment variables
HTTP_PROXY,HTTPS_PROXY, andNO_PROXY.SRC_PROXYstill takes precedence, including overNO_PROXY.Also added fixes for connecting to TLS-enabled proxies, where we want to connect to the SG API using HTTP/2, but many proxies support only HTTP/1.1, so we need to connect to the proxy via HTTP/1.1, then connect through the proxy to the SG API using HTTP/2.
Moved endpoint and proxy parsing to the config building so that typos and malformed urls are caught early and errors returned immediately.
Test plan
Added CI tests validating proxy behavior.
Manual testing
Install
mitmproxyandsquid.Create an SSL cert for
squid:Create a config for
squid:Run mitmproxy -
mitmproxy -v- in one terminal.Run squid -
squid -f ./squid.conf -N -d 1- in another terminal.In a third terminal, in the src-cli repo, build the binary:
go build -o ./src-cli ./cmd/srcEnsure you have
SRC_ENDPOINTandSRC_ACCESS_TOKENset in your environment, and unsetSRC_PROXY.Try connecting with each proxy (
mitmproxyis 8080,squidis 8445):You should get "Authenticated as [USER] on [ENDPOINT]" messages in your terminal, and in the
mitmproxyandsquidterminals, you should see activity.This tests connecting through a proxy that connects with http (
mitmproxy), and one that is TLS-enabled (squid).Ensure that
NO_PROXYis ignored when usingSRC_PROXYand honored when usingHTTPS_PROXY:You should get a successful login, but no entry in the
mitmproxylog.You should get a successful login and also see an entry in the
mitmproxylog.Closes https://linear.app/sourcegraph/issue/CPL-236/src-cli-support-standard-http-proxy