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

105 lines
3.1 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);
}
function headerValues(value: string | string[] | undefined): string[] {
if (Array.isArray(value)) return value;
return value === undefined ? [] : [value];
}
export function buildWebSocketUpgradeHeaders(request: IncomingMessage): string {
const lines: string[] = [];
const append = (name: string, values: string[]) => {
for (const value of values) {
lines.push(`${name}: ${value}`);
}
};
append("host", headerValues(request.headers.host));
append("upgrade", headerValues(request.headers.upgrade));
append("connection", headerValues(request.headers.connection));
for (const [name, value] of Object.entries(request.headers)) {
const lowerName = name.toLowerCase();
if (lowerName.startsWith("sec-websocket-")) {
append(name, headerValues(value));
}
}
append("x-ao-remote-address", headerValues(request.headers["x-ao-remote-address"]));
return lines.join("\r\n");
}
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 = buildWebSocketUpgradeHeaders(request);
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;
}