mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
v2.0.0 (#621)
* Guacd, Docker-Compose, RDP (#475) * fix select edit host but not update view (#438) * fix: Checksum issue with chocolatey * fix: Remove homebrew old stuff * Add Korean translation (#439) Co-authored-by: 송준우 <2484@coreit.co.kr> * feat: Automate flatpak * fix: Add imagemagik to electron builder to resolve build error * fix: Build error with runtime repo flag * fix: Flatpak runtime error and install freedesktop ver warning * fix: Flatpak runtime error and install freedesktop ver warning * feat: Re-add homebrew cask and move scripts to backend * fix: No sandbox flag issue * fix: Change name for electron macos cask output * fix: Sandbox error with Linux * fix: Remove comming soon for app stores in readme * Adding Comment at the end of the public_key on the host on deploy (#440) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * -Add New Interface for Credential DB -Add Credential Name as a comment into the server authorized_key file --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Sudo auto fill password (#441) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Feature Sudo password auto-fill; * Fix locale json shema; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Added Italian Language; (#445) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Auto collapse snippet folders (#448) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * feat: Add collapsable snippets (customizable in user profile) * Translations (#447) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; * Fix translations; Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Remove PTY-level keepalive (#449) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Guacamole support for RDP, VNC, and Telnet connections - Implemented WebSocket support for Guacamole in Nginx configuration. - Added REST API endpoints for generating connection tokens and checking guacd status. - Created Guacamole server using guacamole-lite for handling connections. - Developed frontend components for testing RDP/VNC connections and displaying the remote session. - Updated package dependencies to include guacamole-common-js and guacamole-lite. - Enhanced logging for Guacamole operations. * feat: enhance Guacamole support with RDP and VNC connection settings and UI updates * feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation * fix: finalize adding docker to db * fix: merge syntax errors * feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay * feat: add TypeScript definitions for guacamole-common-js module * feat: enhance Mouse.State constructor to accept optional parameters and object destructuring * feat: Add support for RDP and VNC connections in SSH host management - Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures. - Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert. - Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options. - Implemented token retrieval for RDP/VNC connections using Guacamole API. - Updated UI components to reflect connection type changes and provide appropriate connection buttons. - Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor. - Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions. * feat: Enhance Guacamole integration with extended configuration options - Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings. - Implemented logging for token requests and received options for better debugging. - Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values. - Integrated Guacamole configuration parsing in HostManagerViewer and Host components. - Enhanced API requests to include extended Guacamole configuration parameters in the token request. - Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API. * feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes --------- Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: rename api routes and files * feat: improve guacd ui/backend * feat: improve guacd ui/backend * fix: state persistance issues causing refresh * feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration * feat: continue improving integration also with bug fixes * Merge 2.0.0 with 2.0.0 that includes bug fixes (#620) * Guacd, Docker-Compose, RDP (#475) * fix select edit host but not update view (#438) * fix: Checksum issue with chocolatey * fix: Remove homebrew old stuff * Add Korean translation (#439) Co-authored-by: 송준우 <2484@coreit.co.kr> * feat: Automate flatpak * fix: Add imagemagik to electron builder to resolve build error * fix: Build error with runtime repo flag * fix: Flatpak runtime error and install freedesktop ver warning * fix: Flatpak runtime error and install freedesktop ver warning * feat: Re-add homebrew cask and move scripts to backend * fix: No sandbox flag issue * fix: Change name for electron macos cask output * fix: Sandbox error with Linux * fix: Remove comming soon for app stores in readme * Adding Comment at the end of the public_key on the host on deploy (#440) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * -Add New Interface for Credential DB -Add Credential Name as a comment into the server authorized_key file --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Sudo auto fill password (#441) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Feature Sudo password auto-fill; * Fix locale json shema; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Added Italian Language; (#445) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Auto collapse snippet folders (#448) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * feat: Add collapsable snippets (customizable in user profile) * Translations (#447) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; * Fix translations; Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Remove PTY-level keepalive (#449) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Guacamole support for RDP, VNC, and Telnet connections - Implemented WebSocket support for Guacamole in Nginx configuration. - Added REST API endpoints for generating connection tokens and checking guacd status. - Created Guacamole server using guacamole-lite for handling connections. - Developed frontend components for testing RDP/VNC connections and displaying the remote session. - Updated package dependencies to include guacamole-common-js and guacamole-lite. - Enhanced logging for Guacamole operations. * feat: enhance Guacamole support with RDP and VNC connection settings and UI updates * feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation * fix: finalize adding docker to db * fix: merge syntax errors * feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay * feat: add TypeScript definitions for guacamole-common-js module * feat: enhance Mouse.State constructor to accept optional parameters and object destructuring * feat: Add support for RDP and VNC connections in SSH host management - Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures. - Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert. - Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options. - Implemented token retrieval for RDP/VNC connections using Guacamole API. - Updated UI components to reflect connection type changes and provide appropriate connection buttons. - Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor. - Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions. * feat: Enhance Guacamole integration with extended configuration options - Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings. - Implemented logging for token requests and received options for better debugging. - Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values. - Integrated Guacamole configuration parsing in HostManagerViewer and Host components. - Enhanced API requests to include extended Guacamole configuration parameters in the token request. - Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API. * feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes --------- Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: rename api routes and files * feat: improve guacd ui/backend * feat: improve guacd ui/backend * fix: state persistance issues causing refresh * feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration * feat: continue improving integration also with bug fixes --------- Co-authored-by: Wesley Reid <starhound@lostsouls.org> Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: allow customizing guacd backened url * fix: ssh route mistmatching and guacamole url not changing * chore: increment ver * feat: change default to work with default compose, added splits creen support, updated readmes * fix: linux app not starting due to better sqlite isuses, improved copy/paste system so no context menu, added oidc remember me toggle, improved OS detection for sessions, flatpak invalid key, and sharing hosts with other users errors * fix: global settings not setting * chore: update compose * feat: improve the global status input * chore: cleanup files * chore: update export/improt with new host fields * fix: file manager and docker not loading properly --------- Co-authored-by: Wesley Reid <starhound@lostsouls.org> Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import GuacamoleLite from "guacamole-lite";
|
||||
import { parse as parseUrl } from "url";
|
||||
import { guacLogger } from "../utils/logger.js";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { GuacamoleTokenService } from "./token-service.js";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import type { IncomingMessage } from "http";
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
const tokenService = GuacamoleTokenService.getInstance();
|
||||
|
||||
function parseGuacUrl(url: string): { host: string; port: number } {
|
||||
const parts = url.split(":");
|
||||
return {
|
||||
host: parts[0] || "localhost",
|
||||
port: parseInt(parts[1] || "4822", 10),
|
||||
};
|
||||
}
|
||||
|
||||
function readGuacdOptions(): { host: string; port: number } {
|
||||
let host = process.env.GUACD_HOST || "localhost";
|
||||
let port = parseInt(process.env.GUACD_PORT || "4822", 10);
|
||||
try {
|
||||
const db = getDb();
|
||||
const urlRow = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
|
||||
.get() as { value: string } | undefined;
|
||||
if (urlRow?.value) {
|
||||
const parsed = parseGuacUrl(urlRow.value);
|
||||
host = parsed.host;
|
||||
port = parsed.port;
|
||||
}
|
||||
} catch {
|
||||
// DB not available yet, use env var defaults
|
||||
}
|
||||
return { host, port };
|
||||
}
|
||||
|
||||
const GUAC_WS_PORT = 30008;
|
||||
|
||||
const websocketOptions = {
|
||||
port: GUAC_WS_PORT,
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
crypt: {
|
||||
cypher: "AES-256-CBC",
|
||||
key: tokenService.getEncryptionKey(),
|
||||
},
|
||||
log: {
|
||||
level: "ERRORS",
|
||||
stdLog: (...args: unknown[]) => {
|
||||
guacLogger.info(args.join(" "));
|
||||
},
|
||||
errorLog: (...args: unknown[]) => {
|
||||
guacLogger.error(args.join(" "));
|
||||
},
|
||||
},
|
||||
allowedUnencryptedConnectionSettings: {
|
||||
rdp: ["width", "height", "dpi"],
|
||||
vnc: ["width", "height", "dpi"],
|
||||
telnet: ["width", "height"],
|
||||
},
|
||||
connectionDefaultSettings: {
|
||||
rdp: {
|
||||
security: "any",
|
||||
"ignore-cert": true,
|
||||
"enable-wallpaper": false,
|
||||
"enable-font-smoothing": true,
|
||||
"enable-desktop-composition": false,
|
||||
"disable-audio": false,
|
||||
"enable-drive": false,
|
||||
"resize-method": "display-update",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
dpi: 96,
|
||||
},
|
||||
vnc: {
|
||||
"swap-red-blue": false,
|
||||
cursor: "remote",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
telnet: {
|
||||
"terminal-type": "xterm-256color",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const _origConsoleLog = console.log;
|
||||
console.log = (...args: unknown[]) => {
|
||||
const msg = args[0];
|
||||
if (typeof msg === "string" && msg.startsWith("New client connection"))
|
||||
return;
|
||||
_origConsoleLog(...args);
|
||||
};
|
||||
|
||||
function createGuacServer(): GuacamoleLite {
|
||||
const guacdOptions = readGuacdOptions();
|
||||
const server = new GuacamoleLite(
|
||||
websocketOptions,
|
||||
guacdOptions,
|
||||
clientOptions,
|
||||
);
|
||||
|
||||
server.on(
|
||||
"open",
|
||||
(clientConnection: { connectionSettings?: Record<string, unknown> }) => {
|
||||
guacLogger.info("Guacamole connection opened", {
|
||||
operation: "guac_connection_open",
|
||||
type: clientConnection.connectionSettings?.type,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
server.on(
|
||||
"close",
|
||||
(clientConnection: { connectionSettings?: Record<string, unknown> }) => {
|
||||
guacLogger.info("Guacamole connection closed", {
|
||||
operation: "guac_connection_close",
|
||||
type: clientConnection.connectionSettings?.type,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
server.on(
|
||||
"error",
|
||||
(
|
||||
clientConnection: { connectionSettings?: Record<string, unknown> },
|
||||
error: Error,
|
||||
) => {
|
||||
guacLogger.error("Guacamole connection error", error, {
|
||||
operation: "guac_connection_error",
|
||||
type: clientConnection.connectionSettings?.type,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
let guacServer = createGuacServer();
|
||||
|
||||
export async function restartGuacServer(): Promise<void> {
|
||||
try {
|
||||
guacServer.close();
|
||||
} catch (err) {
|
||||
guacLogger.error("Error closing guac server during restart", err as Error);
|
||||
}
|
||||
guacServer = createGuacServer();
|
||||
}
|
||||
|
||||
export { guacServer, tokenService };
|
||||
@@ -0,0 +1,303 @@
|
||||
import express from "express";
|
||||
import { GuacamoleTokenService } from "./token-service.js";
|
||||
import { guacLogger } from "../utils/logger.js";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { PermissionManager } from "../utils/permission-manager.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { hosts } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import type { AuthenticatedRequest } from "../../types/index.js";
|
||||
|
||||
const router = express.Router();
|
||||
const tokenService = GuacamoleTokenService.getInstance();
|
||||
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
|
||||
* }
|
||||
*/
|
||||
router.post("/token", async (req, res) => {
|
||||
try {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { type, hostname, port, username, password, domain, ...options } =
|
||||
req.body;
|
||||
|
||||
if (!type || !hostname) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Missing required fields: type and hostname" });
|
||||
}
|
||||
|
||||
if (!["rdp", "vnc", "telnet"].includes(type)) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid connection type. Must be rdp, vnc, or telnet",
|
||||
});
|
||||
}
|
||||
|
||||
let token: string;
|
||||
|
||||
switch (type) {
|
||||
case "rdp":
|
||||
token = tokenService.createRdpToken(
|
||||
hostname,
|
||||
username || "",
|
||||
password || "",
|
||||
{
|
||||
port: port || 3389,
|
||||
domain,
|
||||
...options,
|
||||
},
|
||||
);
|
||||
break;
|
||||
case "vnc":
|
||||
token = tokenService.createVncToken(hostname, password, {
|
||||
port: port || 5900,
|
||||
...options,
|
||||
});
|
||||
break;
|
||||
case "telnet":
|
||||
token = tokenService.createTelnetToken(hostname, username, password, {
|
||||
port: port || 23,
|
||||
...options,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({ error: "Invalid connection type" });
|
||||
}
|
||||
|
||||
res.json({ token });
|
||||
} catch (error) {
|
||||
guacLogger.error("Failed to generate guacamole token", error, {
|
||||
operation: "guac_token_error",
|
||||
});
|
||||
res.status(500).json({ error: "Failed to generate connection token" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /guacamole/connect-host/{hostId}:
|
||||
* post:
|
||||
* summary: Generate Guacamole connection token from host configuration
|
||||
* description: Fetches host configuration from database and generates a connection token for RDP/VNC/Telnet
|
||||
* tags:
|
||||
* - Guacamole
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: hostId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Host ID to connect to
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Connection token generated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* token:
|
||||
* type: string
|
||||
* description: Encrypted connection token
|
||||
* 400:
|
||||
* description: Invalid request or unsupported connection type
|
||||
* 403:
|
||||
* description: Access denied to host
|
||||
* 404:
|
||||
* description: Host not found
|
||||
* 500:
|
||||
* description: Server error
|
||||
*/
|
||||
router.post(
|
||||
"/connect-host/:hostId",
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
const userId = (req as AuthenticatedRequest).userId!;
|
||||
const hostId = parseInt(req.params.hostId, 10);
|
||||
|
||||
if (!hostId || isNaN(hostId)) {
|
||||
return res.status(400).json({ error: "Invalid host ID" });
|
||||
}
|
||||
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb().select().from(hosts).where(eq(hosts.id, hostId)),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hostResults.length === 0) {
|
||||
return res.status(404).json({ error: "Host not found" });
|
||||
}
|
||||
|
||||
const host = hostResults[0];
|
||||
|
||||
if (host.userId !== userId) {
|
||||
const permissionManager = PermissionManager.getInstance();
|
||||
const accessInfo = await permissionManager.canAccessHost(
|
||||
userId,
|
||||
hostId,
|
||||
"read",
|
||||
);
|
||||
|
||||
if (!accessInfo.hasAccess) {
|
||||
guacLogger.warn("User attempted to access host without permission", {
|
||||
operation: "guac_access_denied",
|
||||
userId,
|
||||
hostId,
|
||||
});
|
||||
return res.status(403).json({ error: "Access denied to this host" });
|
||||
}
|
||||
}
|
||||
|
||||
const connectionType = (host.connectionType as string) || "ssh";
|
||||
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.`,
|
||||
});
|
||||
}
|
||||
|
||||
let guacConfig: Record<string, unknown> = {};
|
||||
if (host.guacamoleConfig) {
|
||||
try {
|
||||
guacConfig =
|
||||
typeof host.guacamoleConfig === "string"
|
||||
? JSON.parse(host.guacamoleConfig as string)
|
||||
: (host.guacamoleConfig as Record<string, unknown>);
|
||||
} catch (error) {
|
||||
guacLogger.warn("Failed to parse guacamole config", {
|
||||
operation: "guac_config_parse_error",
|
||||
hostId,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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) || "";
|
||||
|
||||
switch (connectionType) {
|
||||
case "rdp":
|
||||
token = tokenService.createRdpToken(hostname, username, password, {
|
||||
port: port || 3389,
|
||||
domain,
|
||||
security: (host.security as string) || undefined,
|
||||
"ignore-cert": (host.ignoreCert as boolean) || false,
|
||||
...guacConfig,
|
||||
});
|
||||
break;
|
||||
case "vnc":
|
||||
token = tokenService.createVncToken(hostname, password, {
|
||||
port: port || 5900,
|
||||
...guacConfig,
|
||||
});
|
||||
break;
|
||||
case "telnet":
|
||||
token = tokenService.createTelnetToken(hostname, username, password, {
|
||||
port: port || 23,
|
||||
...guacConfig,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({ error: "Invalid connection type" });
|
||||
}
|
||||
|
||||
res.json({ token });
|
||||
} catch (error) {
|
||||
guacLogger.error("Failed to generate guacamole token for host", error, {
|
||||
operation: "guac_host_token_error",
|
||||
});
|
||||
res.status(500).json({ error: "Failed to generate connection token" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /guacamole/status
|
||||
* Check if guacd is reachable
|
||||
*/
|
||||
router.get("/status", async (req, res) => {
|
||||
try {
|
||||
let guacdHost = process.env.GUACD_HOST || "localhost";
|
||||
let guacdPort = parseInt(process.env.GUACD_PORT || "4822", 10);
|
||||
try {
|
||||
const db = getDb();
|
||||
const urlRow = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
|
||||
.get() as { value: string } | undefined;
|
||||
if (urlRow?.value) {
|
||||
const parts = urlRow.value.split(":");
|
||||
guacdHost = parts[0] || guacdHost;
|
||||
guacdPort = parseInt(parts[1] || String(guacdPort), 10);
|
||||
}
|
||||
} catch {
|
||||
// Fall back to env vars
|
||||
}
|
||||
|
||||
const net = await import("net");
|
||||
|
||||
const checkConnection = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
socket.setTimeout(3000);
|
||||
|
||||
socket.on("connect", () => {
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
socket.on("timeout", () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.on("error", () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.connect(guacdPort, guacdHost);
|
||||
});
|
||||
};
|
||||
|
||||
const isConnected = await checkConnection();
|
||||
|
||||
res.json({
|
||||
guacd: {
|
||||
host: guacdHost,
|
||||
port: guacdPort,
|
||||
status: isConnected ? "connected" : "disconnected",
|
||||
},
|
||||
websocket: {
|
||||
port: 30008,
|
||||
status: "running",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
guacLogger.error("Failed to check guacamole status", error, {
|
||||
operation: "guac_status_error",
|
||||
});
|
||||
res.status(500).json({ error: "Failed to check status" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,181 @@
|
||||
import crypto from "crypto";
|
||||
import { guacLogger } from "../utils/logger.js";
|
||||
|
||||
export interface GuacamoleConnectionSettings {
|
||||
type: "rdp" | "vnc" | "telnet";
|
||||
settings: {
|
||||
hostname: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
domain?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
dpi?: number;
|
||||
security?: string;
|
||||
"ignore-cert"?: boolean;
|
||||
"enable-wallpaper"?: boolean;
|
||||
"enable-drive"?: boolean;
|
||||
"drive-path"?: string;
|
||||
"create-drive-path"?: boolean;
|
||||
"swap-red-blue"?: boolean;
|
||||
cursor?: string;
|
||||
"terminal-type"?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuacamoleToken {
|
||||
connection: GuacamoleConnectionSettings;
|
||||
}
|
||||
|
||||
const CIPHER = "aes-256-cbc";
|
||||
const KEY_LENGTH = 32;
|
||||
|
||||
export class GuacamoleTokenService {
|
||||
private static instance: GuacamoleTokenService;
|
||||
private encryptionKey: Buffer;
|
||||
|
||||
private constructor() {
|
||||
this.encryptionKey = this.initializeKey();
|
||||
}
|
||||
|
||||
static getInstance(): GuacamoleTokenService {
|
||||
if (!GuacamoleTokenService.instance) {
|
||||
GuacamoleTokenService.instance = new GuacamoleTokenService();
|
||||
}
|
||||
return GuacamoleTokenService.instance;
|
||||
}
|
||||
|
||||
private initializeKey(): Buffer {
|
||||
const existingKey = process.env.GUACAMOLE_ENCRYPTION_KEY;
|
||||
if (existingKey) {
|
||||
if (existingKey.length === 64 && /^[0-9a-fA-F]+$/.test(existingKey)) {
|
||||
return Buffer.from(existingKey, "hex");
|
||||
}
|
||||
if (existingKey.length === KEY_LENGTH) {
|
||||
return Buffer.from(existingKey, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
const jwtSecret = process.env.JWT_SECRET;
|
||||
if (jwtSecret) {
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(jwtSecret + "_guacamole")
|
||||
.digest();
|
||||
}
|
||||
|
||||
guacLogger.warn(
|
||||
"No persistent encryption key found, generating random key",
|
||||
{
|
||||
operation: "guac_key_generation",
|
||||
},
|
||||
);
|
||||
return crypto.randomBytes(KEY_LENGTH);
|
||||
}
|
||||
|
||||
getEncryptionKey(): Buffer {
|
||||
return this.encryptionKey;
|
||||
}
|
||||
|
||||
encryptToken(tokenObject: GuacamoleToken): string {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(CIPHER, this.encryptionKey, iv);
|
||||
|
||||
let encrypted = cipher.update(
|
||||
JSON.stringify(tokenObject),
|
||||
"utf8",
|
||||
"base64",
|
||||
);
|
||||
encrypted += cipher.final("base64");
|
||||
|
||||
const data = {
|
||||
iv: iv.toString("base64"),
|
||||
value: encrypted,
|
||||
};
|
||||
|
||||
return Buffer.from(JSON.stringify(data)).toString("base64");
|
||||
}
|
||||
|
||||
decryptToken(token: string): GuacamoleToken | null {
|
||||
try {
|
||||
const data = JSON.parse(Buffer.from(token, "base64").toString("utf8"));
|
||||
const iv = Buffer.from(data.iv, "base64");
|
||||
const decipher = crypto.createDecipheriv(CIPHER, this.encryptionKey, iv);
|
||||
|
||||
let decrypted = decipher.update(data.value, "base64", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return JSON.parse(decrypted) as GuacamoleToken;
|
||||
} catch (error) {
|
||||
guacLogger.error("Failed to decrypt guacamole token", error, {
|
||||
operation: "guac_token_decrypt_error",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
createRdpToken(
|
||||
hostname: string,
|
||||
username: string,
|
||||
password: string,
|
||||
options: Partial<GuacamoleConnectionSettings["settings"]> = {},
|
||||
): string {
|
||||
const token: GuacamoleToken = {
|
||||
connection: {
|
||||
type: "rdp",
|
||||
settings: {
|
||||
hostname,
|
||||
username,
|
||||
password,
|
||||
port: 3389,
|
||||
security: "nla",
|
||||
"ignore-cert": true,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
};
|
||||
return this.encryptToken(token);
|
||||
}
|
||||
|
||||
createVncToken(
|
||||
hostname: string,
|
||||
password?: string,
|
||||
options: Partial<GuacamoleConnectionSettings["settings"]> = {},
|
||||
): string {
|
||||
const token: GuacamoleToken = {
|
||||
connection: {
|
||||
type: "vnc",
|
||||
settings: {
|
||||
hostname,
|
||||
password,
|
||||
port: 5900,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
};
|
||||
return this.encryptToken(token);
|
||||
}
|
||||
|
||||
createTelnetToken(
|
||||
hostname: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
options: Partial<GuacamoleConnectionSettings["settings"]> = {},
|
||||
): string {
|
||||
const token: GuacamoleToken = {
|
||||
connection: {
|
||||
type: "telnet",
|
||||
settings: {
|
||||
hostname,
|
||||
username,
|
||||
password,
|
||||
port: 23,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
};
|
||||
return this.encryptToken(token);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user