From 98e7dd3e454fa21d202621346fa29a36c50682a4 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 30 May 2026 15:22:13 -0400 Subject: [PATCH] fix(hoverclient): send browser-consistent headers on signin (anti-bot) A bare UA + Referer reads as a bot to Hover's signin protection. Add the client-hint + fetch-metadata headers a real Chrome XHR sends (Sec-Ch-Ua, Sec-Fetch-Site/Mode/Dest, Accept-Language) consistent with the macOS Chrome UA, and bump the UA to Chrome 131. Regression test asserts the key headers. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/hoverclient/client.go | 16 ++++++++++++++-- pkg/hoverclient/client_test.go | 11 +++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/pkg/hoverclient/client.go b/pkg/hoverclient/client.go index e9a07c9..a83ab91 100644 --- a/pkg/hoverclient/client.go +++ b/pkg/hoverclient/client.go @@ -18,7 +18,7 @@ import ( const ( hoverHost = "https://www.hover.com" - defaultUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" + defaultUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" sessionStaleAfter = 1 * time.Hour ) @@ -198,12 +198,24 @@ func (c *Client) postLoginJSON(ctx context.Context, urlStr string, payload map[s if err != nil { return signinResponse{}, err } - req.Header.Set("Accept", "application/json") + // Browser-consistent headers. A bare UA + Referer reads as a bot to + // Hover's signin protection; match what Chrome actually sends for a + // same-origin XHR — client hints (sec-ch-ua) + fetch metadata + // (Sec-Fetch-*) + Accept-Language — kept consistent with the macOS + // Chrome UA in defaultUserAgent. + req.Header.Set("Accept", "application/json, text/plain, */*") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") req.Header.Set("Content-Type", "application/json;charset=UTF-8") req.Header.Set("Origin", hoverHost) req.Header.Set("Referer", hoverHost+"/signin") req.Header.Set("User-Agent", c.UserAgent) req.Header.Set("X-Requested-With", "XMLHttpRequest") + req.Header.Set("Sec-Fetch-Site", "same-origin") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Dest", "empty") + req.Header.Set("Sec-Ch-Ua", `"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"`) + req.Header.Set("Sec-Ch-Ua-Mobile", "?0") + req.Header.Set("Sec-Ch-Ua-Platform", `"macOS"`) resp, err := c.http.Do(req) if err != nil { return signinResponse{}, err diff --git a/pkg/hoverclient/client_test.go b/pkg/hoverclient/client_test.go index 9df89fc..77db5d2 100644 --- a/pkg/hoverclient/client_test.go +++ b/pkg/hoverclient/client_test.go @@ -166,6 +166,17 @@ func TestClient_Login_UsesBrowserSigninShape(t *testing.T) { if got := headers.Get("User-Agent"); !strings.Contains(got, "Mozilla/5.0") { t.Errorf("User-Agent = %q, want browser-like UA", got) } + // Anti-bot fingerprint headers a real Chrome XHR sends. Hover's signin + // rejects bare (botty) requests; do not drop these. + if got := headers.Get("Sec-Fetch-Mode"); got != "cors" { + t.Errorf("Sec-Fetch-Mode = %q, want cors", got) + } + if got := headers.Get("Sec-Ch-Ua"); !strings.Contains(got, "Chrome") { + t.Errorf("Sec-Ch-Ua = %q, want a Chrome client-hint", got) + } + if got := headers.Get("Accept-Language"); got == "" { + t.Error("Accept-Language must be set (browser-consistent)") + } } func TestNewClient_NormalizesPastedSecretNewlines(t *testing.T) {