From bb48d384d3dfe32e20ec0c0ac2b2072eb314a257 Mon Sep 17 00:00:00 2001 From: Jared Rickert Date: Sun, 27 Sep 2026 13:28:59 -0500 Subject: [PATCH] feat(cli): identify tap in the HTTP user agent Hubs list signed-in clients by user agent. Requests now send a tap user agent carrying the version and platform instead of Go's default. --- cmd/tap/tap.go | 2 ++ pkg/cli/user_agent.go | 31 +++++++++++++++++++++++++++++++ pkg/cli/user_agent_test.go | 31 +++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 pkg/cli/user_agent.go create mode 100644 pkg/cli/user_agent_test.go diff --git a/cmd/tap/tap.go b/cmd/tap/tap.go index dfd470a0..e80bcd26 100644 --- a/cmd/tap/tap.go +++ b/cmd/tap/tap.go @@ -19,6 +19,8 @@ var licenseText string func main() { cli.LicenseText = licenseText + // Hubs list signed-in clients by user agent; name tap rather than Go. + cli.InstallUserAgent() ctx := context.Background() // Signal handling is intentionally not registered at the entrypoint. diff --git a/pkg/cli/user_agent.go b/pkg/cli/user_agent.go new file mode 100644 index 00000000..f22c0597 --- /dev/null +++ b/pkg/cli/user_agent.go @@ -0,0 +1,31 @@ +package cli + +import ( + "net/http" + "runtime" +) + +// UserAgent identifies tap to the hubs it talks to, so a hub's list of +// signed-in clients can say which tap, and on what platform, holds a login. +func UserAgent() string { + return "tap/" + Version + " (" + runtime.GOOS + "/" + runtime.GOARCH + ")" +} + +// userAgentTransport sets UserAgent on requests that do not set their own. +type userAgentTransport struct{ base http.RoundTripper } + +func (t userAgentTransport) RoundTrip(r *http.Request) (*http.Response, error) { + if r.Header.Get("User-Agent") == "" { + r = r.Clone(r.Context()) + r.Header.Set("User-Agent", UserAgent()) + } + return t.base.RoundTrip(r) +} + +// InstallUserAgent makes every request sent through http.DefaultTransport, +// which tap's hub clients use, carry UserAgent. Call it once from main. +func InstallUserAgent() { + if _, done := http.DefaultTransport.(userAgentTransport); !done { + http.DefaultTransport = userAgentTransport{base: http.DefaultTransport} + } +} diff --git a/pkg/cli/user_agent_test.go b/pkg/cli/user_agent_test.go new file mode 100644 index 00000000..ba89dbca --- /dev/null +++ b/pkg/cli/user_agent_test.go @@ -0,0 +1,31 @@ +package cli + +import ( + "net/http" + "net/http/httptest" + "runtime" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUserAgentTransport(t *testing.T) { + t.Parallel() + var got []string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got = append(got, r.Header.Get("User-Agent")) + })) + defer srv.Close() + client := &http.Client{Transport: userAgentTransport{base: http.DefaultTransport}} + + _, err := client.Get(srv.URL) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, srv.URL, nil) + require.NoError(t, err) + req.Header.Set("User-Agent", "custom/1") + _, err = client.Do(req) + require.NoError(t, err) + + require.Equal(t, []string{"tap/" + Version + " (" + runtime.GOOS + "/" + runtime.GOARCH + ")", "custom/1"}, got, + "tap's agent is the default; a request's own agent wins") +}