Add Cloudflare 403 fallback via headless Chrome
All checks were successful
Release / build (push) Successful in 1m40s
All checks were successful
Release / build (push) Successful in 1m40s
Plain HTTP requests to claude.ai get blocked by Cloudflare JS challenges (403). The fetcher now falls back to headless Chrome using the persistent browser profile, which can solve challenges natively and reuses existing cf_clearance cookies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
76
internal/browser/fetch.go
Normal file
76
internal/browser/fetch.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
|
||||
"git.davoryn.de/calic/claude-statusline/internal/config"
|
||||
)
|
||||
|
||||
// FetchViaChrome navigates to a URL using headless Chrome with the persistent
|
||||
// browser profile (which has Cloudflare clearance cookies) and returns the
|
||||
// response body. This bypasses Cloudflare JS challenges because Chrome runs
|
||||
// real JavaScript. Falls back to non-headless if headless fails.
|
||||
func FetchViaChrome(url string) ([]byte, error) {
|
||||
profileDir := filepath.Join(config.ConfigDir(), "browser-profile")
|
||||
if err := os.MkdirAll(profileDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create browser profile dir: %w", err)
|
||||
}
|
||||
|
||||
// Remove stale lock file from unclean shutdown
|
||||
_ = os.Remove(filepath.Join(profileDir, "SingletonLock"))
|
||||
|
||||
execPath := findBrowserExec()
|
||||
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", true),
|
||||
chromedp.UserDataDir(profileDir),
|
||||
)
|
||||
if execPath != "" {
|
||||
opts = append(opts, chromedp.ExecPath(execPath))
|
||||
}
|
||||
|
||||
allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
defer allocCancel()
|
||||
|
||||
ctx, cancel := chromedp.NewContext(allocCtx)
|
||||
defer cancel()
|
||||
|
||||
// Set a total timeout for the operation
|
||||
ctx, timeoutCancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer timeoutCancel()
|
||||
|
||||
var body string
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(url),
|
||||
// Wait for the body to have content (Cloudflare challenge resolves via JS)
|
||||
chromedp.WaitReady("body"),
|
||||
// Chrome renders JSON API responses inside a <pre> tag
|
||||
chromedp.Text("pre", &body, chromedp.ByQuery, chromedp.NodeVisible),
|
||||
)
|
||||
if err != nil {
|
||||
// Fallback: try extracting from body directly (some responses may not use <pre>)
|
||||
var bodyFallback string
|
||||
errFb := chromedp.Run(ctx,
|
||||
chromedp.Text("body", &bodyFallback, chromedp.ByQuery),
|
||||
)
|
||||
if errFb == nil && bodyFallback != "" {
|
||||
return []byte(bodyFallback), nil
|
||||
}
|
||||
return nil, fmt.Errorf("chromedp fetch: %w", err)
|
||||
}
|
||||
|
||||
if body == "" {
|
||||
return nil, fmt.Errorf("chromedp fetch: empty response body")
|
||||
}
|
||||
|
||||
// Gracefully close to flush cookies (including refreshed cf_clearance)
|
||||
cancel()
|
||||
|
||||
return []byte(body), nil
|
||||
}
|
||||
Reference in New Issue
Block a user