fix(web): share loopback hostname policy
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IncomingHttpHeaders } from 'node:http'
|
import type { IncomingHttpHeaders } from 'node:http'
|
||||||
|
import { isLoopbackHostname } from './loopback-hostname.ts'
|
||||||
|
|
||||||
/** The request facts the fence reads (structural subset of IncomingMessage). */
|
/** The request facts the fence reads (structural subset of IncomingMessage). */
|
||||||
interface ApiTrustRequest {
|
interface ApiTrustRequest {
|
||||||
@@ -25,14 +26,6 @@ function header(headers: IncomingHttpHeaders, name: string): string | undefined
|
|||||||
return typeof value === 'string' ? value : undefined
|
return typeof value === 'string' ? value : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLoopbackHostname(hostname: string): boolean {
|
|
||||||
if (hostname === 'localhost' || hostname === '[::1]') return true
|
|
||||||
const parts = hostname.split('.')
|
|
||||||
return parts.length === 4
|
|
||||||
&& parts[0] === '127'
|
|
||||||
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Normalized URL of a Host-header authority (hostname lowercased, default port stripped, IPv6 bracketed), or undefined when unparsable. */
|
/** Normalized URL of a Host-header authority (hostname lowercased, default port stripped, IPv6 bracketed), or undefined when unparsable. */
|
||||||
function parseAuthority(authority: string): URL | undefined {
|
function parseAuthority(authority: string): URL | undefined {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { ConnectionController, type ConnectionConfig, type ConnectionSinks, type
|
|||||||
import { FixtureApiClient } from './fixture.ts'
|
import { FixtureApiClient } from './fixture.ts'
|
||||||
import { WebApiClient } from './web-api-client.ts'
|
import { WebApiClient } from './web-api-client.ts'
|
||||||
|
|
||||||
|
export { isLoopbackHostname } from '../loopback-hostname.ts'
|
||||||
|
|
||||||
// ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
|
// ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
|
||||||
export type {
|
export type {
|
||||||
ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
||||||
|
|||||||
12
packages/client/connection/src/loopback-hostname.ts
Normal file
12
packages/client/connection/src/loopback-hostname.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Whether a normalized URL hostname names the local loopback authority.
|
||||||
|
* @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
|
||||||
|
* @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
|
||||||
|
*/
|
||||||
|
export function isLoopbackHostname(hostname: string): boolean {
|
||||||
|
if (hostname === 'localhost' || hostname === '[::1]') return true
|
||||||
|
const parts = hostname.split('.')
|
||||||
|
return parts.length === 4
|
||||||
|
&& parts[0] === '127'
|
||||||
|
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
||||||
|
}
|
||||||
18
packages/client/connection/tests/loopback-hostname.spec.ts
Normal file
18
packages/client/connection/tests/loopback-hostname.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/** Shared loopback-hostname semantics for the Host fence and browser UI. */
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { isLoopbackHostname } from '../src/loopback-hostname.ts'
|
||||||
|
|
||||||
|
describe('isLoopbackHostname', () => {
|
||||||
|
it('accepts localhost, IPv6 loopback, and the whole IPv4 127/8 block', () => {
|
||||||
|
for (const hostname of ['localhost', '[::1]', '127.0.0.1', '127.8.9.10', '127.255.255.255']) {
|
||||||
|
expect(isLoopbackHostname(hostname)).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuses malformed and non-loopback hostnames', () => {
|
||||||
|
for (const hostname of ['remote.localhost', '::1', '128.0.0.1', '127.0.0', '127.0.0.256', '127.0.0.-1']) {
|
||||||
|
expect(isLoopbackHostname(hostname)).toBe(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import { deferRegistration } from '@deepseek-ai/dsh-client-ui-slots'
|
import { deferRegistration } from '@deepseek-ai/dsh-client-ui-slots'
|
||||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
import { isLoopbackHostname, type ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||||
// Type-only: pulls the shell's SlotMap merges (trigger/header/section/item).
|
// Type-only: pulls the shell's SlotMap merges (trigger/header/section/item).
|
||||||
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||||
@@ -41,14 +41,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|||||||
/** Dictionary namespace owned by this plugin (shell chrome + General copy). */
|
/** Dictionary namespace owned by this plugin (shell chrome + General copy). */
|
||||||
const NS = 'settings'
|
const NS = 'settings'
|
||||||
|
|
||||||
function isLoopbackHostname(hostname: string): boolean {
|
|
||||||
if (hostname === 'localhost' || hostname === '[::1]') return true
|
|
||||||
const parts = hostname.split('.')
|
|
||||||
return parts.length === 4
|
|
||||||
&& parts[0] === '127'
|
|
||||||
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
||||||
}
|
|
||||||
|
|
||||||
function welcomePersistence(): 'host' | 'memory' {
|
function welcomePersistence(): 'host' | 'memory' {
|
||||||
return typeof location === 'undefined' || isLoopbackHostname(location.hostname) ? 'host' : 'memory'
|
return typeof location === 'undefined' || isLoopbackHostname(location.hostname) ? 'host' : 'memory'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user