diff --git a/docs/api.md b/docs/api.md index 545dee0..bf9b976 100644 --- a/docs/api.md +++ b/docs/api.md @@ -218,6 +218,7 @@ The `jump_hosts` array defines an ordered chain of SSH bastion hops. Each hop co | `kdc_url` | string | RDP | Kerberos KDC or KDC Proxy URL | | `kerberos_cache` | string | RDP | Path to Kerberos credential cache (advanced) | | `color_depth` | integer | RDP | Color depth in bits (8, 16, 24, 32) | +| `vnc_resize` | boolean | VNC | Ask the VNC server to resize its screen to the browser window (default `false`: the screen is scaled to fit). Only for servers with their own virtual screen (TigerVNC, Xvnc); a physical console such as UltraVNC, RealVNC or x11vnc can't resize, and the session stalls if asked. | | `enable_drive` | boolean | RDP, SSH | Enable file transfer / drive redirection | | `disable_copy` | boolean | All | Disable clipboard copy (server → client) | | `disable_paste` | boolean | All | Disable clipboard paste (client → server) | @@ -647,6 +648,7 @@ Create a connection entry. The body includes a `name` field plus all entry field | `auth_pkg` | string | RDP | NLA auth package | | `kdc_url` | string | RDP | Kerberos KDC URL | | `color_depth` | integer | RDP | Color depth | +| `vnc_resize` | boolean | VNC | Resize the server's screen to the browser window (default `false`). See [`POST /api/sessions`](#post-apisessions). | | `enable_drive` | boolean | RDP, SSH | Enable file transfer | | `disable_copy` | boolean | All | Disable clipboard copy (server → client) | | `disable_paste` | boolean | All | Disable clipboard paste (client → server) | diff --git a/src/api.rs b/src/api.rs index 7c0f6da..c57c337 100644 --- a/src/api.rs +++ b/src/api.rs @@ -4014,6 +4014,7 @@ pub fn session_request_from_entry( login_script: entry.login_script, autofill: entry.autofill, allowed_domains: entry.allowed_domains, + vnc_resize: entry.vnc_resize, disable_copy: entry.disable_copy, disable_paste: entry.disable_paste, enable_gfx: entry.enable_gfx, @@ -7378,6 +7379,7 @@ pub async fn quick_connect( login_script: None, autofill: None, allowed_domains: None, + vnc_resize: None, disable_copy: None, disable_paste: None, enable_gfx: None, @@ -7652,6 +7654,7 @@ mod tests { login_script: ab_entry.login_script, autofill: ab_entry.autofill, allowed_domains: ab_entry.allowed_domains, + vnc_resize: ab_entry.vnc_resize, disable_copy: ab_entry.disable_copy, disable_paste: ab_entry.disable_paste, enable_gfx: ab_entry.enable_gfx, @@ -7746,6 +7749,7 @@ mod tests { login_script: ab_entry.login_script, autofill: ab_entry.autofill, allowed_domains: ab_entry.allowed_domains, + vnc_resize: ab_entry.vnc_resize, disable_copy: ab_entry.disable_copy, disable_paste: ab_entry.disable_paste, enable_gfx: ab_entry.enable_gfx, diff --git a/src/guacd.rs b/src/guacd.rs index cb93c7d..9763bbb 100644 --- a/src/guacd.rs +++ b/src/guacd.rs @@ -107,6 +107,12 @@ pub struct VncParams { pub dpi: u32, pub disable_copy: bool, pub disable_paste: bool, + /// Ask the VNC server to change its screen to the browser's size. Only + /// servers with their own virtual screen can (Xvnc, TigerVNC); a physical + /// console (UltraVNC, RealVNC, x11vnc) can't, and libvncclient then stops + /// requesting updates while it waits for a resize that never comes, so + /// the session all but freezes. Off, guacd scales the screen instead. + pub display_resize: bool, /// Wake-on-LAN parameters. pub wol: WolParams, } @@ -159,6 +165,33 @@ impl SpiceParams { } } +/// The value guacd gets for one VNC argument. +fn vnc_arg_value(name: &str, p: &VncParams) -> String { + match name { + "hostname" => p.hostname.clone(), + "port" => p.port.to_string(), + "width" => p.width.to_string(), + "height" => p.height.to_string(), + "dpi" => p.dpi.to_string(), + "username" => p.username.clone().unwrap_or_default(), + "password" => p.password.clone().unwrap_or_default(), + "color-depth" => p.color_depth.map_or("24".into(), |d| d.to_string()), + "cursor" => "local".into(), + "read-only" => "false".into(), + "swap-red-blue" => "false".into(), + "dest-host" => String::new(), + "dest-port" => String::new(), + "enable-audio" => "false".into(), + "disable-copy" => if p.disable_copy { "true" } else { "false" }.into(), + "disable-paste" => if p.disable_paste { "true" } else { "false" }.into(), + "disable-display-resize" => if p.display_resize { "false" } else { "true" }.into(), + _ => wol_arg_value(name, &p.wol).unwrap_or_else(|| { + tracing::debug!("Unknown guacd VNC parameter '{}', sending empty", name); + String::new() + }), + } +} + /// Resolve a guacd SPICE connect arg name to its value. /// /// SPICE has no width/height/dpi connect args: it sizes via the `size` @@ -383,28 +416,7 @@ pub async fn connect_and_handshake( String::new() }), }, - ConnectionParams::Vnc(p) => match name.as_str() { - "hostname" => p.hostname.clone(), - "port" => p.port.to_string(), - "width" => p.width.to_string(), - "height" => p.height.to_string(), - "dpi" => p.dpi.to_string(), - "username" => p.username.clone().unwrap_or_default(), - "password" => p.password.clone().unwrap_or_default(), - "color-depth" => p.color_depth.map_or("24".into(), |d| d.to_string()), - "cursor" => "local".into(), - "read-only" => "false".into(), - "swap-red-blue" => "false".into(), - "dest-host" => String::new(), - "dest-port" => String::new(), - "enable-audio" => "false".into(), - "disable-copy" => if p.disable_copy { "true" } else { "false" }.into(), - "disable-paste" => if p.disable_paste { "true" } else { "false" }.into(), - _ => wol_arg_value(name, &p.wol).unwrap_or_else(|| { - tracing::debug!("Unknown guacd VNC parameter '{}', sending empty", name); - String::new() - }), - }, + ConnectionParams::Vnc(p) => vnc_arg_value(name.as_str(), p), ConnectionParams::Rdp(p) => match name.as_str() { "hostname" => p.hostname.clone(), "port" => p.port.to_string(), @@ -734,6 +746,32 @@ mod tests { v.iter().map(|s| s.to_string()).collect() } + fn vnc(display_resize: bool) -> VncParams { + VncParams { + hostname: "192.0.2.10".into(), + port: 5900, + username: None, + password: None, + color_depth: None, + width: 2560, + height: 1266, + dpi: 96, + disable_copy: false, + disable_paste: false, + display_resize, + wol: WolParams::default(), + } + } + + #[test] + fn vnc_display_resize_is_off_unless_asked() { + // A physical console can't resize, and libvncclient then stops asking + // for updates while it waits: guacd must be told not to try. + assert_eq!(vnc_arg_value("disable-display-resize", &vnc(false)), "true"); + assert_eq!(vnc_arg_value("disable-display-resize", &vnc(true)), "false"); + assert_eq!(vnc_arg_value("hostname", &vnc(false)), "192.0.2.10"); + } + #[test] fn join_args_read_write_by_default() { let got = join_arg_values(&names(&["VERSION_1_5_0", "hostname", "read-only"]), false); diff --git a/src/import.rs b/src/import.rs index 2d302e3..7c200fe 100644 --- a/src/import.rs +++ b/src/import.rs @@ -155,6 +155,7 @@ pub async fn cmd_import_guacamole( login_script: None, autofill: None, allowed_domains: None, + vnc_resize: None, disable_copy: None, disable_paste: None, banner: None, diff --git a/src/session.rs b/src/session.rs index 57c7284..873d086 100644 --- a/src/session.rs +++ b/src/session.rs @@ -104,6 +104,11 @@ pub struct CreateSessionRequest { /// Allowed domains for web sessions. When set, Chromium can only reach these domains. pub allowed_domains: Option>, /// Disable clipboard copy (server → client). + /// VNC: ask the server to resize its screen to the browser window. + /// Off by default: only servers with a virtual screen (Xvnc, TigerVNC) + /// can, and one that can't leaves the session frozen. + #[serde(default)] + pub vnc_resize: Option, pub disable_copy: Option, /// Disable clipboard paste (client → server). pub disable_paste: Option, @@ -1354,6 +1359,8 @@ impl SessionManager { dpi, disable_copy: req.disable_copy.unwrap_or(false), disable_paste: req.disable_paste.unwrap_or(false), + // Off unless the entry says the server can resize. + display_resize: req.vnc_resize.unwrap_or(false), wol: wol.clone(), }); ( @@ -1664,6 +1671,8 @@ impl SessionManager { dpi, disable_copy: req.disable_copy.unwrap_or(false), disable_paste: req.disable_paste.unwrap_or(false), + // Our own Xvnc can always be resized to the browser. + display_resize: true, // Local Xvnc — WoL not applicable. wol: guacd::WolParams::default(), }); @@ -3150,6 +3159,21 @@ mod tests { } } + #[test] + fn vnc_entries_do_not_resize_the_server_unless_set() { + // The request field defaults to "don't resize", so an entry or ad-hoc + // VNC connection that says nothing leaves the server's screen alone. + let req: CreateSessionRequest = + serde_json::from_value(serde_json::json!({"session_type": "vnc", "hostname": "h"})) + .unwrap(); + assert!(!req.vnc_resize.unwrap_or(false)); + let req: CreateSessionRequest = serde_json::from_value( + serde_json::json!({"session_type": "vnc", "hostname": "h", "vnc_resize": true}), + ) + .unwrap(); + assert_eq!(req.vnc_resize, Some(true)); + } + #[test] fn vdi_username_must_be_a_plain_name() { // It becomes a directory under home_base that is bind-mounted into the diff --git a/src/vault.rs b/src/vault.rs index 58fc0a0..0e779b6 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -350,6 +350,10 @@ pub struct AddressBookEntry { /// these domains (plus localhost). Uses --host-rules to block all others. #[serde(default, skip_serializing_if = "Option::is_none")] pub allowed_domains: Option>, + /// VNC: resize the server's screen to the browser window. Off unless set: + /// only servers with a virtual screen (Xvnc, TigerVNC) can resize. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub vnc_resize: Option, /// Disable clipboard copy (server → client). Prevents copying from the remote session. #[serde(default, skip_serializing_if = "Option::is_none")] pub disable_copy: Option, @@ -591,6 +595,9 @@ pub struct EntryInfo { /// Allowed domains for web sessions. #[serde(skip_serializing_if = "Option::is_none")] pub allowed_domains: Option>, + /// VNC: resize the server's screen to the browser window. + #[serde(skip_serializing_if = "Option::is_none")] + pub vnc_resize: Option, /// Disable clipboard copy (server → client). #[serde(skip_serializing_if = "Option::is_none")] pub disable_copy: Option, @@ -772,6 +779,7 @@ impl From<(&str, &AddressBookEntry)> for EntryInfo { login_script: e.login_script.clone(), autofill: e.autofill.clone(), allowed_domains: e.allowed_domains.clone(), + vnc_resize: e.vnc_resize, disable_copy: e.disable_copy, disable_paste: e.disable_paste, banner: e.banner.clone(), diff --git a/static/connections.html b/static/connections.html index 9d2ad87..5fc7845 100644 --- a/static/connections.html +++ b/static/connections.html @@ -962,6 +962,11 @@ + +
Only for VNC servers with their own virtual screen, such as TigerVNC or Xvnc. Leave off for a physical console (UltraVNC, RealVNC, x11vnc): they can't resize, and the session stalls if asked to. When off, the screen is scaled to fit instead.