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.
- package/dist/index.js +228 -3
- package/dist/index.js.map +1 -1
- package/dist/solo/contract.d.ts +104 -0
- package/dist/solo/contract.js +248 -0
- package/dist/solo/contract.js.map +1 -0
- package/dist/solo/hub-manager.d.ts +85 -0
- package/dist/solo/hub-manager.js +381 -0
- package/dist/solo/hub-manager.js.map +1 -0
- package/dist/solo/panel-manager.d.ts +129 -0
- package/dist/solo/panel-manager.js +715 -0
- package/dist/solo/panel-manager.js.map +1 -0
- package/dist/solo/solo-auth.d.ts +25 -0
- package/dist/solo/solo-auth.js +83 -0
- package/dist/solo/solo-auth.js.map +1 -0
- package/dist/solo/solo-server.d.ts +105 -0
- package/dist/solo/solo-server.js +399 -0
- package/dist/solo/solo-server.js.map +1 -0
- package/dist/solo/ws-proxy.d.ts +55 -0
- package/dist/solo/ws-proxy.js +228 -0
- package/dist/solo/ws-proxy.js.map +1 -0
- package/package.json +1 -1
|
@@ -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;
|