agent-orchestrator/packages/web/server/terminal-proxy.ts

82 lines
2.4 KiB
TypeScript

import { type IncomingMessage } from "node:http";
import { createConnection } from "node:net";
import { type Duplex } from "node:stream";
import {
activeRemoteAuth,
isBasicAuthHeaderAllowed,
verifyRemoteWsToken,
type RemoteAuthCredentials,
} from "./remote-auth.js";
export function getTerminalProxyTarget(requestUrl: string | undefined): string | null {
const url = new URL(requestUrl ?? "/", "ws://localhost");
if (url.pathname === "/ao-terminal-mux") {
url.pathname = "/mux";
return `${url.pathname}${url.search}`;
}
if (url.pathname.startsWith("/ao-terminal/")) {
url.pathname = url.pathname.slice("/ao-terminal".length);
return `${url.pathname}${url.search}`;
}
return null;
}
export function isRemoteUpgradeAllowed(
request: IncomingMessage,
initialConfiguredAuth?: RemoteAuthCredentials,
): boolean {
const expected = activeRemoteAuth(initialConfiguredAuth);
if (!expected.password) return true;
const url = new URL(request.url ?? "/", "ws://localhost");
if (verifyRemoteWsToken(url.searchParams.get("auth_token"), expected)) {
return true;
}
return isBasicAuthHeaderAllowed(request.headers.authorization, expected);
}
export function proxyTerminalUpgrade(
request: IncomingMessage,
socket: Duplex,
head: Buffer,
initialConfiguredAuth?: RemoteAuthCredentials,
): boolean {
const targetPath = getTerminalProxyTarget(request.url);
if (!targetPath) return false;
if (!isRemoteUpgradeAllowed(request, initialConfiguredAuth)) {
socket.destroy();
return true;
}
const directTerminalPort = Number.parseInt(process.env["DIRECT_TERMINAL_PORT"] ?? "14801", 10);
const upstream = createConnection({ host: "127.0.0.1", port: directTerminalPort });
upstream.on("connect", () => {
const headers = Object.entries(request.headers)
.flatMap(([name, value]) => {
if (Array.isArray(value)) return value.map((item) => `${name}: ${item}`);
return value === undefined ? [] : [`${name}: ${value}`];
})
.join("\r\n");
upstream.write(`GET ${targetPath} HTTP/${request.httpVersion}\r\n${headers}\r\n\r\n`);
if (head.length > 0) upstream.write(head);
socket.pipe(upstream).pipe(socket);
});
upstream.on("error", () => {
socket.destroy();
});
upstream.on("close", () => {
socket.destroy();
});
socket.on("error", () => {
upstream.destroy();
});
socket.on("close", () => {
upstream.destroy();
});
return true;
}