diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3212be1e..37c99b04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -191,7 +191,7 @@ jobs: echo "::error::Invalid release tag format: $GITHUB_REF_NAME" exit 1 fi - sed -i '' 's/Version = \"dev\"/Version = \"'"$GITHUB_REF_NAME"'\"/' server/server.go + sed -i '' 's/Version = \"dev\"/Version = \"'"$GITHUB_REF_NAME"'\"/' utils/version.go - name: Build run: | @@ -250,7 +250,7 @@ jobs: echo "::error::Invalid release tag format: $GITHUB_REF_NAME" exit 1 fi - sed -i 's/Version = \"dev\"/Version = \"'"$GITHUB_REF_NAME"'\"/' server/server.go + sed -i 's/Version = \"dev\"/Version = \"'"$GITHUB_REF_NAME"'\"/' utils/version.go - name: Build run: | @@ -304,7 +304,7 @@ jobs: Write-Error "Invalid release tag format: $env:GITHUB_REF_NAME" exit 1 } - sed -i ('s/Version = \"dev\"/Version = \"' + $env:GITHUB_REF_NAME + '\"/') server/server.go + sed -i ('s/Version = \"dev\"/Version = \"' + $env:GITHUB_REF_NAME + '\"/') utils/version.go - name: Build run: | diff --git a/cli/auth.go b/cli/auth.go index be776892..5281d15b 100644 --- a/cli/auth.go +++ b/cli/auth.go @@ -2,6 +2,7 @@ package cli import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -11,6 +12,8 @@ import ( "github.com/spf13/cobra" "github.com/zalando/go-keyring" + + "github.com/mobile-next/mobilecli/utils" ) const ( @@ -73,9 +76,21 @@ var authLoginCmd = &cobra.Command{ }, } +// postJSON posts a JSON body to the auth server with mobilecli's User-Agent, so +// device-login traffic is attributable to the CLI in the server access logs. +func postJSON(url string, body []byte) (*http.Response, error) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", utils.UserAgent()) + return authHTTPClient.Do(req) +} + func requestDeviceCode() (*deviceCodeResponse, error) { reqBody, _ := json.Marshal(deviceCodeRequest{ClientID: deviceFlowClientID}) - resp, err := authHTTPClient.Post(deviceCodeURL, "application/json", bytes.NewReader(reqBody)) + resp, err := postJSON(deviceCodeURL, reqBody) if err != nil { return nil, fmt.Errorf("failed to request device code: %w", err) } @@ -117,7 +132,7 @@ func pollForToken(deviceCode string, interval, expiresIn int) (string, error) { DeviceCode: deviceCode, GrantType: deviceGrantType, }) - resp, err := authHTTPClient.Post(deviceTokenURL, "application/json", bytes.NewReader(reqBody)) + resp, err := postJSON(deviceTokenURL, reqBody) if err != nil { return "", fmt.Errorf("failed to poll for token: %w", err) } diff --git a/cli/root.go b/cli/root.go index 59d98f96..e0ddca65 100644 --- a/cli/root.go +++ b/cli/root.go @@ -6,7 +6,6 @@ import ( "log" "github.com/mobile-next/mobilecli/commands" - "github.com/mobile-next/mobilecli/server" "github.com/mobile-next/mobilecli/utils" "github.com/spf13/cobra" ) @@ -208,7 +207,7 @@ COMMON FLAGS: CompletionOptions: cobra.CompletionOptions{ HiddenDefaultCmd: true, }, - Version: server.Version, + Version: utils.Version, SilenceUsage: true, SilenceErrors: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { diff --git a/rpc/rest.go b/rpc/rest.go index 5b8c5eaa..c27bc7a8 100644 --- a/rpc/rest.go +++ b/rpc/rest.go @@ -8,6 +8,8 @@ import ( "net/http" "net/url" "time" + + "github.com/mobile-next/mobilecli/utils" ) // RESTTimeout is the deadline for a single REST call to the fleet server. @@ -59,6 +61,7 @@ func RESTCall(token, method, path string, body any, result any) error { return fmt.Errorf("failed to build request: %w", err) } req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("User-Agent", utils.UserAgent()) if body != nil { req.Header.Set("Content-Type", "application/json") } diff --git a/rpc/rpc.go b/rpc/rpc.go index 1526952e..c2849ed0 100644 --- a/rpc/rpc.go +++ b/rpc/rpc.go @@ -9,6 +9,7 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/mobile-next/mobilecli/utils" ) type Request struct { @@ -62,6 +63,7 @@ func Dial(token string) (*websocket.Conn, error) { } header := http.Header{} header.Set("Authorization", "Bearer "+token) + header.Set("User-Agent", utils.UserAgent()) conn, _, err := fleetDialer.Dial(u.String(), header) return conn, err } diff --git a/rpc/useragent_test.go b/rpc/useragent_test.go new file mode 100644 index 00000000..6f840653 --- /dev/null +++ b/rpc/useragent_test.go @@ -0,0 +1,39 @@ +package rpc + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/mobile-next/mobilecli/utils" +) + +func restCallAgainstServerCapturingUserAgent(t *testing.T) string { + t.Helper() + + var seen string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seen = r.Header.Get("User-Agent") + if _, err := w.Write([]byte(`{}`)); err != nil { + t.Errorf("failed to write response: %v", err) + } + })) + defer server.Close() + + // GetAPIBaseURL derives the REST base from the fleet websocket URL. + t.Setenv("MOBILECLI_FLEET_URL", strings.Replace(server.URL, "http://", "ws://", 1)) + + if err := RESTCall("token", http.MethodGet, "/api/v1/sessions", nil, nil); err != nil { + t.Fatalf("RESTCall failed: %v", err) + } + return seen +} + +func TestRESTCallIdentifiesItselfAsMobilecli(t *testing.T) { + got := restCallAgainstServerCapturingUserAgent(t) + want := "mobilecli/" + utils.Version + if got != want { + t.Errorf("expected User-Agent %q, got %q", want, got) + } +} diff --git a/server/server.go b/server/server.go index 194c8470..592123f9 100644 --- a/server/server.go +++ b/server/server.go @@ -50,8 +50,6 @@ const ( IdleTimeout = 120 * time.Second ) -var Version = "dev" - var okResponse = map[string]any{"status": "ok"} // StreamSession represents a screen capture streaming session @@ -1416,7 +1414,7 @@ func handleCrashesGet(params json.RawMessage) (any, error) { func handleServerInfo(params json.RawMessage) (any, error) { return map[string]string{ "name": "mobilecli", - "version": Version, + "version": utils.Version, }, nil } diff --git a/utils/version.go b/utils/version.go new file mode 100644 index 00000000..957bcb7b --- /dev/null +++ b/utils/version.go @@ -0,0 +1,12 @@ +package utils + +// Version is the mobilecli release version. The release workflow rewrites the +// literal below (see .github/workflows/build.yml); local builds stay "dev". +var Version = "dev" + +// UserAgent is the User-Agent mobilecli sends on every outbound HTTP and +// WebSocket request to the cloud. Without it Go sends "Go-http-client/2.0", +// which is indistinguishable from every other Go client in the access logs. +func UserAgent() string { + return "mobilecli/" + Version +}