detached 1a2e598

refactor(webkit): remove Turnstile CORS workaround

commit
1a2e598bb3dd764b2cf0e6a21939b5116b695bde
author
bnema · · 2025-11-29 11:38 UTC
parent
ea3a36e
signature
GPG Unverified2B82207D370127A0 The 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) {
5353 EnablePageCache: true, // Instant back/forward navigation (bfcache)
5454 EnableSmoothScrolling: true, // Smooth scrolling animations
5555 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
6059 }
6160
6261 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 animations54 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 WebViews58+ CreateWindow: true, // Default to creating a window for standalone WebViews
59- EnableTurnstileWorkaround: true,
60 }59 }
61 60
62 return cfg, nil61 return cfg, nil
M pkg/webkit/types.go +3 -9
@@ -65,11 +65,6 @@ type Config struct {
6565 // CreateWindow controls whether to create a standalone GTK Window for this WebView
6666 // Set to false for WebViews that will be embedded in workspace panes
6767 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
7368 }
7469
7570 // GetDefaultConfig returns a Config with sensible defaults
@@ -82,9 +77,8 @@ func GetDefaultConfig() *Config {
8277 HardwareAcceleration: true,
8378 DefaultFontSize: 16,
8479 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
8983 }
9084 }
@@ -65,11 +65,6 @@ type Config struct {
65 // CreateWindow controls whether to create a standalone GTK Window for this WebView65 // CreateWindow controls whether to create a standalone GTK Window for this WebView
66 // Set to false for WebViews that will be embedded in workspace panes66 // Set to false for WebViews that will be embedded in workspace panes
67 CreateWindow bool67 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 defaults70 // 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 navigation80+ EnablePageCache: true, // Instant back/forward navigation
86- EnableSmoothScrolling: true, // Better UX81+ EnableSmoothScrolling: true, // Better UX
87- CreateWindow: true, // Default to standalone window82+ 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 (
44 "encoding/json"
55 "fmt"
66 "log"
7- "net/url"
87 "strconv"
9- "strings"
108 "sync"
119 "sync/atomic"
1210
@@ -15,8 +13,6 @@ import (
1513 "github.com/diamondburned/gotk4/pkg/gtk/v4"
1614 )
1715
18-const turnstileHost = "challenges.cloudflare.com"
19-
2016 var (
2117 viewIDCounter uint64
2218 viewRegistry = make(map[uint64]*WebView)
@@ -328,12 +324,6 @@ func (w *WebView) setupEventHandlers() {
328324
329325 // Setup navigation policy handler for middle-click and Ctrl+click interception
330326 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()
337327 }
338328
339329 // setupNavigationPolicyHandler sets up the decide-policy signal handler
@@ -412,91 +402,6 @@ func (w *WebView) setupNavigationPolicyHandler() {
412402 log.Printf("[webkit] Navigation policy handler attached to WebView ID %d", w.id)
413403 }
414404
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-
500405 // LoadURL loads the given URL in the WebView
501406 func (w *WebView) LoadURL(url string) error {
502407 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 uint6417 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 interception325 // 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 handler329 // 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 WebView405 // 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()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close