merge #69: probe embed signals via GET fallback when HEAD omits them (trinhthiminh650-tech)

# Conflicts:
#	src/client/builtins/tabs.tsx
#	src/client/locales.ts
#	src/client/prefs.ts
#	src/config.ts
#	src/index.ts
#	src/prefs-shared.ts
This commit is contained in:
HuanLinOTO
2026-08-24 01:31:54 +08:00
27 changed files with 197 additions and 7 deletions
+40 -2
View File
@@ -232,6 +232,26 @@ function shellOverridesOf(getSettings: () => SidebarSettingsFace | undefined): {
}
}
/**
* Parse the browser tab's `browserAllowedLoopback` allowlist into a matcher
* over host:port (same contract as the client-side helper in
* src/client/browser.ts — kept in sync). Bare hosts (`localhost`,
* `127.0.0.1`) match every port; `host:port` entries match exactly.
*/
function parseLoopbackAllowlist(allowlist: string): (host: string, port: string) => boolean {
const entries = allowlist.split(',').map(entry => entry.trim().toLowerCase()).filter(entry => entry !== '')
const exact = new Set(entries)
const hosts = new Set<string>()
for (const entry of entries) {
if (!entry.includes(':')) hosts.add(entry.replace(/^\[|\]$/g, ''))
}
return (host, port) => {
const key = `${host}:${port}`
if (exact.has(key) || exact.has(host)) return true
return port !== '' && hosts.has(host)
}
}
function buildApi(
ctx: Context,
ptyManager: PtyManager | null,
@@ -488,9 +508,16 @@ function buildApi(
throw new SidebarError('bad-request', 'only http/https urls can be probed', 400)
}
// Mirror the browser tab's address-bar policy: loopback stays unreachable
// from the sidebar, so probing it would leak nothing the tab could use.
// from the sidebar (unless the user allowlisted it), so probing it would
// leak nothing the tab could use.
if (isLoopbackHostname(parsed.hostname)) {
throw new SidebarError('bad-request', 'local addresses are not probed', 400)
const prefs = getSettings()?.get()?.value as SidebarPrefs | undefined
const allowlist = typeof prefs?.browserAllowedLoopback === 'string' ? prefs.browserAllowedLoopback : ''
const allowed = allowlist.trim() !== ''
&& parseLoopbackAllowlist(allowlist)(parsed.hostname, parsed.port)
if (!allowed) {
throw new SidebarError('bad-request', 'local addresses are not probed', 400)
}
}
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 8000)
@@ -501,6 +528,17 @@ function buildApi(
if (response.status === 405 || response.status === 501) {
response = await fetch(parsed, { method: 'GET', redirect: 'follow', signal: controller.signal })
}
// Some servers (e.g. aliyun consoles) answer HEAD without the
// X-Frame-Options / CSP headers that only their GET response
// carries. Without those signals the embeddability check below
// would wrongly report the site as embeddable and the plain iframe
// would surface the browser's misleading "refused to connect".
// Retry once as GET when both signals are absent (body discarded).
const hasEmbedSignals = response.headers.get('content-security-policy') !== null
|| response.headers.get('x-frame-options') !== null
if (!hasEmbedSignals && response.status !== 405 && response.status !== 501) {
response = await fetch(parsed, { method: 'GET', redirect: 'follow', signal: controller.signal })
}
const csp = response.headers.get('content-security-policy')
const frameAncestors = extractFrameAncestors(csp)
const xFrameOptions = response.headers.get('x-frame-options')