linkshell-cli 0.2.52 → 0.2.53
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/cli/src/runtime/bridge-session.d.ts +6 -0
- package/dist/cli/src/runtime/bridge-session.js +161 -0
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +164 -30
- package/dist/shared-protocol/src/index.js +30 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/runtime/bridge-session.ts +204 -0
|
@@ -133,6 +133,7 @@ export class BridgeSession {
|
|
|
133
133
|
private hookMarker = `lsh-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
134
134
|
private screenCapture: ScreenFallback | undefined;
|
|
135
135
|
private screenShare: ScreenShare | undefined;
|
|
136
|
+
private tunnelSockets = new Map<string, WebSocket>();
|
|
136
137
|
|
|
137
138
|
constructor(options: BridgeSessionOptions) {
|
|
138
139
|
this.options = options;
|
|
@@ -442,11 +443,209 @@ export class BridgeSession {
|
|
|
442
443
|
}
|
|
443
444
|
break;
|
|
444
445
|
}
|
|
446
|
+
case "tunnel.request": {
|
|
447
|
+
const p = parseTypedPayload("tunnel.request", envelope.payload);
|
|
448
|
+
this.handleTunnelRequest(p);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case "tunnel.ws.data": {
|
|
452
|
+
const p = parseTypedPayload("tunnel.ws.data", envelope.payload);
|
|
453
|
+
this.handleTunnelWsData(p);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
case "tunnel.ws.close": {
|
|
457
|
+
const p = parseTypedPayload("tunnel.ws.close", envelope.payload);
|
|
458
|
+
this.handleTunnelWsClose(p);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
445
461
|
default:
|
|
446
462
|
break;
|
|
447
463
|
}
|
|
448
464
|
}
|
|
449
465
|
|
|
466
|
+
// ── Tunnel handlers ────────────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
private handleTunnelRequest(payload: {
|
|
469
|
+
requestId: string;
|
|
470
|
+
method: string;
|
|
471
|
+
url: string;
|
|
472
|
+
headers: Record<string, string>;
|
|
473
|
+
body: string | null;
|
|
474
|
+
port: number;
|
|
475
|
+
}): void {
|
|
476
|
+
const { requestId, method, url: reqUrl, headers, body, port } = payload;
|
|
477
|
+
|
|
478
|
+
// WebSocket upgrade request
|
|
479
|
+
if (headers.upgrade === "websocket") {
|
|
480
|
+
this.handleTunnelWsUpgrade(requestId, port, reqUrl);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const parsedUrl = new URL(reqUrl, `http://127.0.0.1:${port}`);
|
|
485
|
+
|
|
486
|
+
const reqOptions: http.RequestOptions = {
|
|
487
|
+
hostname: "127.0.0.1",
|
|
488
|
+
port,
|
|
489
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
490
|
+
method,
|
|
491
|
+
headers: { ...headers, host: `127.0.0.1:${port}` },
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const proxyReq = http.request(reqOptions, (proxyRes) => {
|
|
495
|
+
// Collect response headers
|
|
496
|
+
const resHeaders: Record<string, string> = {};
|
|
497
|
+
for (const [key, val] of Object.entries(proxyRes.headers)) {
|
|
498
|
+
if (typeof val === "string") resHeaders[key] = val;
|
|
499
|
+
else if (Array.isArray(val)) resHeaders[key] = val.join(", ");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
let firstChunk = true;
|
|
503
|
+
proxyRes.on("data", (chunk: Buffer) => {
|
|
504
|
+
this.send(
|
|
505
|
+
createEnvelope({
|
|
506
|
+
type: "tunnel.response",
|
|
507
|
+
sessionId: this.sessionId,
|
|
508
|
+
payload: {
|
|
509
|
+
requestId,
|
|
510
|
+
statusCode: proxyRes.statusCode ?? 200,
|
|
511
|
+
headers: firstChunk ? resHeaders : {},
|
|
512
|
+
body: chunk.toString("base64"),
|
|
513
|
+
isFinal: false,
|
|
514
|
+
},
|
|
515
|
+
}),
|
|
516
|
+
);
|
|
517
|
+
firstChunk = false;
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
proxyRes.on("end", () => {
|
|
521
|
+
this.send(
|
|
522
|
+
createEnvelope({
|
|
523
|
+
type: "tunnel.response",
|
|
524
|
+
sessionId: this.sessionId,
|
|
525
|
+
payload: {
|
|
526
|
+
requestId,
|
|
527
|
+
statusCode: proxyRes.statusCode ?? 200,
|
|
528
|
+
headers: firstChunk ? resHeaders : {},
|
|
529
|
+
body: "",
|
|
530
|
+
isFinal: true,
|
|
531
|
+
},
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
proxyRes.on("error", () => {
|
|
537
|
+
this.sendTunnelError(requestId, 502, "Upstream read error");
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
proxyReq.on("error", () => {
|
|
542
|
+
this.sendTunnelError(requestId, 502, "Connection refused");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
proxyReq.setTimeout(30_000, () => {
|
|
546
|
+
proxyReq.destroy();
|
|
547
|
+
this.sendTunnelError(requestId, 504, "Upstream timeout");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
if (body) {
|
|
551
|
+
proxyReq.write(Buffer.from(body, "base64"));
|
|
552
|
+
}
|
|
553
|
+
proxyReq.end();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private handleTunnelWsUpgrade(requestId: string, port: number, url: string): void {
|
|
557
|
+
const wsUrl = `ws://127.0.0.1:${port}${url}`;
|
|
558
|
+
const localWs = new WebSocket(wsUrl);
|
|
559
|
+
|
|
560
|
+
localWs.on("open", () => {
|
|
561
|
+
this.tunnelSockets.set(requestId, localWs);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
localWs.on("message", (data: Buffer | string) => {
|
|
565
|
+
const isBinary = typeof data !== "string";
|
|
566
|
+
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
|
567
|
+
this.send(
|
|
568
|
+
createEnvelope({
|
|
569
|
+
type: "tunnel.ws.data",
|
|
570
|
+
sessionId: this.sessionId,
|
|
571
|
+
payload: {
|
|
572
|
+
requestId,
|
|
573
|
+
data: buf.toString("base64"),
|
|
574
|
+
isBinary,
|
|
575
|
+
},
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
localWs.on("close", (code, reason) => {
|
|
581
|
+
this.tunnelSockets.delete(requestId);
|
|
582
|
+
this.send(
|
|
583
|
+
createEnvelope({
|
|
584
|
+
type: "tunnel.ws.close",
|
|
585
|
+
sessionId: this.sessionId,
|
|
586
|
+
payload: {
|
|
587
|
+
requestId,
|
|
588
|
+
code,
|
|
589
|
+
reason: reason?.toString() || "",
|
|
590
|
+
},
|
|
591
|
+
}),
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
localWs.on("error", () => {
|
|
596
|
+
this.tunnelSockets.delete(requestId);
|
|
597
|
+
this.send(
|
|
598
|
+
createEnvelope({
|
|
599
|
+
type: "tunnel.ws.close",
|
|
600
|
+
sessionId: this.sessionId,
|
|
601
|
+
payload: {
|
|
602
|
+
requestId,
|
|
603
|
+
code: 1001,
|
|
604
|
+
reason: "Local WebSocket error",
|
|
605
|
+
},
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private handleTunnelWsData(payload: {
|
|
612
|
+
requestId: string;
|
|
613
|
+
data: string;
|
|
614
|
+
isBinary: boolean;
|
|
615
|
+
}): void {
|
|
616
|
+
const ws = this.tunnelSockets.get(payload.requestId);
|
|
617
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
618
|
+
const buf = Buffer.from(payload.data, "base64");
|
|
619
|
+
ws.send(payload.isBinary ? buf : buf.toString("utf8"));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private handleTunnelWsClose(payload: {
|
|
623
|
+
requestId: string;
|
|
624
|
+
code?: number;
|
|
625
|
+
reason?: string;
|
|
626
|
+
}): void {
|
|
627
|
+
const ws = this.tunnelSockets.get(payload.requestId);
|
|
628
|
+
if (!ws) return;
|
|
629
|
+
ws.close(payload.code ?? 1000, payload.reason ?? "");
|
|
630
|
+
this.tunnelSockets.delete(payload.requestId);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private sendTunnelError(requestId: string, statusCode: number, message: string): void {
|
|
634
|
+
this.send(
|
|
635
|
+
createEnvelope({
|
|
636
|
+
type: "tunnel.response",
|
|
637
|
+
sessionId: this.sessionId,
|
|
638
|
+
payload: {
|
|
639
|
+
requestId,
|
|
640
|
+
statusCode,
|
|
641
|
+
headers: { "content-type": "text/plain" },
|
|
642
|
+
body: Buffer.from(message).toString("base64"),
|
|
643
|
+
isFinal: true,
|
|
644
|
+
},
|
|
645
|
+
}),
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
450
649
|
private sendTerminalList(): void {
|
|
451
650
|
const terminals = [...this.terminals.values()].map((t) => ({
|
|
452
651
|
terminalId: t.id,
|
|
@@ -1274,6 +1473,11 @@ export class BridgeSession {
|
|
|
1274
1473
|
}
|
|
1275
1474
|
this.socket?.close();
|
|
1276
1475
|
this.socket = undefined;
|
|
1476
|
+
// Clean up tunnel WebSockets
|
|
1477
|
+
for (const ws of this.tunnelSockets.values()) {
|
|
1478
|
+
ws.close(1001, "Session stopped");
|
|
1479
|
+
}
|
|
1480
|
+
this.tunnelSockets.clear();
|
|
1277
1481
|
for (const term of this.terminals.values()) {
|
|
1278
1482
|
this.cleanupHookServer(term);
|
|
1279
1483
|
if (term.status === "running") term.pty.kill();
|