diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index ddc79413..b68c69e5 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -500,6 +500,7 @@ const migrateSchema = () => { addColumnIfNotExists("ssh_data", "domain", "TEXT"); addColumnIfNotExists("ssh_data", "security", "TEXT"); addColumnIfNotExists("ssh_data", "ignore_cert", "INTEGER NOT NULL DEFAULT 0"); + addColumnIfNotExists("ssh_data", "guacamole_config", "TEXT"); addColumnIfNotExists("ssh_credentials", "private_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "public_key", "TEXT"); diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 51f6f167..e57477be 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -100,6 +100,8 @@ export const sshData = sqliteTable("ssh_data", { domain: text("domain"), security: text("security"), ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false), + // RDP/VNC extended configuration (stored as JSON) + guacamoleConfig: text("guacamole_config"), createdAt: text("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index a341ea72..d1e9c0a1 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -249,6 +249,7 @@ router.post( domain, security, ignoreCert, + guacamoleConfig, } = hostData; if ( !isNonEmptyString(userId) || @@ -299,6 +300,7 @@ router.post( domain: domain || null, security: security || null, ignoreCert: ignoreCert ? 1 : 0, + guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null, }; if (effectiveAuthType === "password") { @@ -363,6 +365,9 @@ router.post( dockerConfig: createdHost.dockerConfig ? JSON.parse(createdHost.dockerConfig as string) : undefined, + guacamoleConfig: createdHost.guacamoleConfig + ? JSON.parse(createdHost.guacamoleConfig as string) + : undefined, }; const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; @@ -490,6 +495,7 @@ router.put( domain, security, ignoreCert, + guacamoleConfig, } = hostData; if ( !isNonEmptyString(userId) || @@ -540,6 +546,7 @@ router.put( domain: domain || null, security: security || null, ignoreCert: ignoreCert ? 1 : 0, + guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null, }; if (effectiveAuthType === "password") { @@ -622,6 +629,9 @@ router.put( dockerConfig: updatedHost.dockerConfig ? JSON.parse(updatedHost.dockerConfig as string) : undefined, + guacamoleConfig: updatedHost.guacamoleConfig + ? JSON.parse(updatedHost.guacamoleConfig as string) + : undefined, }; const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; @@ -730,6 +740,9 @@ router.get( terminalConfig: row.terminalConfig ? JSON.parse(row.terminalConfig as string) : undefined, + guacamoleConfig: row.guacamoleConfig + ? JSON.parse(row.guacamoleConfig as string) + : undefined, forceKeyboardInteractive: row.forceKeyboardInteractive === "true", }; @@ -805,6 +818,9 @@ router.get( terminalConfig: host.terminalConfig ? JSON.parse(host.terminalConfig) : undefined, + guacamoleConfig: host.guacamoleConfig + ? JSON.parse(host.guacamoleConfig) + : undefined, forceKeyboardInteractive: host.forceKeyboardInteractive === "true", }; diff --git a/src/backend/guacamole/routes.ts b/src/backend/guacamole/routes.ts index 634cb661..dd0a955e 100644 --- a/src/backend/guacamole/routes.ts +++ b/src/backend/guacamole/routes.ts @@ -38,6 +38,24 @@ router.post("/token", async (req, res) => { return res.status(400).json({ error: "Invalid connection type. Must be rdp, vnc, or telnet" }); } + // Log received options for debugging + guacLogger.info("Guacamole token request received", { + operation: "guac_token_request", + type, + hostname, + port, + optionKeys: Object.keys(options), + optionsCount: Object.keys(options).length, + }); + + // Log specific option values for debugging + if (Object.keys(options).length > 0) { + guacLogger.info("Guacamole options received", { + operation: "guac_token_options", + options: JSON.stringify(options), + }); + } + let token: string; switch (type) { diff --git a/src/types/index.ts b/src/types/index.ts index 4b7fe408..389610df 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -27,6 +27,97 @@ export interface DockerConfig { export type HostConnectionType = "ssh" | "rdp" | "vnc" | "telnet"; +// Guacamole configuration for RDP/VNC/Telnet connections +export interface GuacamoleConfig { + // Display settings + colorDepth?: number; + width?: number; + height?: number; + dpi?: number; + resizeMethod?: string; + forceLossless?: boolean; + // Audio settings + disableAudio?: boolean; + enableAudioInput?: boolean; + // RDP Performance settings + enableWallpaper?: boolean; + enableTheming?: boolean; + enableFontSmoothing?: boolean; + enableFullWindowDrag?: boolean; + enableDesktopComposition?: boolean; + enableMenuAnimations?: boolean; + disableBitmapCaching?: boolean; + disableOffscreenCaching?: boolean; + disableGlyphCaching?: boolean; + disableGfx?: boolean; + // RDP Device redirection + enablePrinting?: boolean; + printerName?: string; + enableDrive?: boolean; + driveName?: string; + drivePath?: string; + createDrivePath?: boolean; + disableDownload?: boolean; + disableUpload?: boolean; + enableTouch?: boolean; + // RDP Session settings + clientName?: string; + console?: boolean; + initialProgram?: string; + serverLayout?: string; + timezone?: string; + // RDP Gateway settings + gatewayHostname?: string; + gatewayPort?: number; + gatewayUsername?: string; + gatewayPassword?: string; + gatewayDomain?: string; + // RDP RemoteApp settings + remoteApp?: string; + remoteAppDir?: string; + remoteAppArgs?: string; + // RDP Preconnection settings (Hyper-V) + preconnectionId?: number; + preconnectionBlob?: string; + // RDP Load balancing + loadBalanceInfo?: string; + // Clipboard settings + normalizeClipboard?: string; + disableCopy?: boolean; + disablePaste?: boolean; + // VNC specific settings + cursor?: string; + swapRedBlue?: boolean; + readOnly?: boolean; + // VNC Repeater settings + destHost?: string; + destPort?: number; + // VNC Reverse connection + reverseConnect?: boolean; + listenTimeout?: number; + // Common SFTP settings (for RDP/VNC file transfer) + enableSftp?: boolean; + sftpHostname?: string; + sftpPort?: number; + sftpUsername?: string; + sftpPassword?: string; + sftpPrivateKey?: string; + sftpDirectory?: string; + // Recording settings + recordingPath?: string; + recordingName?: string; + createRecordingPath?: boolean; + recordingExcludeOutput?: boolean; + recordingExcludeMouse?: boolean; + recordingIncludeKeys?: boolean; + // Wake-on-LAN settings + wolSendPacket?: boolean; + wolMacAddr?: string; + wolBroadcastAddr?: string; + wolUdpPort?: number; + wolWaitTime?: number; +} + export interface SSHHost { id: number; connectionType: HostConnectionType; @@ -62,10 +153,12 @@ export interface SSHHost { statsConfig?: string; dockerConfig?: string; terminalConfig?: TerminalConfig; - // RDP/VNC specific fields + // RDP/VNC specific fields (basic) domain?: string; security?: string; ignoreCert?: boolean; + // RDP/VNC extended configuration (stored as JSON) + guacamoleConfig?: GuacamoleConfig | string; createdAt: string; updatedAt: string; } @@ -107,10 +200,12 @@ export interface SSHHostData { statsConfig?: string | Record; dockerConfig?: DockerConfig | string; terminalConfig?: TerminalConfig; - // RDP/VNC specific fields + // RDP/VNC specific fields (basic) domain?: string; security?: string; ignoreCert?: boolean; + // RDP/VNC extended configuration + guacamoleConfig?: GuacamoleConfig; } export interface SSHFolder { diff --git a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx index 9e61c682..6fbd41b7 100644 --- a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx @@ -78,7 +78,7 @@ import { DEFAULT_TERMINAL_CONFIG, } from "@/constants/terminal-themes"; import { TerminalPreview } from "@/ui/desktop/apps/terminal/TerminalPreview.tsx"; -import type { TerminalConfig } from "@/types"; +import type { TerminalConfig, SSHHost } from "@/types"; import { Plus, X, Check, ChevronsUpDown } from "lucide-react"; interface JumpHostItemProps { @@ -277,46 +277,6 @@ function QuickActionItem({ ); } -interface SSHHost { - id: number; - name: string; - ip: string; - port: number; - username: string; - folder: string; - tags: string[]; - pin: boolean; - authType: string; - password?: string; - key?: string; - keyPassword?: string; - keyType?: string; - enableTerminal: boolean; - enableTunnel: boolean; - enableFileManager: boolean; - defaultPath: string; - tunnelConnections: Array<{ - sourcePort: number; - endpointPort: number; - endpointHost: string; - maxRetries: number; - retryInterval: number; - autoStart: boolean; - }>; - jumpHosts?: Array<{ - hostId: number; - }>; - quickActions?: Array<{ - name: string; - snippetId: number; - }>; - statsConfig?: StatsConfig; - terminalConfig?: TerminalConfig; - createdAt: string; - updatedAt: string; - credentialId?: number; -} - interface SSHManagerHostEditorProps { editingHost?: SSHHost | null; onFormSubmit?: (updatedHost?: SSHHost) => void; @@ -448,6 +408,77 @@ export function HostManagerEditor({ domain: z.string().optional(), security: z.string().optional(), ignoreCert: z.boolean().default(false), + // RDP/VNC extended configuration + guacamoleConfig: z.object({ + // Display settings + colorDepth: z.coerce.number().optional(), + width: z.coerce.number().optional(), + height: z.coerce.number().optional(), + dpi: z.coerce.number().optional(), + resizeMethod: z.string().optional(), + forceLossless: z.boolean().optional(), + // Audio settings + disableAudio: z.boolean().optional(), + enableAudioInput: z.boolean().optional(), + // RDP Performance settings + enableWallpaper: z.boolean().optional(), + enableTheming: z.boolean().optional(), + enableFontSmoothing: z.boolean().optional(), + enableFullWindowDrag: z.boolean().optional(), + enableDesktopComposition: z.boolean().optional(), + enableMenuAnimations: z.boolean().optional(), + disableBitmapCaching: z.boolean().optional(), + disableOffscreenCaching: z.boolean().optional(), + disableGlyphCaching: z.boolean().optional(), + disableGfx: z.boolean().optional(), + // RDP Device redirection + enablePrinting: z.boolean().optional(), + printerName: z.string().optional(), + enableDrive: z.boolean().optional(), + driveName: z.string().optional(), + drivePath: z.string().optional(), + createDrivePath: z.boolean().optional(), + disableDownload: z.boolean().optional(), + disableUpload: z.boolean().optional(), + enableTouch: z.boolean().optional(), + // RDP Session settings + clientName: z.string().optional(), + console: z.boolean().optional(), + initialProgram: z.string().optional(), + serverLayout: z.string().optional(), + timezone: z.string().optional(), + // RDP Gateway settings + gatewayHostname: z.string().optional(), + gatewayPort: z.coerce.number().optional(), + gatewayUsername: z.string().optional(), + gatewayPassword: z.string().optional(), + gatewayDomain: z.string().optional(), + // RDP RemoteApp settings + remoteApp: z.string().optional(), + remoteAppDir: z.string().optional(), + remoteAppArgs: z.string().optional(), + // Clipboard settings + normalizeClipboard: z.string().optional(), + disableCopy: z.boolean().optional(), + disablePaste: z.boolean().optional(), + // VNC specific settings + cursor: z.string().optional(), + swapRedBlue: z.boolean().optional(), + readOnly: z.boolean().optional(), + // Recording settings + recordingPath: z.string().optional(), + recordingName: z.string().optional(), + createRecordingPath: z.boolean().optional(), + recordingExcludeOutput: z.boolean().optional(), + recordingExcludeMouse: z.boolean().optional(), + recordingIncludeKeys: z.boolean().optional(), + // Wake-on-LAN settings + wolSendPacket: z.boolean().optional(), + wolMacAddr: z.string().optional(), + wolBroadcastAddr: z.string().optional(), + wolUdpPort: z.coerce.number().optional(), + wolWaitTime: z.coerce.number().optional(), + }).optional(), credentialId: z.number().optional().nullable(), overrideCredentialUsername: z.boolean().optional(), password: z.string().optional(), @@ -692,6 +723,76 @@ export function HostManagerEditor({ domain: "", security: "", ignoreCert: false, + guacamoleConfig: { + // Display settings + colorDepth: undefined, + width: undefined, + height: undefined, + dpi: 96, + resizeMethod: "display-update", + forceLossless: false, + // Audio settings + disableAudio: false, + enableAudioInput: false, + // RDP Performance settings + enableWallpaper: false, + enableTheming: false, + enableFontSmoothing: true, + enableFullWindowDrag: false, + enableDesktopComposition: false, + enableMenuAnimations: false, + disableBitmapCaching: false, + disableOffscreenCaching: false, + disableGlyphCaching: false, + disableGfx: false, + // RDP Device redirection + enablePrinting: false, + printerName: "", + enableDrive: false, + driveName: "", + drivePath: "", + createDrivePath: false, + disableDownload: false, + disableUpload: false, + enableTouch: false, + // RDP Session settings + clientName: "", + console: false, + initialProgram: "", + serverLayout: "en-us-qwerty", + timezone: "", + // RDP Gateway settings + gatewayHostname: "", + gatewayPort: 443, + gatewayUsername: "", + gatewayPassword: "", + gatewayDomain: "", + // RDP RemoteApp settings + remoteApp: "", + remoteAppDir: "", + remoteAppArgs: "", + // Clipboard settings + normalizeClipboard: "preserve", + disableCopy: false, + disablePaste: false, + // VNC specific settings + cursor: "remote", + swapRedBlue: false, + readOnly: false, + // Recording settings + recordingPath: "", + recordingName: "", + createRecordingPath: false, + recordingExcludeOutput: false, + recordingExcludeMouse: false, + recordingIncludeKeys: false, + // Wake-on-LAN settings + wolSendPacket: false, + wolMacAddr: "", + wolBroadcastAddr: "", + wolUdpPort: 9, + wolWaitTime: 0, + }, }, }); @@ -768,6 +869,81 @@ export function HostManagerEditor({ console.error("Failed to parse dockerConfig:", error); } + // Parse guacamoleConfig if it exists - merge with defaults + const defaultGuacamoleConfig = { + colorDepth: undefined, + width: undefined, + height: undefined, + dpi: 96, + resizeMethod: "display-update", + forceLossless: false, + disableAudio: false, + enableAudioInput: false, + enableWallpaper: false, + enableTheming: false, + enableFontSmoothing: true, + enableFullWindowDrag: false, + enableDesktopComposition: false, + enableMenuAnimations: false, + disableBitmapCaching: false, + disableOffscreenCaching: false, + disableGlyphCaching: false, + disableGfx: false, + enablePrinting: false, + printerName: "", + enableDrive: false, + driveName: "", + drivePath: "", + createDrivePath: false, + disableDownload: false, + disableUpload: false, + enableTouch: false, + clientName: "", + console: false, + initialProgram: "", + serverLayout: "en-us-qwerty", + timezone: "", + gatewayHostname: "", + gatewayPort: 443, + gatewayUsername: "", + gatewayPassword: "", + gatewayDomain: "", + remoteApp: "", + remoteAppDir: "", + remoteAppArgs: "", + normalizeClipboard: "preserve", + disableCopy: false, + disablePaste: false, + cursor: "remote", + swapRedBlue: false, + readOnly: false, + recordingPath: "", + recordingName: "", + createRecordingPath: false, + recordingExcludeOutput: false, + recordingExcludeMouse: false, + recordingIncludeKeys: false, + wolSendPacket: false, + wolMacAddr: "", + wolBroadcastAddr: "", + wolUdpPort: 9, + wolWaitTime: 0, + }; + let parsedGuacamoleConfig = { ...defaultGuacamoleConfig }; + try { + if (cleanedHost.guacamoleConfig) { + console.log("[HostManagerEditor] Loading host guacamoleConfig:", cleanedHost.guacamoleConfig); + const parsed = + typeof cleanedHost.guacamoleConfig === "string" + ? JSON.parse(cleanedHost.guacamoleConfig) + : cleanedHost.guacamoleConfig; + parsedGuacamoleConfig = { ...defaultGuacamoleConfig, ...parsed }; + console.log("[HostManagerEditor] Merged guacamoleConfig:", parsedGuacamoleConfig); + } + } catch (error) { + console.error("Failed to parse guacamoleConfig:", error); + } + const formData = { connectionType: (cleanedHost.connectionType || "ssh") as "ssh" | "rdp" | "vnc" | "telnet", name: cleanedHost.name || "", @@ -816,6 +992,8 @@ export function HostManagerEditor({ domain: cleanedHost.domain || "", security: cleanedHost.security || "", ignoreCert: Boolean(cleanedHost.ignoreCert), + // Guacamole config for RDP/VNC + guacamoleConfig: parsedGuacamoleConfig, }; if (defaultAuthType === "password") { @@ -904,6 +1082,12 @@ export function HostManagerEditor({ isSubmittingRef.current = true; setFormError(null); + // Debug: log form data being submitted + console.log("[HostManagerEditor] Form data on submit:", data); + console.log("[HostManagerEditor] data.guacamoleConfig:", data.guacamoleConfig); + console.log("[HostManagerEditor] data.guacamoleConfig.enableWallpaper:", data.guacamoleConfig?.enableWallpaper); + console.log("[HostManagerEditor] form.getValues('guacamoleConfig'):", form.getValues("guacamoleConfig")); + if (!data.name || data.name.trim() === "") { data.name = `${data.username}@${data.ip}`; } @@ -956,8 +1140,13 @@ export function HostManagerEditor({ domain: data.domain || null, security: data.security || null, ignoreCert: Boolean(data.ignoreCert), + // Guacamole configuration for RDP/VNC + guacamoleConfig: data.guacamoleConfig || null, }; + // Debug: log what we're submitting + console.log("[HostManagerEditor] submitData.guacamoleConfig:", submitData.guacamoleConfig); + submitData.credentialId = null; submitData.password = null; submitData.key = null; @@ -1274,6 +1463,38 @@ export function HostManagerEditor({ )} + {/* RDP tabs */} + {form.watch("connectionType") === "rdp" && ( + + + {t("hosts.general")} + + + {t("hosts.display", "Display")} + + + {t("hosts.audio", "Audio")} + + + {t("hosts.performance", "Performance")} + + + + )} + {/* VNC tabs */} + {form.watch("connectionType") === "vnc" && ( + + + {t("hosts.general")} + + + {t("hosts.display", "Display")} + + + {t("hosts.audio", "Audio")} + + + )} {t("hosts.connectionType", "Connection Type")} @@ -3816,6 +4037,521 @@ export function HostManagerEditor({ /> + + {/* RDP/VNC Display Tab */} + + + {t("hosts.displaySettings", "Display Settings")} + + +
+ ( + + {t("hosts.width", "Width")} + + field.onChange(e.target.value ? parseInt(e.target.value) : undefined)} + /> + + + {t("hosts.widthDesc", "Display width in pixels (leave empty for auto)")} + + + )} + /> + + ( + + {t("hosts.height", "Height")} + + field.onChange(e.target.value ? parseInt(e.target.value) : undefined)} + /> + + + {t("hosts.heightDesc", "Display height in pixels (leave empty for auto)")} + + + )} + /> +
+ +
+ ( + + {t("hosts.dpi", "DPI")} + + field.onChange(e.target.value ? parseInt(e.target.value) : undefined)} + /> + + + {t("hosts.dpiDesc", "Display resolution in DPI")} + + + )} + /> + + ( + + {t("hosts.colorDepth", "Color Depth")} + + + {t("hosts.colorDepthDesc", "Color depth for the remote display")} + + + )} + /> +
+ + {form.watch("connectionType") === "rdp" && ( + <> + ( + + {t("hosts.resizeMethod", "Resize Method")} + + + {t("hosts.resizeMethodDesc", "Method to use when resizing the display")} + + + )} + /> + + ( + +
+ {t("hosts.forceLossless", "Force Lossless")} + + {t("hosts.forceLosslessDesc", "Force lossless compression (higher bandwidth)")} + +
+ + + +
+ )} + /> + + )} + + {form.watch("connectionType") === "vnc" && ( + <> + ( + + {t("hosts.cursor", "Cursor Mode")} + + + {t("hosts.cursorDesc", "How to render the mouse cursor")} + + + )} + /> + + ( + +
+ {t("hosts.swapRedBlue", "Swap Red/Blue")} + + {t("hosts.swapRedBlueDesc", "Swap red and blue color components")} + +
+ + + +
+ )} + /> + + ( + +
+ {t("hosts.readOnly", "Read Only")} + + {t("hosts.readOnlyDesc", "View only mode - no input sent to server")} + +
+ + + +
+ )} + /> + + )} +
+ + {/* RDP/VNC Audio Tab */} + + + {t("hosts.audioSettings", "Audio Settings")} + + + ( + +
+ {t("hosts.disableAudio", "Disable Audio")} + + {t("hosts.disableAudioDesc", "Disable audio playback from the remote session")} + +
+ + + +
+ )} + /> + + {form.watch("connectionType") === "rdp" && ( + ( + +
+ {t("hosts.enableAudioInput", "Enable Audio Input")} + + {t("hosts.enableAudioInputDesc", "Enable microphone input to the remote session")} + +
+ + + +
+ )} + /> + )} +
+ + {/* RDP Performance Tab */} + + + {t("hosts.performanceSettings", "Performance Settings")} + + +
+ ( + +
+ {t("hosts.enableWallpaper", "Wallpaper")} + + {t("hosts.enableWallpaperDesc", "Show desktop wallpaper")} + +
+ + { + console.log("[HostManagerEditor] Wallpaper toggled to:", checked); + field.onChange(checked); + // Log the full guacamoleConfig after change + setTimeout(() => { + console.log("[HostManagerEditor] After toggle, guacamoleConfig:", form.getValues("guacamoleConfig")); + }, 0); + }} + /> + +
+ )} + /> + + ( + +
+ {t("hosts.enableTheming", "Theming")} + + {t("hosts.enableThemingDesc", "Enable window theming")} + +
+ + + +
+ )} + /> + + ( + +
+ {t("hosts.enableFontSmoothing", "Font Smoothing")} + + {t("hosts.enableFontSmoothingDesc", "Enable ClearType font smoothing")} + +
+ + + +
+ )} + /> + + ( + +
+ {t("hosts.enableFullWindowDrag", "Full Window Drag")} + + {t("hosts.enableFullWindowDragDesc", "Show window contents while dragging")} + +
+ + + +
+ )} + /> + + ( + +
+ {t("hosts.enableDesktopComposition", "Desktop Composition")} + + {t("hosts.enableDesktopCompositionDesc", "Enable Aero glass effects")} + +
+ + + +
+ )} + /> + + ( + +
+ {t("hosts.enableMenuAnimations", "Menu Animations")} + + {t("hosts.enableMenuAnimationsDesc", "Enable menu animations")} + +
+ + + +
+ )} + /> +
+ + + {t("hosts.cachingSettings", "Caching Settings")} + + +
+ ( + +
+ {t("hosts.disableBitmapCaching", "Disable Bitmap Caching")} + + {t("hosts.disableBitmapCachingDesc", "Disable bitmap caching")} + +
+ + + +
+ )} + /> + + ( + +
+ {t("hosts.disableOffscreenCaching", "Disable Offscreen Caching")} + + {t("hosts.disableOffscreenCachingDesc", "Disable offscreen caching")} + +
+ + + +
+ )} + /> + + ( + +
+ {t("hosts.disableGlyphCaching", "Disable Glyph Caching")} + + {t("hosts.disableGlyphCachingDesc", "Disable glyph caching")} + +
+ + + +
+ )} + /> + + ( + +
+ {t("hosts.disableGfx", "Disable GFX")} + + {t("hosts.disableGfxDesc", "Disable graphics pipeline extension")} + +
+ + + +
+ )} + /> +
+
diff --git a/src/ui/desktop/apps/host-manager/HostManagerViewer.tsx b/src/ui/desktop/apps/host-manager/HostManagerViewer.tsx index 20907c3c..47f47266 100644 --- a/src/ui/desktop/apps/host-manager/HostManagerViewer.tsx +++ b/src/ui/desktop/apps/host-manager/HostManagerViewer.tsx @@ -1496,6 +1496,11 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) { }); } else if (connectionType === "rdp" || connectionType === "vnc") { try { + // Parse guacamoleConfig if it's a string + const guacConfig = typeof host.guacamoleConfig === "string" + ? JSON.parse(host.guacamoleConfig) + : host.guacamoleConfig; + const tokenResponse = await getGuacamoleToken({ protocol: connectionType, hostname: host.ip, @@ -1505,6 +1510,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) { domain: host.domain, security: host.security, ignoreCert: host.ignoreCert, + guacamoleConfig: guacConfig, }); addTab({ type: connectionType, diff --git a/src/ui/desktop/navigation/LeftSidebar.tsx b/src/ui/desktop/navigation/LeftSidebar.tsx index 90724a8c..164f00ac 100644 --- a/src/ui/desktop/navigation/LeftSidebar.tsx +++ b/src/ui/desktop/navigation/LeftSidebar.tsx @@ -36,30 +36,7 @@ import { Button } from "@/components/ui/button.tsx"; import { FolderCard } from "@/ui/desktop/navigation/hosts/FolderCard.tsx"; import { getSSHHosts, getSSHFolders } from "@/ui/main-axios.ts"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; -import type { SSHFolder } from "@/types/index.ts"; - -interface SSHHost { - id: number; - name: string; - ip: string; - port: number; - username: string; - folder: string; - tags: string[]; - pin: boolean; - authType: string; - password?: string; - key?: string; - keyPassword?: string; - keyType?: string; - enableTerminal: boolean; - enableTunnel: boolean; - enableFileManager: boolean; - defaultPath: string; - tunnelConnections: unknown[]; - createdAt: string; - updatedAt: string; -} +import type { SSHFolder, SSHHost } from "@/types/index.ts"; interface SidebarProps { disabled?: boolean; diff --git a/src/ui/desktop/navigation/hosts/Host.tsx b/src/ui/desktop/navigation/hosts/Host.tsx index 05da70d6..e94fcddb 100644 --- a/src/ui/desktop/navigation/hosts/Host.tsx +++ b/src/ui/desktop/navigation/hosts/Host.tsx @@ -115,6 +115,16 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement { addTab({ type: "terminal", title, hostConfig: host }); } else if (connectionType === "rdp" || connectionType === "vnc") { try { + // Parse guacamoleConfig if it's a string + const guacConfig = typeof host.guacamoleConfig === "string" + ? JSON.parse(host.guacamoleConfig) + : host.guacamoleConfig; + + // Debug: log what guacamoleConfig we have + console.log("[Host.tsx] host.guacamoleConfig type:", typeof host.guacamoleConfig); + console.log("[Host.tsx] host.guacamoleConfig:", host.guacamoleConfig); + console.log("[Host.tsx] Parsed guacConfig:", guacConfig); + // Get guacamole token for RDP/VNC connection const tokenResponse = await getGuacamoleToken({ protocol: connectionType, @@ -125,6 +135,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement { domain: host.domain, security: host.security, ignoreCert: host.ignoreCert, + guacamoleConfig: guacConfig, }); addTab({ diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index d375e0c1..244e2b11 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -878,6 +878,8 @@ export async function createSSHHost(hostData: SSHHostData): Promise { domain: hostData.domain || null, security: hostData.security || null, ignoreCert: Boolean(hostData.ignoreCert), + // Guacamole configuration for RDP/VNC + guacamoleConfig: hostData.guacamoleConfig || null, }; if (!submitData.enableTunnel) { @@ -955,6 +957,8 @@ export async function updateSSHHost( domain: hostData.domain || null, security: hostData.security || null, ignoreCert: Boolean(hostData.ignoreCert), + // Guacamole configuration for RDP/VNC + guacamoleConfig: hostData.guacamoleConfig || null, }; if (!submitData.enableTunnel) { @@ -3142,16 +3146,171 @@ export interface GuacamoleTokenRequest { domain?: string; security?: string; ignoreCert?: boolean; + // Extended guacamole configuration + guacamoleConfig?: { + // Display settings + colorDepth?: number; + width?: number; + height?: number; + dpi?: number; + resizeMethod?: string; + forceLossless?: boolean; + // Audio settings + disableAudio?: boolean; + enableAudioInput?: boolean; + // RDP Performance settings + enableWallpaper?: boolean; + enableTheming?: boolean; + enableFontSmoothing?: boolean; + enableFullWindowDrag?: boolean; + enableDesktopComposition?: boolean; + enableMenuAnimations?: boolean; + disableBitmapCaching?: boolean; + disableOffscreenCaching?: boolean; + disableGlyphCaching?: boolean; + disableGfx?: boolean; + // RDP Device redirection + enablePrinting?: boolean; + printerName?: string; + enableDrive?: boolean; + driveName?: string; + drivePath?: string; + createDrivePath?: boolean; + disableDownload?: boolean; + disableUpload?: boolean; + enableTouch?: boolean; + // RDP Session settings + clientName?: string; + console?: boolean; + initialProgram?: string; + serverLayout?: string; + timezone?: string; + // RDP Gateway settings + gatewayHostname?: string; + gatewayPort?: number; + gatewayUsername?: string; + gatewayPassword?: string; + gatewayDomain?: string; + // RDP RemoteApp settings + remoteApp?: string; + remoteAppDir?: string; + remoteAppArgs?: string; + // Clipboard settings + normalizeClipboard?: string; + disableCopy?: boolean; + disablePaste?: boolean; + // VNC specific settings + cursor?: string; + swapRedBlue?: boolean; + readOnly?: boolean; + // Recording settings + recordingPath?: string; + recordingName?: string; + createRecordingPath?: boolean; + recordingExcludeOutput?: boolean; + recordingExcludeMouse?: boolean; + recordingIncludeKeys?: boolean; + // Wake-on-LAN settings + wolSendPacket?: boolean; + wolMacAddr?: string; + wolBroadcastAddr?: string; + wolUdpPort?: number; + wolWaitTime?: number; + }; } export interface GuacamoleTokenResponse { token: string; } +// Helper to convert camelCase to kebab-case for guacamole parameters +function toGuacamoleParams(config: GuacamoleTokenRequest["guacamoleConfig"]): Record { + if (!config) return {}; + + const params: Record = {}; + + // Map camelCase to guacamole's kebab-case parameter names + const mappings: Record = { + colorDepth: "color-depth", + resizeMethod: "resize-method", + forceLossless: "force-lossless", + disableAudio: "disable-audio", + enableAudioInput: "enable-audio-input", + enableWallpaper: "enable-wallpaper", + enableTheming: "enable-theming", + enableFontSmoothing: "enable-font-smoothing", + enableFullWindowDrag: "enable-full-window-drag", + enableDesktopComposition: "enable-desktop-composition", + enableMenuAnimations: "enable-menu-animations", + disableBitmapCaching: "disable-bitmap-caching", + disableOffscreenCaching: "disable-offscreen-caching", + disableGlyphCaching: "disable-glyph-caching", + disableGfx: "disable-gfx", + enablePrinting: "enable-printing", + printerName: "printer-name", + enableDrive: "enable-drive", + driveName: "drive-name", + drivePath: "drive-path", + createDrivePath: "create-drive-path", + disableDownload: "disable-download", + disableUpload: "disable-upload", + enableTouch: "enable-touch", + clientName: "client-name", + initialProgram: "initial-program", + serverLayout: "server-layout", + gatewayHostname: "gateway-hostname", + gatewayPort: "gateway-port", + gatewayUsername: "gateway-username", + gatewayPassword: "gateway-password", + gatewayDomain: "gateway-domain", + remoteApp: "remote-app", + remoteAppDir: "remote-app-dir", + remoteAppArgs: "remote-app-args", + normalizeClipboard: "normalize-clipboard", + disableCopy: "disable-copy", + disablePaste: "disable-paste", + swapRedBlue: "swap-red-blue", + readOnly: "read-only", + recordingPath: "recording-path", + recordingName: "recording-name", + createRecordingPath: "create-recording-path", + recordingExcludeOutput: "recording-exclude-output", + recordingExcludeMouse: "recording-exclude-mouse", + recordingIncludeKeys: "recording-include-keys", + wolSendPacket: "wol-send-packet", + wolMacAddr: "wol-mac-addr", + wolBroadcastAddr: "wol-broadcast-addr", + wolUdpPort: "wol-udp-port", + wolWaitTime: "wol-wait-time", + }; + + for (const [key, value] of Object.entries(config)) { + if (value !== undefined && value !== null && value !== "") { + const paramName = mappings[key] || key; + // Guacamole expects boolean values as strings "true" or "false" + if (typeof value === "boolean") { + params[paramName] = value ? "true" : "false"; + } else { + params[paramName] = value; + } + } + } + + return params; +} + export async function getGuacamoleToken( request: GuacamoleTokenRequest, ): Promise { try { + // Convert guacamoleConfig to guacamole parameter format + const guacParams = toGuacamoleParams(request.guacamoleConfig); + + // Debug: log guacamoleConfig and converted params + console.log("[Guacamole] Request guacamoleConfig:", request.guacamoleConfig); + console.log("[Guacamole] Converted params:", guacParams); + console.log("[Guacamole] Param count:", Object.keys(guacParams).length); + // Use authApi (port 30001 without /ssh prefix) since guacamole routes are at /guacamole const response = await authApi.post("/guacamole/token", { type: request.protocol, @@ -3162,6 +3321,7 @@ export async function getGuacamoleToken( domain: request.domain, security: request.security, "ignore-cert": request.ignoreCert, + ...guacParams, }); return response.data; } catch (error) {