Skip to content

Commit 73d609a

Browse files
intel352claude
andauthored
fix(hoverclient): send browser-consistent headers on signin (anti-bot) (#29)
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) <noreply@anthropic.com>
1 parent c3a0115 commit 73d609a

2 files changed

Lines changed: 25 additions & 2 deletions

File tree

pkg/hoverclient/client.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818

1919
const (
2020
hoverHost = "https://www.hover.com"
21-
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"
21+
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"
2222
sessionStaleAfter = 1 * time.Hour
2323
)
2424

@@ -198,12 +198,24 @@ func (c *Client) postLoginJSON(ctx context.Context, urlStr string, payload map[s
198198
if err != nil {
199199
return signinResponse{}, err
200200
}
201-
req.Header.Set("Accept", "application/json")
201+
// Browser-consistent headers. A bare UA + Referer reads as a bot to
202+
// Hover's signin protection; match what Chrome actually sends for a
203+
// same-origin XHR — client hints (sec-ch-ua) + fetch metadata
204+
// (Sec-Fetch-*) + Accept-Language — kept consistent with the macOS
205+
// Chrome UA in defaultUserAgent.
206+
req.Header.Set("Accept", "application/json, text/plain, */*")
207+
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
202208
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
203209
req.Header.Set("Origin", hoverHost)
204210
req.Header.Set("Referer", hoverHost+"/signin")
205211
req.Header.Set("User-Agent", c.UserAgent)
206212
req.Header.Set("X-Requested-With", "XMLHttpRequest")
213+
req.Header.Set("Sec-Fetch-Site", "same-origin")
214+
req.Header.Set("Sec-Fetch-Mode", "cors")
215+
req.Header.Set("Sec-Fetch-Dest", "empty")
216+
req.Header.Set("Sec-Ch-Ua", `"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"`)
217+
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
218+
req.Header.Set("Sec-Ch-Ua-Platform", `"macOS"`)
207219
resp, err := c.http.Do(req)
208220
if err != nil {
209221
return signinResponse{}, err

pkg/hoverclient/client_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,17 @@ func TestClient_Login_UsesBrowserSigninShape(t *testing.T) {
166166
if got := headers.Get("User-Agent"); !strings.Contains(got, "Mozilla/5.0") {
167167
t.Errorf("User-Agent = %q, want browser-like UA", got)
168168
}
169+
// Anti-bot fingerprint headers a real Chrome XHR sends. Hover's signin
170+
// rejects bare (botty) requests; do not drop these.
171+
if got := headers.Get("Sec-Fetch-Mode"); got != "cors" {
172+
t.Errorf("Sec-Fetch-Mode = %q, want cors", got)
173+
}
174+
if got := headers.Get("Sec-Ch-Ua"); !strings.Contains(got, "Chrome") {
175+
t.Errorf("Sec-Ch-Ua = %q, want a Chrome client-hint", got)
176+
}
177+
if got := headers.Get("Accept-Language"); got == "" {
178+
t.Error("Accept-Language must be set (browser-consistent)")
179+
}
169180
}
170181

171182
func TestNewClient_NormalizesPastedSecretNewlines(t *testing.T) {

0 commit comments

Comments
 (0)