Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions extensions/dsh-browser/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,25 @@ function isLocalBridgeAddress(url: string): boolean {
}
}

/**
* 把回环别名统一成扩展 CSP 允许的 `127.0.0.1`。
*
* `isLocalBridgeAddress` 接受 `localhost` / `::1`,但扩展 `connect-src` 只列了
* `127.0.0.1`:用别名探测时 fetch 会先被扩展自己的 CSP 拦掉,预检因此失败,
* 自动探测又把该端口当作「已试过」排除;于是一个明明在该端口监听的服务反而
* 连不上(或者误连到另一个本机实例)。只改连接地址,不改用户保存的设置。
*/
function canonicalLoopbackUrl(raw: string): string {
if (!isLocalBridgeAddress(raw)) return raw
try {
const parsed = new URL(raw)
parsed.hostname = '127.0.0.1'
return parsed.toString()
} catch {
return raw
}
}

/** 记下成功的地址,供下次面板打开时优先尝试。 */
function rememberBridgeUrl(url: string): void {
if (lastBridgeUrl === url) return
Expand Down Expand Up @@ -1658,6 +1677,8 @@ async function startBridge(): Promise<void> {
const lease = (): boolean => revision === bridgeStartRevision && panelPorts.size > 0
let url = settings.bridgeUrl
if (url !== '') {
// The extension CSP only whitelists 127.0.0.1; probe and connect through it.
url = canonicalLoopbackUrl(url)
if (isLocalBridgeAddress(url) && !await probeBridge(url)) {
if (!lease()) return
url = await discoverBridgeUrl(lease, url) ?? ''
Expand Down
28 changes: 28 additions & 0 deletions extensions/dsh-browser/tests/background-bridge-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,34 @@ describe('background bridge lifecycle', () => {
expect(fetchMock).not.toHaveBeenCalled()
})

it.each([
['localhost', 'ws://localhost:3080/ext/bridge'],
['IPv6 loopback', 'ws://[::1]:3080/ext/bridge'],
])('reaches a manual %s address through the CSP-allowed loopback origin', async (_alias, bridgeUrl) => {
const chromeMock = mockChrome({
localGet: async () => ({ dshSettings: { bridgeUrl } }),
})
// The extension's connect-src lists only 127.0.0.1, so the browser blocks
// every other loopback alias before the request reaches the host. The
// requested bridge is the only instance listening on this machine.
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const target = new URL(String(input))
if (target.hostname !== '127.0.0.1') throw new TypeError('Failed to fetch: blocked by connect-src')
if (target.port !== '3080') return new Response(null, { status: 503 })
return new Response(JSON.stringify({ wsUrl: `ws://127.0.0.1:${target.port}/ext/bridge` }), { status: 200 })
})
vi.stubGlobal('fetch', fetchMock)
vi.stubGlobal('WebSocket', FakeWebSocket)
await import('../src/background/index.ts')

const panel = panelPort()
chromeMock.onConnect.emit(panel.port)
await vi.waitFor(() => { expect(FakeWebSocket.instances).toHaveLength(1) })

expect(FakeWebSocket.instances[0]!.url).toBe('ws://127.0.0.1:3080/ext/bridge')
expect(String(fetchMock.mock.calls[0]![0])).toBe('http://127.0.0.1:3080/ext/bridge-config')
})

it('does not probe again while the lease remembers a confirmed miss', async () => {
const chromeMock = mockChrome({
// DSH stayed stopped across a service-worker restart: the latch survives.
Expand Down
Loading