| // Copyright 2017 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| // Package httpproxy provides support for HTTP proxy determination | |
| // based on environment variables, as provided by | |
| // [net/http.ProxyFromEnvironment] function. | |
| // | |
| // The API is not subject to the Go 1 compatibility promise and may change at | |
| // any time. | |
| package httpproxy | |
| import ( | |
| "errors" | |
| "fmt" | |
| "net" | |
| "net/netip" | |
| "net/url" | |
| "os" | |
| "strings" | |
| "unicode/utf8" | |
| "golang.org/x/net/idna" | |
| ) | |
| // Config holds configuration for HTTP proxy settings. See | |
| // FromEnvironment for details. | |
| type Config struct { | |
| // HTTPProxy represents the value of the HTTP_PROXY or | |
| // http_proxy environment variable. It will be used as the proxy | |
| // URL for HTTP requests unless overridden by NoProxy. | |
| HTTPProxy string | |
| // HTTPSProxy represents the HTTPS_PROXY or https_proxy | |
| // environment variable. It will be used as the proxy URL for | |
| // HTTPS requests unless overridden by NoProxy. | |
| HTTPSProxy string | |
| // NoProxy represents the NO_PROXY or no_proxy environment | |
| // variable. It specifies a string that contains comma-separated values | |
| // specifying hosts that should be excluded from proxying. Each value is | |
| // represented by an IP address prefix (1.2.3.4), an IP address prefix in | |
| // CIDR notation (1.2.3.4/8), a domain name, or a special DNS label (*). | |
| // An IP address prefix and domain name can also include a literal port | |
| // number (1.2.3.4:80). | |
| // A domain name matches that name and all subdomains. A domain name with | |
| // a leading "." matches subdomains only. For example "foo.com" matches | |
| // "foo.com" and "bar.foo.com"; ".y.com" matches "x.y.com" but not "y.com". | |
| // A single asterisk (*) indicates that no proxying should be done. | |
| // A best effort is made to parse the string and errors are | |
| // ignored. | |
| NoProxy string | |
| // CGI holds whether the current process is running | |
| // as a CGI handler (FromEnvironment infers this from the | |
| // presence of a REQUEST_METHOD environment variable). | |
| // When this is set, ProxyForURL will return an error | |
| // when HTTPProxy applies, because a client could be | |
| // setting HTTP_PROXY maliciously. See https://go.dev/s/cgihttpproxy. | |
| CGI bool | |
| } | |
| // config holds the parsed configuration for HTTP proxy settings. | |
| type config struct { | |
| // Config represents the original configuration as defined above. | |
| Config | |
| // httpsProxy is the parsed URL of the HTTPSProxy if defined. | |
| httpsProxy *url.URL | |
| // httpProxy is the parsed URL of the HTTPProxy if defined. | |
| httpProxy *url.URL | |
| // ipMatchers represent all values in the NoProxy that are IP address | |
| // prefixes or an IP address in CIDR notation. | |
| ipMatchers []matcher | |
| // domainMatchers represent all values in the NoProxy that are a domain | |
| // name or hostname & domain name | |
| domainMatchers []matcher | |
| } | |
| // FromEnvironment returns a Config instance populated from the environment | |
| // variables HTTP_PROXY, HTTPS_PROXY and NO_PROXY (or the lowercase versions | |
| // thereof). When both the uppercase and lowercase versions are provided, the | |
| // lowercase versions are prioritized. | |
| // | |
| // The environment values may be either a complete URL or a | |
| // "host[:port]", in which case the "http" scheme is assumed. An error | |
| // is returned if the value is a different form. | |
| func FromEnvironment() *Config { | |
| return &Config{ | |
| HTTPProxy: getEnvAny("http_proxy", "HTTP_PROXY"), | |
| HTTPSProxy: getEnvAny("https_proxy", "HTTPS_PROXY"), | |
| NoProxy: getEnvAny("no_proxy", "NO_PROXY"), | |
| CGI: os.Getenv("REQUEST_METHOD") != "", | |
| } | |
| } | |
| func getEnvAny(names ...string) string { | |
| for _, n := range names { | |
| if val := os.Getenv(n); val != "" { | |
| return val | |
| } | |
| } | |
| return "" | |
| } | |
| // ProxyFunc returns a function that determines the proxy URL to use for | |
| // a given request URL. Changing the contents of cfg will not affect | |
| // proxy functions created earlier. | |
| // | |
| // A nil URL and nil error are returned if no proxy is defined in the | |
| // environment, or a proxy should not be used for the given request, as | |
| // defined by NO_PROXY. | |
| // | |
| // As a special case, if reqURL.Host is "localhost" or a loopback address | |
| // (with or without a port number), then a nil URL and nil error will be returned. | |
| func (cfg *Config) ProxyFunc() func(reqURL *url.URL) (*url.URL, error) { | |
| // Preprocess the Config settings for more efficient evaluation. | |
| cfg1 := &config{ | |
| Config: *cfg, | |
| } | |
| cfg1.init() | |
| return cfg1.proxyForURL | |
| } | |
| func (cfg *config) proxyForURL(reqURL *url.URL) (*url.URL, error) { | |
| var proxy *url.URL | |
| if reqURL.Scheme == "https" { | |
| proxy = cfg.httpsProxy | |
| } else if reqURL.Scheme == "http" { | |
| proxy = cfg.httpProxy | |
| if proxy != nil && cfg.CGI { | |
| return nil, errors.New("refusing to use HTTP_PROXY value in CGI environment; see golang.org/s/cgihttpproxy") | |
| } | |
| } | |
| if proxy == nil { | |
| return nil, nil | |
| } | |
| if !cfg.useProxy(canonicalAddr(reqURL)) { | |
| return nil, nil | |
| } | |
| return proxy, nil | |
| } | |
| func parseProxy(proxy string) (*url.URL, error) { | |
| if proxy == "" { | |
| return nil, nil | |
| } | |
| proxyURL, err := url.Parse(proxy) | |
| if err != nil || proxyURL.Scheme == "" || proxyURL.Host == "" { | |
| // 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 | |
| } | |
| // 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 (cfg *config) useProxy(addr string) bool { | |
| if len(addr) == 0 { | |
| return true | |
| } | |
| host, port, err := net.SplitHostPort(addr) | |
| if err != nil { | |
| return false | |
| } | |
| if host == "localhost" { | |
| return false | |
| } | |
| nip, err := netip.ParseAddr(host) | |
| var ip net.IP | |
| if err == nil { | |
| ip = net.IP(nip.AsSlice()) | |
| if ip.IsLoopback() { | |
| return false | |
| } | |
| } | |
| addr = strings.ToLower(strings.TrimSpace(host)) | |
| if ip != nil { | |
| for _, m := range cfg.ipMatchers { | |
| if m.match(addr, port, ip) { | |
| return false | |
| } | |
| } | |
| } | |
| for _, m := range cfg.domainMatchers { | |
| if m.match(addr, port, ip) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| func (c *config) init() { | |
| if parsed, err := parseProxy(c.HTTPProxy); err == nil { | |
| c.httpProxy = parsed | |
| } | |
| if parsed, err := parseProxy(c.HTTPSProxy); err == nil { | |
| c.httpsProxy = parsed | |
| } | |
| for _, p := range strings.Split(c.NoProxy, ",") { | |
| p = strings.ToLower(strings.TrimSpace(p)) | |
| if len(p) == 0 { | |
| continue | |
| } | |
| if p == "*" { | |
| c.ipMatchers = []matcher{allMatch{}} | |
| c.domainMatchers = []matcher{allMatch{}} | |
| return | |
| } | |
| // IPv4/CIDR, IPv6/CIDR | |
| if _, pnet, err := net.ParseCIDR(p); err == nil { | |
| c.ipMatchers = append(c.ipMatchers, cidrMatch{cidr: pnet}) | |
| continue | |
| } | |
| // IPv4:port, [IPv6]:port | |
| phost, pport, err := net.SplitHostPort(p) | |
| if err == nil { | |
| if len(phost) == 0 { | |
| // There is no host part, likely the entry is malformed; ignore. | |
| continue | |
| } | |
| if phost[0] == '[' && phost[len(phost)-1] == ']' { | |
| phost = phost[1 : len(phost)-1] | |
| } | |
| } else { | |
| phost = p | |
| } | |
| // IPv4, IPv6 | |
| if pip := net.ParseIP(phost); pip != nil { | |
| c.ipMatchers = append(c.ipMatchers, ipMatch{ip: pip, port: pport}) | |
| continue | |
| } | |
| if len(phost) == 0 { | |
| // There is no host part, likely the entry is malformed; ignore. | |
| continue | |
| } | |
| // domain.com or domain.com:80 | |
| // foo.com matches bar.foo.com | |
| // .domain.com or .domain.com:port | |
| // *.domain.com or *.domain.com:port | |
| if strings.HasPrefix(phost, "*.") { | |
| phost = phost[1:] | |
| } | |
| matchHost := false | |
| if phost[0] != '.' { | |
| matchHost = true | |
| phost = "." + phost | |
| } | |
| if v, err := idnaASCII(phost); err == nil { | |
| phost = v | |
| } | |
| c.domainMatchers = append(c.domainMatchers, domainMatch{host: phost, port: pport, matchHost: matchHost}) | |
| } | |
| } | |
| var portMap = map[string]string{ | |
| "http": "80", | |
| "https": "443", | |
| "socks5": "1080", | |
| } | |
| // canonicalAddr returns url.Host but always with a ":port" suffix | |
| func canonicalAddr(url *url.URL) string { | |
| addr := url.Hostname() | |
| if v, err := idnaASCII(addr); err == nil { | |
| addr = v | |
| } | |
| port := url.Port() | |
| if port == "" { | |
| port = portMap[url.Scheme] | |
| } | |
| return net.JoinHostPort(addr, port) | |
| } | |
| // 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, "]") } | |
| func idnaASCII(v string) (string, error) { | |
| // TODO: Consider removing this check after verifying performance is okay. | |
| // Right now punycode verification, length checks, context checks, and the | |
| // permissible character tests are all omitted. It also prevents the ToASCII | |
| // call from salvaging an invalid IDN, when possible. As a result it may be | |
| // possible to have two IDNs that appear identical to the user where the | |
| // ASCII-only version causes an error downstream whereas the non-ASCII | |
| // version does not. | |
| // Note that for correct ASCII IDNs ToASCII will only do considerably more | |
| // work, but it will not cause an allocation. | |
| if isASCII(v) { | |
| return v, nil | |
| } | |
| return idna.Lookup.ToASCII(v) | |
| } | |
| func isASCII(s string) bool { | |
| for i := 0; i < len(s); i++ { | |
| if s[i] >= utf8.RuneSelf { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| // matcher represents the matching rule for a given value in the NO_PROXY list | |
| type matcher interface { | |
| // match returns true if the host and optional port or ip and optional port | |
| // are allowed | |
| match(host, port string, ip net.IP) bool | |
| } | |
| // allMatch matches on all possible inputs | |
| type allMatch struct{} | |
| func (a allMatch) match(host, port string, ip net.IP) bool { | |
| return true | |
| } | |
| type cidrMatch struct { | |
| cidr *net.IPNet | |
| } | |
| func (m cidrMatch) match(host, port string, ip net.IP) bool { | |
| return m.cidr.Contains(ip) | |
| } | |
| type ipMatch struct { | |
| ip net.IP | |
| port string | |
| } | |
| func (m ipMatch) match(host, port string, ip net.IP) bool { | |
| if m.ip.Equal(ip) { | |
| return m.port == "" || m.port == port | |
| } | |
| return false | |
| } | |
| type domainMatch struct { | |
| host string | |
| port string | |
| matchHost bool | |
| } | |
| func (m domainMatch) match(host, port string, ip net.IP) bool { | |
| if ip != nil { | |
| return false | |
| } | |
| if strings.HasSuffix(host, m.host) || (m.matchHost && host == m.host[1:]) { | |
| return m.port == "" || m.port == port | |
| } | |
| return false | |
| } |