mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-17 05:43:36 +00:00
v2.3.0
This commit is contained in:
@@ -75,6 +75,7 @@ const clientOptions = {
|
||||
vnc: {
|
||||
"swap-red-blue": false,
|
||||
cursor: "remote",
|
||||
security: "any",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
|
||||
+214
-23
@@ -7,6 +7,8 @@ import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { hosts } from "../database/db/schema.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Client } from "ssh2";
|
||||
import net from "net";
|
||||
import type { AuthenticatedRequest } from "../../types/index.js";
|
||||
|
||||
const router = express.Router();
|
||||
@@ -16,18 +18,52 @@ const authManager = AuthManager.getInstance();
|
||||
router.use(authManager.createAuthMiddleware());
|
||||
|
||||
/**
|
||||
* POST /guacamole/token
|
||||
* Generate an encrypted connection token for guacamole-lite
|
||||
*
|
||||
* Body: {
|
||||
* type: "rdp" | "vnc" | "telnet",
|
||||
* hostname: string,
|
||||
* port?: number,
|
||||
* username?: string,
|
||||
* password?: string,
|
||||
* domain?: string,
|
||||
* // Additional protocol-specific options
|
||||
* }
|
||||
* @openapi
|
||||
* /guacamole/token:
|
||||
* post:
|
||||
* summary: Generate an encrypted Guacamole connection token
|
||||
* description: Creates an AES-256-CBC encrypted token for guacamole-lite with the given connection parameters
|
||||
* tags:
|
||||
* - Guacamole
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - type
|
||||
* - hostname
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: [rdp, vnc, telnet]
|
||||
* hostname:
|
||||
* type: string
|
||||
* port:
|
||||
* type: integer
|
||||
* username:
|
||||
* type: string
|
||||
* password:
|
||||
* type: string
|
||||
* domain:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Encrypted connection token
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* token:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: Invalid request
|
||||
* 500:
|
||||
* description: Server error
|
||||
*/
|
||||
router.post("/token", async (req, res) => {
|
||||
try {
|
||||
@@ -108,6 +144,17 @@ router.post("/token", async (req, res) => {
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Host ID to connect to
|
||||
* requestBody:
|
||||
* required: false
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* protocol:
|
||||
* type: string
|
||||
* enum: [rdp, vnc, telnet]
|
||||
* description: Override the host's default connection type
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Connection token generated successfully
|
||||
@@ -169,13 +216,27 @@ router.post(
|
||||
}
|
||||
}
|
||||
|
||||
const connectionType = (host.connectionType as string) || "ssh";
|
||||
const requestedProtocol = req.body?.protocol as string | undefined;
|
||||
const connectionType =
|
||||
requestedProtocol || (host.connectionType as string);
|
||||
|
||||
if (!["rdp", "vnc", "telnet"].includes(connectionType)) {
|
||||
return res.status(400).json({
|
||||
error: `Connection type '${connectionType}' is not supported for remote desktop. Only RDP, VNC, and Telnet are supported.`,
|
||||
});
|
||||
}
|
||||
|
||||
const protocolEnabledMap: Record<string, boolean> = {
|
||||
rdp: !!host.enableRdp,
|
||||
vnc: !!host.enableVnc,
|
||||
telnet: !!host.enableTelnet,
|
||||
};
|
||||
if (!protocolEnabledMap[connectionType]) {
|
||||
return res.status(400).json({
|
||||
error: `${connectionType.toUpperCase()} is not enabled for this host.`,
|
||||
});
|
||||
}
|
||||
|
||||
let guacConfig: Record<string, unknown> = {};
|
||||
if (host.guacamoleConfig) {
|
||||
try {
|
||||
@@ -192,20 +253,149 @@ router.post(
|
||||
}
|
||||
}
|
||||
|
||||
if (guacConfig.dpi != null) {
|
||||
const parsed = parseInt(String(guacConfig.dpi), 10);
|
||||
guacConfig.dpi =
|
||||
Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
||||
}
|
||||
|
||||
let token: string;
|
||||
const hostname = host.ip as string;
|
||||
const port = host.port as number;
|
||||
const username = (host.username as string) || "";
|
||||
const password = (host.password as string) || "";
|
||||
const domain = (host.domain as string) || "";
|
||||
let hostname = host.ip as string;
|
||||
let port = host.port as number;
|
||||
let username: string;
|
||||
let password: string;
|
||||
|
||||
switch (connectionType) {
|
||||
case "rdp":
|
||||
username =
|
||||
(host.rdpUser as string) || (host.username as string) || "";
|
||||
password =
|
||||
(host.rdpPassword as string) || (host.password as string) || "";
|
||||
port = (host.rdpPort as number) || port || 3389;
|
||||
break;
|
||||
case "vnc":
|
||||
username = (host.vncUser as string) || "";
|
||||
password =
|
||||
(host.vncPassword as string) || (host.password as string) || "";
|
||||
port = (host.vncPort as number) || port || 5900;
|
||||
break;
|
||||
case "telnet":
|
||||
username = (host.telnetUser as string) || "";
|
||||
password =
|
||||
(host.telnetPassword as string) || (host.password as string) || "";
|
||||
port = (host.telnetPort as number) || port || 23;
|
||||
break;
|
||||
default:
|
||||
username = "";
|
||||
password = "";
|
||||
}
|
||||
const domain =
|
||||
(host.rdpDomain as string) || (host.domain as string) || "";
|
||||
|
||||
// Establish SSH tunnel if jump hosts are configured
|
||||
let jumpHosts: Array<{ hostId: number }> = [];
|
||||
if (host.jumpHosts) {
|
||||
try {
|
||||
jumpHosts =
|
||||
typeof host.jumpHosts === "string"
|
||||
? JSON.parse(host.jumpHosts as string)
|
||||
: (host.jumpHosts as Array<{ hostId: number }>);
|
||||
} catch {
|
||||
jumpHosts = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (jumpHosts.length > 0) {
|
||||
try {
|
||||
const { resolveHostById } = await import("../ssh/host-resolver.js");
|
||||
const jumpHost = await resolveHostById(jumpHosts[0].hostId, userId);
|
||||
if (jumpHost) {
|
||||
const tunnelPort = await new Promise<number>((resolve, reject) => {
|
||||
const sshClient = new Client();
|
||||
sshClient.on("ready", () => {
|
||||
const server = net.createServer((sock) => {
|
||||
sshClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
hostname,
|
||||
port,
|
||||
(err, stream) => {
|
||||
if (err) {
|
||||
sock.destroy();
|
||||
return;
|
||||
}
|
||||
sock.pipe(stream).pipe(sock);
|
||||
},
|
||||
);
|
||||
});
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address() as net.AddressInfo;
|
||||
// Auto-cleanup after 1 hour
|
||||
setTimeout(
|
||||
() => {
|
||||
server.close();
|
||||
sshClient.end();
|
||||
},
|
||||
60 * 60 * 1000,
|
||||
);
|
||||
resolve(addr.port);
|
||||
});
|
||||
});
|
||||
sshClient.on("error", reject);
|
||||
|
||||
const connectOpts: Record<string, unknown> = {
|
||||
host: jumpHost.ip,
|
||||
port: jumpHost.port || 22,
|
||||
username: jumpHost.username,
|
||||
readyTimeout: 30000,
|
||||
};
|
||||
if (jumpHost.key) {
|
||||
connectOpts.privateKey = jumpHost.key;
|
||||
if (jumpHost.keyPassword)
|
||||
connectOpts.passphrase = jumpHost.keyPassword;
|
||||
} else if (jumpHost.password) {
|
||||
connectOpts.password = jumpHost.password;
|
||||
}
|
||||
sshClient.connect(connectOpts);
|
||||
});
|
||||
hostname = "127.0.0.1";
|
||||
port = tunnelPort;
|
||||
guacLogger.info("SSH tunnel established for guacamole", {
|
||||
operation: "guac_ssh_tunnel",
|
||||
hostId,
|
||||
tunnelPort,
|
||||
});
|
||||
}
|
||||
} catch (tunnelError) {
|
||||
guacLogger.error("Failed to establish SSH tunnel", tunnelError, {
|
||||
operation: "guac_ssh_tunnel_error",
|
||||
hostId,
|
||||
});
|
||||
return res.status(500).json({
|
||||
error: "Failed to establish SSH tunnel to remote host",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switch (connectionType) {
|
||||
case "rdp":
|
||||
if (guacConfig["enable-drive"] && !guacConfig["drive-path"]) {
|
||||
guacConfig["drive-path"] = "/drive";
|
||||
guacConfig["create-drive-path"] = true;
|
||||
}
|
||||
token = tokenService.createRdpToken(hostname, username, password, {
|
||||
port: port || 3389,
|
||||
port,
|
||||
domain,
|
||||
security: (host.security as string) || undefined,
|
||||
"ignore-cert": (host.ignoreCert as boolean) || false,
|
||||
security:
|
||||
(host.rdpSecurity as string) ||
|
||||
(host.security as string) ||
|
||||
undefined,
|
||||
"ignore-cert":
|
||||
host.rdpIgnoreCert !== undefined
|
||||
? !!host.rdpIgnoreCert
|
||||
: host.ignoreCert !== undefined
|
||||
? !!host.ignoreCert
|
||||
: true,
|
||||
...guacConfig,
|
||||
});
|
||||
break;
|
||||
@@ -215,14 +405,15 @@ router.post(
|
||||
username || undefined,
|
||||
password,
|
||||
{
|
||||
port: port || 5900,
|
||||
port,
|
||||
security: "any",
|
||||
...guacConfig,
|
||||
},
|
||||
);
|
||||
break;
|
||||
case "telnet":
|
||||
token = tokenService.createTelnetToken(hostname, username, password, {
|
||||
port: port || 23,
|
||||
port,
|
||||
...guacConfig,
|
||||
});
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user