Skip to content

Commit

Permalink
sockets: add TCPProxyFromEnvironment to keep pre-go1.16 behavior
Browse files Browse the repository at this point in the history
TCPProxyFromEnvironment wraps http.ProxyFromEnvironment, to preserve the
pre-go1.16 behavior for URLs using the 'tcp://' scheme. For other schemes,
golang's standard behavior is preserved (and depends on the Go version used).

Prior to go1.16, `https://` schemes would use HTTPS_PROXY, and any other
scheme would use HTTP_PROXY. However, golang/net@7b1cca2
(per a request in golang/go#40909) changed this behavior to only use
HTTP_PROXY for `http://` schemes, no longer using a proxy for any other
scheme.

Docker uses the `tcp://` scheme as a default for API connections, to indicate
that the API is not "purely" HTTP. Various parts in the code also *require*

this scheme to be used. While we could change the default and allow http(s)
schemes to be used, doing so will take time, taking into account that there
are many installs in existence that have tcp:// configured as DOCKER_HOST.

This function detects if the `tcp://` scheme is used; if it is, it creates
a shallow copy of req, containing just the URL, and overrides the scheme with
'http', which should be sufficient to perform proxy detection.
For other (non-'tcp://') schemes, http.ProxyFromEnvironment is called without
altering the request.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
  • Loading branch information
thaJeztah committed Jul 22, 2021
1 parent 57e36d5 commit ce64c61
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 1 deletion.
35 changes: 34 additions & 1 deletion sockets/sockets.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package sockets
import (
"errors"
"net/http"
"net/url"
)

// ErrProtocolNotAvailable is returned when a given transport protocol is not provided by the operating system.
Expand All @@ -20,7 +21,39 @@ func ConfigureTransport(tr *http.Transport, proto, addr string) error {
case "npipe":
return configureNpipeTransport(tr, proto, addr)
default:
tr.Proxy = http.ProxyFromEnvironment
tr.Proxy = TCPProxyFromEnvironment
}
return nil
}

// TCPProxyFromEnvironment wraps http.ProxyFromEnvironment, to preserve the
// pre-go1.16 behavior for URLs using the 'tcp://' scheme. For other schemes,
// golang's standard behavior is preserved (and depends on the Go version used).
//
// Prior to go1.16, `https://` schemes would use HTTPS_PROXY, and any other
// scheme would use HTTP_PROXY. However, https://github.com/golang/net/commit/7b1cca2348c07eb09fef635269c8e01611260f9f
// (per a request in golang/go#40909) changed this behavior to only use
// HTTP_PROXY for `http://` schemes, no longer using a proxy for any other
// scheme.
//
// Docker uses the `tcp://` scheme as a default for API connections, to indicate
// that the API is not "purely" HTTP. Various parts in the code also *require*
// this scheme to be used. While we could change the default and allow http(s)
// schemes to be used, doing so will take time, taking into account that there
// are many installs in existence that have tcp:// configured as DOCKER_HOST.
//
// This function detects if the `tcp://` scheme is used; if it is, it creates
// a shallow copy of req, containing just the URL, and overrides the scheme with
// 'http', which should be sufficient to perform proxy detection.
// For other (non-'tcp://') schemes, http.ProxyFromEnvironment is called without
// altering the request.
func TCPProxyFromEnvironment(req *http.Request) (*url.URL, error) {
if req.URL.Scheme != "tcp" {
return http.ProxyFromEnvironment(req)
}
u := req.URL
if u.Scheme == "tcp" {
u.Scheme = "http"
}
return http.ProxyFromEnvironment(&http.Request{URL: u})
}
101 changes: 101 additions & 0 deletions sockets/sockets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package sockets

import (
"net/http"
"net/url"
"os"
"testing"
)

var (
httpProxy = "http://proxy.example.com"
httpsProxy = "https://proxy.example.com"
)

func TestConfigureTransportProxy(t *testing.T) {
// roughly based on defaultHTTPClient in the docker client
u := &url.URL{
Scheme: "tcp",
Host: "docker.acme.example.com",
}
transport := new(http.Transport)
err := ConfigureTransport(transport, u.Scheme, u.Host)
if err != nil {
t.Fatal(err)
}
if err := os.Setenv("HTTP_PROXY", httpProxy); err != nil {
t.Fatal(err)
}
if err := os.Setenv("HTTPS_PROXY", httpsProxy); err != nil {
t.Fatal(err)
}
defer func() {
_ = os.Unsetenv("HTTP_PROXY")
_ = os.Unsetenv("HTTPS_PROXY")
}()

request, err := http.NewRequest(http.MethodGet, "tcp://docker.acme.example.com:2376", nil)
if err != nil {
t.Fatal(err)
}
proxyURL, err := transport.Proxy(request)
if err != nil {
t.Fatal(err)
}
if proxyURL.String() != httpProxy {
t.Fatalf("expected %s, got %s", httpProxy, proxyURL)
}
}

func TestTCPProxyFromEnvironment(t *testing.T) {
if err := os.Setenv("HTTP_PROXY", httpProxy); err != nil {
t.Fatal(err)
}
if err := os.Setenv("HTTPS_PROXY", httpsProxy); err != nil {
t.Fatal(err)
}
defer func() {
_ = os.Unsetenv("HTTP_PROXY")
_ = os.Unsetenv("HTTPS_PROXY")
}()

tests := []struct {
url string
expected *string
}{
{
url: "tcp://example.com:2376",
expected: &httpProxy,
},
{
url: "http://example.com:2375",
expected: &httpProxy,
},
{
url: "https://example.com:2376",
expected: &httpsProxy,
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.url, func(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, tc.url, nil)
if err != nil {
t.Fatal(err)
}

proxyURL, err := TCPProxyFromEnvironment(request)
if err != nil {
t.Fatal(err)
}
if tc.expected == nil {
if proxyURL != nil {
t.Fatalf("expected no proxy, got %s", proxyURL)
}
} else if proxyURL.String() != *tc.expected {
t.Fatalf("expected %s, got %s", *tc.expected, proxyURL)
}
})
}
}

0 comments on commit ce64c61

Please sign in to comment.