mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(desktop): keep server CORS headers so preflights cache (#47560)
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { addRendererHeaders, documentPolicyHeader, jsCallStacksDocumentPolicy, upsertHeader } from "./headers"
|
||||
|
||||
describe("renderer response headers", () => {
|
||||
test("keeps the server's exact allow-headers list so Chromium can reuse the cached preflight", () => {
|
||||
const headers = {
|
||||
"access-control-allow-origin": ["oc://renderer"],
|
||||
"access-control-allow-headers": ["authorization"],
|
||||
"access-control-max-age": ["86400"],
|
||||
}
|
||||
addRendererHeaders(headers, { document: false })
|
||||
expect(headers).toEqual({
|
||||
"access-control-allow-origin": ["*"],
|
||||
"access-control-allow-headers": ["authorization"],
|
||||
"access-control-max-age": ["86400"],
|
||||
})
|
||||
})
|
||||
|
||||
test("fills CORS headers for servers that send none, naming authorization explicitly", () => {
|
||||
const headers: Record<string, string[]> = { "content-type": ["application/json"] }
|
||||
addRendererHeaders(headers, { document: false })
|
||||
expect(headers["Access-Control-Allow-Origin"]).toEqual(["*"])
|
||||
expect(headers["Access-Control-Allow-Headers"]).toEqual(["*, authorization"])
|
||||
expect(headers["Access-Control-Max-Age"]).toEqual(["7200"])
|
||||
})
|
||||
|
||||
test("adds the crash-report document policy only to renderer documents", () => {
|
||||
const document = {}
|
||||
addRendererHeaders(document, { document: true })
|
||||
expect(Reflect.get(document, documentPolicyHeader)).toEqual([jsCallStacksDocumentPolicy])
|
||||
const asset = {}
|
||||
addRendererHeaders(asset, { document: false })
|
||||
expect(Object.keys(asset)).not.toContain(documentPolicyHeader)
|
||||
})
|
||||
|
||||
test("upsert replaces a header regardless of key casing", () => {
|
||||
const headers = { "X-Test": ["a"] }
|
||||
upsertHeader(headers, "x-test", ["b"])
|
||||
expect(headers).toEqual({ "X-Test": ["b"] })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
export const documentPolicyHeader = "Document-Policy"
|
||||
export const jsCallStacksDocumentPolicy = "include-js-call-stacks-in-crash-reports"
|
||||
|
||||
// The renderer lives at oc://renderer, so every server it talks to is cross-origin. Servers that
|
||||
// send no CORS headers get permissive ones here so the renderer can still reach them.
|
||||
//
|
||||
// A server's own `Access-Control-Allow-Headers` is kept as-is. Chromium reuses a cached preflight
|
||||
// only when that header names `authorization` explicitly (`*` never covers it), so overwriting the
|
||||
// server's exact list with `*` forced a fresh OPTIONS round trip in front of every API call.
|
||||
export function addRendererHeaders(headers: object, options: { document: boolean }) {
|
||||
upsertHeader(headers, "Access-Control-Allow-Origin", ["*"])
|
||||
if (!hasHeader(headers, "Access-Control-Allow-Headers")) {
|
||||
upsertHeader(headers, "Access-Control-Allow-Headers", ["*, authorization"])
|
||||
}
|
||||
if (!hasHeader(headers, "Access-Control-Max-Age")) {
|
||||
// Chromium caps preflight cache lifetime at two hours; without the header it caches for 5s.
|
||||
upsertHeader(headers, "Access-Control-Max-Age", ["7200"])
|
||||
}
|
||||
if (options.document) upsertHeader(headers, documentPolicyHeader, [jsCallStacksDocumentPolicy])
|
||||
}
|
||||
|
||||
export function hasHeader(headers: object, key: string) {
|
||||
return Object.keys(headers).some((header) => header.toLowerCase() === key.toLowerCase())
|
||||
}
|
||||
|
||||
export function upsertHeader(headers: object, key: string, value: string | string[]) {
|
||||
const current = Object.keys(headers).find((header) => header.toLowerCase() === key.toLowerCase())
|
||||
Reflect.set(headers, current ?? key, value)
|
||||
}
|
||||
@@ -4,11 +4,10 @@ import { pathToFileURL } from "node:url"
|
||||
import { Effect, Path } from "effect"
|
||||
import { scoped } from "../native/logging"
|
||||
import { DesktopPaths } from "../paths"
|
||||
import { documentPolicyHeader, jsCallStacksDocumentPolicy } from "./headers"
|
||||
|
||||
const rendererProtocol = "oc"
|
||||
const rendererHost = "renderer"
|
||||
const documentPolicyHeader = "Document-Policy"
|
||||
const jsCallStacksDocumentPolicy = "include-js-call-stacks-in-crash-reports"
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
@@ -85,17 +84,6 @@ export function isRendererUrl(value?: string, html = false) {
|
||||
return url.origin === new URL(devUrl).origin
|
||||
}
|
||||
|
||||
export function addRendererHeaders(value: string, headers: object) {
|
||||
upsertHeader(headers, "Access-Control-Allow-Origin", ["*"])
|
||||
upsertHeader(headers, "Access-Control-Allow-Headers", ["*"])
|
||||
if (isRendererUrl(value, true)) upsertHeader(headers, documentPolicyHeader, [jsCallStacksDocumentPolicy])
|
||||
}
|
||||
|
||||
export function upsertHeader(headers: object, key: string, value: string | string[]) {
|
||||
const current = Object.keys(headers).find((header) => header.toLowerCase() === key.toLowerCase())
|
||||
Reflect.set(headers, current ?? key, value)
|
||||
}
|
||||
|
||||
function addDocumentPolicy(response: Response, file: string) {
|
||||
if (!file.toLowerCase().endsWith(".html")) return response
|
||||
const headers = new Headers(response.headers)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { BrowserWindow } from "electron"
|
||||
import { addRendererHeaders, isRendererUrl, upsertHeader } from "./protocol"
|
||||
import { addRendererHeaders } from "./headers"
|
||||
import { isRendererUrl } from "./protocol"
|
||||
|
||||
const rendererPermissions = new Set(["clipboard-sanitized-write", "notifications"])
|
||||
|
||||
@@ -30,13 +31,9 @@ export function wireNavigationPolicy(win: BrowserWindow, openExternalURL: (url:
|
||||
}
|
||||
|
||||
export function wireRendererHeaders(win: BrowserWindow) {
|
||||
win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||
upsertHeader(details.requestHeaders, "Access-Control-Allow-Origin", ["*"])
|
||||
callback({ requestHeaders: details.requestHeaders })
|
||||
})
|
||||
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
const responseHeaders = details.responseHeaders ?? {}
|
||||
addRendererHeaders(details.url, responseHeaders)
|
||||
addRendererHeaders(responseHeaders, { document: isRendererUrl(details.url, true) })
|
||||
callback({ responseHeaders })
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user