feat: convert serial console into the serial plugin

This commit is contained in:
LukeGus committed 2026-09-23 23:06:37 -05:00
1 parent 708eedb861
commit 791d8656a1
121 files changed
+2216 -1425

No files matched your search

-195
View File
@@ -1,195 +0,0 @@
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();
});
});