fix: expose webkit config api to trusted fetch
- commit
- 85f934a5e0fb909aa890b7b4d7eec761c60d9441
- author
- brice · · 2026-06-24 04:43 UTC
- parent
- d823634
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +40 -1
M internal/infrastructure/webkit/scheme_handler.go +12 -1
| @@ -309,15 +309,26 @@ func privateJSONErrorResponse(status int, message string) *SchemeResponse { | |||
| 309 | 309 | return resp | |
| 310 | 310 | } | |
| 311 | 311 | ||
| 312 | + | func trustedPrivateAPIHeaders() map[string]string { | |
| 313 | + | return map[string]string{ | |
| 314 | + | "Access-Control-Allow-Origin": "*", | |
| 315 | + | "Access-Control-Allow-Methods": "GET, OPTIONS", | |
| 316 | + | "Access-Control-Allow-Headers": "Content-Type", | |
| 317 | + | "Cache-Control": "no-store", | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | ||
| 312 | 321 | func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse { | |
| 313 | 322 | if build == nil { | |
| 314 | 323 | resp := privateJSONErrorResponse(http.StatusInternalServerError, "config payload builder not configured") | |
| 324 | + | resp.Headers = trustedPrivateAPIHeaders() | |
| 315 | 325 | return resp | |
| 316 | 326 | } | |
| 317 | 327 | ||
| 318 | 328 | data, err := build() | |
| 319 | 329 | if err != nil { | |
| 320 | 330 | resp := privateJSONErrorResponse(http.StatusInternalServerError, err.Error()) | |
| 331 | + | resp.Headers = trustedPrivateAPIHeaders() | |
| 321 | 332 | return resp | |
| 322 | 333 | } | |
| 323 | 334 | ||
| @@ -325,7 +336,7 @@ func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse { | |||
| 325 | 336 | Data: data, | |
| 326 | 337 | ContentType: "application/json", | |
| 327 | 338 | StatusCode: http.StatusOK, | |
| 328 | - | Headers: map[string]string{"Cache-Control": "no-store"}, | |
| 339 | + | Headers: trustedPrivateAPIHeaders(), | |
| 329 | 340 | SuppressDefaultHeaders: true, | |
| 330 | 341 | } | |
| 331 | 342 | } | |
| @@ -309,15 +309,26 @@ func privateJSONErrorResponse(status int, message string) *SchemeResponse { | |||||
| 309 | return resp | 309 | return resp | ||
| 310 | } | 310 | } | ||
| 311 | 311 | ||||
| 312 | + | func trustedPrivateAPIHeaders() map[string]string { | |||
| 313 | + | return map[string]string{ | |||
| 314 | + | "Access-Control-Allow-Origin": "*", | |||
| 315 | + | "Access-Control-Allow-Methods": "GET, OPTIONS", | |||
| 316 | + | "Access-Control-Allow-Headers": "Content-Type", | |||
| 317 | + | "Cache-Control": "no-store", | |||
| 318 | + | } | |||
| 319 | + | } | |||
| 320 | + | ||||
| 312 | func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse { | 321 | func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse { | ||
| 313 | if build == nil { | 322 | if build == nil { | ||
| 314 | resp := privateJSONErrorResponse(http.StatusInternalServerError, "config payload builder not configured") | 323 | resp := privateJSONErrorResponse(http.StatusInternalServerError, "config payload builder not configured") | ||
| 324 | + | resp.Headers = trustedPrivateAPIHeaders() | |||
| 315 | return resp | 325 | return resp | ||
| 316 | } | 326 | } | ||
| 317 | 327 | ||||
| 318 | data, err := build() | 328 | data, err := build() | ||
| 319 | if err != nil { | 329 | if err != nil { | ||
| 320 | resp := privateJSONErrorResponse(http.StatusInternalServerError, err.Error()) | 330 | resp := privateJSONErrorResponse(http.StatusInternalServerError, err.Error()) | ||
| 331 | + | resp.Headers = trustedPrivateAPIHeaders() | |||
| 321 | return resp | 332 | return resp | ||
| 322 | } | 333 | } | ||
| 323 | 334 | ||||
| @@ -325,7 +336,7 @@ func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse { | |||||
| 325 | Data: data, | 336 | Data: data, | ||
| 326 | ContentType: "application/json", | 337 | ContentType: "application/json", | ||
| 327 | StatusCode: http.StatusOK, | 338 | StatusCode: http.StatusOK, | ||
| 328 | - | Headers: map[string]string{"Cache-Control": "no-store"}, | 339 | + | Headers: trustedPrivateAPIHeaders(), |
| 329 | SuppressDefaultHeaders: true, | 340 | SuppressDefaultHeaders: true, | ||
| 330 | } | 341 | } | ||
| 331 | } | 342 | } | ||
M internal/infrastructure/webkit/scheme_handler_test.go +28 -0
| @@ -268,6 +268,34 @@ func TestConfigHandlersAllowOpaqueOriginWithTrustedReferer(t *testing.T) { | |||
| 268 | 268 | assert.Equal(t, []byte(`{"current":true}`), resp.Data) | |
| 269 | 269 | } | |
| 270 | 270 | ||
| 271 | + | func TestConfigHandlersExposeTrustedResponsesToWebKitFetch(t *testing.T) { | |
| 272 | + | h := NewDumbSchemeHandler(context.Background()) | |
| 273 | + | h.SetConfigPayloadBuilders( | |
| 274 | + | func() ([]byte, error) { return []byte(`{"current":true}`), nil }, | |
| 275 | + | func() ([]byte, error) { return []byte(`{"default":true}`), nil }, | |
| 276 | + | ) | |
| 277 | + | ||
| 278 | + | h.mu.RLock() | |
| 279 | + | currentHandler := h.handlers["/api/config"] | |
| 280 | + | h.mu.RUnlock() | |
| 281 | + | require.NotNil(t, currentHandler) | |
| 282 | + | ||
| 283 | + | resp := currentHandler.Handle(&SchemeRequest{ | |
| 284 | + | URI: "dumb://config/api/config", | |
| 285 | + | Path: "/api/config", | |
| 286 | + | Method: http.MethodGet, | |
| 287 | + | Scheme: "dumb", | |
| 288 | + | Origin: "null", | |
| 289 | + | Referer: "dumb://config", | |
| 290 | + | }) | |
| 291 | + | ||
| 292 | + | require.NotNil(t, resp) | |
| 293 | + | assert.Equal(t, http.StatusOK, resp.StatusCode) | |
| 294 | + | assert.Equal(t, "*", resp.Headers["Access-Control-Allow-Origin"]) | |
| 295 | + | assert.Equal(t, "GET, OPTIONS", resp.Headers["Access-Control-Allow-Methods"]) | |
| 296 | + | assert.Equal(t, "Content-Type", resp.Headers["Access-Control-Allow-Headers"]) | |
| 297 | + | } | |
| 298 | + | ||
| 271 | 299 | func TestShouldAddCORSHeaders(t *testing.T) { | |
| 272 | 300 | t.Parallel() | |
| 273 | 301 | ||
| @@ -268,6 +268,34 @@ func TestConfigHandlersAllowOpaqueOriginWithTrustedReferer(t *testing.T) { | |||||
| 268 | assert.Equal(t, []byte(`{"current":true}`), resp.Data) | 268 | assert.Equal(t, []byte(`{"current":true}`), resp.Data) | ||
| 269 | } | 269 | } | ||
| 270 | 270 | ||||
| 271 | + | func TestConfigHandlersExposeTrustedResponsesToWebKitFetch(t *testing.T) { | |||
| 272 | + | h := NewDumbSchemeHandler(context.Background()) | |||
| 273 | + | h.SetConfigPayloadBuilders( | |||
| 274 | + | func() ([]byte, error) { return []byte(`{"current":true}`), nil }, | |||
| 275 | + | func() ([]byte, error) { return []byte(`{"default":true}`), nil }, | |||
| 276 | + | ) | |||
| 277 | + | ||||
| 278 | + | h.mu.RLock() | |||
| 279 | + | currentHandler := h.handlers["/api/config"] | |||
| 280 | + | h.mu.RUnlock() | |||
| 281 | + | require.NotNil(t, currentHandler) | |||
| 282 | + | ||||
| 283 | + | resp := currentHandler.Handle(&SchemeRequest{ | |||
| 284 | + | URI: "dumb://config/api/config", | |||
| 285 | + | Path: "/api/config", | |||
| 286 | + | Method: http.MethodGet, | |||
| 287 | + | Scheme: "dumb", | |||
| 288 | + | Origin: "null", | |||
| 289 | + | Referer: "dumb://config", | |||
| 290 | + | }) | |||
| 291 | + | ||||
| 292 | + | require.NotNil(t, resp) | |||
| 293 | + | assert.Equal(t, http.StatusOK, resp.StatusCode) | |||
| 294 | + | assert.Equal(t, "*", resp.Headers["Access-Control-Allow-Origin"]) | |||
| 295 | + | assert.Equal(t, "GET, OPTIONS", resp.Headers["Access-Control-Allow-Methods"]) | |||
| 296 | + | assert.Equal(t, "Content-Type", resp.Headers["Access-Control-Allow-Headers"]) | |||
| 297 | + | } | |||
| 298 | + | ||||
| 271 | func TestShouldAddCORSHeaders(t *testing.T) { | 299 | func TestShouldAddCORSHeaders(t *testing.T) { | ||
| 272 | t.Parallel() | 300 | t.Parallel() | ||
| 273 | 301 | ||||