mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 21:33:41 +00:00
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:
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,17 @@
|
||||
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 { AuthManager } from "../utils/auth-manager.js";
|
||||
import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js";
|
||||
import { systemLogger } from "../utils/logger.js";
|
||||
import type { SSHHost } from "../../types/index.js";
|
||||
import { applyAgentAuth } from "./terminal-auth-helpers.js";
|
||||
import { AuthManager } from "../../utils/auth-manager.js";
|
||||
import { createCurrentHostResolutionRepository } from "../../database/repositories/factory.js";
|
||||
import { systemLogger } from "../../utils/logger.js";
|
||||
import type { SSHHost } from "../../../types/index.js";
|
||||
import { applyAgentAuth } from "../terminal-auth-helpers.js";
|
||||
import {
|
||||
containerCommand,
|
||||
getContainerRuntimeConfig,
|
||||
type ContainerRuntime,
|
||||
} from "./container-runtime.js";
|
||||
import { resolveSshConnectConfigHost } from "./ssh-dns.js";
|
||||
} from "../container-runtime.js";
|
||||
import { resolveSshConnectConfigHost } from "../ssh-dns.js";
|
||||
|
||||
const sshLogger = systemLogger;
|
||||
|
||||
@@ -383,7 +383,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
|
||||
try {
|
||||
// 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);
|
||||
|
||||
if (!resolvedHost) {
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import type express from "express";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import {
|
||||
containerCommand,
|
||||
type ContainerRuntime,
|
||||
} from "./container-runtime.js";
|
||||
} from "../container-runtime.js";
|
||||
|
||||
const sshLogger = logger;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -158,8 +158,8 @@ import {
|
||||
await import("./ssh/tunnel.js");
|
||||
await import("./ssh/file-manager.js");
|
||||
await import("./ssh/host-metrics.js");
|
||||
await import("./ssh/docker.js");
|
||||
await import("./ssh/docker-console.js");
|
||||
await import("./ssh/docker/index.js");
|
||||
await import("./ssh/docker/console.js");
|
||||
await import("./ssh/tmux-monitor.js"); // --- tmux-monitor ---
|
||||
await import("./serial/serial.js");
|
||||
await import("./dashboard.js");
|
||||
|
||||
@@ -125,6 +125,7 @@ const swaggerOptions: SwaggerJSDocOptions = {
|
||||
path.join(__dirname, "database", "routes", "*.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, "guacamole", "routes.js").replace(/\\/g, "/"),
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user