package credhelper import ( "fmt" "net/http" "runtime" "sync" "time" ) // userAgent returns the User-Agent string for outgoing HTTP requests. // // Format: / (/; commit ) // // Format follows the convention Docker's own clients use, so it parses // cleanly with the same regexes server-side log analyzers already // understand. The commit suffix lets users on the device-approval page // distinguish two devices on the same version line if they ever need to. func userAgent() string { short := cfg.Commit if len(short) > 7 { short = short[:7] } return fmt.Sprintf("%s/%s (%s/%s; commit %s)", cfg.BinaryName, cfg.Version, runtime.GOOS, runtime.GOARCH, short) } // uaTransport wraps another RoundTripper and sets the User-Agent header // on every request that doesn't already carry one. Used as the default // transport for the helper's shared http.Client so we can't forget to // set the UA on a future call site. type uaTransport struct { base http.RoundTripper } func (t *uaTransport) RoundTrip(req *http.Request) (*http.Response, error) { if req.Header.Get("User-Agent") == "" { // Clone before mutating: net/http may retry a request and the // caller could be using the same *Request elsewhere. clone := req.Clone(req.Context()) clone.Header.Set("User-Agent", userAgent()) req = clone } base := t.base if base == nil { base = http.DefaultTransport } return base.RoundTrip(req) } var sharedHTTPClient = sync.OnceValue(func() *http.Client { return &http.Client{ Transport: &uaTransport{base: http.DefaultTransport}, } }) // httpClient returns the shared UA-tagged http.Client used for all of // the helper's outgoing HTTP requests. It carries no per-request // timeout — call sites that want one should use httpClientWithTimeout. func httpClient() *http.Client { return sharedHTTPClient() } // httpClientWithTimeout returns a fresh client that shares the shared // transport (so connection pooling and the UA header are preserved) but // scopes a per-client timeout. CheckRedirect can be supplied for cases // like fetchLatestVersion that need to inspect a redirect rather than // follow it. func httpClientWithTimeout(timeout time.Duration, checkRedirect func(*http.Request, []*http.Request) error) *http.Client { return &http.Client{ Transport: sharedHTTPClient().Transport, Timeout: timeout, CheckRedirect: checkRedirect, } }