ftown-bridge 0.19.19 → 0.19.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,399 @@
1
+ /**
2
+ * ftown Solo — solo-server: the single public front server.
3
+ *
4
+ * Composes the routing table from contract.ts (ROUTING PRECEDENCE):
5
+ * 1. exact /api/solo/bootstrap | /api/solo/token | /healthz
6
+ * 2. /hub/* → WS upgrade proxy only (P1), everything else 404
7
+ * 3. other /api/* AND unknown /api/solo/* → existing local API child,
8
+ * with the S18 mechanism applied before forwarding (Host is rewritten
9
+ * by ws-proxy; Origin is replaced here) so the hosted loopback-Host and
10
+ * Origin guards pass byte-for-byte with ZERO edits to hosted files
11
+ * 4. everything else → panel child, or the byte-static placeholder page
12
+ * on GET / while the panel is not yet healthy (S12)
13
+ *
14
+ * Security invariants implemented here: S2 (auth gate before handlers),
15
+ * S3 (rate limiting), S12 (placeholder while children start), S13 (no
16
+ * X-Forwarded-* relay — enforced inside ws-proxy), S14 (no-store everywhere
17
+ * incl. placeholder plus a passthrough guard for proxied responses arriving
18
+ * without cache headers), S18 mechanism, S19 (host validation + absolute-form
19
+ * rejection).
20
+ */
21
+ import { randomInt } from 'node:crypto';
22
+ import http from 'node:http';
23
+ import { HUB_JWT_TTL_SECONDS, SOLO_USER_ID, } from './contract.js';
24
+ import { mintHubJwt, verifyAccessKey } from './solo-auth.js';
25
+ import { handleHubUpgrade, parseHubTarget, proxyHttpRequest } from './ws-proxy.js';
26
+ /**
27
+ * Sliding-window limiter owned by the front server:
28
+ * - key failures: >=10 failures/60s per peer → limited
29
+ * - backstop: >240 /api/solo/* requests/min per peer → limited
30
+ * A successful auth resets that peer's failure count. NO XFF parsing in v1 —
31
+ * peers come exclusively from the injected peerAddress seam (S3).
32
+ */
33
+ export class RateLimiter {
34
+ options;
35
+ keyFailures = new Map();
36
+ backstopHits = new Map();
37
+ constructor(options = {}) {
38
+ this.options = options;
39
+ }
40
+ get windowMs() {
41
+ return this.options.windowMs ?? 60_000;
42
+ }
43
+ /** Record one /api/solo/* request for `peer` and evaluate the backstop (>240/min). */
44
+ hitBackstop(peer) {
45
+ const now = (this.options.now ?? Date.now)();
46
+ const hits = this.slide(this.backstopHits.get(peer) ?? [], now);
47
+ hits.push(now);
48
+ this.backstopHits.set(peer, hits);
49
+ const threshold = this.options.backstopThreshold ?? 240;
50
+ return this.decision(hits.length > threshold);
51
+ }
52
+ /** Record one failed auth for `peer` and evaluate the failure window (>=10/min). */
53
+ recordKeyFailure(peer) {
54
+ const now = (this.options.now ?? Date.now)();
55
+ const failures = this.slide(this.keyFailures.get(peer) ?? [], now);
56
+ failures.push(now);
57
+ this.keyFailures.set(peer, failures);
58
+ const threshold = this.options.keyFailureThreshold ?? 10;
59
+ return this.decision(failures.length >= threshold);
60
+ }
61
+ /** Successful auth: the peer's failure count resets. Backstop is unaffected. */
62
+ resetKeyFailures(peer) {
63
+ this.keyFailures.delete(peer);
64
+ }
65
+ slide(stamps, now) {
66
+ const cutoff = now - this.windowMs;
67
+ let start = 0;
68
+ while (start < stamps.length && stamps[start] <= cutoff)
69
+ start++;
70
+ return stamps.slice(start);
71
+ }
72
+ decision(limited) {
73
+ if (!limited)
74
+ return { limited: false, retryAfterSeconds: 0 };
75
+ const base = this.options.retryAfterBaseSeconds ?? 60;
76
+ const jitter = this.options.retryAfterJitterSeconds ?? 5;
77
+ const bounded = base < jitter ? 0 : jitter;
78
+ const seconds = Math.max(1, base + randomInt(-bounded, bounded + 1));
79
+ return { limited: true, retryAfterSeconds: seconds };
80
+ }
81
+ }
82
+ // ---------- Placeholder page (S12/S14) ----------
83
+ /**
84
+ * Byte-static pre-panel page. ZERO request-derived bytes (S14); the only HTML
85
+ * the front ever generates. Auto-refreshes so the browser lands on the real
86
+ * panel as soon as it becomes healthy behind '/'.
87
+ */
88
+ export const PLACEHOLDER_HTML = '<!doctype html>\n' +
89
+ '<html lang="en">\n' +
90
+ '<head>\n' +
91
+ '<meta charset="utf-8">\n' +
92
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n' +
93
+ '<meta http-equiv="refresh" content="2">\n' +
94
+ '<title>ftown Solo</title>\n' +
95
+ '<style>\n' +
96
+ 'html,body{margin:0;height:100%;background:#0b0e14;color:#c9d4e3;' +
97
+ "font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif}\n" +
98
+ 'body{display:flex;align-items:center;justify-content:center}\n' +
99
+ '.card{text-align:center;padding:2rem 3rem;border:1px solid #1f2937;border-radius:12px;' +
100
+ 'background:#111827}\n' +
101
+ '.dot{display:inline-block;width:10px;height:10px;margin-right:.6rem;border-radius:50%;' +
102
+ 'background:#38bdf8;animation:pulse 1.2s ease-in-out infinite}\n' +
103
+ '@keyframes pulse{0%,100%{opacity:.25}50%{opacity:1}}\n' +
104
+ '</style>\n' +
105
+ '</head>\n' +
106
+ '<body><main class="card"><p><span class="dot"></span>Starting ftown Solo\u2026</p></main></body>\n' +
107
+ '</html>\n';
108
+ // ---------- Small helpers ----------
109
+ const NO_STORE = 'no-store';
110
+ const JSON_TYPE = 'application/json; charset=utf-8';
111
+ const HUB_WS_PATH = '/hub/connection/websocket';
112
+ const WILDCARD_ADDRESSES = new Set(['0.0.0.0', '::', '']);
113
+ function sendJson(res, status, body) {
114
+ // S14: every /api/solo/* response carries no-store — including errors.
115
+ res.writeHead(status, { 'content-type': JSON_TYPE, 'cache-control': NO_STORE });
116
+ res.end(JSON.stringify(body));
117
+ }
118
+ function sendError(res, status, message) {
119
+ sendJson(res, status, { error: message });
120
+ }
121
+ function send429(res, retryAfterSeconds) {
122
+ res.writeHead(429, {
123
+ 'content-type': JSON_TYPE,
124
+ 'cache-control': NO_STORE,
125
+ 'retry-after': String(retryAfterSeconds),
126
+ });
127
+ res.end(JSON.stringify({ error: 'rate limited' }));
128
+ }
129
+ /** Loopback check used by scheme derivation (peer must be loopback for wss). */
130
+ function isLoopbackAddress(address) {
131
+ if (address === '::1' || address === 'localhost')
132
+ return true;
133
+ if (address.startsWith('::ffff:'))
134
+ return isLoopbackAddress(address.slice('::ffff:'.length));
135
+ return address.startsWith('127.');
136
+ }
137
+ /** Bearer extraction per RFC 6750 (case-insensitive scheme, single token). */
138
+ function bearerToken(req) {
139
+ const header = req.headers.authorization;
140
+ if (typeof header !== 'string')
141
+ return null;
142
+ const match = /^Bearer (\S+)$/i.exec(header.trim());
143
+ return match?.[1] ?? null;
144
+ }
145
+ /** Split a Host header into lowercase hostname + numeric port (IPv6 aware). */
146
+ function splitHostHeader(hostHeader) {
147
+ const trimmed = hostHeader.trim();
148
+ if (trimmed.startsWith('[')) {
149
+ const close = trimmed.indexOf(']');
150
+ if (close !== -1) {
151
+ const hostname = trimmed.slice(0, close + 1).toLowerCase();
152
+ const rest = trimmed.slice(close + 1);
153
+ if (rest.startsWith(':')) {
154
+ const port = Number.parseInt(rest.slice(1), 10);
155
+ return { hostname, port: Number.isFinite(port) ? port : null };
156
+ }
157
+ return { hostname, port: null };
158
+ }
159
+ }
160
+ const colon = trimmed.lastIndexOf(':');
161
+ if (colon !== -1) {
162
+ const port = Number.parseInt(trimmed.slice(colon + 1), 10);
163
+ if (Number.isFinite(port)) {
164
+ return { hostname: trimmed.slice(0, colon).toLowerCase(), port };
165
+ }
166
+ }
167
+ return { hostname: trimmed.toLowerCase(), port: null };
168
+ }
169
+ /**
170
+ * S19: the reflected Host must be THIS server's local address:port (or a
171
+ * member of the injected allowedHosts seam). Wildcard binds accept any
172
+ * hostname whose port matches the bound port — tunnels/LAN deployments pass
173
+ * allowedHosts to tighten further. Absolute-form request lines are rejected
174
+ * by the router before this runs.
175
+ */
176
+ function isAllowedHost(hostHeader, bound, allowedHosts) {
177
+ if (allowedHosts !== undefined && allowedHosts.length > 0) {
178
+ return allowedHosts.includes(hostHeader);
179
+ }
180
+ const split = splitHostHeader(hostHeader);
181
+ if (split.port !== bound.port)
182
+ return false;
183
+ if (WILDCARD_ADDRESSES.has(bound.address))
184
+ return true;
185
+ const normalizedBound = bound.address.toLowerCase();
186
+ if (split.hostname === normalizedBound)
187
+ return true;
188
+ if (isLoopbackAddress(normalizedBound)) {
189
+ return (split.hostname === 'localhost' || split.hostname === '127.0.0.1' || split.hostname === '[::1]');
190
+ }
191
+ return false;
192
+ }
193
+ function isWebSocketUpgrade(req) {
194
+ const upgrade = req.headers.upgrade;
195
+ if (typeof upgrade !== 'string' || upgrade.toLowerCase() !== 'websocket')
196
+ return false;
197
+ const connection = req.headers.connection;
198
+ return typeof connection === 'string' && connection.toLowerCase().includes('upgrade');
199
+ }
200
+ /**
201
+ * S14 passthrough guard: patch res.writeHead so any proxied response that
202
+ * arrives WITHOUT cache headers gets Cache-Control: no-store before it
203
+ * reaches the client. Upstream-provided cache headers pass untouched.
204
+ */
205
+ function applyNoStorePassthrough(res) {
206
+ const original = res.writeHead.bind(res);
207
+ const wrapped = (...args) => {
208
+ for (let i = 1; i < args.length; i++) {
209
+ const arg = args[i];
210
+ if (arg !== null && typeof arg === 'object' && !Array.isArray(arg)) {
211
+ const headers = arg;
212
+ const hasCacheHeader = Object.keys(headers).some((name) => {
213
+ const lower = name.toLowerCase();
214
+ return lower === 'cache-control' || lower === 'expires' || lower === 'pragma';
215
+ });
216
+ if (!hasCacheHeader)
217
+ headers['cache-control'] = NO_STORE;
218
+ break;
219
+ }
220
+ }
221
+ return original(...args);
222
+ };
223
+ res.writeHead = wrapped;
224
+ }
225
+ // ---------- createSoloServer ----------
226
+ export async function createSoloServer(options) {
227
+ const { config, localApiPort } = options;
228
+ const peerAddress = options.peerAddress ?? ((req) => req.socket.remoteAddress ?? 'unknown');
229
+ const limiter = options.rateLimiter ?? new RateLimiter();
230
+ const server = http.createServer((req, res) => {
231
+ void routeRequest(req, res).catch(() => {
232
+ if (!res.headersSent)
233
+ sendError(res, 500, 'internal error');
234
+ else
235
+ res.destroy();
236
+ });
237
+ });
238
+ // WS upgrades: P1 allowlist only. Genuine upgrades never reach the request
239
+ // handler in Node — they arrive here. Anything non-conforming is rejected
240
+ // at the socket without ever touching the hub.
241
+ server.on('upgrade', (req, socket, head) => {
242
+ if (parseHubTarget(req.url ?? '').isHubUpgradePath && isWebSocketUpgrade(req)) {
243
+ handleHubUpgrade(req, socket, head, config.hubPort);
244
+ return;
245
+ }
246
+ socket.write('HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
247
+ socket.destroy();
248
+ });
249
+ await new Promise((resolve, reject) => {
250
+ server.once('error', reject);
251
+ server.listen(config.port, options.host, () => resolve());
252
+ });
253
+ const bound = server.address();
254
+ async function routeRequest(req, res) {
255
+ // ONE parsed representation of req.url feeds routing + proxying (S20).
256
+ const rawUrl = req.url ?? '/';
257
+ // S19: absolute-form request lines are rejected outright.
258
+ if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) {
259
+ sendError(res, 400, 'bad request');
260
+ return;
261
+ }
262
+ const path = rawUrl.split('?')[0].split('#')[0];
263
+ // ---- (1) exact match: unauthenticated liveness -------------------------
264
+ if (path === '/healthz' && req.method === 'GET') {
265
+ const health = {
266
+ ok: true,
267
+ hub: options.hub.isHealthy() ? 'up' : 'down',
268
+ panel: options.panel.isHealthy() ? 'up' : 'down',
269
+ };
270
+ res.writeHead(200, { 'content-type': JSON_TYPE, 'cache-control': NO_STORE });
271
+ res.end(JSON.stringify(health));
272
+ return;
273
+ }
274
+ // ---- Auth gate BEFORE any /api/solo/* handler runs (S2/S3) -------------
275
+ if (path === '/api/solo' || path.startsWith('/api/solo/')) {
276
+ const peer = peerAddress(req);
277
+ const backstop = limiter.hitBackstop(peer);
278
+ if (backstop.limited) {
279
+ send429(res, backstop.retryAfterSeconds);
280
+ return;
281
+ }
282
+ const presented = bearerToken(req);
283
+ if (presented === null || !verifyAccessKey(presented, config.accessKeyHash)) {
284
+ const failure = limiter.recordKeyFailure(peer);
285
+ if (failure.limited)
286
+ send429(res, failure.retryAfterSeconds);
287
+ else
288
+ sendError(res, 401, 'unauthorized');
289
+ return;
290
+ }
291
+ limiter.resetKeyFailures(peer);
292
+ // Authenticated: exact endpoints first...
293
+ if (path === '/api/solo/bootstrap' && req.method === 'GET') {
294
+ handleBootstrap(req, res);
295
+ return;
296
+ }
297
+ if (path === '/api/solo/token' && req.method === 'POST') {
298
+ handleToken(res);
299
+ return;
300
+ }
301
+ // ...then fall-through to the EXISTING bridge API with the S18
302
+ // mechanism (covers unknown /api/solo/<segment> paths).
303
+ forwardToLocalApi(req, res);
304
+ return;
305
+ }
306
+ // ---- (2) /hub/* : upgrades only (P1); plain HTTP always 404 ------------
307
+ if (path === '/hub' || path.startsWith('/hub/')) {
308
+ if (parseHubTarget(rawUrl).isHubUpgradePath && req.method === 'GET' && isWebSocketUpgrade(req)) {
309
+ handleHubUpgrade(req, req.socket, Buffer.alloc(0), config.hubPort);
310
+ return;
311
+ }
312
+ sendError(res, 404, 'not found');
313
+ return;
314
+ }
315
+ // ---- (3) remaining /api/* → the EXISTING bridge local API --------------
316
+ if (path === '/api' || path.startsWith('/api/')) {
317
+ forwardToLocalApi(req, res);
318
+ return;
319
+ }
320
+ // ---- (4) everything else → panel, or placeholder on / pre-panel (S12) --
321
+ const panelHealthy = options.panel.isHealthy();
322
+ if (path === '/' && !panelHealthy) {
323
+ res.writeHead(200, {
324
+ 'content-type': 'text/html; charset=utf-8',
325
+ 'cache-control': NO_STORE,
326
+ });
327
+ res.end(PLACEHOLDER_HTML);
328
+ return;
329
+ }
330
+ if (!panelHealthy) {
331
+ sendError(res, 502, 'panel unavailable');
332
+ return;
333
+ }
334
+ applyNoStorePassthrough(res);
335
+ proxyHttpRequest(req, res, config.panelPort);
336
+ }
337
+ /**
338
+ * Forward to the existing local API implementing the S18 MECHANISM: mutate
339
+ * the inbound Origin BEFORE ws-proxy builds its sanitized hop (it rewrites
340
+ * Host itself), so the hosted loopback-Host guard AND Origin allowlist both
341
+ * pass byte-for-byte with zero edits to hosted files. Inbound
342
+ * X-Forwarded-* never survives (stripped inside ws-proxy, S13).
343
+ */
344
+ function forwardToLocalApi(req, res) {
345
+ const origin = req.headers.origin;
346
+ if (typeof origin === 'string' && origin !== '') {
347
+ req.headers['origin'] = `http://127.0.0.1:${String(localApiPort)}`;
348
+ }
349
+ applyNoStorePassthrough(res);
350
+ proxyHttpRequest(req, res, localApiPort);
351
+ }
352
+ function handleBootstrap(req, res) {
353
+ const hostHeader = req.headers.host;
354
+ if (typeof hostHeader !== 'string' ||
355
+ hostHeader.length === 0 ||
356
+ !isAllowedHost(hostHeader, { address: bound.address, port: bound.port }, options.allowedHosts)) {
357
+ sendError(res, 400, 'invalid host header');
358
+ return;
359
+ }
360
+ // Scheme derivation: wss ONLY when X-Forwarded-Proto is https AND the
361
+ // socket peer is loopback (a locally-running tunnel sets both); else ws.
362
+ const forwardedProto = req.headers['x-forwarded-proto'];
363
+ const protoIsHttps = typeof forwardedProto === 'string' && forwardedProto.split(',')[0]?.trim() === 'https';
364
+ const scheme = protoIsHttps && isLoopbackAddress(peerAddress(req)) ? 'wss' : 'ws';
365
+ const bootstrap = {
366
+ userId: SOLO_USER_ID,
367
+ token: mintHubJwt({ secret: config.hubSecret, ttlSeconds: options.mintTtlSeconds }),
368
+ centrifugoUrl: `${scheme}://${hostHeader}${HUB_WS_PATH}`,
369
+ };
370
+ res.writeHead(200, { 'content-type': JSON_TYPE, 'cache-control': NO_STORE });
371
+ res.end(JSON.stringify(bootstrap));
372
+ }
373
+ function handleToken(res) {
374
+ const ttl = options.mintTtlSeconds ?? HUB_JWT_TTL_SECONDS;
375
+ const payload = {
376
+ token: mintHubJwt({ secret: config.hubSecret, ttlSeconds: options.mintTtlSeconds }),
377
+ expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
378
+ };
379
+ res.writeHead(200, { 'content-type': JSON_TYPE, 'cache-control': NO_STORE });
380
+ res.end(JSON.stringify(payload));
381
+ }
382
+ const tracked = new Set();
383
+ server.on('connection', (socket) => {
384
+ tracked.add(socket);
385
+ socket.on('close', () => tracked.delete(socket));
386
+ });
387
+ return {
388
+ port: bound.port,
389
+ async close() {
390
+ await new Promise((resolve) => {
391
+ server.close(() => resolve());
392
+ for (const socket of tracked)
393
+ socket.destroy();
394
+ server.closeAllConnections?.();
395
+ });
396
+ },
397
+ };
398
+ }
399
+ //# sourceMappingURL=solo-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solo-server.js","sourceRoot":"","sources":["../../src/solo/solo-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { Duplex } from 'node:stream';
3
+ /**
4
+ * ws-proxy — P1-P5 proxying for solo mode (contract v4).
5
+ *
6
+ * S20 SINGLE-PARSE SEAM — the ONE coherent interpretation of routing-table row
7
+ * "GET /hub/connection/websocket → proxied to hub" + P1 + the hub-config note:
8
+ *
9
+ * 1. The PUBLIC request path is matched against ^/hub/connection/websocket$
10
+ * (case-sensitive, exact — no trailing segments, no percent-encoded
11
+ * slashes, no double slashes, no NUL suffixes; query strings allowed).
12
+ * 2. On match, the FORWARD path sent to the hub is the public path with the
13
+ * single leading '/hub' prefix stripped → '/connection/websocket', which
14
+ * is centrifugo's default path (no hub path options needed).
15
+ *
16
+ * (P1's regex describes the public allowlist; stripping happens only for
17
+ * forwarding. Anything else under /hub is never forwarded — solo-server 404s
18
+ * it; this module exposes only parse + forwarding.)
19
+ */
20
+ /** Public path allowlist (P1) — exact match required. */
21
+ export declare const HUB_UPGRADE_PUBLIC_PATH = "/hub/connection/websocket";
22
+ /** Forward path to the hub after stripping the single leading '/hub'. */
23
+ export declare const HUB_UPSTREAM_WS_PATH = "/connection/websocket";
24
+ /**
25
+ * Inbound hop-by-hop headers stripped per P3 — exact contract list.
26
+ */
27
+ export declare const HOP_BY_HOP_HEADERS: readonly string[];
28
+ /**
29
+ * S20 seam: parse req.url ONCE and decide whether it targets the proxied hub
30
+ * upgrade path. Percent-decoded / differently-cased / extra-segment variants
31
+ * are NOT the allowlist path (goldens pinned in tests).
32
+ */
33
+ export declare function parseHubTarget(url: string): {
34
+ isHubUpgradePath: boolean;
35
+ };
36
+ /**
37
+ * P2/P3/P4 plain-HTTP proxy hop to 127.0.0.1:<targetPort>. Request and response
38
+ * body streams are piped both directions; status/headers propagate minus
39
+ * hop-by-hop. Upstream failure yields {"error":...} with 502 (headers not yet
40
+ * sent) or destroys the response.
41
+ */
42
+ export declare function proxyHttpRequest(req: IncomingMessage, res: ServerResponse, targetPort: number, forwardedProto?: string): void;
43
+ /**
44
+ * WebSocket upgrade proxy (P1/P2/P3/P5): validates the S20 allowlist first,
45
+ * then opens an HTTP upgrade to 127.0.0.1:<targetPort> at
46
+ * HUB_UPSTREAM_WS_PATH with sanitized headers. All inbound sec-websocket-*
47
+ * are stripped (no extension offer survives — compression stays off), then
48
+ * the handshake identity is re-set: version 13 plus the CLIENT'S OWN
49
+ * Sec-WebSocket-Key (fresh random fallback). Preserving the client key lets
50
+ * the UPSTREAM handshake compute a Sec-WebSocket-Accept that validates
51
+ * end-to-end while this module recomputes nothing locally. On upstream 101
52
+ * the upstream-computed sec-websocket-* headers are relayed verbatim, then
53
+ * both sockets are piped so protocol pings/pongs pass untouched (P5).
54
+ */
55
+ export declare function handleHubUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, targetPort: number, forwardedProto?: string): void;