mirror of
https://github.com/zhu1090093659/dsh-web.git
synced 2026-09-28 14:24:03 +08:00
Display and source layers rename to dsh-web: GitHub slug, docs and prose, aggregate package dir packages/dsh-web-all with npm name @linxin666/dsh-web-all, settings package dir packages/dsh-web-settings, private shared package dsh-web-shared, repo-local skill dirs, and the docs banner asset. Runtime, wire, and storage identifiers are frozen byte-identical so installed profiles keep resolving with zero migration: web-ui-* bundle ids, the dsh-web-ui-market settings section id, /api/dsh-web-ui-settings and its proxy-token header, and the dsh-web-ui-telemetry-* storage keys. Frozen history (docs/archive, docs/release-notes, archived notes), the JAVA-LW fork reference, and local filesystem paths keep the old name. npm migration: the next tag release dual-publishes @linxin666/dsh-web-all alongside the final @linxin666/dsh-web-ui-all version, then the old name is deprecated with a pointer; dual-publish lasts two releases. Decision record: .agents/notes/implemented/architecture/2026-08-24-product-rename-dsh-web.md
63 lines
2.3 KiB
TypeScript
63 lines
2.3 KiB
TypeScript
/**
|
|
* Loopback trust fence shared by the host route families: socket address,
|
|
* Host header, and browser same-origin markers. Packages receive this file as
|
|
* a generated copy via scripts/sync-shared.mjs; edit the shared source and
|
|
* re-run the sync instead of editing a copy.
|
|
*
|
|
* Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
|
|
* remote-web-ui gate), localhost hostnames, plus the browser same-origin
|
|
* markers (sec-fetch-site and Origin) for the request-level fence.
|
|
* @module dsh-web-shared/host/loopback
|
|
*/
|
|
|
|
import type { IncomingMessage } from 'node:http'
|
|
|
|
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
export function isIPv4Loopback(v4: string): boolean {
|
|
const parts = v4.split('.')
|
|
return parts.length === 4
|
|
&& parts[0] === '127'
|
|
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
}
|
|
|
|
/** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
|
|
export function isLoopbackAddress(address: string | undefined): boolean {
|
|
if (address === undefined) return false
|
|
const normalized = address.toLowerCase()
|
|
if (normalized === '::1') return true
|
|
if (normalized.startsWith('::ffff:')) return isIPv4Loopback(normalized.slice('::ffff:'.length))
|
|
return isIPv4Loopback(normalized)
|
|
}
|
|
|
|
/** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
|
|
export function isLoopbackHostname(hostname: string): boolean {
|
|
if (hostname === 'localhost' || hostname === '[::1]') return true
|
|
return isIPv4Loopback(hostname)
|
|
}
|
|
|
|
/**
|
|
* Request-level trust fence: a loopback socket address AND a loopback Host
|
|
* header, plus browser same-origin markers. The socket address is
|
|
* authoritative; X-Forwarded-For is never trusted.
|
|
*/
|
|
export function isLoopbackRequest(request: IncomingMessage): boolean {
|
|
if (!isLoopbackAddress(request.socket.remoteAddress)) return false
|
|
const host = request.headers.host
|
|
if (typeof host !== 'string') return false
|
|
let hostUrl: URL
|
|
try {
|
|
hostUrl = new URL('http://' + host)
|
|
} catch {
|
|
return false
|
|
}
|
|
if (!isLoopbackHostname(hostUrl.hostname)) return false
|
|
if (request.headers['sec-fetch-site'] === 'cross-site') return false
|
|
const origin = request.headers.origin
|
|
if (origin === undefined) return true
|
|
try {
|
|
return new URL(origin).host === hostUrl.host
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|