diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bec422..2622b61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,3 +32,4 @@ jobs: - run: node --check assets/tmwd_cdp_bridge/background.js - run: node --check assets/tmwd_cdp_bridge/content.js - run: node --check assets/tmwd_cdp_bridge/popup.js + - run: node --test tests/*.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 84794c6..0a1e5b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 所有重要变更都会记录在这里。日期使用北京时间自然日。 +## 未发布 + +### 修复 + +- Chrome 扩展不再全局移除页面 CSP 响应头或 meta 标签;升级时会清理旧版持久化的动态规则,避免与 Tampermonkey 等注入脚本共同导致 YouTube renderer 满载卡死。受 CSP 限制的页面执行继续使用现有 CDP fallback。 + ## 已发布 ## v0.3.4 - 2026-05-19 diff --git a/assets/tmwd_cdp_bridge/background.js b/assets/tmwd_cdp_bridge/background.js index baeee07..89f4dca 100644 --- a/assets/tmwd_cdp_bridge/background.js +++ b/assets/tmwd_cdp_bridge/background.js @@ -1,18 +1,19 @@ // background.js - Cookie + CDP Bridge +const LEGACY_CSP_RULE_ID = 9999; + +async function removeLegacyCspRule() { + try { + // Dynamic rules survive extension upgrades, so explicitly migrate existing installs. + await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [LEGACY_CSP_RULE_ID] }); + } catch (e) { + console.warn('[TMWD] Failed to remove legacy CSP rule:', e.message); + } +} + +removeLegacyCspRule(); chrome.runtime.onInstalled.addListener(() => { console.log('CDP Bridge installed'); - // Strip CSP headers to allow eval/inline scripts - chrome.declarativeNetRequest.updateDynamicRules({ - removeRuleIds: [9999], - addRules: [{ - id: 9999, priority: 1, - action: { type: 'modifyHeaders', responseHeaders: [ - { header: 'content-security-policy', operation: 'remove' }, - { header: 'content-security-policy-report-only', operation: 'remove' } - ]}, - condition: { urlFilter: '*', resourceTypes: ['main_frame', 'sub_frame'] } - }] - }); + removeLegacyCspRule(); }); let lastCommandAt = 0; diff --git a/assets/tmwd_cdp_bridge/content.js b/assets/tmwd_cdp_bridge/content.js index 63984da..bf40a9a 100644 --- a/assets/tmwd_cdp_bridge/content.js +++ b/assets/tmwd_cdp_bridge/content.js @@ -4,9 +4,6 @@ if (window.__agentBrowserCliCleanup) window.__agentBrowserCliCleanup(); if (window.__agentBrowserCliObserverCleanup) window.__agentBrowserCliObserverCleanup(); document.querySelectorAll('#agent-browser-cli-ind,#agent-browser-cli-style').forEach(e => e.remove()); -// Remove meta CSP tags -document.querySelectorAll('meta[http-equiv="Content-Security-Policy"]').forEach(e => e.remove()); - /** * Render a right-side floating badge that reflects the real bridge connection status. */ diff --git a/tests/extension_csp_policy.test.mjs b/tests/extension_csp_policy.test.mjs new file mode 100644 index 0000000..aedc8a4 --- /dev/null +++ b/tests/extension_csp_policy.test.mjs @@ -0,0 +1,121 @@ +import assert from 'node:assert/strict'; +import { webcrypto } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import vm from 'node:vm'; + +const backgroundPath = new URL('../assets/tmwd_cdp_bridge/background.js', import.meta.url); +const contentPath = new URL('../assets/tmwd_cdp_bridge/content.js', import.meta.url); + +function eventTarget(listeners = []) { + return { + addListener(listener) { + listeners.push(listener); + }, + removeListener() {}, + }; +} + +function createBackgroundContext(dynamicRuleCalls) { + const onInstalled = []; + const noOpEvent = eventTarget(); + const chrome = { + alarms: { + clear: async () => true, + create() {}, + onAlarm: noOpEvent, + }, + debugger: { + onDetach: noOpEvent, + onEvent: noOpEvent, + }, + declarativeNetRequest: { + async updateDynamicRules(update) { + dynamicRuleCalls.push(update); + }, + }, + extension: { + async isAllowedFileSchemeAccess() { + return false; + }, + }, + runtime: { + getManifest() { + return { version: 'test' }; + }, + onInstalled: eventTarget(onInstalled), + onMessage: noOpEvent, + onStartup: noOpEvent, + }, + scripting: { + async executeScript() { + return []; + }, + }, + storage: { + local: { + async get(defaults) { + return defaults; + }, + async set() {}, + }, + }, + tabGroups: { + async query() { + return []; + }, + }, + tabs: { + async query() { + return []; + }, + onCreated: noOpEvent, + onRemoved: noOpEvent, + onUpdated: noOpEvent, + }, + }; + + class FakeWebSocket {} + FakeWebSocket.CONNECTING = 0; + FakeWebSocket.OPEN = 1; + + return { + context: vm.createContext({ + AbortController, + WebSocket: FakeWebSocket, + chrome, + console: { log() {}, warn() {}, error() {} }, + crypto: webcrypto, + fetch: async () => { + throw new Error('daemon unavailable in test'); + }, + setTimeout: () => 1, + }), + onInstalled, + }; +} + +async function flushMicrotasks() { + await Promise.resolve(); + await Promise.resolve(); +} + +test('extension startup removes the legacy CSP rule without installing a replacement', async () => { + const source = await readFile(backgroundPath, 'utf8'); + const dynamicRuleCalls = []; + const { context, onInstalled } = createBackgroundContext(dynamicRuleCalls); + + vm.runInContext(source, context, { filename: backgroundPath.pathname }); + await flushMicrotasks(); + + assert.deepEqual(JSON.parse(JSON.stringify(dynamicRuleCalls[0])), { removeRuleIds: [9999] }); + assert.ok(dynamicRuleCalls.every((update) => !('addRules' in update))); + + for (const listener of onInstalled) await listener({ reason: 'update' }); + assert.ok(dynamicRuleCalls.every((update) => !('addRules' in update))); +}); + +test('content script leaves page CSP metadata untouched', async () => { + const source = await readFile(contentPath, 'utf8'); + assert.doesNotMatch(source, /Content-Security-Policy/i); +});