refactor(webkit): remove Turnstile CORS workaround
- commit
- 1a2e598bb3dd764b2cf0e6a21939b5116b695bde
- author
- bnema · · 2025-11-29 11:38 UTC
- parent
- ea3a36e
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
3 files · +6 -108
M internal/app/browser/webview.go +3 -4
| @@ -53,10 +53,9 @@ func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) { | |||
| 53 | 53 | EnablePageCache: true, // Instant back/forward navigation (bfcache) | |
| 54 | 54 | EnableSmoothScrolling: true, // Smooth scrolling animations | |
| 55 | 55 | DataDir: webkitData, | |
| 56 | - | CacheDir: webkitCache, | |
| 57 | - | AppearanceConfigJSON: app.buildAppearanceConfigJSON(), | |
| 58 | - | CreateWindow: true, // Default to creating a window for standalone WebViews | |
| 59 | - | EnableTurnstileWorkaround: true, | |
| 56 | + | CacheDir: webkitCache, | |
| 57 | + | AppearanceConfigJSON: app.buildAppearanceConfigJSON(), | |
| 58 | + | CreateWindow: true, // Default to creating a window for standalone WebViews | |
| 60 | 59 | } | |
| 61 | 60 | ||
| 62 | 61 | return cfg, nil | |
| @@ -53,10 +53,9 @@ func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) { | |||||
| 53 | EnablePageCache: true, // Instant back/forward navigation (bfcache) | 53 | EnablePageCache: true, // Instant back/forward navigation (bfcache) | ||
| 54 | EnableSmoothScrolling: true, // Smooth scrolling animations | 54 | EnableSmoothScrolling: true, // Smooth scrolling animations | ||
| 55 | DataDir: webkitData, | 55 | DataDir: webkitData, | ||
| 56 | - | CacheDir: webkitCache, | 56 | + | CacheDir: webkitCache, |
| 57 | - | AppearanceConfigJSON: app.buildAppearanceConfigJSON(), | 57 | + | AppearanceConfigJSON: app.buildAppearanceConfigJSON(), |
| 58 | - | CreateWindow: true, // Default to creating a window for standalone WebViews | 58 | + | CreateWindow: true, // Default to creating a window for standalone WebViews |
| 59 | - | EnableTurnstileWorkaround: true, | |||
| 60 | } | 59 | } | ||
| 61 | 60 | ||||
| 62 | return cfg, nil | 61 | return cfg, nil | ||
M pkg/webkit/types.go +3 -9
| @@ -65,11 +65,6 @@ type Config struct { | |||
| 65 | 65 | // CreateWindow controls whether to create a standalone GTK Window for this WebView | |
| 66 | 66 | // Set to false for WebViews that will be embedded in workspace panes | |
| 67 | 67 | CreateWindow bool | |
| 68 | - | ||
| 69 | - | // EnableTurnstileWorkaround temporarily disables CORS enforcement for the | |
| 70 | - | // Cloudflare Turnstile host to work around WebKit's credentialless COEP gap. | |
| 71 | - | // Remove once upstream WebKit properly supports COEP: credentialless. | |
| 72 | - | EnableTurnstileWorkaround bool | |
| 73 | 68 | } | |
| 74 | 69 | ||
| 75 | 70 | // GetDefaultConfig returns a Config with sensible defaults | |
| @@ -82,9 +77,8 @@ func GetDefaultConfig() *Config { | |||
| 82 | 77 | HardwareAcceleration: true, | |
| 83 | 78 | DefaultFontSize: 16, | |
| 84 | 79 | MinimumFontSize: 8, | |
| 85 | - | EnablePageCache: true, // Instant back/forward navigation | |
| 86 | - | EnableSmoothScrolling: true, // Better UX | |
| 87 | - | CreateWindow: true, // Default to standalone window | |
| 88 | - | EnableTurnstileWorkaround: true, | |
| 80 | + | EnablePageCache: true, // Instant back/forward navigation | |
| 81 | + | EnableSmoothScrolling: true, // Better UX | |
| 82 | + | CreateWindow: true, // Default to standalone window | |
| 89 | 83 | } | |
| 90 | 84 | } | |
| @@ -65,11 +65,6 @@ type Config struct { | |||||
| 65 | // CreateWindow controls whether to create a standalone GTK Window for this WebView | 65 | // CreateWindow controls whether to create a standalone GTK Window for this WebView | ||
| 66 | // Set to false for WebViews that will be embedded in workspace panes | 66 | // Set to false for WebViews that will be embedded in workspace panes | ||
| 67 | CreateWindow bool | 67 | CreateWindow bool | ||
| 68 | - | ||||
| 69 | - | // EnableTurnstileWorkaround temporarily disables CORS enforcement for the | |||
| 70 | - | // Cloudflare Turnstile host to work around WebKit's credentialless COEP gap. | |||
| 71 | - | // Remove once upstream WebKit properly supports COEP: credentialless. | |||
| 72 | - | EnableTurnstileWorkaround bool | |||
| 73 | } | 68 | } | ||
| 74 | 69 | ||||
| 75 | // GetDefaultConfig returns a Config with sensible defaults | 70 | // GetDefaultConfig returns a Config with sensible defaults | ||
| @@ -82,9 +77,8 @@ func GetDefaultConfig() *Config { | |||||
| 82 | HardwareAcceleration: true, | 77 | HardwareAcceleration: true, | ||
| 83 | DefaultFontSize: 16, | 78 | DefaultFontSize: 16, | ||
| 84 | MinimumFontSize: 8, | 79 | MinimumFontSize: 8, | ||
| 85 | - | EnablePageCache: true, // Instant back/forward navigation | 80 | + | EnablePageCache: true, // Instant back/forward navigation |
| 86 | - | EnableSmoothScrolling: true, // Better UX | 81 | + | EnableSmoothScrolling: true, // Better UX |
| 87 | - | CreateWindow: true, // Default to standalone window | 82 | + | CreateWindow: true, // Default to standalone window |
| 88 | - | EnableTurnstileWorkaround: true, | |||
| 89 | } | 83 | } | ||
| 90 | } | 84 | } | ||
M pkg/webkit/webview.go +0 -95
| @@ -4,9 +4,7 @@ import ( | |||
| 4 | 4 | "encoding/json" | |
| 5 | 5 | "fmt" | |
| 6 | 6 | "log" | |
| 7 | - | "net/url" | |
| 8 | 7 | "strconv" | |
| 9 | - | "strings" | |
| 10 | 8 | "sync" | |
| 11 | 9 | "sync/atomic" | |
| 12 | 10 | ||
| @@ -15,8 +13,6 @@ import ( | |||
| 15 | 13 | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 16 | 14 | ) | |
| 17 | 15 | ||
| 18 | - | const turnstileHost = "challenges.cloudflare.com" | |
| 19 | - | ||
| 20 | 16 | var ( | |
| 21 | 17 | viewIDCounter uint64 | |
| 22 | 18 | viewRegistry = make(map[uint64]*WebView) | |
| @@ -328,12 +324,6 @@ func (w *WebView) setupEventHandlers() { | |||
| 328 | 324 | ||
| 329 | 325 | // Setup navigation policy handler for middle-click and Ctrl+click interception | |
| 330 | 326 | w.setupNavigationPolicyHandler() | |
| 331 | - | ||
| 332 | - | // Log CORP/CORP headers for Cloudflare Turnstile resources to help diagnose COEP issues | |
| 333 | - | w.attachTurnstileResponseLogger() | |
| 334 | - | ||
| 335 | - | // Apply temporary Turnstile workaround until WebKit supports COEP: credentialless | |
| 336 | - | w.applyTurnstileCorsAllowlist() | |
| 337 | 327 | } | |
| 338 | 328 | ||
| 339 | 329 | // setupNavigationPolicyHandler sets up the decide-policy signal handler | |
| @@ -412,91 +402,6 @@ func (w *WebView) setupNavigationPolicyHandler() { | |||
| 412 | 402 | log.Printf("[webkit] Navigation policy handler attached to WebView ID %d", w.id) | |
| 413 | 403 | } | |
| 414 | 404 | ||
| 415 | - | func (w *WebView) applyTurnstileCorsAllowlist() { | |
| 416 | - | if w.view == nil || w.config == nil || !w.config.EnableTurnstileWorkaround { | |
| 417 | - | return | |
| 418 | - | } | |
| 419 | - | ||
| 420 | - | pattern := fmt.Sprintf("https://%s/*", turnstileHost) | |
| 421 | - | w.view.SetCorsAllowlist([]string{pattern}) | |
| 422 | - | log.Printf("[turnstile] Applied CORS allowlist for %s on WebView ID %d", turnstileHost, w.id) | |
| 423 | - | } | |
| 424 | - | ||
| 425 | - | func (w *WebView) attachTurnstileResponseLogger() { | |
| 426 | - | if w.view == nil { | |
| 427 | - | return | |
| 428 | - | } | |
| 429 | - | ||
| 430 | - | w.view.ConnectResourceLoadStarted(func(resource *webkit.WebResource, request *webkit.URIRequest) { | |
| 431 | - | if resource == nil || request == nil { | |
| 432 | - | return | |
| 433 | - | } | |
| 434 | - | ||
| 435 | - | targetURL := request.URI() | |
| 436 | - | if targetURL == "" { | |
| 437 | - | targetURL = resource.URI() | |
| 438 | - | } | |
| 439 | - | if !isTurnstileURL(targetURL) { | |
| 440 | - | return | |
| 441 | - | } | |
| 442 | - | ||
| 443 | - | method := request.HTTPMethod() | |
| 444 | - | if method == "" { | |
| 445 | - | method = "GET" | |
| 446 | - | } | |
| 447 | - | ||
| 448 | - | referer := "<none>" | |
| 449 | - | if reqHeaders := request.HTTPHeaders(); reqHeaders != nil { | |
| 450 | - | if ref := strings.TrimSpace(reqHeaders.One("Referer")); ref != "" { | |
| 451 | - | referer = ref | |
| 452 | - | } | |
| 453 | - | } | |
| 454 | - | ||
| 455 | - | resource.Connect("notify::response", func() { | |
| 456 | - | response := resource.Response() | |
| 457 | - | if response == nil { | |
| 458 | - | log.Printf("[turnstile] resource=%s method=%s status=<unknown> referer=%s corp=<missing> coep=<missing> coop=<missing>", targetURL, method, referer) | |
| 459 | - | return | |
| 460 | - | } | |
| 461 | - | ||
| 462 | - | headers := response.HTTPHeaders() | |
| 463 | - | corp := headerValue(headers, "Cross-Origin-Resource-Policy") | |
| 464 | - | coep := headerValue(headers, "Cross-Origin-Embedder-Policy") | |
| 465 | - | coop := headerValue(headers, "Cross-Origin-Opener-Policy") | |
| 466 | - | ||
| 467 | - | log.Printf("[turnstile] resource=%s method=%s status=%d referer=%s corp=%s coep=%s coop=%s", | |
| 468 | - | targetURL, method, response.StatusCode(), referer, corp, coep, coop) | |
| 469 | - | }) | |
| 470 | - | }) | |
| 471 | - | } | |
| 472 | - | ||
| 473 | - | type headerLookup interface { | |
| 474 | - | One(string) string | |
| 475 | - | } | |
| 476 | - | ||
| 477 | - | func headerValue(headers headerLookup, name string) string { | |
| 478 | - | if headers == nil { | |
| 479 | - | return "<missing>" | |
| 480 | - | } | |
| 481 | - | value := strings.TrimSpace(headers.One(name)) | |
| 482 | - | if value == "" { | |
| 483 | - | return "<missing>" | |
| 484 | - | } | |
| 485 | - | return value | |
| 486 | - | } | |
| 487 | - | ||
| 488 | - | func isTurnstileURL(raw string) bool { | |
| 489 | - | if raw == "" { | |
| 490 | - | return false | |
| 491 | - | } | |
| 492 | - | parsed, err := url.Parse(raw) | |
| 493 | - | if err != nil { | |
| 494 | - | return false | |
| 495 | - | } | |
| 496 | - | host := strings.ToLower(parsed.Hostname()) | |
| 497 | - | return host == turnstileHost | |
| 498 | - | } | |
| 499 | - | ||
| 500 | 405 | // LoadURL loads the given URL in the WebView | |
| 501 | 406 | func (w *WebView) LoadURL(url string) error { | |
| 502 | 407 | w.mu.RLock() | |
| @@ -4,9 +4,7 @@ import ( | |||||
| 4 | "encoding/json" | 4 | "encoding/json" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "log" | 6 | "log" | ||
| 7 | - | "net/url" | |||
| 8 | "strconv" | 7 | "strconv" | ||
| 9 | - | "strings" | |||
| 10 | "sync" | 8 | "sync" | ||
| 11 | "sync/atomic" | 9 | "sync/atomic" | ||
| 12 | 10 | ||||
| @@ -15,8 +13,6 @@ import ( | |||||
| 15 | "github.com/diamondburned/gotk4/pkg/gtk/v4" | 13 | "github.com/diamondburned/gotk4/pkg/gtk/v4" | ||
| 16 | ) | 14 | ) | ||
| 17 | 15 | ||||
| 18 | - | const turnstileHost = "challenges.cloudflare.com" | |||
| 19 | - | ||||
| 20 | var ( | 16 | var ( | ||
| 21 | viewIDCounter uint64 | 17 | viewIDCounter uint64 | ||
| 22 | viewRegistry = make(map[uint64]*WebView) | 18 | viewRegistry = make(map[uint64]*WebView) | ||
| @@ -328,12 +324,6 @@ func (w *WebView) setupEventHandlers() { | |||||
| 328 | 324 | ||||
| 329 | // Setup navigation policy handler for middle-click and Ctrl+click interception | 325 | // Setup navigation policy handler for middle-click and Ctrl+click interception | ||
| 330 | w.setupNavigationPolicyHandler() | 326 | w.setupNavigationPolicyHandler() | ||
| 331 | - | ||||
| 332 | - | // Log CORP/CORP headers for Cloudflare Turnstile resources to help diagnose COEP issues | |||
| 333 | - | w.attachTurnstileResponseLogger() | |||
| 334 | - | ||||
| 335 | - | // Apply temporary Turnstile workaround until WebKit supports COEP: credentialless | |||
| 336 | - | w.applyTurnstileCorsAllowlist() | |||
| 337 | } | 327 | } | ||
| 338 | 328 | ||||
| 339 | // setupNavigationPolicyHandler sets up the decide-policy signal handler | 329 | // setupNavigationPolicyHandler sets up the decide-policy signal handler | ||
| @@ -412,91 +402,6 @@ func (w *WebView) setupNavigationPolicyHandler() { | |||||
| 412 | log.Printf("[webkit] Navigation policy handler attached to WebView ID %d", w.id) | 402 | log.Printf("[webkit] Navigation policy handler attached to WebView ID %d", w.id) | ||
| 413 | } | 403 | } | ||
| 414 | 404 | ||||
| 415 | - | func (w *WebView) applyTurnstileCorsAllowlist() { | |||
| 416 | - | if w.view == nil || w.config == nil || !w.config.EnableTurnstileWorkaround { | |||
| 417 | - | return | |||
| 418 | - | } | |||
| 419 | - | ||||
| 420 | - | pattern := fmt.Sprintf("https://%s/*", turnstileHost) | |||
| 421 | - | w.view.SetCorsAllowlist([]string{pattern}) | |||
| 422 | - | log.Printf("[turnstile] Applied CORS allowlist for %s on WebView ID %d", turnstileHost, w.id) | |||
| 423 | - | } | |||
| 424 | - | ||||
| 425 | - | func (w *WebView) attachTurnstileResponseLogger() { | |||
| 426 | - | if w.view == nil { | |||
| 427 | - | return | |||
| 428 | - | } | |||
| 429 | - | ||||
| 430 | - | w.view.ConnectResourceLoadStarted(func(resource *webkit.WebResource, request *webkit.URIRequest) { | |||
| 431 | - | if resource == nil || request == nil { | |||
| 432 | - | return | |||
| 433 | - | } | |||
| 434 | - | ||||
| 435 | - | targetURL := request.URI() | |||
| 436 | - | if targetURL == "" { | |||
| 437 | - | targetURL = resource.URI() | |||
| 438 | - | } | |||
| 439 | - | if !isTurnstileURL(targetURL) { | |||
| 440 | - | return | |||
| 441 | - | } | |||
| 442 | - | ||||
| 443 | - | method := request.HTTPMethod() | |||
| 444 | - | if method == "" { | |||
| 445 | - | method = "GET" | |||
| 446 | - | } | |||
| 447 | - | ||||
| 448 | - | referer := "<none>" | |||
| 449 | - | if reqHeaders := request.HTTPHeaders(); reqHeaders != nil { | |||
| 450 | - | if ref := strings.TrimSpace(reqHeaders.One("Referer")); ref != "" { | |||
| 451 | - | referer = ref | |||
| 452 | - | } | |||
| 453 | - | } | |||
| 454 | - | ||||
| 455 | - | resource.Connect("notify::response", func() { | |||
| 456 | - | response := resource.Response() | |||
| 457 | - | if response == nil { | |||
| 458 | - | log.Printf("[turnstile] resource=%s method=%s status=<unknown> referer=%s corp=<missing> coep=<missing> coop=<missing>", targetURL, method, referer) | |||
| 459 | - | return | |||
| 460 | - | } | |||
| 461 | - | ||||
| 462 | - | headers := response.HTTPHeaders() | |||
| 463 | - | corp := headerValue(headers, "Cross-Origin-Resource-Policy") | |||
| 464 | - | coep := headerValue(headers, "Cross-Origin-Embedder-Policy") | |||
| 465 | - | coop := headerValue(headers, "Cross-Origin-Opener-Policy") | |||
| 466 | - | ||||
| 467 | - | log.Printf("[turnstile] resource=%s method=%s status=%d referer=%s corp=%s coep=%s coop=%s", | |||
| 468 | - | targetURL, method, response.StatusCode(), referer, corp, coep, coop) | |||
| 469 | - | }) | |||
| 470 | - | }) | |||
| 471 | - | } | |||
| 472 | - | ||||
| 473 | - | type headerLookup interface { | |||
| 474 | - | One(string) string | |||
| 475 | - | } | |||
| 476 | - | ||||
| 477 | - | func headerValue(headers headerLookup, name string) string { | |||
| 478 | - | if headers == nil { | |||
| 479 | - | return "<missing>" | |||
| 480 | - | } | |||
| 481 | - | value := strings.TrimSpace(headers.One(name)) | |||
| 482 | - | if value == "" { | |||
| 483 | - | return "<missing>" | |||
| 484 | - | } | |||
| 485 | - | return value | |||
| 486 | - | } | |||
| 487 | - | ||||
| 488 | - | func isTurnstileURL(raw string) bool { | |||
| 489 | - | if raw == "" { | |||
| 490 | - | return false | |||
| 491 | - | } | |||
| 492 | - | parsed, err := url.Parse(raw) | |||
| 493 | - | if err != nil { | |||
| 494 | - | return false | |||
| 495 | - | } | |||
| 496 | - | host := strings.ToLower(parsed.Hostname()) | |||
| 497 | - | return host == turnstileHost | |||
| 498 | - | } | |||
| 499 | - | ||||
| 500 | // LoadURL loads the given URL in the WebView | 405 | // LoadURL loads the given URL in the WebView | ||
| 501 | func (w *WebView) LoadURL(url string) error { | 406 | func (w *WebView) LoadURL(url string) error { | ||
| 502 | w.mu.RLock() | 407 | w.mu.RLock() | ||