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 { 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) {
|
||||||
+2
-2
@@ -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;
|
||||||
|
|
||||||
@@ -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/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");
|
||||||
|
|||||||
@@ -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, "/"),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user