fix: stream runner tran ssh output

This commit is contained in:
Codex
2026-05-25 11:41:56 +00:00
parent 230c13efe4
commit 4a03052747
12 changed files with 381 additions and 152 deletions
+117 -4
View File
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import { join } from "node:path";
import type { Server, ServerWebSocket } from "bun";
import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../shared/src/rotating-jsonl";
import { notificationStyles } from "./notification-styles";
@@ -11,6 +12,7 @@ interface RuntimeConfig {
providerIngressPublicUrl: string;
authUsername: string;
authPassword: string;
providerToken: string | null;
sessionSecret: string;
sessionTtlSeconds: number;
logFile: string;
@@ -61,6 +63,13 @@ interface OperationPerformanceSample {
detail: string;
}
interface FrontendWsData {
channel: "ssh-proxy";
upstream: WebSocket | null;
upstreamOpen: boolean;
pendingToUpstream: string[];
}
const sessionCookieName = "unidesk_session";
const config = readConfig();
const logger = createLogger("frontend", config.logFile);
@@ -142,6 +151,25 @@ function requiredEnv(name: string): string {
return value;
}
function optionalEnv(name: string): string | null {
const value = process.env[name]?.trim() ?? "";
return value.length > 0 ? value : null;
}
function optionalFile(path: string): string | null {
try {
const value = readFileSync(path, "utf8").trim();
return value.length > 0 ? value : null;
} catch {
return null;
}
}
function optionalFileEnv(name: string, fallbackPath: string | null = null): string | null {
const path = optionalEnv(name) ?? fallbackPath;
return path === null ? null : optionalFile(path);
}
function readNumberEnv(name: string): number {
const raw = requiredEnv(name);
const parsed = Number(raw);
@@ -169,6 +197,10 @@ function readConfig(): RuntimeConfig {
providerIngressPublicUrl: requiredEnv("PROVIDER_INGRESS_PUBLIC_URL"),
authUsername: requiredEnv("AUTH_USERNAME"),
authPassword: requiredEnv("AUTH_PASSWORD"),
providerToken: optionalEnv("PROVIDER_TOKEN")
?? optionalEnv("UNIDESK_PROVIDER_TOKEN")
?? optionalFileEnv("PROVIDER_TOKEN_FILE", "/run/secrets/unidesk_provider_token")
?? optionalFileEnv("UNIDESK_PROVIDER_TOKEN_FILE", "/tmp/unidesk-provider-token"),
sessionSecret: requiredEnv("SESSION_SECRET"),
sessionTtlSeconds: readNumberEnv("SESSION_TTL_SECONDS"),
logFile: requiredEnv("LOG_FILE"),
@@ -744,6 +776,68 @@ async function proxyApi(req: Request, url: URL): Promise<Response> {
return new Response(await upstream.arrayBuffer(), { status: upstream.status, headers: responseHeaders });
}
function coreSshWebSocketUrl(): string {
const url = new URL("/ws/ssh", config.coreInternalUrl);
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
url.searchParams.set("token", config.providerToken ?? "");
return url.toString();
}
function webSocketDataText(message: string | Buffer): string {
return typeof message === "string" ? message : Buffer.from(message).toString("utf8");
}
function flushSshProxyPending(ws: ServerWebSocket<FrontendWsData>): void {
const upstream = ws.data.upstream;
if (upstream === null || !ws.data.upstreamOpen || upstream.readyState !== WebSocket.OPEN) return;
while (ws.data.pendingToUpstream.length > 0) upstream.send(ws.data.pendingToUpstream.shift()!);
}
function closeSshProxy(ws: ServerWebSocket<FrontendWsData>, code = 1000, reason = "ssh proxy closed"): void {
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
ws.close(code, reason);
}
}
function openSshProxyUpstream(ws: ServerWebSocket<FrontendWsData>): void {
const upstream = new WebSocket(coreSshWebSocketUrl());
ws.data.upstream = upstream;
upstream.addEventListener("open", () => {
ws.data.upstreamOpen = true;
flushSshProxyPending(ws);
});
upstream.addEventListener("message", (event) => {
if (ws.readyState === WebSocket.OPEN) ws.send(event.data as string | Buffer);
});
upstream.addEventListener("close", (event) => {
closeSshProxy(ws, event.code || 1000, event.reason || "ssh upstream closed");
});
upstream.addEventListener("error", () => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ssh.error", message: "frontend ssh upstream websocket error" }));
}
closeSshProxy(ws, 1011, "ssh upstream websocket error");
});
}
function proxySshWebSocket(req: Request, server: Server<FrontendWsData>): Response | undefined {
if (sessionFromRequest(req) === null) {
return jsonResponse({ ok: false, error: "authentication required" }, 401);
}
if (config.providerToken === null) {
return jsonResponse({ ok: false, error: "frontend ssh proxy is missing provider token" }, 503);
}
const upgraded = server.upgrade(req, {
data: {
channel: "ssh-proxy",
upstream: null,
upstreamOpen: false,
pendingToUpstream: [],
} satisfies FrontendWsData,
});
return upgraded ? undefined : jsonResponse({ ok: false, error: "websocket upgrade failed" }, 400);
}
function vendorPath(pathname: string): string | null {
if (pathname === "/vendor/react.production.min.js") return join(vendorDir, "react", "umd", "react.production.min.js");
if (pathname === "/vendor/react-dom.production.min.js") return join(vendorDir, "react-dom", "umd", "react-dom.production.min.js");
@@ -768,7 +862,7 @@ function isStaticAssetPath(pathname: string): boolean {
return /\/[^/]+\.[a-z0-9]+$/iu.test(pathname);
}
async function handleRequest(req: Request): Promise<Response> {
async function handleRequest(req: Request, server: Server<FrontendWsData>): Promise<Response | undefined> {
const url = new URL(req.url);
logger("debug", "request", { path: url.pathname });
try {
@@ -779,6 +873,7 @@ async function handleRequest(req: Request): Promise<Response> {
if (url.pathname === "/logout" && req.method === "POST") return logout();
if (url.pathname === "/api/session") return sessionResponse(req);
if (url.pathname === "/api/frontend-performance") return frontendPerformanceResponse(req);
if (url.pathname === "/ws/ssh") return proxySshWebSocket(req, server);
if (url.pathname.startsWith("/api/") || url.pathname === "/logs") return proxyApi(req, url);
if (url.pathname === "/docs" || url.pathname.startsWith("/docs/")) return docsResponse(req, url);
if (url.pathname === "/" || url.pathname === "/index.html") {
@@ -798,21 +893,39 @@ async function handleRequest(req: Request): Promise<Response> {
}
}
const server = Bun.serve({
const server = Bun.serve<FrontendWsData>({
port: config.port,
hostname: "0.0.0.0",
idleTimeout: 120,
async fetch(req) {
async fetch(req, server) {
const started = performance.now();
const url = new URL(req.url);
let response: Response | undefined;
try {
response = await handleRequest(req);
response = await handleRequest(req, server);
return response;
} finally {
recordRequestPerformance(req, url.pathname, response, performance.now() - started);
}
},
websocket: {
open(ws) {
if (ws.data.channel === "ssh-proxy") openSshProxyUpstream(ws);
},
message(ws, message) {
if (ws.data.channel !== "ssh-proxy") return;
ws.data.pendingToUpstream.push(webSocketDataText(message));
flushSshProxyPending(ws);
},
close(ws) {
if (ws.data.channel !== "ssh-proxy") return;
try {
ws.data.upstream?.close();
} catch {
// Closing an already-closed upstream socket is harmless.
}
},
},
});
logger("info", "server_listening", {
@@ -233,6 +233,11 @@ spec:
secretKeyRef:
name: unidesk-dev-runtime-secrets
key: AUTH_PASSWORD
- name: PROVIDER_TOKEN
valueFrom:
secretKeyRef:
name: unidesk-dev-runtime-secrets
key: PROVIDER_TOKEN
- name: SESSION_SECRET
valueFrom:
secretKeyRef: