From 4f3bdd7d58cb2d7e3116e9913249fd8a5f0f65b4 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 8 Mar 2026 22:19:04 -0600 Subject: [PATCH] feat(uadetect): add FromRequest for full agent detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user agent identifies itself through multiple signals — the User-Agent header and query parameters (?os, ?arch). FromRequest unifies both, with explicit query params taking precedence. --- internal/uadetect/uadetect.go | 39 ++++++++++++++++++---- internal/uadetect/uadetect_test.go | 53 ++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/internal/uadetect/uadetect.go b/internal/uadetect/uadetect.go index 66176e1..92ec16c 100644 --- a/internal/uadetect/uadetect.go +++ b/internal/uadetect/uadetect.go @@ -1,15 +1,19 @@ -// Package uadetect identifies the requesting system's OS, CPU architecture, -// and libc from its User-Agent string. +// Package uadetect identifies the requesting agent's OS, CPU architecture, +// and libc so the server can select the correct release artifact. // -// Webi's bootstrap scripts send "$(uname -srm)" as the User-Agent, e.g. -// "Darwin 23.1.0 arm64" or "Linux 6.1.0 x86_64". This package parses those -// into [buildmeta.OS], [buildmeta.Arch], and [buildmeta.Libc] values so the -// server can select the correct release artifact. +// An agent identifies itself through multiple signals: +// - The User-Agent header: Webi's bootstrap scripts send "$(uname -srm)", +// e.g. "Darwin 23.1.0 arm64". Browsers, curl, and PowerShell send their +// own UA strings. +// - Query parameters: ?os=linux&arch=arm64 are an explicit declaration +// that takes precedence over the header. // -// Also handles non-uname agents like "PowerShell/7.3.0" and "MS AMD64". +// Use [FromRequest] to detect from an HTTP request (preferred). +// Use [Parse] to detect from a raw UA string. package uadetect import ( + "net/http" "strings" "github.com/webinstall/webi-installers/internal/buildmeta" @@ -22,6 +26,27 @@ type Result struct { Libc buildmeta.Libc } +// FromRequest detects the agent's platform from an HTTP request. +// Query parameters ?os and ?arch override the User-Agent header. +func FromRequest(r *http.Request) Result { + qOS := r.URL.Query().Get("os") + qArch := r.URL.Query().Get("arch") + + var ua string + switch { + case qOS != "" && qArch != "": + ua = qOS + " " + qArch + case qOS != "": + ua = qOS + case qArch != "": + ua = qArch + default: + ua = r.Header.Get("User-Agent") + } + + return Parse(ua) +} + // Parse extracts OS, arch, and libc from a User-Agent string. func Parse(ua string) Result { if ua == "-" { diff --git a/internal/uadetect/uadetect_test.go b/internal/uadetect/uadetect_test.go index a7664c2..1b6d8c1 100644 --- a/internal/uadetect/uadetect_test.go +++ b/internal/uadetect/uadetect_test.go @@ -1,6 +1,7 @@ package uadetect_test import ( + "net/http" "testing" "github.com/webinstall/webi-installers/internal/buildmeta" @@ -103,6 +104,58 @@ func TestLibc(t *testing.T) { } } +func TestFromRequest(t *testing.T) { + tests := []struct { + name string + ua string // User-Agent header + query string // raw query string + wantOS buildmeta.OS + wantAr buildmeta.Arch + }{ + { + name: "UA header only", + ua: "Darwin 23.1.0 arm64", + wantOS: buildmeta.OSDarwin, + wantAr: buildmeta.ArchARM64, + }, + { + name: "query params override UA", + ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", + query: "os=linux&arch=aarch64", + wantOS: buildmeta.OSLinux, + wantAr: buildmeta.ArchARM64, + }, + { + name: "os param only", + ua: "curl/8.1.2", + query: "os=windows", + wantOS: buildmeta.OSWindows, + }, + { + name: "arch param only", + ua: "curl/8.1.2", + query: "arch=arm64", + wantAr: buildmeta.ArchARM64, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com/api?"+tt.query, nil) + if tt.ua != "" { + req.Header.Set("User-Agent", tt.ua) + } + got := uadetect.FromRequest(req) + if tt.wantOS != "" && got.OS != tt.wantOS { + t.Errorf("OS = %q, want %q", got.OS, tt.wantOS) + } + if tt.wantAr != "" && got.Arch != tt.wantAr { + t.Errorf("Arch = %q, want %q", got.Arch, tt.wantAr) + } + }) + } +} + func TestFullParse(t *testing.T) { r := uadetect.Parse("Darwin 23.1.0 arm64") if r.OS != buildmeta.OSDarwin {