mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-20 02:21:09 +00:00
Moves docker backend and frontend into plugins/docker/, alongside ssh-terminal. Adds a host editor tab seam, a dashboard card registry, and a build step to compile plugin backends into dist.
175 lines
4.6 KiB
TypeScript
175 lines
4.6 KiB
TypeScript
// Core imports below point at TypeScript source so tsc can type-check them.
|
|
// scripts/copy-bundled-plugins.cjs rewrites the prefix to the compiled
|
|
// output path after tsc -p tsconfig.plugins.json runs -- see that script.
|
|
import { Client as SSHClient } from "ssh2";
|
|
import { logger } from "../../../src/backend/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);
|
|
});
|
|
});
|
|
});
|
|
}
|