Skip to content

fix(extension): probe a manual localhost bridge through the CSP-allowed 127.0.0.1 origin - #139

Open
PerryLink wants to merge 1 commit into
omdsh-dev:mainfrom
PerryLink:fix/localhost-loopback-csp-20261109
Open

PerryLink wants to merge 1 commit into
omdsh-dev:mainfrom
PerryLink:fix/localhost-loopback-csp-20261109

Conversation

@PerryLink

Copy link
Copy Markdown

概述 / Summary

Probe and connect a manually entered loopback bridge URL through 127.0.0.1 — the only loopback origin the extension's connect-src allows — instead of failing on localhost / ::1 aliases.

动机 / Motivation

isLocalBridgeAddress accepts localhost, [::1], and ::1 as local bridge addresses (extensions/dsh-browser/src/background/index.ts), and the settings address field is free text. But the extension's own CSP lists only 127.0.0.1:

  • extensions/dsh-browser/manifest.json → connect-src ws://127.0.0.1:* http://127.0.0.1:* https://raw.githubusercontent.com
  • extensions/dsh-browser/manifest.firefox.json → same

Chrome applies content_security_policy.extension_pages to the background service worker, and connect-src governs fetch() and WebSocket (https://developer.chrome.com/docs/extensions/reference/manifest/content-security-policy, https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src).

So for a pasted ws://localhost:3080/ext/bridge:

  1. probeBridge() fetches http://localhost:3080/ext/bridge-config, which the extension's own CSP blocks before it reaches the host → the pre-flight reports "not reachable".
  2. startBridge() then falls back to one discovery pass, passing the failed URL as excludeUrl; planDiscovery() excludes its port.
  3. A bridge that is actually listening on that port is skipped: the panel either finds nothing or silently attaches to another local instance on a different port.

dsh web prints localhost, so a manual paste is a realistic path.

改动内容 / Changes

  • Add canonicalLoopbackUrl(): rewrite the hostname of a local bridge URL to 127.0.0.1 (port, path, and query preserved); remote URLs pass through untouched.
  • Call it in startBridge() before the pre-flight and before the socket is opened. The saved dshSettings.bridgeUrl is not rewritten — the user's text stays as typed, matching the existing contract.
  • No CSP change: keeping connect-src at 127.0.0.1 preserves the current least-privilege policy and still covers IPv6 loopback.

测试 / Testing

  • New it.each cases for ws://localhost:3080/ext/bridge and ws://[::1]:3080/ext/bridge in tests/background-bridge-lifecycle.spec.ts. The fetch mock rejects any non-127.0.0.1 host (the CSP rule) and only port 3080 answers; the test asserts the probe goes to http://127.0.0.1:3080/ext/bridge-config and the WebSocket opens at ws://127.0.0.1:3080/ext/bridge.
  • Red → green: before the fix, 0 sockets (the test fails with expected [] to have a length of 1); with a second instance answering on 3081, the old flow attached to ws://127.0.0.1:3081/ext/bridge instead of the requested 3080. After the fix, 2/2 pass.
  • npx vitest run tests/background-bridge-lifecycle.spec.ts → 21 passed
  • pnpm test → extension 77 files / 668 tests passed; bridge 18 files / 211 tests passed
  • npx tsc -b --pretty false → clean
  • pnpm run build → clean

This is a unit-level reproduction of the CSP-blocked pre-flight, not a real-browser E2E run (no browser harness in this environment). The CSP rule itself is taken from the Chrome extension CSP contract and from this repo's own connect-src, which already whitelists https://raw.githubusercontent.com for update fetches.

影响范围 / Impact

  • Chrome and Firefox extension builds; the bridge plugin is unchanged.
  • Only the manual-address path is affected; auto-discovery and 127.0.0.1 URLs behave exactly as before.
  • macOS / Linux / Windows: no platform-specific code.

Checklist

  • 已在相关平台实测通过 / Tested on the relevant platforms — unit + build only; no browser E2E (see above)
  • 已通过 pnpm run build / bash -n scripts/install.sh 等基础检查 / Passed build checks
  • 文档已同步更新(如需要)/ Docs updated if needed — not needed
  • 不包含无关改动 / No unrelated changes included

`isLocalBridgeAddress` accepts `localhost` and `::1`, but the extension's
`connect-src` only lists `127.0.0.1`. A pasted `ws://localhost:<port>` URL
was therefore blocked by the extension's own CSP during the pre-flight; the
fallback discovery then excluded that port and either found nothing or
attached to another local instance.

Rewrite the hostname of a local bridge URL to `127.0.0.1` before probing and
connecting. The saved setting is left untouched, and the CSP stays at the
current least-privilege value.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are recorded for the manual loopback bridge connection fix.

No open issues 🎉

What changed in this PR

Fixes manual loopback bridge connections by routing probes and WebSocket connections through the extension’s CSP-allowed 127.0.0.1 origin.

Changes:

  • Normalizes local bridge hostnames before probing and connecting, without changing saved settings or CSP.
  • Adds regression tests for localhost and IPv6 loopback addresses, verifying both connection URLs.
File Description
extensions/​dsh-browser/​tests/​background-bridge-lifecycle.spec.ts Tests manual loopback aliases against a simulated CSP restriction.
extensions/​dsh-browser/​src/​background/​index.ts Adds loopback URL normalization and applies it before the manual connection preflight.

Review effort: Max

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants