@mmmbuto/nexuscrew 0.8.58 → 0.9.1
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/CHANGELOG.md +197 -2
- package/README.md +23 -4
- package/frontend/dist/assets/index-0vuhL1YP.css +32 -0
- package/frontend/dist/assets/index-Bu_-2-Uu.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/audio/adapters.js +50 -6
- package/lib/cli/commands.js +54 -2
- package/lib/cli/doctor.js +95 -12
- package/lib/cli/init.js +25 -3
- package/lib/cli/path.js +43 -10
- package/lib/cli/pidfile.js +23 -2
- package/lib/config.js +24 -0
- package/lib/fleet/builtin.js +161 -19
- package/lib/fleet/catalogs/opencode-go.json +50 -4
- package/lib/fleet/cell-exec.js +87 -9
- package/lib/fleet/cell-lease-server.js +719 -0
- package/lib/fleet/cell-lease.js +112 -0
- package/lib/fleet/definitions.js +101 -7
- package/lib/fleet/launch-broker.js +115 -3
- package/lib/fleet/lease-client.js +191 -0
- package/lib/fleet/lease-routes.js +92 -0
- package/lib/fleet/lease-verifier.js +230 -0
- package/lib/fleet/managed.js +366 -48
- package/lib/fleet/prompt-delivery.js +50 -2
- package/lib/fleet/provider.js +1 -1
- package/lib/fleet/runtime.js +53 -6
- package/lib/live-host/bridge.js +369 -0
- package/lib/live-host/routes.js +190 -0
- package/lib/live-host/store.js +96 -0
- package/lib/mcp/tools.js +51 -0
- package/lib/nodes/commands.js +21 -3
- package/lib/nodes/store.js +57 -0
- package/lib/nodes/tunnel.js +23 -1
- package/lib/proxy/federation.js +130 -9
- package/lib/proxy/node-proxy.js +33 -0
- package/lib/proxy/panel-auth.js +337 -0
- package/lib/proxy/panel-proxy.js +336 -0
- package/lib/server.js +252 -7
- package/lib/settings/pairing-coordinator.js +32 -0
- package/lib/settings/public-peering-routes.js +13 -1
- package/package.json +1 -1
- package/skills/aidesktop/SKILL.md +201 -0
- package/skills/aidesktop/docker/Dockerfile +21 -0
- package/skills/aidesktop/docker/custom-cont-init.d/10-cdp-relay.sh +20 -0
- package/skills/aidesktop/docker/docker-compose.example.yml +75 -0
- package/skills/crew/SKILL.md +15 -0
- package/skills/live/SKILL.md +90 -0
- package/skills/mail-assistant/SKILL.md +15 -0
- package/skills/memory/SKILL.md +15 -0
- package/skills/nexuscrew/SKILL.md +113 -0
- package/skills/nexuscrew-agent/SKILL.md +18 -0
- package/skills/vl-msa/SKILL.md +15 -0
- package/frontend/dist/assets/index-BEGNtmx2.js +0 -93
- package/frontend/dist/assets/index-CYi_lhCg.css +0 -32
- package/skills/alibaba-token-media/SKILL.md +0 -133
- package/skills/alibaba-token-media/agents/openai.yaml +0 -4
- package/skills/alibaba-token-media/references/api-contract.md +0 -97
- package/skills/alibaba-token-media/scripts/alibaba_token_media.py +0 -550
- package/skills/fill-forms/SKILL.md +0 -154
- package/skills/fill-forms/agents/openai.yaml +0 -4
- package/skills/fill-forms/references/overlay-technique.md +0 -99
- package/skills/fill-forms/requirements.txt +0 -4
- package/skills/fill-forms/scripts/dump_docx.py +0 -70
- package/skills/fill-forms/scripts/fill_docx.py +0 -207
- package/skills/fill-forms/scripts/fill_pdf.py +0 -424
- package/skills/fill-forms/scripts/inspect_pdf.py +0 -188
- package/skills/fill-forms/scripts/prepare_signature.py +0 -171
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// lib/proxy/panel-proxy.js — inoltro del pannello di UNA cella locale.
|
|
3
|
+
//
|
|
4
|
+
// Perche' esiste: `panelUrl` e' un endpoint su loopback (es. il desktop in
|
|
5
|
+
// container). L'iframe della PWA risolve `127.0.0.1` sul browser di CHI GUARDA,
|
|
6
|
+
// quindi il pannello di una cella si vede solo stando sulla stessa macchina.
|
|
7
|
+
// Questa route mette il traffico dalla parte giusta del loopback.
|
|
8
|
+
//
|
|
9
|
+
// TRE VINCOLI, che sono il disegno e non dettagli:
|
|
10
|
+
//
|
|
11
|
+
// 1. LA DESTINAZIONE NON ARRIVA MAI DAL CHIAMANTE. Si risolve dal `panelUrl`
|
|
12
|
+
// della cella indicata, e quella cella deve essere LOCALE. Non e' un
|
|
13
|
+
// port-forward: un cellId sconosciuto o senza pannello e' un rifiuto, non un
|
|
14
|
+
// default. Chi chiama sceglie QUALE cella, mai VERSO DOVE.
|
|
15
|
+
//
|
|
16
|
+
// 2. IL TOKEN DI NEXUSCREW NON ESCE DA QUI. Il pannello non e' un nodo della
|
|
17
|
+
// federazione: e' un servizio terzo che gira accanto. Inoltrargli la nostra
|
|
18
|
+
// Authorization sarebbe consegnare la credenziale del control plane a un
|
|
19
|
+
// container. L'header viene rimosso, e l'eventuale 401 del pannello resta
|
|
20
|
+
// suo — l'autenticazione al contenuto avviene dentro il frame.
|
|
21
|
+
//
|
|
22
|
+
// 3. IL VALIDATORE E' QUELLO, NON UN SECONDO. `validPanelUrl` e' importato da
|
|
23
|
+
// fleet/definitions.js, la stessa funzione che accetta il campo quando viene
|
|
24
|
+
// scritto. Riscriverne una copia qui creerebbe due decisioni sullo stesso
|
|
25
|
+
// fatto, e la seconda divergerebbe: e' il difetto che passiamo le giornate a
|
|
26
|
+
// chiudere.
|
|
27
|
+
//
|
|
28
|
+
// Certificato self-signed: il container tipico serve HTTPS con un certificato
|
|
29
|
+
// che nessuno ha firmato, e non possiamo installarlo nel trust store della
|
|
30
|
+
// macchina di chi ci ospita. Verso di lui la verifica e' disattivata di
|
|
31
|
+
// proposito, ma SOLO se la destinazione e' loopback — e la condizione viene
|
|
32
|
+
// ricontrollata qui contro la stessa lista che autorizza il campo, non data per
|
|
33
|
+
// scontata dal validatore. Fra noi e una porta della stessa macchina non c'e'
|
|
34
|
+
// un uomo in mezzo da temere; verso qualunque altro host la verifica resta
|
|
35
|
+
// attiva e il collegamento fallisce invece di degradare in silenzio. In cambio, il browser del visualizzatore parla solo con la NOSTRA
|
|
36
|
+
// origine: il frame smette di restare bianco in attesa che qualcuno accetti
|
|
37
|
+
// quel certificato in una scheda separata.
|
|
38
|
+
|
|
39
|
+
const http = require('node:http');
|
|
40
|
+
const https = require('node:https');
|
|
41
|
+
const { validPanelUrl, PANELURL_LOOPBACK_HOSTS } = require('../fleet/definitions.js');
|
|
42
|
+
const { CELL_ID_RE } = require('../live-host/store.js');
|
|
43
|
+
|
|
44
|
+
const PANEL_TIMEOUT_MS = 30000;
|
|
45
|
+
|
|
46
|
+
// Hop-by-hop (RFC 7230 §6.1) + Proxy-*: mai inoltrati end-to-end.
|
|
47
|
+
const HOP_BY_HOP = new Set([
|
|
48
|
+
'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
|
|
49
|
+
'te', 'trailer', 'transfer-encoding', 'upgrade',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// `/api/panel/<cellId>` oppure `/api/panel/<cellId>/<rest...>`.
|
|
53
|
+
function splitPanelPath(url) {
|
|
54
|
+
const raw = String(url || '');
|
|
55
|
+
const qIndex = raw.indexOf('?');
|
|
56
|
+
const pathname = qIndex === -1 ? raw : raw.slice(0, qIndex);
|
|
57
|
+
const search = qIndex === -1 ? '' : raw.slice(qIndex);
|
|
58
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
59
|
+
if (!parts.length) return null;
|
|
60
|
+
const cellId = decodeURIComponent(parts[0]);
|
|
61
|
+
const tail = parts.slice(1);
|
|
62
|
+
// Un `..` verrebbe inoltrato cosi' com'e' e normalizzato dal pannello: non
|
|
63
|
+
// cambia host ne' porta, ma e' comunque un percorso che il chiamante disegna
|
|
64
|
+
// dentro il pannello. Si ferma qui, in entrambe le forme in cui puo' arrivare.
|
|
65
|
+
for (const seg of tail) {
|
|
66
|
+
const decoded = (() => { try { return decodeURIComponent(seg); } catch (_) { return seg; } })();
|
|
67
|
+
if (seg === '..' || decoded === '..') return null;
|
|
68
|
+
}
|
|
69
|
+
const rest = tail.length ? `/${tail.join('/')}` : '/';
|
|
70
|
+
return { cellId, rest, search };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Il token locale puo' viaggiare in query (il browser non puo' mettere header
|
|
74
|
+
// sull'upgrade WebSocket), e cosi' il ticket d'ingresso dell'iframe: nessuno
|
|
75
|
+
// dei due deve proseguire verso il pannello.
|
|
76
|
+
function stripLocalTokenQuery(search) {
|
|
77
|
+
if (!search) return '';
|
|
78
|
+
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);
|
|
79
|
+
params.delete('token');
|
|
80
|
+
params.delete('ticket');
|
|
81
|
+
const out = params.toString();
|
|
82
|
+
return out ? `?${out}` : '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Gli stessi header che il proxy verso i nodi non inoltra. Avevo lasciato
|
|
86
|
+
// passare i cookie con la motivazione «sono del pannello»: era **falsa**.
|
|
87
|
+
// Dietro questo proxy l'origine e' la NOSTRA, quindi il browser manda i cookie
|
|
88
|
+
// del nostro dominio — inoltrarli significherebbe consegnare al container la
|
|
89
|
+
// sessione del control plane, esattamente cio' che l'Authorization rimossa
|
|
90
|
+
// doveva impedire. E `x-forwarded-*` da un client sono valori che un pannello
|
|
91
|
+
// potrebbe credere veri. Rilievo di un audit indipendente.
|
|
92
|
+
function isStrippedRequestHeader(key) {
|
|
93
|
+
if (HOP_BY_HOP.has(key)) return true;
|
|
94
|
+
if (key === 'authorization' || key === 'cookie' || key === 'host') return true;
|
|
95
|
+
// `referer` porterebbe il ticket d'ingresso (viaggiava in query sulla prima
|
|
96
|
+
// richiesta dell'iframe) fino al pannello: al container non deve arrivare
|
|
97
|
+
// NESSUNA credenziale, nemmeno di seconda mano.
|
|
98
|
+
if (key === 'referer') return true;
|
|
99
|
+
if (key.startsWith('proxy-')) return true;
|
|
100
|
+
if (key === 'forwarded' || key.startsWith('x-forwarded-')) return true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function forwardHeaders(headers, targetHost) {
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const [k, v] of Object.entries(headers || {})) {
|
|
107
|
+
if (isStrippedRequestHeader(k.toLowerCase())) continue;
|
|
108
|
+
out[k] = v;
|
|
109
|
+
}
|
|
110
|
+
// L'host deve essere quello della destinazione: un pannello che genera
|
|
111
|
+
// redirect assoluti li costruisce da qui.
|
|
112
|
+
out.host = targetHost;
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Header che il PANNELLO non deve poter porre sulla nostra origine. Sono una
|
|
117
|
+
// cosa diversa dagli hop-by-hop: quelli non hanno senso oltre un hop, questi
|
|
118
|
+
// avrebbero senso e proprio per questo vanno tolti.
|
|
119
|
+
//
|
|
120
|
+
// `set-cookie`: il pannello e' servito sotto la nostra origine, quindi un suo
|
|
121
|
+
// cookie atterra li'. Non gli darebbe l'accesso al pannello di un'altra cella
|
|
122
|
+
// — il cookie di visione non e' autoportante, `verifyCookie` cerca il valore
|
|
123
|
+
// in una mappa del server e un valore inventato non c'e' — ma puo'
|
|
124
|
+
// SOVRASCRIVERE o invalidare il cookie di visione legittimo, cioe' rompere il
|
|
125
|
+
// pannello dell'utente dal di dentro. E puo' piantare cookie arbitrari sulla
|
|
126
|
+
// nostra origine.
|
|
127
|
+
const NON_INOLTRATI_DAL_PANNELLO = new Set(['set-cookie']);
|
|
128
|
+
|
|
129
|
+
function responseHeaders(headers) {
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const [k, v] of Object.entries(headers || {})) {
|
|
132
|
+
const key = k.toLowerCase();
|
|
133
|
+
if (HOP_BY_HOP.has(key)) continue;
|
|
134
|
+
if (NON_INOLTRATI_DAL_PANNELLO.has(key)) continue;
|
|
135
|
+
out[k] = v;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Risolve la destinazione, o dice PERCHE' non si puo'. I motivi restano
|
|
141
|
+
// distinti: «cella sconosciuta», «cella senza pannello» e «pannello non valido»
|
|
142
|
+
// mandano chi indaga in tre posti diversi, e un rifiuto unico li confonderebbe.
|
|
143
|
+
async function resolveTarget(resolveCellPanel, cellId) {
|
|
144
|
+
if (!CELL_ID_RE.test(cellId)) return { ok: false, reason: 'cell-id-invalid' };
|
|
145
|
+
let panelUrl;
|
|
146
|
+
try { panelUrl = await resolveCellPanel(cellId); } catch (_) { return { ok: false, reason: 'fleet-unavailable' }; }
|
|
147
|
+
if (panelUrl === null) return { ok: false, reason: 'fleet-unavailable' };
|
|
148
|
+
if (panelUrl === undefined) return { ok: false, reason: 'cell-unknown' };
|
|
149
|
+
if (panelUrl === '') return { ok: false, reason: 'no-panel' };
|
|
150
|
+
// Stesso validatore della scrittura: se un valore e' finito nello stato per
|
|
151
|
+
// un'altra strada, qui viene fermato comunque.
|
|
152
|
+
if (!validPanelUrl(panelUrl)) return { ok: false, reason: 'panel-url-invalid' };
|
|
153
|
+
const parsed = new URL(panelUrl);
|
|
154
|
+
// Guardia in profondita': la verifica TLS viene disattivata SOLO se la
|
|
155
|
+
// destinazione e' loopback, e questa riga lo ricontrolla contro la stessa
|
|
156
|
+
// lista che autorizza il campo. Se un giorno il validatore ammettesse un host
|
|
157
|
+
// remoto, la disattivazione NON lo seguirebbe: si parlerebbe in TLS
|
|
158
|
+
// verificato, o si fallirebbe — mai in chiaro con un ignoto.
|
|
159
|
+
const loopback = PANELURL_LOOPBACK_HOSTS.has(parsed.hostname);
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
loopback,
|
|
163
|
+
secure: parsed.protocol === 'https:',
|
|
164
|
+
host: parsed.hostname,
|
|
165
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
166
|
+
hostHeader: parsed.host,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// IL PATH DEL PANNELLO E' `rest`, TAL QUALE. Il contratto D8 (CellPanel): il
|
|
171
|
+
// frame carica /api/panel/<cella> + il pathname del panelUrl, e il browser
|
|
172
|
+
// risolve le sotto-risorse RELATIVE rispetto a quell'origine — al proxy
|
|
173
|
+
// arrivano sempre path assoluti del pannello. Ricomporli col basePath del
|
|
174
|
+
// panelUrl (joinPath, rimossa) li RADDOPPIAVA appena il panelUrl aveva un
|
|
175
|
+
// path: /vnc/lite.html diventava /vnc/lite.html/vnc/lite.html, il pannello
|
|
176
|
+
// rispondeva 404 e il frame restava su «not found» — provato nel browser
|
|
177
|
+
// vero. Il pathname del panelUrl NON e' un prefisso da applicare: e' la
|
|
178
|
+
// prima richiesta che il client fa, non una radice.
|
|
179
|
+
|
|
180
|
+
// Risponde con o senza Express. Il resto del progetto monta le route su un
|
|
181
|
+
// router Express e usa `res.status().json()`, ma questo modulo e' un pezzo di
|
|
182
|
+
// trasporto: farlo dipendere dal framework significa che si rompe appena lo si
|
|
183
|
+
// usa altrove — ed e' successo alla prima prova con un server nudo, mentre la
|
|
184
|
+
// suite mockata non poteva accorgersene perche' il finto offriva `status()`.
|
|
185
|
+
function respond(res, code, body) {
|
|
186
|
+
const payload = JSON.stringify(body);
|
|
187
|
+
if (typeof res.status === 'function' && typeof res.json === 'function') {
|
|
188
|
+
res.status(code).json(body);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
res.writeHead(code, { 'content-type': 'application/json' });
|
|
192
|
+
res.end(payload);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function createPanelProxy({ resolveCellPanel, log = () => {}, requestImpl }) {
|
|
196
|
+
return async function panelProxy(req, res) {
|
|
197
|
+
const parsed = splitPanelPath(req.url);
|
|
198
|
+
if (!parsed) {
|
|
199
|
+
log({ event: 'panel-proxy', outcome: 'rejected', reason: 'no-cell', cell: '' });
|
|
200
|
+
return respond(res, 404, { error: 'pannello non trovato' });
|
|
201
|
+
}
|
|
202
|
+
const target = await resolveTarget(resolveCellPanel, parsed.cellId);
|
|
203
|
+
if (!target.ok) {
|
|
204
|
+
log({ event: 'panel-proxy', outcome: 'rejected', reason: target.reason, cell: parsed.cellId });
|
|
205
|
+
return respond(res, 404, { error: `pannello non disponibile: ${target.reason}` });
|
|
206
|
+
}
|
|
207
|
+
const request = requestImpl || (target.secure ? https.request : http.request);
|
|
208
|
+
const options = {
|
|
209
|
+
host: target.host,
|
|
210
|
+
port: target.port,
|
|
211
|
+
method: req.method,
|
|
212
|
+
path: `${parsed.rest}${stripLocalTokenQuery(parsed.search)}`,
|
|
213
|
+
headers: forwardHeaders(req.headers, target.hostHeader),
|
|
214
|
+
...(target.secure ? { rejectUnauthorized: !(target.loopback) } : {}),
|
|
215
|
+
};
|
|
216
|
+
let upstream;
|
|
217
|
+
try {
|
|
218
|
+
upstream = request(options, (up) => {
|
|
219
|
+
res.writeHead(up.statusCode, responseHeaders(up.headers));
|
|
220
|
+
up.pipe(res);
|
|
221
|
+
});
|
|
222
|
+
} catch (_) {
|
|
223
|
+
log({ event: 'panel-proxy', outcome: 'error', reason: 'request-failed', cell: parsed.cellId });
|
|
224
|
+
if (!res.headersSent) respond(res, 502, { error: 'pannello non raggiungibile' });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
upstream.setTimeout(PANEL_TIMEOUT_MS, () => upstream.destroy(new Error('panel timeout')));
|
|
228
|
+
upstream.on('error', () => {
|
|
229
|
+
log({ event: 'panel-proxy', outcome: 'error', reason: 'upstream-error', cell: parsed.cellId });
|
|
230
|
+
if (!res.headersSent) respond(res, 502, { error: 'pannello non raggiungibile' });
|
|
231
|
+
else res.destroy();
|
|
232
|
+
});
|
|
233
|
+
req.on('aborted', () => upstream.destroy());
|
|
234
|
+
log({ event: 'panel-proxy', outcome: 'forwarded', cell: parsed.cellId });
|
|
235
|
+
req.pipe(upstream);
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Upgrade WebSocket. Un pannello che si apre senza i suoi frame e' nero: un
|
|
240
|
+
// inoltro solo-HTTP darebbe esattamente quello, con l'aria di funzionare.
|
|
241
|
+
//
|
|
242
|
+
// `authorize(req, url)` decide chi apre l'upgrade ed e' il posto del cookie di
|
|
243
|
+
// visione: le WS del pannello partono dalla pagina nel frame, con URL relativi
|
|
244
|
+
// e senza query — portano il cookie, non possono portare altro. Con il solo
|
|
245
|
+
// `verifyToken` (compatibilita' con i test) resta il vecchio contratto:
|
|
246
|
+
// Bearer oppure ?token=.
|
|
247
|
+
function handlePanelUpgrade({ req, socket, head, resolveCellPanel, verifyToken, authorize, log = () => {}, requestImpl }) {
|
|
248
|
+
// Un upgrade rifiutato deve RISPONDERE prima di chiudere. Chiudere e basta
|
|
249
|
+
// lascia il client in attesa finche' non decide lui di rinunciare: dal lato
|
|
250
|
+
// di chi guarda e' un pannello che non arriva mai, indistinguibile da uno
|
|
251
|
+
// lento. Trovato da una prova con socket veri — la suite mockata non poteva
|
|
252
|
+
// vederlo, perche' li' nessuno aspettava una risposta.
|
|
253
|
+
const kill = (code = 400, motivo = 'Bad Request') => {
|
|
254
|
+
try { socket.end(`HTTP/1.1 ${code} ${motivo}\r\nConnection: close\r\n\r\n`); } catch (_) {}
|
|
255
|
+
try { socket.destroy(); } catch (_) {}
|
|
256
|
+
};
|
|
257
|
+
let url;
|
|
258
|
+
try { url = new URL(req.url, 'http://127.0.0.1'); } catch (_) { return kill(400, 'Bad Request'); }
|
|
259
|
+
const allowed = typeof authorize === 'function'
|
|
260
|
+
? authorize(req, url)
|
|
261
|
+
: (() => {
|
|
262
|
+
const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
|
263
|
+
const given = bearer || url.searchParams.get('token') || '';
|
|
264
|
+
return verifyToken(given);
|
|
265
|
+
})();
|
|
266
|
+
if (!allowed) {
|
|
267
|
+
log({ event: 'panel-proxy', outcome: 'rejected', reason: 'unauthorized', cell: '' });
|
|
268
|
+
return kill(401, 'Unauthorized');
|
|
269
|
+
}
|
|
270
|
+
// Il prefisso varia con la porta: /api/panel sul control plane (compat),
|
|
271
|
+
// /panel nudo sulla porta pannello dedicata (P0 sicurezza 2026-08-16).
|
|
272
|
+
const parsed = splitPanelPath(req.url.replace(/^\/(?:api\/)?panel/, ''));
|
|
273
|
+
if (!parsed) return kill(404, 'Not Found');
|
|
274
|
+
Promise.resolve(resolveTarget(resolveCellPanel, parsed.cellId)).then((target) => {
|
|
275
|
+
if (!target.ok) {
|
|
276
|
+
log({ event: 'panel-proxy', outcome: 'rejected', reason: target.reason, cell: parsed.cellId });
|
|
277
|
+
return kill(404, 'Not Found');
|
|
278
|
+
}
|
|
279
|
+
const request = requestImpl || (target.secure ? https.request : http.request);
|
|
280
|
+
const headers = forwardHeaders(req.headers, target.hostHeader);
|
|
281
|
+
headers.connection = 'Upgrade';
|
|
282
|
+
headers.upgrade = 'websocket';
|
|
283
|
+
let upstream;
|
|
284
|
+
try {
|
|
285
|
+
upstream = request({
|
|
286
|
+
host: target.host,
|
|
287
|
+
port: target.port,
|
|
288
|
+
method: 'GET',
|
|
289
|
+
path: `${parsed.rest}${stripLocalTokenQuery(parsed.search)}`,
|
|
290
|
+
headers,
|
|
291
|
+
...(target.secure ? { rejectUnauthorized: !(target.loopback) } : {}),
|
|
292
|
+
});
|
|
293
|
+
} catch (_) { return kill(502, 'Bad Gateway'); }
|
|
294
|
+
upstream.on('upgrade', (upRes, upSocket, upHead) => {
|
|
295
|
+
const lines = [`HTTP/1.1 ${upRes.statusCode} ${upRes.statusMessage}`];
|
|
296
|
+
// La 101 e' una risposta come le altre per il browser, che ne persiste i
|
|
297
|
+
// cookie: `set-cookie` va tolto QUI come nel ramo HTTP. Non si puo' pero'
|
|
298
|
+
// riusare `responseHeaders()`, perche' toglierebbe anche gli hop-by-hop —
|
|
299
|
+
// e nell'handshake `connection`/`upgrade` SONO la risposta.
|
|
300
|
+
for (const [k, v] of Object.entries(upRes.headers)) {
|
|
301
|
+
if (NON_INOLTRATI_DAL_PANNELLO.has(k.toLowerCase())) continue;
|
|
302
|
+
// Un header ripetuto arriva come array (`set-cookie` sempre): va emesso
|
|
303
|
+
// su piu' righe, non unito in una sola con la virgola.
|
|
304
|
+
if (Array.isArray(v)) { for (const uno of v) lines.push(`${k}: ${uno}`); continue; }
|
|
305
|
+
lines.push(`${k}: ${v}`);
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
socket.write(`${lines.join('\r\n')}\r\n\r\n`);
|
|
309
|
+
if (upHead && upHead.length) socket.write(upHead);
|
|
310
|
+
if (head && head.length) upSocket.write(head);
|
|
311
|
+
} catch (_) { return kill(502, 'Bad Gateway'); }
|
|
312
|
+
log({ event: 'panel-proxy', outcome: 'upgraded', cell: parsed.cellId });
|
|
313
|
+
upSocket.pipe(socket);
|
|
314
|
+
socket.pipe(upSocket);
|
|
315
|
+
const close = () => {
|
|
316
|
+
try { upSocket.destroy(); } catch (_) {}
|
|
317
|
+
try { socket.destroy(); } catch (_) {} // qui l'upgrade e' gia' avvenuto: niente risposta HTTP
|
|
318
|
+
};
|
|
319
|
+
upSocket.on('error', close); socket.on('error', close);
|
|
320
|
+
upSocket.on('close', close); socket.on('close', close);
|
|
321
|
+
});
|
|
322
|
+
// Il pannello ha risposto senza accettare l'upgrade: non e' un WebSocket,
|
|
323
|
+
// e fingere il contrario lascerebbe il frame in attesa per sempre.
|
|
324
|
+
upstream.on('response', () => {
|
|
325
|
+
log({ event: 'panel-proxy', outcome: 'rejected', reason: 'upgrade-refused', cell: parsed.cellId });
|
|
326
|
+
kill(502, 'Bad Gateway');
|
|
327
|
+
});
|
|
328
|
+
upstream.on('error', () => {
|
|
329
|
+
log({ event: 'panel-proxy', outcome: 'error', reason: 'upstream-error', cell: parsed.cellId });
|
|
330
|
+
kill(502, 'Bad Gateway');
|
|
331
|
+
});
|
|
332
|
+
upstream.end();
|
|
333
|
+
}).catch(() => kill(500, 'Internal Server Error'));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = { createPanelProxy, handlePanelUpgrade, splitPanelPath, resolveTarget };
|