import { getErrorMessage } from "../utils/error-message.js"; import { WebSocketServer, WebSocket, type RawData } from "ws"; import { SerialPort } from "serialport"; import { AuthManager } from "../utils/auth-manager.js"; import { DataCrypto } from "../utils/data-crypto.js"; import { sshLogger } from "../utils/logger.js"; import { parseWsMessage } from "../utils/ws-message.js"; import { extractWebSocketToken } from "../utils/ws-auth.js"; interface SerialConnectData { path: string; baudRate: number; dataBits?: 5 | 6 | 7 | 8; stopBits?: 1 | 2; parity?: "none" | "even" | "odd"; } const authManager = AuthManager.getInstance(); const wss = new WebSocketServer({ host: "127.0.0.1", port: 30011 }); wss.on("error", (error) => { sshLogger.error("Serial WebSocket server error", error, { operation: "wss_error", }); }); wss.on("connection", async (ws: WebSocket, req) => { let userId: string | undefined; try { const token = extractWebSocketToken(req); if (!token) { ws.close(1008, "Authentication required"); return; } const payload = await authManager.verifyJWTToken(token); if (!payload?.userId || payload.pendingTOTP) { ws.close(1008, "Authentication required"); return; } userId = payload.userId; } catch { ws.close(1008, "Authentication required"); return; } const dataKey = DataCrypto.getUserDataKey(userId); if (!dataKey) { ws.send(JSON.stringify({ type: "error", data: "Data locked" })); ws.close(1008, "Data access required"); return; } let port: SerialPort | null = null; const send = (msg: object) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(msg)); } }; const cleanup = () => { if (port?.isOpen) { port.close(); } port = null; }; ws.on("message", async (raw: RawData) => { let type: string; let data: unknown; try { ({ type, data } = parseWsMessage(raw)); } catch { return; } try { switch (type) { case "list_ports": { try { const ports = await SerialPort.list(); send({ type: "ports_list", data: ports }); } catch (err) { send({ type: "error", data: getErrorMessage(err, "Failed to list ports"), }); } break; } case "connect": { if (port?.isOpen) { port.close(); port = null; } const cfg = data as SerialConnectData; if (!cfg?.path || !cfg?.baudRate) { send({ type: "error", data: "Missing port path or baud rate" }); break; } try { port = new SerialPort({ path: cfg.path, baudRate: cfg.baudRate, dataBits: cfg.dataBits ?? 8, stopBits: cfg.stopBits ?? 1, parity: cfg.parity ?? "none", autoOpen: false, }); port.open((err) => { if (err) { sshLogger.error("Serial port open failed", err, { operation: "serial_open", path: cfg.path, userId, }); send({ type: "error", data: err.message }); port = null; return; } sshLogger.info("Serial port opened", { operation: "serial_open", path: cfg.path, baudRate: cfg.baudRate, userId, }); send({ type: "connected" }); }); port.on("data", (chunk: Buffer) => { send({ type: "data", data: chunk.toString("binary") }); }); port.on("error", (err) => { send({ type: "error", data: err.message }); }); port.on("close", () => { send({ type: "disconnected" }); port = null; }); } catch (err) { send({ type: "error", data: getErrorMessage(err, "Failed to open serial port"), }); } break; } case "input": { if (!port?.isOpen) break; const input = typeof data === "string" ? data : ""; if (!input) break; port.write(Buffer.from(input, "binary"), (err) => { if (err) { send({ type: "error", data: err.message }); } }); break; } case "disconnect": { cleanup(); send({ type: "disconnected" }); break; } } } catch (err) { sshLogger.error("Error handling serial WebSocket message", err, { operation: "serial_message_handler_error", userId, messageType: type, }); send({ type: "error", data: "Failed to process message" }); } }); ws.on("close", () => { cleanup(); }); ws.on("error", () => { cleanup(); }); });