mirror of
https://github.com/sol1/rustguac.git
synced 2026-10-25 13:09:56 +00:00
vnc: don't ask the server to resize unless the entry says it can
guacd asks a VNC server to resize its screen to the browser window by
default. A physical console (UltraVNC, RealVNC, x11vnc) can't, and
libvncclient then stops requesting updates while it waits for a resize
that never comes ("Skipping Update - resize in progress"), so the
session all but freezes. Seen with UltraVNC on a two-monitor console.
VNC entries and ad-hoc VNC now send disable-display-resize unless the
new vnc_resize option is on ("Resize the remote desktop to fit the
window" in the editor), and guacd scales the screen instead. Web
sessions, on our own Xvnc, still resize. The VNC argument mapping moves
into vnc_arg_value() so it can be tested.
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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,
|
||||
|
||||
+60
-22
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -104,6 +104,11 @@ pub struct CreateSessionRequest {
|
||||
/// Allowed domains for web sessions. When set, Chromium can only reach these domains.
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
/// 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<bool>,
|
||||
pub disable_copy: Option<bool>,
|
||||
/// Disable clipboard paste (client → server).
|
||||
pub disable_paste: Option<bool>,
|
||||
@@ -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
|
||||
|
||||
@@ -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<Vec<String>>,
|
||||
/// 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<bool>,
|
||||
/// Disable clipboard copy (server → client). Prevents copying from the remote session.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub disable_copy: Option<bool>,
|
||||
@@ -591,6 +595,9 @@ pub struct EntryInfo {
|
||||
/// Allowed domains for web sessions.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
/// VNC: resize the server's screen to the browser window.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vnc_resize: Option<bool>,
|
||||
/// Disable clipboard copy (server → client).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub disable_copy: Option<bool>,
|
||||
@@ -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(),
|
||||
|
||||
@@ -962,6 +962,11 @@
|
||||
<option value="32">32-bit</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="text-transform:none;letter-spacing:0;cursor:pointer;margin-top:0.6em">
|
||||
<input type="checkbox" id="em-vnc-resize" style="display:inline;width:auto;margin-right:0.5em">
|
||||
Resize the remote desktop to fit the window
|
||||
</label>
|
||||
<div class="field-hint">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.</div>
|
||||
<div class="entry-only">
|
||||
<label style="margin-top:0.8em">
|
||||
<input type="checkbox" id="em-vnc-prompt-creds" style="display:inline;width:auto;margin-right:0.4em"> Prompt for credentials at connect time
|
||||
@@ -4408,6 +4413,7 @@
|
||||
document.getElementById('em-vnc-username').value = '';
|
||||
document.getElementById('em-vnc-password').value = '';
|
||||
document.getElementById('em-vnc-color-depth').value = '';
|
||||
document.getElementById('em-vnc-resize').checked = false;
|
||||
document.getElementById('em-vnc-prompt-creds').checked = false;
|
||||
document.getElementById('em-spice-hostname').value = '';
|
||||
document.getElementById('em-spice-port').value = '5900';
|
||||
@@ -4569,6 +4575,7 @@
|
||||
document.getElementById('em-vnc-port').value = entryData.port || '5900';
|
||||
document.getElementById('em-vnc-username').value = entryData.username || '';
|
||||
document.getElementById('em-vnc-color-depth').value = entryData.color_depth ? String(entryData.color_depth) : '';
|
||||
document.getElementById('em-vnc-resize').checked = !!entryData.vnc_resize;
|
||||
document.getElementById('em-vnc-prompt-creds').checked = !!entryData.prompt_credentials;
|
||||
} else if (type === 'spice') {
|
||||
document.getElementById('em-spice-hostname').value = entryData.hostname || '';
|
||||
@@ -4794,6 +4801,7 @@
|
||||
if (pw) entry.password = pw;
|
||||
var cd = document.getElementById('em-vnc-color-depth').value;
|
||||
if (cd) entry.color_depth = parseInt(cd);
|
||||
if (document.getElementById('em-vnc-resize').checked) entry.vnc_resize = true;
|
||||
if (document.getElementById('em-vnc-prompt-creds').checked) entry.prompt_credentials = true;
|
||||
} else if (type === 'spice') {
|
||||
var h = document.getElementById('em-spice-hostname').value.trim();
|
||||
|
||||
@@ -245,3 +245,27 @@ test('file transfer is offered when the site has it on, and always to admins', a
|
||||
if (role === 'admin') await expect(page.getByText('above the site\'s defaults are capped')).toBeHidden();
|
||||
}
|
||||
});
|
||||
|
||||
// VNC display resize is off unless asked: a physical console can't resize,
|
||||
// and the session stalls if it is asked to.
|
||||
test('VNC asks the server to resize only when the box is ticked', async ({ page }) => {
|
||||
const api = await installApi(page, { role: 'poweruser' });
|
||||
await page.goto('/connections.html');
|
||||
await settle(page);
|
||||
for (const tick of [false, true]) {
|
||||
await openEditor(page);
|
||||
await page.locator('#em-type').selectOption('vnc');
|
||||
const box = page.locator('#em-vnc-resize');
|
||||
await expect(box).toBeVisible();
|
||||
await expect(box).not.toBeChecked();
|
||||
if (tick) await box.check();
|
||||
await page.locator('#em-vnc-hostname').fill('192.0.2.20');
|
||||
const before = adhocCalls(api).length;
|
||||
await Promise.all([page.waitForEvent('popup'), page.locator('#em-connect').click()]);
|
||||
await expect.poll(() => adhocCalls(api).length).toBe(before + 1);
|
||||
const sent = adhocCalls(api)[before].body.entry;
|
||||
expect(sent.type).toBe('vnc');
|
||||
if (tick) expect(sent.vnc_resize).toBe(true);
|
||||
else expect(sent.vnc_resize).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user