/** * Standalone smoke test for the LAN listener. It mounts the plugin against a * fake Cordis context, drives a real upstream HTTP + WebSocket server, and * asserts the forwarding contract. Run: `node scripts/smoke.mjs`. */ import { createServer } from 'node:http' import { connect as netConnect } from 'node:net' import vm from 'node:vm' import { apply, internals, OWNS_HOST_COMPAT_SCRIPT } from '../index.js' const results = [] function check(label, condition, detail) { results.push({ label, ok: Boolean(condition), detail }) console.log(`${condition ? 'PASS' : 'FAIL'} ${label}${detail === undefined || condition ? '' : ` — ${detail}`}`) } /** Bind a throwaway server on an OS-assigned loopback port. */ function listen(server, host = '127.0.0.1') { return new Promise((resolve) => { server.listen(0, host, () => resolve(server.address().port)) }) } const seen = [] const upstream = createServer((req, res) => { seen.push({ url: req.url, host: req.headers.host, origin: req.headers.origin, cookie: req.headers.cookie }) res.writeHead(200, { 'content-type': 'application/json', 'set-cookie': 'dsh-auth-test=abc123; Max-Age=60; Path=/; HttpOnly; SameSite=Strict', 'x-upstream': 'yes', }) res.end(JSON.stringify({ url: req.url, host: req.headers.host, origin: req.headers.origin ?? null })) }) // DSH serves HTTP and WebSocket on the same port, so the upstream carries both. upstream.on('upgrade', (req, socket) => { seen.push({ upgrade: true, url: req.url, host: req.headers.host, origin: req.headers.origin }) socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n') socket.write('hello-from-upstream') }) const upstreamPort = await listen(upstream) // ── fake Cordis context ───────────────────────────────────────────────────── // Cordis exposes injected services both as `ctx.` and through `ctx.get`. const routes = [] let admitted = 0 const connection = { admit: () => { admitted += 1 return { peer: {} } }, authenticatedUrl: (base) => `${base}?token=TESTTOKEN`, } const webServer = { port: upstreamPort, register: (route) => { routes.push(route) return () => {} }, } const ctx = { webServer, connection, get(name) { if (name === 'connection') return connection if (name === 'webServer') return webServer return undefined }, inject(deps, callback) { if (deps.every((d) => ctx.get(d) !== undefined)) callback(ctx) return () => {} }, effect(callback) { callback() return () => {} }, } const LAN_PORT = 39_991 apply(ctx, { enabled: true, host: '127.0.0.1', port: LAN_PORT, targetHost: '127.0.0.1', targetPort: upstreamPort, printBanner: false, }) // Wait for the listener to bind. await new Promise((resolve) => setTimeout(resolve, 250)) /** Issue a request through the LAN listener. */ function viaLan(path, headers = {}) { return new Promise((resolve, reject) => { const req = netConnect(LAN_PORT, '127.0.0.1', () => { req.write(`GET ${path} HTTP/1.1\r\nHost: 127.0.0.1:${LAN_PORT}\r\nOrigin: http://127.0.0.1:${LAN_PORT}\r\nCookie: dsh-auth-test=abc123\r\nConnection: close\r\n\r\n`) }) let raw = '' req.on('data', (chunk) => { raw += chunk }) req.on('end', () => resolve(raw)) req.on('error', reject) }) } const response = await viaLan('/api/remote.mux') check('LAN listener returns the upstream status', response.startsWith('HTTP/1.1 200'), response.split('\r\n')[0]) check('upstream sees a loopback Host', seen[0]?.host === `127.0.0.1:${upstreamPort}`, seen[0]?.host) check('upstream sees a matching loopback Origin', seen[0]?.origin === `http://127.0.0.1:${upstreamPort}`, seen[0]?.origin) check('upstream receives the browser cookie', seen[0]?.cookie === 'dsh-auth-test=abc123', seen[0]?.cookie) check('Set-Cookie is forwarded untouched', /set-cookie: dsh-auth-test=abc123/i.test(response), 'no set-cookie') check('custom upstream header survives', /x-upstream: yes/i.test(response)) // ── WebSocket upgrade ─────────────────────────────────────────────────────── const wsRaw = await new Promise((resolve, reject) => { const socket = netConnect(LAN_PORT, '127.0.0.1', () => { socket.write( `GET /api/remote.mux HTTP/1.1\r\nHost: 127.0.0.1:${LAN_PORT}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\nOrigin: http://127.0.0.1:${LAN_PORT}\r\n\r\n`, ) }) let raw = '' socket.on('data', (chunk) => { raw += chunk }) socket.on('close', () => resolve(raw)) socket.on('error', reject) setTimeout(() => { socket.destroy(); resolve(raw) }, 900) }) const upgrade = seen.find((entry) => entry.upgrade) check('WebSocket upgrade reached the upstream', upgrade !== undefined) check('upgraded request kept a loopback Host', upgrade?.host === `127.0.0.1:${upstreamPort}`, upgrade?.host) check('upgrade response flows back to the client', /hello-from-upstream/.test(wsRaw)) // ── the panel data route ──────────────────────────────────────────────────── const route = routes.find((entry) => entry.path === '/api/dsh-lan-access/summary') check('summary route registered as exact', route?.kind === 'exact', route?.kind) let body = '' const fakeRes = { writeHead(status, headers) { this.status = status; this.headers = headers }, end(value) { body = value ?? '' }, } route?.handler({ headers: { host: `127.0.0.1:${upstreamPort}` } }, fakeRes) check('summary route requires admission', admitted === 1, `admit calls: ${admitted}`) check('summary reports the listen port', JSON.parse(body).listen.port === LAN_PORT, body) check('summary carries a tokenized LAN url', /token=TESTTOKEN/.test(body), body) // ── pure helpers ──────────────────────────────────────────────────────────── check('isLoopbackHost accepts 127.0.0.1', internals.isLoopbackHost('127.0.0.1') === true) check('isLoopbackHost accepts ::1', internals.isLoopbackHost('::1') === true) check('isLoopbackHost rejects 192.168.1.5', internals.isLoopbackHost('192.168.1.5') === false) check('hostnameOf strips the port', internals.hostnameOf('192.168.1.5:3081') === '192.168.1.5') check('upstreamHeaders drops hop-by-hop', internals.upstreamHeaders({ connection: 'keep-alive', 'x-a': '1' }, 'h:1').connection === undefined) // ── bind failure is reported, never thrown ────────────────────────────────── const blocker = createServer(() => {}) const blockerPort = await listen(blocker) const routes2 = [] const connection2 = { admit: () => ({ peer: {} }), authenticatedUrl: (base) => `${base}?token=T` } const webServer2 = { port: upstreamPort, register: (route) => { routes2.push(route); return () => {} } } const ctx2 = { webServer: webServer2, connection: connection2, get: (name) => (name === 'connection' ? connection2 : name === 'webServer' ? webServer2 : undefined), inject: (deps, callback) => { if (deps.every((d) => ctx2.get(d) !== undefined)) callback(ctx2) return () => {} }, effect: (callback) => { callback(); return () => {} }, } apply(ctx2, { enabled: true, host: '127.0.0.1', port: blockerPort, targetHost: '127.0.0.1', targetPort: upstreamPort, printBanner: false, }) await new Promise((resolve) => setTimeout(resolve, 250)) const route2 = routes2.find((entry) => entry.path === '/api/dsh-lan-access/summary') let body2 = '' route2?.handler({ headers: { host: `127.0.0.1:${upstreamPort}` } }, { writeHead(status) { this.status = status }, end(value) { body2 = value ?? '' }, }) const payload2 = JSON.parse(body2) check('taken port is reported as listening:false', payload2.listening === false, body2) check('taken port carries the bind error', String(payload2.bindError ?? '').includes('EADDRINUSE'), payload2.bindError) blocker.close() // ── ownsHost compat is opt-in and page-conditional ────────────────────────── /** * Mount the plugin with a given config and return the registered index taps. * @param config - extra config over merged onto a loopback test binding. * @returns the tap functions the plugin registered. */ let compatPort = 39_990 async function tapsFor(config) { compatPort += 1 const taps = [] const records = [] const c = { get: () => undefined, inject: (deps, callback) => { callback({ webServer: { port: upstreamPort, register: (route) => { records.push(route); return () => {} }, tapIndex: (transform) => { taps.push(transform); return () => {} }, }, effect: (callback2) => { callback2(); return () => {} }, }) return () => {} }, effect: (callback2) => { callback2(); return () => {} }, } apply(c, { enabled: true, host: '127.0.0.1', port: compatPort, targetHost: '127.0.0.1', targetPort: upstreamPort, printBanner: false, ...config, }) await new Promise((resolve) => setTimeout(resolve, 120)) return taps } const offTaps = await tapsFor({ ownsHostCompat: false }) check('ownsHost compat is off by default', offTaps.length === 0, `taps: ${offTaps.length}`) const onTaps = await tapsFor({ ownsHostCompat: true }) check('ownsHost compat registers one index tap', onTaps.length === 1, `taps: ${onTaps.length}`) const html = 't' const tapped = onTaps[0]?.(html) ?? html const scriptAt = tapped.indexOf('ownsHost = true') const bundleAt = tapped.indexOf('x.js') check('compat script lands in the head', scriptAt !== -1 && tapped.indexOf('') > scriptAt, tapped.slice(0, 80)) check('compat script precedes the app bundle', scriptAt !== -1 && scriptAt < bundleAt) /** Run the compat script against a stubbed page location. */ function runCompat(hostname) { const sandbox = { globalThis: undefined, location: hostname === undefined ? undefined : { hostname } } sandbox.globalThis = sandbox vm.createContext(sandbox) vm.runInContext(OWNS_HOST_COMPAT_SCRIPT, sandbox) return sandbox.__DSH_TRANSPORT__ } check('compat forges ownsHost on a LAN page', runCompat('192.168.1.5')?.ownsHost === true) check('compat leaves a loopback page untouched', runCompat('127.0.0.1') === undefined) check('compat leaves localhost untouched', runCompat('localhost') === undefined) check('compat survives a missing location', runCompat(undefined) === undefined) upstream.close() const failed = results.filter((entry) => !entry.ok) console.log(`\n${results.length - failed.length}/${results.length} checks passed`) process.exit(failed.length === 0 ? 0 : 1)