mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 13:13:25 +00:00
fix(hosts): share daemon links across isolated routed sessions
Multiplex logical sessions over the monitored daemon connection and wrap original binary bytes in routing headers. Admit channels under their authenticated parent and current grants; preserve scoped delivery and physical send completion.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/** Counts bytes until the socket's send callback, shared by all links owned by a daemon. */
|
||||
export class HostTransportBudget {
|
||||
private used = 0;
|
||||
constructor(readonly limit = 64 * 1024 * 1024) {}
|
||||
constructor(readonly limit = 256 * 1024 * 1024) {}
|
||||
get bufferedBytes(): number {
|
||||
return this.used;
|
||||
}
|
||||
|
||||
@@ -51,3 +51,24 @@ test("a slow consumer cannot exceed the shared byte budget and close releases re
|
||||
transport.close();
|
||||
await expect.poll(() => budget.bufferedBytes).toBe(0);
|
||||
});
|
||||
test("direct connector preserves 25 MiB binary frames in both directions", async () => {
|
||||
const server = await endpoint();
|
||||
const connected = once(server.wss, "connection");
|
||||
const transport = createHostTransportFactory(server.url)({ url: server.url });
|
||||
cleanup.push(async () => transport.close());
|
||||
const ready = new Promise<void>((resolve) => transport.onOpen(resolve));
|
||||
const received = new Promise<Uint8Array>((resolve, reject) => {
|
||||
transport.onMessage((bytes, binary) => binary && resolve(bytes as Uint8Array));
|
||||
transport.onError(reject);
|
||||
});
|
||||
const [socket] = (await connected) as [WebSocket];
|
||||
await ready;
|
||||
const bytes = new Uint8Array(25 * 1024 * 1024).fill(173);
|
||||
const echoed = once(socket, "message");
|
||||
transport.send(bytes);
|
||||
const [sent, binary] = await echoed;
|
||||
expect(binary).toBe(true);
|
||||
expect(Buffer.from(sent as Uint8Array).equals(bytes)).toBe(true);
|
||||
socket.send(bytes);
|
||||
expect(Buffer.from(await received).equals(bytes)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { createHostChannelTransport } from "../hosts/index.js";
|
||||
import { HostTransportBudget } from "./buffering.js";
|
||||
export { HostTransportBudget } from "./buffering.js";
|
||||
import { homedir } from "node:os";
|
||||
@@ -32,6 +33,7 @@ export function createHostTransportFactory(
|
||||
let closeEvent: unknown;
|
||||
let transport: DaemonTransport | undefined;
|
||||
let teardown = () => {};
|
||||
let disposeSocket = () => {};
|
||||
let sshTunnel: SshTunnel | null = null;
|
||||
const close = (code?: number, reason?: string) => {
|
||||
if (closed) return;
|
||||
@@ -40,6 +42,7 @@ export function createHostTransportFactory(
|
||||
transport?.close(code, reason);
|
||||
} finally {
|
||||
try {
|
||||
disposeSocket();
|
||||
teardown();
|
||||
} finally {
|
||||
for (const handler of closes) handler(closeEvent);
|
||||
@@ -66,9 +69,10 @@ export function createHostTransportFactory(
|
||||
{
|
||||
headers: options?.headers,
|
||||
...(socketPath ? { socketPath } : {}),
|
||||
maxPayload: 24 * 1024 * 1024,
|
||||
maxPayload: 100 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
disposeSocket = () => socket.terminate();
|
||||
const pending = new Set<() => void>();
|
||||
const send = socket.send.bind(socket);
|
||||
socket.send = (data, ...args: unknown[]) => {
|
||||
@@ -76,19 +80,12 @@ export function createHostTransportFactory(
|
||||
typeof data === "string"
|
||||
? Buffer.byteLength(data)
|
||||
: Buffer.byteLength(data as Uint8Array);
|
||||
if (socket.bufferedAmount + bytes > 24 * 1024 * 1024) {
|
||||
socket.terminate();
|
||||
if (socket.bufferedAmount + bytes > 128 * 1024 * 1024) {
|
||||
throw Object.assign(new Error("Host socket buffer limit reached"), {
|
||||
code: "resource_limit",
|
||||
});
|
||||
}
|
||||
let release: () => void;
|
||||
try {
|
||||
release = budget.reserve(bytes);
|
||||
} catch (error) {
|
||||
socket.terminate();
|
||||
throw error;
|
||||
}
|
||||
const release = budget.reserve(bytes);
|
||||
pending.add(release);
|
||||
const callback = args.find(
|
||||
(value): value is (error?: Error) => void => typeof value === "function",
|
||||
@@ -129,6 +126,7 @@ export function createHostTransportFactory(
|
||||
close();
|
||||
});
|
||||
transport.onError((error) => {
|
||||
if (closed) return;
|
||||
const detail = sshTunnel?.failureDetail() ?? "";
|
||||
const failure =
|
||||
/permission denied|authentication failed|host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED/i.test(
|
||||
@@ -179,12 +177,13 @@ export function createHostTransportFactory(
|
||||
export function createHostClient(
|
||||
connection: string,
|
||||
config: Omit<DaemonClientConfig, "url" | "transportFactory">,
|
||||
budget?: HostTransportBudget,
|
||||
): DaemonClient {
|
||||
const target = parseHostTarget(connection);
|
||||
return new DaemonClient({
|
||||
...config,
|
||||
url: "ws://host-connector/ws",
|
||||
...(target.password ? { password: target.password } : {}),
|
||||
transportFactory: createHostTransportFactory(connection),
|
||||
transportFactory: createHostTransportFactory(connection, budget),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user