This commit is contained in:
LukeGus
2026-05-28 22:29:20 -05:00
parent 33dcde0827
commit 5777351145
238 changed files with 62303 additions and 98955 deletions
@@ -75,6 +75,7 @@ const clientOptions = {
vnc: {
"swap-red-blue": false,
cursor: "remote",
security: "any",
width: 1280,
height: 720,
},
+214 -23
View File
@@ -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;