82 lines
2.4 KiB
TypeScript
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;
|
|
}
|