fix(leonardo): get-session 改直连并先做 cross-origin-cookie 预热,429 才退回代理
This commit is contained in:
@@ -33,9 +33,11 @@ const (
|
|||||||
// sec-ch-ua must agree with userAgent's major version — a mismatch is itself a
|
// sec-ch-ua must agree with userAgent's major version — a mismatch is itself a
|
||||||
// bot signal.
|
// bot signal.
|
||||||
secChUA = `"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"`
|
secChUA = `"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"`
|
||||||
// get-session is fronted by Vercel's checkpoint, which 429s most machine
|
// get-session only answers with a token once better-auth's cookie cache has
|
||||||
// requests; a proxied retry (new exit IP per attempt) succeeds roughly one time
|
// been refreshed by a cross-origin-cookie call; called cold it returns 200
|
||||||
// in five, so several spaced attempts are needed for one refresh to land.
|
// null, which looks exactly like a dead cookie. It also sits behind Vercel's
|
||||||
|
// checkpoint, which 429s machine requests from some exit IPs — so a failed
|
||||||
|
// attempt is retried, falling back to the proxy for a different exit IP.
|
||||||
getSessionAttempts = 10
|
getSessionAttempts = 10
|
||||||
getSessionRetryDelay = 2 * time.Second
|
getSessionRetryDelay = 2 * time.Second
|
||||||
)
|
)
|
||||||
@@ -194,14 +196,11 @@ func (c *Client) GetSession(ctx context.Context, cookie string) (*Session, error
|
|||||||
}
|
}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
// Vercel's checkpoint 429s the vast majority of get-session calls, whatever the
|
|
||||||
// TLS fingerprint — but a proxied retry does get through every few attempts
|
|
||||||
// (each new client takes a new exit IP), so retry instead of giving up on the
|
|
||||||
// first 429.
|
|
||||||
var body []byte
|
var body []byte
|
||||||
var status int
|
var status int
|
||||||
var setCookies []string
|
var setCookies []string
|
||||||
var err error
|
var err error
|
||||||
|
var warmed bool
|
||||||
for attempt := 0; attempt < getSessionAttempts; attempt++ {
|
for attempt := 0; attempt < getSessionAttempts; attempt++ {
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
select {
|
select {
|
||||||
@@ -210,8 +209,26 @@ func (c *Client) GetSession(ctx context.Context, cookie string) (*Session, error
|
|||||||
case <-time.After(getSessionRetryDelay):
|
case <-time.After(getSessionRetryDelay):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
status, body, setCookies, err = c.fetchSession(ctx, send, c.proxy != "")
|
// The local IP normally passes the checkpoint; only once it doesn't is the
|
||||||
if err == nil && status != 429 && status != 403 {
|
// proxy worth its latency (and its own share of 429s).
|
||||||
|
useProxy := attempt > 0 && c.proxy != ""
|
||||||
|
client, cerr := c.newTLSClientP(useProxy)
|
||||||
|
if cerr != nil {
|
||||||
|
err = cerr
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Warm up on the same connection: cross-origin-cookie hands back a fresh
|
||||||
|
// better-auth cookie cache (and CF_Access_Token). Without it get-session
|
||||||
|
// answers 200 null even for a perfectly healthy cookie.
|
||||||
|
warmed = false
|
||||||
|
if warmCookies, werr := c.warmSession(ctx, client, send); werr == nil {
|
||||||
|
warmed = true
|
||||||
|
if merged := mergeCookies(send, warmCookies); merged != send && keepsSession(merged) {
|
||||||
|
send = merged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status, body, setCookies, err = c.fetchSession(ctx, client, send)
|
||||||
|
if err == nil && warmed && status != 429 && status != 403 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,12 +240,14 @@ func (c *Client) GetSession(ctx context.Context, cookie string) (*Session, error
|
|||||||
// that would strip the session_data cache — after which get-session answers
|
// that would strip the session_data cache — after which get-session answers
|
||||||
// 200 null and a perfectly healthy account looks dead.
|
// 200 null and a perfectly healthy account looks dead.
|
||||||
if status == 200 {
|
if status == 200 {
|
||||||
if merged := mergeCookies(send, setCookies); merged != send && keepsSession(merged) {
|
if merged := mergeCookies(send, setCookies); keepsSession(merged) {
|
||||||
c.mu.Lock()
|
|
||||||
c.rotated[cookie] = merged
|
|
||||||
c.mu.Unlock()
|
|
||||||
send = merged
|
send = merged
|
||||||
}
|
}
|
||||||
|
if send != cookie {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.rotated[cookie] = send
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if status == 401 {
|
if status == 401 {
|
||||||
return nil, fmt.Errorf("%w: get-session http 401: %s", ErrAuth, clip(body, 160))
|
return nil, fmt.Errorf("%w: get-session http 401: %s", ErrAuth, clip(body, 160))
|
||||||
@@ -256,6 +275,11 @@ func (c *Client) GetSession(ctx context.Context, cookie string) (*Session, error
|
|||||||
return nil, fmt.Errorf("%w: get-session non-json", ErrTemporaryUpstream)
|
return nil, fmt.Errorf("%w: get-session non-json", ErrTemporaryUpstream)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(raw.Session.AccessToken) == "" {
|
if strings.TrimSpace(raw.Session.AccessToken) == "" {
|
||||||
|
if !warmed {
|
||||||
|
// A cold get-session (the cookie cache was never refreshed) answers null
|
||||||
|
// for healthy accounts too — temporary, never a reason to kill the account.
|
||||||
|
return nil, fmt.Errorf("%w: get-session 200 without accessToken (no warmup): %s", ErrTemporaryUpstream, clip(body, 160))
|
||||||
|
}
|
||||||
// No bearer despite 200 → the cookie no longer authenticates. Carry the body
|
// No bearer despite 200 → the cookie no longer authenticates. Carry the body
|
||||||
// so the log says WHICH shape it was (null session vs a session without a
|
// so the log says WHICH shape it was (null session vs a session without a
|
||||||
// token) instead of a bare "auth failed".
|
// token) instead of a bare "auth failed".
|
||||||
@@ -285,23 +309,52 @@ func (c *Client) GetSession(ctx context.Context, cookie string) (*Session, error
|
|||||||
return sess, nil
|
return sess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// warmSession calls cross-origin-cookie, whose response refreshes better-auth's
|
||||||
|
// cookie cache and CF_Access_Token. It returns the Set-Cookie headers; a
|
||||||
|
// checkpoint answer (403/429) is an error, since get-session would then be cold.
|
||||||
|
func (c *Client) warmSession(ctx context.Context, client tlsclient.HttpClient, cookie string) ([]string, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, appBase+"/api/auth/cross-origin-cookie", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
req.Header = sessionHeader(cookie)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
|
if resp.StatusCode == 403 || resp.StatusCode == 429 {
|
||||||
|
return nil, fmt.Errorf("cross-origin-cookie http %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return resp.Header["Set-Cookie"], nil
|
||||||
|
}
|
||||||
|
|
||||||
// fetchSession performs one get-session call and returns its status, body and
|
// fetchSession performs one get-session call and returns its status, body and
|
||||||
// Set-Cookie headers.
|
// Set-Cookie headers.
|
||||||
func (c *Client) fetchSession(ctx context.Context, cookie string, useProxy bool) (int, []byte, []string, error) {
|
func (c *Client) fetchSession(ctx context.Context, client tlsclient.HttpClient, cookie string) (int, []byte, []string, error) {
|
||||||
client, err := c.newTLSClientP(useProxy)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, nil, err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(http.MethodGet, appBase+"/api/auth/get-session", nil)
|
req, err := http.NewRequest(http.MethodGet, appBase+"/api/auth/get-session", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, nil, err
|
return 0, nil, nil, err
|
||||||
}
|
}
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
// Header set/order copied from a real browser's get-session call (HAR): a
|
req.Header = sessionHeader(cookie)
|
||||||
// same-origin GET carries NO origin header and DOES carry the ua client hints
|
resp, err := client.Do(req)
|
||||||
// + priority — sending origin while omitting the hints is exactly the shape
|
if err != nil {
|
||||||
// Vercel's checkpoint 429s.
|
return 0, nil, nil, err
|
||||||
req.Header = http.Header{
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return resp.StatusCode, body, resp.Header["Set-Cookie"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sessionHeader is the auth endpoints' request shape, copied from a real
|
||||||
|
// browser's call (HAR): a same-origin GET carries NO origin header and DOES
|
||||||
|
// carry the ua client hints + priority — sending origin while omitting the
|
||||||
|
// hints is exactly the shape Vercel's checkpoint 429s.
|
||||||
|
func sessionHeader(cookie string) http.Header {
|
||||||
|
return http.Header{
|
||||||
"accept": {"*/*"},
|
"accept": {"*/*"},
|
||||||
"accept-language": {"en-US,en;q=0.9"},
|
"accept-language": {"en-US,en;q=0.9"},
|
||||||
"cache-control": {"no-cache"},
|
"cache-control": {"no-cache"},
|
||||||
@@ -323,13 +376,6 @@ func (c *Client) fetchSession(ctx context.Context, cookie string, useProxy bool)
|
|||||||
"sec-fetch-site", "user-agent",
|
"sec-fetch-site", "user-agent",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
return resp.StatusCode, body, resp.Header["Set-Cookie"], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// session returns the cookie's access token, optionally forcing a fresh mint
|
// session returns the cookie's access token, optionally forcing a fresh mint
|
||||||
|
|||||||
Reference in New Issue
Block a user