package dockerversion import ( "context" "runtime" "strings" "sync" "github.com/moby/moby/v2/pkg/parsers/kernel" "github.com/moby/moby/v2/pkg/useragent" ) // uaStringKey is used as key type for user-agent string in net/context struct type uaStringKey struct{} // WithUpstreamUserAgent returns a new context carrying the upstream client's // User-Agent string. func WithUpstreamUserAgent(ctx context.Context, ua string) context.Context { if ua == "" { return ctx } return context.WithValue(ctx, uaStringKey{}, ua) } // DockerUserAgent is the User-Agent used by the daemon. // // It consists of the daemon's User-Agent, optional version metadata, and // an optional upstream client comment: // // [daemon user agent] [extra] [UpstreamClient()] // // "UpstreamClient" is a Docker-defined convention. The upstream value is // sanitized before inclusion. See [RFC9110], section 10.1.5. // // [RFC9110]: https://www.rfc-editor.org/rfc/rfc9110#section-10.1.5 func DockerUserAgent(ctx context.Context, extraVersions ...useragent.VersionInfo) string { ua := useragent.AppendVersions(getDaemonUserAgent(), extraVersions...) if upstreamUA := getUpstreamUserAgent(ctx); upstreamUA != "" { ua += " " + upstreamUA } return ua } var ( daemonUAOnce sync.Once daemonUA string ) // getDaemonUserAgent returns the user-agent to use for requests made by // the daemon. // // It includes: // // - the docker version // - go version // - git-commit // - kernel version // - os // - architecture func getDaemonUserAgent() string { daemonUAOnce.Do(func() { httpVersion := make([]useragent.VersionInfo, 0, 6) httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: Version}) httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()}) httpVersion = append(httpVersion, useragent.VersionInfo{Name: "git-commit", Version: GitCommit}) if kernelVersion, err := kernel.GetKernelVersion(); err == nil { httpVersion = append(httpVersion, useragent.VersionInfo{Name: "kernel", Version: kernelVersion.String()}) } httpVersion = append(httpVersion, useragent.VersionInfo{Name: "os", Version: runtime.GOOS}) httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH}) daemonUA = useragent.AppendVersions("", httpVersion...) }) return daemonUA } // getUpstreamUserAgent returns the previously saved user-agent context stored // in ctx, if one exists, and formats it as: // // UpstreamClient() // // It returns an empty string if no user-agent is present in the context. func getUpstreamUserAgent(ctx context.Context) string { upstreamUA, ok := ctx.Value(uaStringKey{}).(string) if !ok || upstreamUA == "" { return "" } return "UpstreamClient(" + escapeStr(upstreamUA) + ")" } // escapeStr escapes and sanitizes s for use in a User-Agent comment ([RFC9110]). // // [RFC9110]: https://www.rfc-editor.org/rfc/rfc9110#section-10.1.5 func escapeStr(s string) string { var b strings.Builder b.Grow(len(s)) for i := range len(s) { switch c := s[i]; c { // TODO(thaJeztah): remove redundant escaping semicolons; see https://github.com/moby/moby/pull/52356#discussion_r3234266285 case '(', ')', ';', '\\': b.WriteByte('\\') b.WriteByte(c) case '\t': b.WriteByte(c) default: if c >= 0x20 && c != 0x7f { b.WriteByte(c) } } } return b.String() }