diff --git a/config/config.go b/config/config.go index 5c53f4a9..186195f1 100644 --- a/config/config.go +++ b/config/config.go @@ -85,6 +85,7 @@ func NewConfig() *Configuration { func NewFromValues(gitconfig map[string]string) *Configuration { config := &Configuration{ gitConfig: make(map[string]string, 0), + envVars: make(map[string]string, 0), } buf := bytes.NewBuffer([]byte{}) diff --git a/httputil/http.go b/httputil/http.go index 3f727ed1..99530e86 100644 --- a/httputil/http.go +++ b/httputil/http.go @@ -12,7 +12,6 @@ import ( "net" "net/http" "net/http/httputil" - "net/url" "os" "path/filepath" "strings" @@ -146,21 +145,6 @@ func NewHttpClient(c *config.Configuration, host string) *HttpClient { return client } -func ProxyFromGitConfigOrEnvironment(c *config.Configuration) func(req *http.Request) (*url.URL, error) { - return func(req *http.Request) (*url.URL, error) { - proxyURL, err := http.ProxyFromEnvironment(req) - if proxyURL != nil || err != nil { - return proxyURL, err - } - - if proxy, ok := c.GitConfig("http.proxy"); ok { - return url.Parse(proxy) - } - - return nil, nil - } -} - func CheckRedirect(req *http.Request, via []*http.Request) error { if len(via) >= 3 { return errors.New("stopped after 3 redirects") diff --git a/httputil/proxy.go b/httputil/proxy.go new file mode 100644 index 00000000..13136543 --- /dev/null +++ b/httputil/proxy.go @@ -0,0 +1,139 @@ +package httputil + +import ( + "net" + "net/http" + "net/url" + "strings" + + "fmt" + + "github.com/github/git-lfs/config" +) + +func ProxyFromGitConfigOrEnvironment(c *config.Configuration) func(req *http.Request) (*url.URL, error) { + https_proxy := c.Getenv("HTTPS_PROXY") + if len(https_proxy) == 0 { + https_proxy = c.Getenv("https_proxy") + } + + http_proxy := c.Getenv("HTTP_PROXY") + if len(http_proxy) == 0 { + http_proxy = c.Getenv("http_proxy") + } + + if len(http_proxy) == 0 { + http_proxy, _ = c.GitConfig("http.proxy") + } + + no_proxy := c.Getenv("NO_PROXY") + if len(no_proxy) == 0 { + no_proxy = c.Getenv("no_proxy") + } + + return func(req *http.Request) (*url.URL, error) { + var proxy string + if req.URL.Scheme == "https" { + proxy = https_proxy + } + + if len(proxy) == 0 { + proxy = http_proxy + } + + if len(proxy) == 0 { + return nil, nil + } + + if !useProxy(no_proxy, canonicalAddr(req.URL)) { + return nil, nil + } + + proxyURL, err := url.Parse(proxy) + if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") { + // proxy was bogus. Try prepending "http://" to it and + // see if that parses correctly. If not, we fall + // through and complain about the original one. + if proxyURL, err := url.Parse("http://" + proxy); err == nil { + return proxyURL, nil + } + } + if err != nil { + return nil, fmt.Errorf("invalid proxy address: %q: %v", proxy, err) + } + return proxyURL, nil + } +} + +// canonicalAddr returns url.Host but always with a ":port" suffix +func canonicalAddr(url *url.URL) string { + addr := url.Host + if !hasPort(addr) { + return addr + ":" + portMap[url.Scheme] + } + return addr +} + +// useProxy reports whether requests to addr should use a proxy, +// according to the NO_PROXY or no_proxy environment variable. +// addr is always a canonicalAddr with a host and port. +func useProxy(no_proxy, addr string) bool { + if len(addr) == 0 { + return true + } + + if no_proxy == "*" { + return false + } + + host, _, err := net.SplitHostPort(addr) + if err != nil { + return false + } + if host == "localhost" { + return false + } + if ip := net.ParseIP(host); ip != nil { + if ip.IsLoopback() { + return false + } + } + + addr = strings.ToLower(strings.TrimSpace(addr)) + if hasPort(addr) { + addr = addr[:strings.LastIndex(addr, ":")] + } + + for _, p := range strings.Split(no_proxy, ",") { + p = strings.ToLower(strings.TrimSpace(p)) + if len(p) == 0 { + continue + } + if hasPort(p) { + p = p[:strings.LastIndex(p, ":")] + } + if addr == p { + return false + } + if p[0] == '.' && (strings.HasSuffix(addr, p) || addr == p[1:]) { + // no_proxy ".foo.com" matches "bar.foo.com" or "foo.com" + return false + } + if p[0] != '.' && strings.HasSuffix(addr, p) && addr[len(addr)-len(p)-1] == '.' { + // no_proxy "foo.com" matches "bar.foo.com" + return false + } + } + return true +} + +// Given a string of the form "host", "host:port", or "[ipv6::address]:port", +// return true if the string includes a port. +func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } + +var ( + portMap = map[string]string{ + "http": "80", + "https": "443", + } +) diff --git a/lfs/proxy_test.go b/lfs/proxy_test.go index 04436b39..0f65244d 100644 --- a/lfs/proxy_test.go +++ b/lfs/proxy_test.go @@ -10,6 +10,30 @@ import ( "github.com/stretchr/testify/assert" ) +func TestProxyFromEnvironment(t *testing.T) { + cfg := config.NewFromValues(map[string]string{ + "http.proxy": "https://proxy-from-git-config:8080", + }) + cfg.SetAllEnv(map[string]string{ + "HTTPS_PROXY": "https://proxy-from-env:8080", + }) + + u, err := url.Parse("https://some-host.com:123/foo/bar") + if err != nil { + t.Fatal(err) + } + + req := &http.Request{ + URL: u, + Header: http.Header{}, + } + + proxyURL, err := httputil.ProxyFromGitConfigOrEnvironment(cfg)(req) + + assert.Equal(t, "proxy-from-env:8080", proxyURL.Host) + assert.Equal(t, nil, err) +} + func TestProxyFromGitConfig(t *testing.T) { cfg := config.NewFromValues(map[string]string{ "http.proxy": "https://proxy-from-git-config:8080",