| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- // Package tshttpproxy contains Tailscale additions to httpproxy not available
- // in golang.org/x/net/http/httpproxy. Notably, it aims to support Windows better.
- package tshttpproxy
- import (
- "context"
- "errors"
- "fmt"
- "log"
- "net"
- "net/http"
- "net/url"
- "os"
- "runtime"
- "strings"
- "sync"
- "time"
- "golang.org/x/net/http/httpproxy"
- "tailscale.com/util/mak"
- )
- // InvalidateCache invalidates the package-level cache for ProxyFromEnvironment.
- //
- // It's intended to be called on network link/routing table changes.
- func InvalidateCache() {
- mu.Lock()
- defer mu.Unlock()
- noProxyUntil = time.Time{}
- }
- var (
- mu sync.Mutex
- noProxyUntil time.Time // if non-zero, time at which ProxyFromEnvironment should check again
- config *httpproxy.Config // used to create proxyFunc
- proxyFunc func(*url.URL) (*url.URL, error)
- )
- // SetProxyFunc can be used by clients to set a platform-specific function for proxy resolution.
- // If config is set when this function is called, an error will be returned.
- // The provided function should return a proxy URL for the given request URL,
- // nil if no proxy is enabled for the request URL, or an error if proxy settings cannot be resolved.
- func SetProxyFunc(fn func(*url.URL) (*url.URL, error)) error {
- mu.Lock()
- defer mu.Unlock()
- // Allow override only if config is not set
- if config != nil {
- return errors.New("tshttpproxy: SetProxyFunc can only be called when config is not set")
- }
- proxyFunc = fn
- return nil
- }
- func getProxyFunc() func(*url.URL) (*url.URL, error) {
- // Create config/proxyFunc if it's not created
- mu.Lock()
- defer mu.Unlock()
- if config == nil {
- config = httpproxy.FromEnvironment()
- }
- if proxyFunc == nil {
- proxyFunc = config.ProxyFunc()
- }
- return proxyFunc
- }
- // setNoProxyUntil stops calls to sysProxyEnv (if any) for the provided duration.
- func setNoProxyUntil(d time.Duration) {
- mu.Lock()
- defer mu.Unlock()
- noProxyUntil = time.Now().Add(d)
- }
- var _ = setNoProxyUntil // quiet staticcheck; Windows uses the above, more might later
- // SetSelfProxy configures this package to avoid proxying through any of the
- // provided addresses–e.g. if they refer to proxies being run by this process.
- func SetSelfProxy(addrs ...string) {
- mu.Lock()
- defer mu.Unlock()
- // Ensure we have a valid config
- if config == nil {
- config = httpproxy.FromEnvironment()
- }
- normalizeHostPort := func(s string) string {
- host, portStr, err := net.SplitHostPort(s)
- if err != nil {
- return s
- }
- // Normalize the localhost IP into "localhost", to avoid IPv4/IPv6 confusion.
- if host == "127.0.0.1" || host == "::1" {
- return "localhost:" + portStr
- }
- // On Linux, all 127.0.0.1/8 IPs are also localhost.
- if runtime.GOOS == "linux" && strings.HasPrefix(host, "127.0.0.") {
- return "localhost:" + portStr
- }
- return s
- }
- normHTTP := normalizeHostPort(config.HTTPProxy)
- normHTTPS := normalizeHostPort(config.HTTPSProxy)
- // If any of our proxy variables point to one of the configured
- // addresses, ignore them.
- for _, addr := range addrs {
- normAddr := normalizeHostPort(addr)
- if normHTTP != "" && normHTTP == normAddr {
- log.Printf("tshttpproxy: skipping HTTP_PROXY pointing to self: %q", addr)
- config.HTTPProxy = ""
- normHTTP = ""
- }
- if normHTTPS != "" && normHTTPS == normAddr {
- log.Printf("tshttpproxy: skipping HTTPS_PROXY pointing to self: %q", addr)
- config.HTTPSProxy = ""
- normHTTPS = ""
- }
- }
- // Invalidate to cause it to get re-created
- proxyFunc = nil
- }
- // sysProxyFromEnv, if non-nil, specifies a platform-specific ProxyFromEnvironment
- // func to use if http.ProxyFromEnvironment doesn't return a proxy.
- // For example, WPAD PAC files on Windows.
- var sysProxyFromEnv func(*http.Request) (*url.URL, error)
- // These variables track whether we've printed a log message for a given proxy
- // URL; we only print them once to avoid log spam.
- var (
- logMessageMu sync.Mutex
- logMessagePrinted map[string]bool
- )
- // ProxyFromEnvironment is like the standard library's http.ProxyFromEnvironment
- // but additionally does OS-specific proxy lookups if the environment variables
- // alone don't specify a proxy.
- func ProxyFromEnvironment(req *http.Request) (ret *url.URL, _ error) {
- defer func() {
- if ret == nil {
- return
- }
- ss := ret.String()
- logMessageMu.Lock()
- defer logMessageMu.Unlock()
- if logMessagePrinted[ss] {
- return
- }
- log.Printf("tshttpproxy: using proxy %q for URL: %q", ss, req.URL.String())
- mak.Set(&logMessagePrinted, ss, true)
- }()
- localProxyFunc := getProxyFunc()
- u, err := localProxyFunc(req.URL)
- if u != nil && err == nil {
- return u, nil
- }
- mu.Lock()
- noProxyTime := noProxyUntil
- mu.Unlock()
- if time.Now().Before(noProxyTime) {
- return nil, nil
- }
- if sysProxyFromEnv != nil {
- u, err := sysProxyFromEnv(req)
- if u != nil && err == nil {
- return u, nil
- }
- }
- return nil, err
- }
- var sysAuthHeader func(*url.URL) (string, error)
- // GetAuthHeader returns the Authorization header value to send to proxy u.
- func GetAuthHeader(u *url.URL) (string, error) {
- if fake := os.Getenv("TS_DEBUG_FAKE_PROXY_AUTH"); fake != "" {
- return fake, nil
- }
- if user := u.User.Username(); user != "" {
- pass, ok := u.User.Password()
- if !ok {
- return "", nil
- }
- req := &http.Request{Header: make(http.Header)}
- req.SetBasicAuth(user, pass)
- return req.Header.Get("Authorization"), nil
- }
- if sysAuthHeader != nil {
- return sysAuthHeader(u)
- }
- return "", nil
- }
- const proxyAuthHeader = "Proxy-Authorization"
- // SetTransportGetProxyConnectHeader sets the provided Transport's
- // GetProxyConnectHeader field, and adds logging of the received response.
- func SetTransportGetProxyConnectHeader(tr *http.Transport) {
- tr.GetProxyConnectHeader = func(ctx context.Context, proxyURL *url.URL, target string) (http.Header, error) {
- v, err := GetAuthHeader(proxyURL)
- if err != nil {
- log.Printf("failed to get proxy Auth header for %v; ignoring: %v", proxyURL, err)
- return nil, nil
- }
- if v == "" {
- return nil, nil
- }
- return http.Header{proxyAuthHeader: []string{v}}, nil
- }
- tr.OnProxyConnectResponse = func(ctx context.Context, proxyURL *url.URL, connectReq *http.Request, res *http.Response) error {
- auth := connectReq.Header.Get(proxyAuthHeader)
- const truncLen = 20
- if len(auth) > truncLen {
- auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
- }
- log.Printf("tshttpproxy: CONNECT response from %v for target %q (auth %q): %v", proxyURL, connectReq.Host, auth, res.Status)
- return nil
- }
- }
|