Răsfoiți Sursa

fix(web): share loopback hostname policy

ZiyaZhang 1 lună în urmă
părinte
comite
7a71ab9a92

+ 1 - 8
packages/client/connection/src/api-request-trust.ts

@@ -14,6 +14,7 @@
  */
 
 import type { IncomingHttpHeaders } from 'node:http'
+import { isLoopbackHostname } from './loopback-hostname.ts'
 
 /** The request facts the fence reads (structural subset of IncomingMessage). */
 interface ApiTrustRequest {
@@ -25,14 +26,6 @@ function header(headers: IncomingHttpHeaders, name: string): string | 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. */
 function parseAuthority(authority: string): URL | undefined {
   try {

+ 2 - 0
packages/client/connection/src/client/index.ts

@@ -9,6 +9,8 @@ import { ConnectionController, type ConnectionConfig, type ConnectionSinks, type
 import { FixtureApiClient } from './fixture.ts'
 import { WebApiClient } from './web-api-client.ts'
 
+export { isLoopbackHostname } from '../loopback-hostname.ts'
+
 // ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
 export type {
   ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,

+ 12 - 0
packages/client/connection/src/loopback-hostname.ts

@@ -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 - 0
packages/client/connection/tests/loopback-hostname.spec.ts

@@ -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)
+    }
+  })
+})

+ 1 - 9
packages/client/ui-settings-general/src/client/index.ts

@@ -7,7 +7,7 @@
  */
 import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
 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'
 // Type-only: pulls the shell's SlotMap merges (trigger/header/section/item).
 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). */
 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' {
   return typeof location === 'undefined' || isLoopbackHostname(location.hostname) ? 'host' : 'memory'
 }