pi-remote-control/extensions/remote-control/server/upgrade.ts

93 lines
2.9 KiB
TypeScript

/**
* WebSocket upgrade routing.
*
* Routes incoming HTTP Upgrade requests to the appropriate WebSocket handler
* based on the request path and session/topic. Non-matching paths are
* destroyed immediately.
*
* Routes:
* /ws — LEGACY browser HTML client WebSocket endpoint
* /sessions/:id/stream — binary ANSI stream per tmux session (T-1.5)
*/
import type { IncomingMessage } from "node:http";
import type { Socket } from "node:net";
import { handleSideUpgrade, type SideRouteOptions } from "./routes/side.js";
import {
handleStreamUpgrade,
type StreamRouteOptions,
} from "./routes/stream.js";
import type { WsClient, WsServer } from "./types.js";
export interface UpgradeHandlerOptions {
wss: WsServer;
isAuthenticated: (req: IncomingMessage) => boolean;
stream: Omit<StreamRouteOptions, "wss" | "isAuthenticated">;
side: Omit<SideRouteOptions, "wss" | "isAuthenticated">;
}
/**
* Create the HTTP `upgrade` event handler.
*
* @param opts - Handler options including wss, auth predicate, and stream config.
* @returns A handler suitable for `httpServer.on("upgrade", handler)`.
*/
export function createUpgradeHandler(
wss: WsServer,
isAuthenticated: (req: IncomingMessage) => boolean,
streamOpts?: Omit<StreamRouteOptions, "wss" | "isAuthenticated">,
): (request: IncomingMessage, socket: Socket, head: Buffer) => void {
return (request: IncomingMessage, socket: Socket, head: Buffer): void => {
const url = new URL(request.url ?? "/", "http://localhost");
if (url.pathname === "/ws") {
// LEGACY: browser HTML client WebSocket endpoint — auth guard
if (!isAuthenticated(request)) {
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
socket.destroy();
return;
}
wss.handleUpgrade(request, socket, head, (ws: WsClient) => {
wss.emit("connection", ws, request);
});
return;
}
// /sessions/:id/stream
const streamMatch = url.pathname.match(/^\/sessions\/([^/]+)\/stream$/);
if (streamMatch) {
if (!isAuthenticated(request)) {
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
socket.destroy();
return;
}
const sessionId = decodeURIComponent(streamMatch[1]);
handleStreamUpgrade(sessionId, request, socket, head, {
wss,
isAuthenticated,
...streamOpts,
});
return;
}
// /sessions/:id/side
const sideMatch = url.pathname.match(/^\/sessions\/([^/]+)\/side$/);
if (sideMatch) {
if (!isAuthenticated(request)) {
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
socket.destroy();
return;
}
const sessionId = decodeURIComponent(sideMatch[1]);
handleSideUpgrade(sessionId, request, socket, head, {
wss,
isAuthenticated,
});
return;
}
// Unknown upgrade path — reject
socket.destroy();
};
}