refactor(ssh): split docker module into layered directory

ssh/docker/{index,routes,session-manager,container-routes,console}.ts:
server boot and wiring in index, HTTP handlers in routes, SSH session
registry and command execution in session-manager. Code motion only;
port 30007/30009 and endpoints unchanged. Swagger now scans ssh
subdirectories.
This commit is contained in:
LukeGus
2026-07-16 00:45:16 -05:00
parent 35cde3fdfd
commit 9805edfb93
8 changed files with 2031 additions and 1968 deletions
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,17 @@
import { Client as SSHClient } from "ssh2"; import { Client as SSHClient } from "ssh2";
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js";
import { WebSocketServer, WebSocket } from "ws"; import { WebSocketServer, WebSocket } from "ws";
import { AuthManager } from "../utils/auth-manager.js"; import { AuthManager } from "../../utils/auth-manager.js";
import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { createCurrentHostResolutionRepository } from "../../database/repositories/factory.js";
import { systemLogger } from "../utils/logger.js"; import { systemLogger } from "../../utils/logger.js";
import type { SSHHost } from "../../types/index.js"; import type { SSHHost } from "../../../types/index.js";
import { applyAgentAuth } from "./terminal-auth-helpers.js"; import { applyAgentAuth } from "../terminal-auth-helpers.js";
import { import {
containerCommand, containerCommand,
getContainerRuntimeConfig, getContainerRuntimeConfig,
type ContainerRuntime, type ContainerRuntime,
} from "./container-runtime.js"; } from "../container-runtime.js";
import { resolveSshConnectConfigHost } from "./ssh-dns.js"; import { resolveSshConnectConfigHost } from "../ssh-dns.js";
const sshLogger = systemLogger; const sshLogger = systemLogger;
@@ -383,7 +383,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
try { try {
// Resolve host with credentials server-side // Resolve host with credentials server-side
const { resolveHostById } = await import("./host-resolver.js"); const { resolveHostById } = await import("../host-resolver.js");
const resolvedHost = await resolveHostById(hostId, userId); const resolvedHost = await resolveHostById(hostId, userId);
if (!resolvedHost) { if (!resolvedHost) {
@@ -1,9 +1,9 @@
import type express from "express"; import type express from "express";
import { logger } from "../utils/logger.js"; import { logger } from "../../utils/logger.js";
import { import {
containerCommand, containerCommand,
type ContainerRuntime, type ContainerRuntime,
} from "./container-runtime.js"; } from "../container-runtime.js";
const sshLogger = logger; const sshLogger = logger;
+70
View File
@@ -0,0 +1,70 @@
import express from "express";
import cookieParser from "cookie-parser";
import { createCorsMiddleware } from "../../utils/cors-config.js";
import { logger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import { registerDockerContainerRoutes } from "./container-routes.js";
import {
sshSessions,
pendingTOTPSessions,
cleanupSession,
executeDockerCommand,
} from "./session-manager.js";
import {
DOCKER_TIMESTAMP_RE,
getRequestUserId,
registerDockerSshRoutes,
} from "./routes.js";
const sshLogger = logger;
const app = express();
app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"]));
app.use(cookieParser());
app.use(express.json({ limit: "100mb" }));
app.use(express.urlencoded({ limit: "100mb", extended: true }));
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "no-store");
next();
});
const authManager = AuthManager.getInstance();
app.use(authManager.createAuthMiddleware());
registerDockerSshRoutes(app);
registerDockerContainerRoutes(app, {
sshSessions,
pendingTOTPSessions,
getRequestUserId,
executeDockerCommand,
dockerTimestampPattern: DOCKER_TIMESTAMP_RE,
});
const PORT = 30007;
app.listen(PORT, async () => {
try {
await authManager.initialize();
} catch (err) {
sshLogger.error("Failed to initialize Docker backend", err, {
operation: "startup",
});
}
});
process.on("SIGINT", () => {
Object.keys(sshSessions).forEach((sessionId) => {
cleanupSession(sessionId);
});
process.exit(0);
});
process.on("SIGTERM", () => {
Object.keys(sshSessions).forEach((sessionId) => {
cleanupSession(sessionId);
});
process.exit(0);
});
File diff suppressed because it is too large Load Diff
+171
View File
@@ -0,0 +1,171 @@
import { Client as SSHClient } from "ssh2";
import { logger } from "../../utils/logger.js";
import type { ContainerRuntime } from "../container-runtime.js";
const sshLogger = logger;
export interface SSHSession {
client: SSHClient;
isConnected: boolean;
lastActive: number;
timeout?: NodeJS.Timeout;
activeOperations: number;
hostId?: number;
userId?: string;
isWindows?: boolean;
containerRuntime?: ContainerRuntime;
}
export interface PendingTOTPSession {
client: SSHClient;
finish: (responses: string[]) => void;
config: Record<string, unknown>;
createdAt: number;
sessionId: string;
hostId?: number;
ip?: string;
port?: number;
username?: string;
userId?: string;
prompts?: Array<{ prompt: string; echo: boolean }>;
totpPromptIndex?: number;
resolvedPassword?: string;
totpAttempts: number;
isWarpgate?: boolean;
containerRuntime?: ContainerRuntime;
}
export const sshSessions: Record<string, SSHSession> = {};
export const pendingTOTPSessions: Record<string, PendingTOTPSession> = {};
const SESSION_IDLE_TIMEOUT = 60 * 60 * 1000;
setInterval(() => {
const now = Date.now();
Object.keys(pendingTOTPSessions).forEach((sessionId) => {
const session = pendingTOTPSessions[sessionId];
if (now - session.createdAt > 180000) {
try {
session.client.end();
} catch {
// expected
}
delete pendingTOTPSessions[sessionId];
}
});
}, 60000);
export function cleanupSession(sessionId: string) {
const session = sshSessions[sessionId];
if (session) {
if (session.activeOperations > 0) {
sshLogger.warn(
`Deferring session cleanup for ${sessionId} - ${session.activeOperations} active operations`,
{
operation: "cleanup_deferred",
sessionId,
activeOperations: session.activeOperations,
},
);
scheduleSessionCleanup(sessionId);
return;
}
try {
session.client.end();
} catch {
// expected
}
clearTimeout(session.timeout);
delete sshSessions[sessionId];
}
}
export function scheduleSessionCleanup(sessionId: string) {
const session = sshSessions[sessionId];
if (session) {
if (session.timeout) clearTimeout(session.timeout);
session.timeout = setTimeout(() => {
cleanupSession(sessionId);
}, SESSION_IDLE_TIMEOUT);
}
}
export async function executeDockerCommand(
session: SSHSession,
command: string,
sessionId?: string,
userId?: string,
hostId?: number,
): Promise<string> {
const startTime = Date.now();
sshLogger.info("Executing Docker command", {
operation: "docker_command_exec",
sessionId,
userId,
hostId,
command: command.split(" ")[1],
});
return new Promise((resolve, reject) => {
session.client.exec(command, (err, stream) => {
if (err) {
sshLogger.error("Docker command execution error", err, {
operation: "execute_docker_command",
sessionId,
userId,
hostId,
command: command.split(" ")[1],
});
return reject(err);
}
let stdout = "";
let stderr = "";
stream.on("close", (code: number) => {
if (code !== 0) {
sshLogger.error("Docker command failed", undefined, {
operation: "execute_docker_command",
sessionId,
userId,
hostId,
command: command.split(" ")[1],
exitCode: code,
stderr,
});
reject(new Error(stderr || `Command exited with code ${code}`));
} else {
sshLogger.success("Docker command completed", {
operation: "docker_command_success",
sessionId,
userId,
hostId,
command: command.split(" ")[1],
duration: Date.now() - startTime,
});
resolve(stdout);
}
});
stream.on("data", (data: Buffer) => {
stdout += data.toString();
});
stream.stderr.on("data", (data: Buffer) => {
stderr += data.toString();
});
stream.on("error", (streamErr: Error) => {
sshLogger.error("Docker command stream error", streamErr, {
operation: "execute_docker_command",
sessionId,
userId,
hostId,
command: command.split(" ")[1],
});
reject(streamErr);
});
});
});
}
+2 -2
View File
@@ -158,8 +158,8 @@ import {
await import("./ssh/tunnel.js"); await import("./ssh/tunnel.js");
await import("./ssh/file-manager.js"); await import("./ssh/file-manager.js");
await import("./ssh/host-metrics.js"); await import("./ssh/host-metrics.js");
await import("./ssh/docker.js"); await import("./ssh/docker/index.js");
await import("./ssh/docker-console.js"); await import("./ssh/docker/console.js");
await import("./ssh/tmux-monitor.js"); // --- tmux-monitor --- await import("./ssh/tmux-monitor.js"); // --- tmux-monitor ---
await import("./serial/serial.js"); await import("./serial/serial.js");
await import("./dashboard.js"); await import("./dashboard.js");
+1
View File
@@ -125,6 +125,7 @@ const swaggerOptions: SwaggerJSDocOptions = {
path.join(__dirname, "database", "routes", "*.js").replace(/\\/g, "/"), path.join(__dirname, "database", "routes", "*.js").replace(/\\/g, "/"),
path.join(__dirname, "dashboard.js").replace(/\\/g, "/"), path.join(__dirname, "dashboard.js").replace(/\\/g, "/"),
path.join(__dirname, "ssh", "*.js").replace(/\\/g, "/"), path.join(__dirname, "ssh", "*.js").replace(/\\/g, "/"),
path.join(__dirname, "ssh", "**", "*.js").replace(/\\/g, "/"),
path.join(__dirname, "guacamole", "routes.js").replace(/\\/g, "/"), path.join(__dirname, "guacamole", "routes.js").replace(/\\/g, "/"),
], ],
}; };