@mmmbuto/nexuscrew 0.9.38 → 0.9.39
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 +7 -0
- package/frontend/dist/assets/index-Csz8VaFz.css +32 -0
- package/frontend/dist/assets/index-Dhmlcepq.js +167 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/proxy/federation.js +67 -2
- package/lib/ws/bridge.js +24 -1
- package/package.json +1 -1
- package/frontend/dist/assets/index-DDuZWnkm.js +0 -167
- package/frontend/dist/assets/index-XusWm2UG.css +0 -32
package/frontend/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-Dhmlcepq.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Csz8VaFz.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.9.
|
|
1
|
+
{"version":"0.9.39"}
|
package/lib/proxy/federation.js
CHANGED
|
@@ -1528,6 +1528,33 @@ function forwardUpgrade({ req, socket, head, nodesPath, localPort, localCredenti
|
|
|
1528
1528
|
path = `/federation/route/${rest.length ? `${rest.join('/')}/` : ''}${ROUTE_DELIMITER}${parsed.resource}`;
|
|
1529
1529
|
}
|
|
1530
1530
|
const up = net.connect({ host: '127.0.0.1', port });
|
|
1531
|
+
// Osservatore NON consumante della testa di risposta dell'upstream: serve
|
|
1532
|
+
// solo a sapere se il 101 e' transitato (client in stato WebSocket) o se
|
|
1533
|
+
// l'hop e' morto prima. La pipe resta l'unico trasporto dei byte: il
|
|
1534
|
+
// listener non sottrae nulla, guarda gli stessi chunk.
|
|
1535
|
+
let saw101 = false;
|
|
1536
|
+
let headBuf = Buffer.alloc(0);
|
|
1537
|
+
let gone = false;
|
|
1538
|
+
up.on('data', (chunk) => {
|
|
1539
|
+
if (saw101 || headBuf.length >= 1024) return;
|
|
1540
|
+
headBuf = Buffer.concat([headBuf, chunk.subarray(0, Math.max(0, 1024 - headBuf.length))]);
|
|
1541
|
+
if (headBuf.includes(' 101')) saw101 = true;
|
|
1542
|
+
});
|
|
1543
|
+
// L'hop e' morto (errore di connessione o chiusura a stream avviato): il
|
|
1544
|
+
// client deve riceverlo come codice, non come troncamento.
|
|
1545
|
+
const upstreamGone = () => {
|
|
1546
|
+
if (gone) return;
|
|
1547
|
+
gone = true;
|
|
1548
|
+
if (saw101) {
|
|
1549
|
+
// Handshake gia' transitato: un frame di close e' cio' che il browser
|
|
1550
|
+
// sa leggere. Se l'upstream aveva gia' chiuso con un suo codice, quel
|
|
1551
|
+
// frame e' arrivato prima e vince (il browser si ferma al primo).
|
|
1552
|
+
try { socket.write(wsCloseFrame(4402, 'upstream gone')); } catch (_) { /* socket morto */ }
|
|
1553
|
+
try { socket.end(); } catch (_) { /* idem */ }
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
acceptWsThenClose(socket, req, 4402, 'upstream gone');
|
|
1557
|
+
};
|
|
1531
1558
|
up.once('connect', () => {
|
|
1532
1559
|
// Ultimo hop: l'upgrade rientra nell'API locale col Bearer locale e da li'
|
|
1533
1560
|
// in poi sarebbe indistinguibile da un attach diretto di chi possiede quel
|
|
@@ -1554,9 +1581,15 @@ function forwardUpgrade({ req, socket, head, nodesPath, localPort, localCredenti
|
|
|
1554
1581
|
const remove = () => { activeSockets.delete(socket); activeSockets.delete(up); };
|
|
1555
1582
|
socket.once('close', remove); up.once('close', remove);
|
|
1556
1583
|
}
|
|
1557
|
-
socket.pipe(up);
|
|
1584
|
+
socket.pipe(up);
|
|
1585
|
+
// end:false: la chiusura verso il browser la governiamo noi, o la pipe
|
|
1586
|
+
// chiuderebbe il socket PRIMA che il frame di close 4402 possa partire.
|
|
1587
|
+
up.pipe(socket, { end: false });
|
|
1558
1588
|
});
|
|
1559
|
-
up.on('error', () =>
|
|
1589
|
+
up.on('error', () => upstreamGone());
|
|
1590
|
+
up.on('end', () => upstreamGone());
|
|
1591
|
+
up.on('close', () => upstreamGone());
|
|
1592
|
+
socket.on('error', () => up.destroy());
|
|
1560
1593
|
}
|
|
1561
1594
|
|
|
1562
1595
|
// Indistinguibile da «sessione inesistente» (lib/ws/bridge.js): le stesse
|
|
@@ -1579,6 +1612,38 @@ function rejectAsNoSuchSession(socket, req) {
|
|
|
1579
1612
|
|
|
1580
1613
|
function reject(socket, code) { try { socket.end(`HTTP/1.1 ${code} Error\r\nConnection: close\r\n\r\n`); } catch (_) {} }
|
|
1581
1614
|
|
|
1615
|
+
// Frame di close costruito a mano: sul ramo "hop morto DOPO il 101" non c'e'
|
|
1616
|
+
// piu' una richiesta di upgrade da consegnare a handleUpgrade (l'handshake e'
|
|
1617
|
+
// gia' transitato), quindi il frame va scritto direttamente.
|
|
1618
|
+
function wsCloseFrame(code, reason = '') {
|
|
1619
|
+
const text = String(reason).slice(0, 100);
|
|
1620
|
+
const payload = Buffer.alloc(2 + Buffer.byteLength(text));
|
|
1621
|
+
payload.writeUInt16BE((Number(code) || 1000) & 0xffff, 0);
|
|
1622
|
+
payload.write(text, 2, 'utf8');
|
|
1623
|
+
const len = payload.length;
|
|
1624
|
+
return len < 126
|
|
1625
|
+
? Buffer.concat([Buffer.from([0x88, len]), payload])
|
|
1626
|
+
: Buffer.concat([Buffer.from([0x88, 126, (len >> 8) & 0xff, len & 0xff]), payload]);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Hop morto e il client NON ha ancora visto il 101: completiamo l'handshake
|
|
1630
|
+
// qui, cosi' il browser entra in stato WebSocket e puo' leggere un frame di
|
|
1631
|
+
// close LEGGIBILE (4402) invece di un 502 grezzo o di un EOF muto. L'accept
|
|
1632
|
+
// lo calcola la libreria (stesso REJECT_WSS del rifiuto "no such session"),
|
|
1633
|
+
// cosi' il GUID della RFC 6455 non vive in questo file. Per una richiesta che
|
|
1634
|
+
// non e' un vero upgrade resta il rifiuto HTTP di prima.
|
|
1635
|
+
function acceptWsThenClose(socket, req, code, reason) {
|
|
1636
|
+
const key = req && req.headers && req.headers['sec-websocket-key'];
|
|
1637
|
+
if (!key) return reject(socket, 502);
|
|
1638
|
+
try {
|
|
1639
|
+
REJECT_WSS.handleUpgrade(req, socket, Buffer.alloc(0), (ws) => {
|
|
1640
|
+
try { ws.send(JSON.stringify({ type: 'error', reason })); } catch (_) { /* socket morto */ }
|
|
1641
|
+
try { ws.close(code, reason); } catch (_) { /* idem */ }
|
|
1642
|
+
});
|
|
1643
|
+
} catch (_) { try { socket.destroy(); } catch (_) { /* idem */ } }
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
|
|
1582
1647
|
module.exports = {
|
|
1583
1648
|
isPanelResource, panelAllowedFor, isLiveHostResource, liveHostAllowedFor, routeHandler,
|
|
1584
1649
|
MAX_HOPS, ROUTE_DELIMITER, TOPOLOGY_PEER_TIMEOUT_MS, SHARE_NOT_READY_CODE, classifyShareFailure, activeReversePort,
|
package/lib/ws/bridge.js
CHANGED
|
@@ -117,8 +117,20 @@ function sendJson(ws, value) {
|
|
|
117
117
|
try { ws.send(JSON.stringify(value)); } catch (_) {}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Resync del buffer dopo una riconnessione: il client chiede {type:'resync'}
|
|
121
|
+
// e riceve l'intero pane (scrollback cap 2000 righe) con le sequenze di
|
|
122
|
+
// escape (-e) per ridipingere lo schermo senza mai mostrarlo vuoto.
|
|
123
|
+
const RESYNC_MAX_LINES = 2000;
|
|
124
|
+
function defaultCapturePane(session, tmuxBin, cb) {
|
|
125
|
+
try {
|
|
126
|
+
require('node:child_process').execFile(tmuxBin || 'tmux',
|
|
127
|
+
['capture-pane', '-e', '-p', '-S', `-${RESYNC_MAX_LINES}`, '-t', String(session)],
|
|
128
|
+
{ maxBuffer: 16 * 1024 * 1024 }, (err, stdout) => cb(err, stdout));
|
|
129
|
+
} catch (e) { cb(e); }
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
function bindWs(ws, deps) {
|
|
121
|
-
const { openAttach, verifyToken, isValidSession = () => true, runAction = () => false, countClients = () => 0, defaults = {}, onAttach = () => {}, ptyGrace = null, diagnostics = null, dropCounter = null } = deps;
|
|
133
|
+
const { openAttach, verifyToken, isValidSession = () => true, runAction = () => false, countClients = () => 0, defaults = {}, onAttach = () => {}, ptyGrace = null, diagnostics = null, dropCounter = null, capturePane = defaultCapturePane } = deps;
|
|
122
134
|
let record = null;
|
|
123
135
|
let attached = false;
|
|
124
136
|
let session = null;
|
|
@@ -218,6 +230,7 @@ function bindWs(ws, deps) {
|
|
|
218
230
|
onAttach(session, ws);
|
|
219
231
|
logReattach();
|
|
220
232
|
sendJson(ws, { type: 'attached', reconnectToken: record.resumeToken });
|
|
233
|
+
sendJson(ws, { type: 'link', state: 'live' });
|
|
221
234
|
return;
|
|
222
235
|
}
|
|
223
236
|
attached = true;
|
|
@@ -279,6 +292,7 @@ function bindWs(ws, deps) {
|
|
|
279
292
|
try { record.ws.close(1000, 'exit'); } catch (_) {}
|
|
280
293
|
});
|
|
281
294
|
sendJson(ws, { type: 'attached', reconnectToken: record.resumeToken });
|
|
295
|
+
sendJson(ws, { type: 'link', state: 'live' });
|
|
282
296
|
return;
|
|
283
297
|
}
|
|
284
298
|
// dopo l'attach
|
|
@@ -294,6 +308,15 @@ function bindWs(ws, deps) {
|
|
|
294
308
|
else if (msg.type === 'input' && !record.readonly) record.pty.write(typeof msg.data === 'string' ? msg.data : '');
|
|
295
309
|
else if (msg.type === 'key' && !record.readonly) record.pty.write(typeof msg.seq === 'string' ? msg.seq.slice(0, 64) : '');
|
|
296
310
|
else if (msg.type === 'action') runAction(session, msg.name); // nav window/pane server-side
|
|
311
|
+
// Resync post-riconnessione: ridipinge il buffer del client dal pane
|
|
312
|
+
// reale. Best-effort — se il capture fallisce lo stream live continua e
|
|
313
|
+
// il client resta sul buffer che aveva (mai uno schermo vuoto forzato).
|
|
314
|
+
else if (msg.type === 'resync') {
|
|
315
|
+
capturePane(session, defaults.tmuxBin || 'tmux', (err, text) => {
|
|
316
|
+
if (err) return;
|
|
317
|
+
sendJson(ws, { type: 'snapshot', data: String(text || '') });
|
|
318
|
+
});
|
|
319
|
+
}
|
|
297
320
|
}
|
|
298
321
|
|
|
299
322
|
// Alla riconnessione la DURATA della caduta e' il gap close->reopen della
|