diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 1f5dbd0..d51ab0d 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2321,6 +2321,8 @@ dependencies = [ "num-traits", "png", "tiff", + "zune-core", + "zune-jpeg", ] [[package]] @@ -2817,7 +2819,10 @@ dependencies = [ "argon2 0.5.3", "base64 0.22.1", "chrono", + "des", + "flate2", "hmac 0.13.0", + "image", "keyring", "libc", "portable-pty", diff --git a/rust/core/Cargo.toml b/rust/core/Cargo.toml index b198b60..c95c29b 100644 --- a/rust/core/Cargo.toml +++ b/rust/core/Cargo.toml @@ -23,6 +23,9 @@ hmac = "0.13.0" sha1 = "0.11.0" portable-pty = "0.9.0" serialport = { version = "4", default-features = false } +des = "0.9" +flate2 = "1" +image = { version = "0.25", default-features = false, features = ["jpeg"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } webpki-roots = "1" diff --git a/rust/core/src/clients/mod.rs b/rust/core/src/clients/mod.rs index 69f0f2b..1fac69d 100644 --- a/rust/core/src/clients/mod.rs +++ b/rust/core/src/clients/mod.rs @@ -42,6 +42,7 @@ impl Callbacks { pub mod raw; pub mod serial; pub mod telnet; +pub mod vnc; /// Called with protocol negotiation debug messages (Go's `onDebug`). pub type DebugFn = Arc; diff --git a/rust/core/src/clients/vnc.rs b/rust/core/src/clients/vnc.rs new file mode 100644 index 0000000..1efa569 --- /dev/null +++ b/rust/core/src/clients/vnc.rs @@ -0,0 +1,1043 @@ +//! Native VNC (RFB 3.3/3.7/3.8) client: None and VNC Authentication, Raw, CopyRect, Hextile, ZRLE +//! and Tight (zlib + JPEG), plus the DesktopSize, ExtendedDesktopSize and LastRect pseudo-encodings. +//! +//! One I/O thread owns the transport (TCP, or a caller-supplied stream such as an SSH tunnel). It +//! decodes into a private framebuffer and, after each FramebufferUpdate, copies the changed rows into +//! the shared `Arc>` the UI reads, so the lock is never held across network reads. +//! Outgoing input is queued and written whenever a read polls out, so the transport is never split. + +use std::io::{BufReader, ErrorKind, Read, Write}; +use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::Duration; + +use flate2::{Decompress, FlushDecompress}; + +pub use super::irc::{Connector, ReadWrite}; +use super::{CloseFn, ErrorFn}; + +const READ_POLL: Duration = Duration::from_millis(50); + +pub type FrameFn = Arc; +pub type ResizeFn = Arc; +pub type ClipboardFn = Arc; + +#[derive(Clone)] +pub struct Config { + pub host: String, + pub port: u16, + pub password: Option, + /// ClientInit's shared flag: leave other viewers connected. + pub shared: bool, + pub view_only: bool, + /// Tight JPEG quality, 0–9. + pub quality: u8, + /// Provides the transport instead of a direct TCP connect (e.g. an SSH direct-tcpip stream). + pub connector: Option, + pub connect_timeout: Duration, + /// Called after each FramebufferUpdate has been applied to the shared framebuffer. + pub on_frame: FrameFn, + pub on_resize: ResizeFn, + pub on_clipboard: ClipboardFn, + pub on_error: ErrorFn, + pub on_close: CloseFn, +} + +impl Default for Config { + fn default() -> Config { + Config { + host: String::new(), + port: 5900, + password: None, + shared: true, + view_only: false, + quality: 6, + connector: None, + connect_timeout: Duration::ZERO, + on_frame: Arc::new(|| {}), + on_resize: Arc::new(|_, _| {}), + on_clipboard: Arc::new(|_| {}), + on_error: Arc::new(|_| {}), + on_close: Arc::new(|| {}), + } + } +} + +/// The remote desktop as RGBA, row-major, `width * height * 4` bytes. The I/O thread sets `dirty` +/// after each update; the UI clears it once it has uploaded the texture. +#[derive(Clone, Debug, Default)] +pub struct Framebuffer { + pub width: u16, + pub height: u16, + pub rgba: Vec, + pub dirty: bool, +} + +impl Framebuffer { + fn new(width: u16, height: u16) -> Framebuffer { + let mut rgba = vec![0u8; width as usize * height as usize * 4]; + rgba.chunks_exact_mut(4).for_each(|p| p[3] = 255); + Framebuffer { width, height, rgba, dirty: true } + } + + fn check(&self, x: u16, y: u16, w: u16, h: u16) -> crate::Result<()> { + if x as usize + w as usize > self.width as usize || y as usize + h as usize > self.height as usize { + return Err(format!("rectangle {w}x{h}+{x}+{y} outside the {}x{} framebuffer", self.width, self.height).into()); + } + Ok(()) + } + + /// Writes `w*h` pixels of `bpp` bytes each (R, G, B first; anything after is ignored). + fn put(&mut self, x: u16, y: u16, w: u16, h: u16, px: &[u8], bpp: usize) { + let (w, stride) = (w as usize, self.width as usize); + for row in 0..h as usize { + let dst = ((y as usize + row) * stride + x as usize) * 4; + for (d, s) in self.rgba[dst..dst + w * 4].chunks_exact_mut(4).zip(px[row * w * bpp..].chunks_exact(bpp)) { + d[..3].copy_from_slice(&s[..3]); + d[3] = 255; + } + } + } + + fn fill(&mut self, x: u16, y: u16, w: u16, h: u16, rgb: &[u8]) { + let stride = self.width as usize; + for row in 0..h as usize { + let dst = ((y as usize + row) * stride + x as usize) * 4; + for d in self.rgba[dst..dst + w as usize * 4].chunks_exact_mut(4) { + d[..3].copy_from_slice(&rgb[..3]); + d[3] = 255; + } + } + } +} + +struct State { + connected: bool, + closing: bool, + ready: Option>>, + name: String, +} + +pub struct Client { + cfg: Config, + fb: Arc>, + state: Mutex, + out: Mutex>>>, +} + +impl Client { + pub fn new(mut cfg: Config) -> Arc { + if cfg.connect_timeout.is_zero() { + cfg.connect_timeout = Duration::from_secs(30); + } + cfg.quality = cfg.quality.min(9); + Arc::new(Client { + cfg, + fb: Arc::new(Mutex::new(Framebuffer::default())), + state: Mutex::new(State { connected: false, closing: false, ready: None, name: String::new() }), + out: Mutex::new(None), + }) + } + + fn st(&self) -> MutexGuard<'_, State> { + self.state.lock().unwrap_or_else(|e| e.into_inner()) + } + + /// Connects and blocks until the handshake completes (ServerInit received), fails, or times out. + pub fn connect(self: &Arc) -> crate::Result<()> { + let (ready_tx, ready_rx) = mpsc::channel(); + let (out_tx, out_rx) = mpsc::channel(); + self.st().ready = Some(ready_tx); + *self.out.lock().unwrap_or_else(|e| e.into_inner()) = Some(out_tx); + + let client = self.clone(); + std::thread::spawn(move || client.io_thread(out_rx)); + + let timeout = self.cfg.connect_timeout; + match ready_rx.recv_timeout(timeout) { + Ok(Ok(())) => Ok(()), + Ok(Err(e)) => Err(e.into()), + Err(_) => { + self.close(); + Err(format!("VNC connection timed out after {timeout:?}").into()) + } + } + } + + /// Disconnects. Idempotent; `on_close` fires from the I/O thread. + pub fn close(&self) { + { + let mut st = self.st(); + if st.closing { + return; + } + st.closing = true; + } + // Dropping the sender makes the I/O thread's next poll end the connection. + *self.out.lock().unwrap_or_else(|e| e.into_inner()) = None; + } + + pub fn framebuffer(&self) -> Arc> { + self.fb.clone() + } + + pub fn desktop_name(&self) -> String { + self.st().name.clone() + } + + /// PointerEvent. `buttons` bit 0 is left, 1 middle, 2 right, 3/4 wheel up/down. + pub fn pointer(&self, x: u16, y: u16, buttons: u8) -> crate::Result<()> { + if self.cfg.view_only { + return Ok(()); + } + let mut m = vec![5, buttons]; + m.extend_from_slice(&x.to_be_bytes()); + m.extend_from_slice(&y.to_be_bytes()); + self.queue(m) + } + + /// KeyEvent with an X11 keysym (see `keysym_for`). + pub fn key(&self, keysym: u32, down: bool) -> crate::Result<()> { + if self.cfg.view_only { + return Ok(()); + } + let mut m = vec![4, down as u8, 0, 0]; + m.extend_from_slice(&keysym.to_be_bytes()); + self.queue(m) + } + + /// ClientCutText. RFB clipboard text is Latin-1; other characters become `?`. + pub fn send_clipboard(&self, text: &str) -> crate::Result<()> { + if self.cfg.view_only { + return Ok(()); + } + let bytes: Vec = text.chars().map(|c| u8::try_from(c).unwrap_or(b'?')).collect(); + let mut m = vec![6, 0, 0, 0]; + m.extend_from_slice(&(bytes.len() as u32).to_be_bytes()); + m.extend_from_slice(&bytes); + self.queue(m) + } + + /// Asks for a non-incremental update of the whole screen. + pub fn request_full_update(&self) -> crate::Result<()> { + let (w, h) = { + let fb = self.fb.lock().unwrap_or_else(|e| e.into_inner()); + (fb.width, fb.height) + }; + self.queue(update_request(false, w, h)) + } + + fn queue(&self, msg: Vec) -> crate::Result<()> { + let guard = self.out.lock().unwrap_or_else(|e| e.into_inner()); + if guard.as_ref().is_some_and(|tx| tx.send(msg).is_ok()) { Ok(()) } else { Err("not connected".into()) } + } + + fn io_thread(self: Arc, out_rx: Receiver>) { + let result = self.open_transport().and_then(|io| self.run(io, out_rx)); + *self.out.lock().unwrap_or_else(|e| e.into_inner()) = None; + let (was_closing, was_connected, ready) = { + let mut st = self.st(); + let r = (st.closing, st.connected, st.ready.take()); + st.connected = false; + r + }; + if let Err(e) = result { + if !was_closing { + let msg = e.to_string(); + (self.cfg.on_error)(msg.clone()); + if let Some(ready) = ready { + let _ = ready.send(Err(msg)); + } + } + } + if was_connected { + (self.cfg.on_close)(); + } + } + + fn open_transport(&self) -> crate::Result> { + if let Some(connector) = &self.cfg.connector { + return connector(); + } + let (addr, tcp) = super::dial(&self.cfg.host, self.cfg.port as i64, self.cfg.connect_timeout.as_secs().max(1) as i64); + let tcp = tcp.map_err(|e| format!("dial tcp {addr}: {e}"))?; + tcp.set_read_timeout(Some(READ_POLL))?; + tcp.set_write_timeout(Some(self.cfg.connect_timeout))?; + let _ = tcp.set_nodelay(true); + Ok(Box::new(tcp)) + } + + fn run(&self, io: Box, out_rx: Receiver>) -> crate::Result<()> { + let mut r = BufReader::with_capacity(1 << 16, Conn { io, out_rx }); + let (w, h) = self.handshake(&mut r)?; + let mut local = Framebuffer::new(w, h); + *self.fb.lock().unwrap_or_else(|e| e.into_inner()) = local.clone(); + (self.cfg.on_resize)(w, h); + { + let mut st = self.st(); + if st.closing { + return Ok(()); + } + st.connected = true; + if let Some(ready) = st.ready.take() { + let _ = ready.send(Ok(())); + } + } + let mut dec = Decoders::default(); + r.get_mut().send(&update_request(false, w, h))?; + loop { + match read_u8(&mut r)? { + 0 => { + let (y0, y1, resized) = read_update(&mut r, &mut local, &mut dec)?; + { + let mut fb = self.fb.lock().unwrap_or_else(|e| e.into_inner()); + if resized { + *fb = local.clone(); + } else if y0 < y1 { + let row = local.width as usize * 4; + fb.rgba[y0 * row..y1 * row].copy_from_slice(&local.rgba[y0 * row..y1 * row]); + } + fb.dirty = true; + } + if resized { + (self.cfg.on_resize)(local.width, local.height); + } + (self.cfg.on_frame)(); + r.get_mut().send(&update_request(!resized, local.width, local.height))?; + } + 1 => { + // SetColourMapEntries: unused with true colour. + let hdr = read_n(&mut r, 5)?; + read_n(&mut r, u16::from_be_bytes([hdr[3], hdr[4]]) as usize * 6)?; + } + 2 => {} // Bell + 3 => { + read_n(&mut r, 3)?; + let len = read_u32(&mut r)? as usize; + if len > 16 << 20 { + return Err("server clipboard text too long".into()); + } + let text: String = read_n(&mut r, len)?.iter().map(|&b| b as char).collect(); + (self.cfg.on_clipboard)(text); + } + t => return Err(format!("unsupported server message type {t}").into()), + } + } + } + + /// ProtocolVersion, security, ClientInit/ServerInit, SetPixelFormat and SetEncodings. + fn handshake(&self, r: &mut BufReader) -> crate::Result<(u16, u16)> { + let ver = read_n(r, 12)?; + let ver = String::from_utf8_lossy(&ver); + let minor = match ver.strip_prefix("RFB 003.").and_then(|s| s.trim_end().parse::().ok()) { + Some(m) => m, + None => return Err(format!("not a VNC server (got {:?})", ver.trim_end()).into()), + }; + // 3.889 is Apple's 3.8; anything between 3.3 and 3.7 is treated as 3.3, as RFB specifies. + let minor = if minor >= 8 { 8 } else if minor == 7 { 7 } else { 3 }; + r.get_mut().send(format!("RFB 003.00{minor}\n").as_bytes())?; + + let security = if minor == 3 { + match read_u32(r)? { + 0 => return Err(format!("VNC server refused the connection: {}", read_string(r)?).into()), + t => t, + } + } else { + let n = read_u8(r)? as usize; + if n == 0 { + return Err(format!("VNC server refused the connection: {}", read_string(r)?).into()); + } + let types = read_n(r, n)?; + let pick = if types.contains(&1) && (self.cfg.password.is_none() || !types.contains(&2)) { + 1 + } else if types.contains(&2) { + 2 + } else { + return Err(unsupported_security(&types).into()); + }; + r.get_mut().send(&[pick])?; + pick as u32 + }; + + match security { + 1 => {} + 2 => { + let password = self.cfg.password.as_deref().ok_or("the VNC server requires a password")?; + let challenge: [u8; 16] = read_n(r, 16)?.try_into().unwrap(); + r.get_mut().send(&vnc_auth_response(password, &challenge))?; + } + t => return Err(unsupported_security(&[t as u8]).into()), + } + if minor == 8 || security == 2 { + if read_u32(r)? != 0 { + let reason = if minor == 8 { read_string(r)? } else { String::new() }; + return Err(if reason.is_empty() { + "VNC authentication failed (wrong password?)".into() + } else { + format!("VNC authentication failed: {reason}").into() + }); + } + } + + r.get_mut().send(&[self.cfg.shared as u8])?; + let init = read_n(r, 20)?; + let (w, h) = (u16::from_be_bytes([init[0], init[1]]), u16::from_be_bytes([init[2], init[3]])); + self.st().name = read_string(r)?; + + // 32bpp, depth 24, little-endian true colour with red in the low byte: pixels arrive as R,G,B,X. + let mut m = vec![0, 0, 0, 0, 32, 24, 0, 1, 0, 255, 0, 255, 0, 255, 0, 8, 16, 0, 0, 0]; + let mut encodings: Vec = vec![7, 16, 5, 1, 0, -223, -308, -224]; + encodings.push(-32 + self.cfg.quality as i32); // JPEG quality level + encodings.push(-256 + 2); // compression level 2, as the noVNC viewer used + m.extend_from_slice(&[2, 0]); + m.extend_from_slice(&(encodings.len() as u16).to_be_bytes()); + encodings.iter().for_each(|e| m.extend_from_slice(&e.to_be_bytes())); + r.get_mut().send(&m)?; + Ok((w, h)) + } +} + +fn unsupported_security(types: &[u8]) -> String { + if types.contains(&30) { + return "the VNC server only offers Apple Remote Desktop authentication, which isn't supported; enable \"VNC viewers may control screen with password\" in Screen Sharing settings".into(); + } + format!("the VNC server offers no supported security type (offered {types:?}; supported: None, VNC password)") +} + +/// VNC Authentication: DES-encrypt the challenge with the password (first 8 bytes, zero padded) +/// as the key, each key byte bit-reversed. +pub fn vnc_auth_response(password: &str, challenge: &[u8; 16]) -> [u8; 16] { + use des::cipher::{BlockCipherEncrypt, KeyInit}; + let mut key = [0u8; 8]; + for (k, b) in key.iter_mut().zip(password.bytes()) { + *k = b.reverse_bits(); + } + let cipher = des::Des::new(&key.into()); + let mut out = *challenge; + for chunk in out.chunks_exact_mut(8) { + let mut block: [u8; 8] = chunk.try_into().unwrap(); + let b = (&mut block).into(); + cipher.encrypt_block(b); + chunk.copy_from_slice(&block); + } + out +} + +fn update_request(incremental: bool, w: u16, h: u16) -> Vec { + let mut m = vec![3, incremental as u8, 0, 0, 0, 0]; + m.extend_from_slice(&w.to_be_bytes()); + m.extend_from_slice(&h.to_be_bytes()); + m +} + +/// The transport as seen by the decoder: reads that poll out flush the outgoing queue and retry, +/// and a dropped queue (`Client::close`) ends the connection. +struct Conn { + io: Box, + out_rx: Receiver>, +} + +impl Conn { + fn send(&mut self, data: &[u8]) -> std::io::Result<()> { + self.io.write_all(data)?; + self.io.flush() + } +} + +impl Read for Conn { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + loop { + match self.io.read(buf) { + Ok(0) => return Err(std::io::Error::new(ErrorKind::UnexpectedEof, "connection closed by server")), + Ok(n) => return Ok(n), + Err(e) if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut | ErrorKind::Interrupted) => {} + Err(e) => return Err(e), + } + loop { + match self.out_rx.try_recv() { + Ok(msg) => self.io.write_all(&msg)?, + Err(TryRecvError::Empty) => break, + Err(TryRecvError::Disconnected) => return Err(std::io::Error::other("closed")), + } + } + self.io.flush()?; + } + } +} + +fn read_n(r: &mut impl Read, n: usize) -> crate::Result> { + let mut v = vec![0u8; n]; + r.read_exact(&mut v)?; + Ok(v) +} + +fn read_u8(r: &mut impl Read) -> crate::Result { + Ok(read_n(r, 1)?[0]) +} + +fn read_u16(r: &mut impl Read) -> crate::Result { + let b = read_n(r, 2)?; + Ok(u16::from_be_bytes([b[0], b[1]])) +} + +fn read_u32(r: &mut impl Read) -> crate::Result { + let b = read_n(r, 4)?; + Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) +} + +fn read_string(r: &mut impl Read) -> crate::Result { + let len = read_u32(r)? as usize; + if len > 1 << 20 { + return Err("server string too long".into()); + } + Ok(String::from_utf8_lossy(&read_n(r, len)?).into_owned()) +} + +/// Persistent zlib streams: ZRLE has one, Tight four. +struct Decoders { + zrle: Decompress, + tight: [Decompress; 4], + buf: Vec, +} + +impl Default for Decoders { + fn default() -> Decoders { + Decoders { + zrle: Decompress::new(true), + tight: std::array::from_fn(|_| Decompress::new(true)), + buf: Vec::new(), + } + } +} + +/// Inflates one server chunk (which ends at a sync flush) into `out`. +fn inflate(z: &mut Decompress, mut input: &[u8], out: &mut Vec) -> crate::Result<()> { + out.clear(); + loop { + out.reserve(64 << 10); + let (i0, o0) = (z.total_in(), z.total_out()); + z.decompress_vec(input, out, FlushDecompress::Sync).map_err(|e| format!("zlib: {e}"))?; + input = &input[(z.total_in() - i0) as usize..]; + let stalled = z.total_in() == i0 && z.total_out() == o0; + if (input.is_empty() && out.len() < out.capacity()) || stalled { + return Ok(()); + } + } +} + +/// Reads one FramebufferUpdate (after the type byte) into `fb`. Returns the changed row range and +/// whether the desktop was resized. +fn read_update(r: &mut impl Read, fb: &mut Framebuffer, dec: &mut Decoders) -> crate::Result<(usize, usize, bool)> { + read_u8(r)?; + let n = read_u16(r)?; + let (mut y0, mut y1, mut resized) = (usize::MAX, 0, false); + for _ in 0..n { + let h = read_n(r, 12)?; + let be = |i: usize| u16::from_be_bytes([h[i], h[i + 1]]); + let (x, y, w, ht) = (be(0), be(2), be(4), be(6)); + let enc = i32::from_be_bytes([h[8], h[9], h[10], h[11]]); + match enc { + -224 => break, // LastRect + -223 | -308 => { + if enc == -308 { + let screens = read_n(r, 4)?[0] as usize; + read_n(r, screens * 16)?; + } + // ExtendedDesktopSize y is the status; non-zero answers a SetDesktopSize we never send. + if enc == -223 || y == 0 { + *fb = Framebuffer::new(w, ht); + resized = true; + } + continue; + } + _ => {} + } + fb.check(x, y, w, ht)?; + match enc { + 0 => { + let px = read_n(r, w as usize * ht as usize * 4)?; + fb.put(x, y, w, ht, &px, 4); + } + 1 => { + let (sx, sy) = (read_u16(r)?, read_u16(r)?); + fb.check(sx, sy, w, ht)?; + let row = w as usize * 4; + let stride = fb.width as usize * 4; + let src: Vec = (0..ht as usize) + .flat_map(|i| { + let o = (sy as usize + i) * stride + sx as usize * 4; + fb.rgba[o..o + row].to_vec() + }) + .collect(); + fb.put(x, y, w, ht, &src, 4); + } + 5 => hextile(r, fb, x, y, w, ht)?, + 7 => tight(r, fb, dec, x, y, w, ht)?, + 16 => { + let len = read_u32(r)? as usize; + let data = read_n(r, len)?; + inflate(&mut dec.zrle, &data, &mut dec.buf)?; + zrle(&dec.buf, fb, x, y, w, ht)?; + } + e => return Err(format!("unsupported encoding {e}").into()), + } + y0 = y0.min(y as usize); + y1 = y1.max(y as usize + ht as usize); + } + Ok((y0.min(y1), y1, resized)) +} + +fn hextile(r: &mut impl Read, fb: &mut Framebuffer, x: u16, y: u16, w: u16, h: u16) -> crate::Result<()> { + let (mut bg, mut fg) = ([0u8; 4], [0u8; 4]); + for ty in (y..y + h).step_by(16) { + for tx in (x..x + w).step_by(16) { + let (tw, th) = (16.min(x + w - tx), 16.min(y + h - ty)); + let sub = read_u8(r)?; + if sub & 1 != 0 { + let px = read_n(r, tw as usize * th as usize * 4)?; + fb.put(tx, ty, tw, th, &px, 4); + continue; + } + if sub & 2 != 0 { + bg.copy_from_slice(&read_n(r, 4)?); + } + if sub & 4 != 0 { + fg.copy_from_slice(&read_n(r, 4)?); + } + fb.fill(tx, ty, tw, th, &bg); + if sub & 8 != 0 { + for _ in 0..read_u8(r)? { + let colour = if sub & 16 != 0 { read_n(r, 4)? } else { fg.to_vec() }; + let b = read_n(r, 2)?; + let (sx, sy, sw, sh) = ((b[0] >> 4) as u16, (b[0] & 15) as u16, (b[1] >> 4) as u16 + 1, (b[1] & 15) as u16 + 1); + if sx + sw > tw || sy + sh > th { + return Err("hextile subrect outside its tile".into()); + } + fb.fill(tx + sx, ty + sy, sw, sh, &colour); + } + } + } + } + Ok(()) +} + +/// ZRLE: 64×64 tiles of 3-byte CPIXELs (R, G, B with our pixel format). +fn zrle(data: &[u8], fb: &mut Framebuffer, x: u16, y: u16, w: u16, h: u16) -> crate::Result<()> { + let mut p = 0usize; + let mut take = |n: usize| take(data, &mut p, n); + for ty in (y..y + h).step_by(64) { + for tx in (x..x + w).step_by(64) { + let (tw, th) = (64.min(x + w - tx), 64.min(y + h - ty)); + let count = tw as usize * th as usize; + let sub = take(1)?[0]; + let mut px = Vec::with_capacity(count * 3); + match sub { + 0 => px.extend_from_slice(take(count * 3)?), + 1 => fb.fill(tx, ty, tw, th, take(3)?), + 2..=16 => { + let palette = take(sub as usize * 3)?.to_vec(); + let bits = match sub { + 2 => 1, + 3..=4 => 2, + _ => 4, + }; + let row_bytes = (tw as usize * bits).div_ceil(8); + let packed = take(row_bytes * th as usize)?; + for row in packed.chunks_exact(row_bytes) { + for i in 0..tw as usize { + let bit = i * bits; + let idx = (row[bit / 8] >> (8 - bits - bit % 8)) as usize & ((1 << bits) - 1); + px.extend_from_slice(palette.get(idx * 3..idx * 3 + 3).ok_or("ZRLE palette index out of range")?); + } + } + } + 128 | 130..=255 => { + let palette = if sub == 128 { Vec::new() } else { take((sub as usize - 128) * 3)?.to_vec() }; + while px.len() < count * 3 { + let (colour, run) = if sub == 128 { + (take(3)?.to_vec(), Some(())) + } else { + let b = take(1)?[0]; + let i = (b & 127) as usize; + (palette.get(i * 3..i * 3 + 3).ok_or("ZRLE palette index out of range")?.to_vec(), (b & 128 != 0).then_some(())) + }; + let mut len = 1; + if run.is_some() { + loop { + let b = take(1)?[0]; + len += b as usize; + if b != 255 { + break; + } + } + } + if px.len() + len * 3 > count * 3 { + return Err("ZRLE run overflows its tile".into()); + } + (0..len).for_each(|_| px.extend_from_slice(&colour)); + } + } + s => return Err(format!("invalid ZRLE subencoding {s}").into()), + } + if sub != 1 { + fb.put(tx, ty, tw, th, &px, 3); + } + } + } + Ok(()) +} + +fn take<'a>(data: &'a [u8], p: &mut usize, n: usize) -> crate::Result<&'a [u8]> { + let s = data.get(*p..*p + n).ok_or("truncated ZRLE data")?; + *p += n; + Ok(s) +} + +fn compact_len(r: &mut impl Read) -> crate::Result { + let mut len = 0usize; + for i in 0..3 { + let b = read_u8(r)? as usize; + if i == 2 { + return Ok(len | b << 14); + } + len |= (b & 127) << (7 * i); + if b & 128 == 0 { + break; + } + } + Ok(len) +} + +/// Tight: fill, JPEG, or basic compression (copy/palette/gradient filters over four zlib streams). +fn tight(r: &mut impl Read, fb: &mut Framebuffer, dec: &mut Decoders, x: u16, y: u16, w: u16, h: u16) -> crate::Result<()> { + let ctl = read_u8(r)?; + for (i, z) in dec.tight.iter_mut().enumerate() { + if ctl & (1 << i) != 0 { + *z = Decompress::new(true); + } + } + let (wu, hu) = (w as usize, h as usize); + match ctl >> 4 { + 8 => { + let rgb = read_n(r, 3)?; + fb.fill(x, y, w, h, &rgb); + return Ok(()); + } + 9 => { + let len = compact_len(r)?; + let jpeg = read_n(r, len)?; + let img = image::load_from_memory_with_format(&jpeg, image::ImageFormat::Jpeg) + .map_err(|e| format!("Tight JPEG: {e}"))? + .to_rgb8(); + if img.width() as usize != wu || img.height() as usize != hu { + return Err("Tight JPEG size doesn't match its rectangle".into()); + } + fb.put(x, y, w, h, img.as_raw(), 3); + return Ok(()); + } + c if c & 8 != 0 => return Err(format!("unsupported Tight compression type {c:#x}").into()), + _ => {} + } + let filter = if ctl & 0x40 != 0 { read_u8(r)? } else { 0 }; + let palette = if filter == 1 { + let n = read_u8(r)? as usize + 1; + read_n(r, n * 3)? + } else { + Vec::new() + }; + let size = match (filter, palette.len() / 3) { + (0 | 2, _) => wu * hu * 3, + (1, 2) => wu.div_ceil(8) * hu, + (1, _) => wu * hu, + (f, _) => return Err(format!("unsupported Tight filter {f}").into()), + }; + let data = if size < 12 { + read_n(r, size)? + } else { + let len = compact_len(r)?; + let compressed = read_n(r, len)?; + inflate(&mut dec.tight[(ctl >> 4 & 3) as usize], &compressed, &mut dec.buf)?; + std::mem::take(&mut dec.buf) + }; + if data.len() < size { + return Err("truncated Tight data".into()); + } + let px: Vec = match filter { + 1 => { + let mut px = Vec::with_capacity(wu * hu * 3); + let mut colour = |i: usize| -> crate::Result<()> { + px.extend_from_slice(palette.get(i * 3..i * 3 + 3).ok_or("Tight palette index out of range")?); + Ok(()) + }; + if palette.len() == 6 { + for row in data[..size].chunks_exact(wu.div_ceil(8)) { + for i in 0..wu { + colour((row[i / 8] >> (7 - i % 8)) as usize & 1)?; + } + } + } else { + data[..size].iter().try_for_each(|&i| colour(i as usize))?; + } + px + } + 2 => { + let mut px = vec![0u8; size]; + for j in 0..hu { + for i in 0..wu { + for c in 0..3 { + let at = |i: usize, j: usize| px[(j * wu + i) * 3 + c] as i32; + let left = if i > 0 { at(i - 1, j) } else { 0 }; + let up = if j > 0 { at(i, j - 1) } else { 0 }; + let ul = if i > 0 && j > 0 { at(i - 1, j - 1) } else { 0 }; + let pred = (left + up - ul).clamp(0, 255) as u8; + px[(j * wu + i) * 3 + c] = pred.wrapping_add(data[(j * wu + i) * 3 + c]); + } + } + } + px + } + _ => data, + }; + fb.put(x, y, w, h, &px, 3); + Ok(()) +} + +/// The X11 keysym for a key: a named key (DOM `KeyboardEvent.key` / egui names), otherwise the +/// typed character (Latin-1 maps directly, other Unicode to `0x01000000 | codepoint`). +pub fn keysym_for(name: &str, ch: Option) -> Option { + let named = match name { + "Enter" | "Return" => 0xff0d, + "Backspace" => 0xff08, + "Tab" => 0xff09, + "Escape" | "Esc" => 0xff1b, + "ArrowLeft" | "Left" => 0xff51, + "ArrowUp" | "Up" => 0xff52, + "ArrowRight" | "Right" => 0xff53, + "ArrowDown" | "Down" => 0xff54, + "Home" => 0xff50, + "End" => 0xff57, + "PageUp" => 0xff55, + "PageDown" => 0xff56, + "Insert" => 0xff63, + "Delete" => 0xffff, + "Shift" | "ShiftLeft" => 0xffe1, + "ShiftRight" => 0xffe2, + "Control" | "Ctrl" | "ControlLeft" => 0xffe3, + "ControlRight" => 0xffe4, + "Alt" | "AltLeft" => 0xffe9, + "AltRight" => 0xffea, + "AltGraph" => 0xfe03, + "Super" | "Meta" | "Command" | "OS" | "SuperLeft" | "MetaLeft" => 0xffeb, + "SuperRight" | "MetaRight" => 0xffec, + "CapsLock" => 0xffe5, + "Space" => 0x20, + _ => match name.strip_prefix('F').and_then(|n| n.parse::().ok()) { + Some(n @ 1..=12) => 0xffbe + n - 1, + _ => 0, + }, + }; + if named != 0 { + return Some(named); + } + let c = ch? as u32; + match c { + 0x20..=0x7e | 0xa0..=0xff => Some(c), + 0..=0x1f | 0x7f..=0x9f => None, + _ => Some(0x0100_0000 | c), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::TcpListener; + + #[test] + fn des_response_matches_known_vector() { + // openssl enc -des-ecb -K 0e86ceceeef64e26 (bit-reversed "password") over "0123456789abcdef". + let got = vnc_auth_response("password", b"0123456789abcdef"); + let want = [0x56, 0x45, 0xab, 0xeb, 0x5f, 0x1e, 0x64, 0x75, 0xe8, 0xfe, 0xb1, 0x1b, 0xeb, 0x66, 0xea, 0x19]; + assert_eq!(got, want); + // Only the first 8 password bytes count. + assert_eq!(vnc_auth_response("password-too-long", b"0123456789abcdef"), want); + } + + #[test] + fn keysyms() { + assert_eq!(keysym_for("Enter", None), Some(0xff0d)); + assert_eq!(keysym_for("F1", None), Some(0xffbe)); + assert_eq!(keysym_for("F12", None), Some(0xffc9)); + assert_eq!(keysym_for("F13", None), None); + assert_eq!(keysym_for("Control", None), Some(0xffe3)); + assert_eq!(keysym_for("ArrowDown", None), Some(0xff54)); + assert_eq!(keysym_for("a", Some('a')), Some(0x61)); + assert_eq!(keysym_for("", Some('é')), Some(0xe9)); + assert_eq!(keysym_for("", Some('€')), Some(0x0100_20ac)); + assert_eq!(keysym_for("", Some('\u{1}')), None); + } + + #[test] + fn compact_lengths() { + assert_eq!(compact_len(&mut &[0x05][..]).unwrap(), 5); + assert_eq!(compact_len(&mut &[0x90, 0x4e][..]).unwrap(), 10000); + assert_eq!(compact_len(&mut &[0xff, 0xff, 0xff][..]).unwrap(), 4194303); + } + + fn zlib(data: &[u8]) -> Vec { + let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default()); + z.write_all(data).unwrap(); + z.finish().unwrap() + } + + fn rect(m: &mut Vec, x: u16, y: u16, w: u16, h: u16, enc: i32) { + for v in [x, y, w, h] { + m.extend_from_slice(&v.to_be_bytes()); + } + m.extend_from_slice(&enc.to_be_bytes()); + } + + fn fake_server(listener: TcpListener, password: &'static str) -> std::thread::JoinHandle> { + std::thread::spawn(move || { + let (mut s, _) = listener.accept().unwrap(); + s.set_read_timeout(Some(Duration::from_secs(5))).unwrap(); + let read = |s: &mut std::net::TcpStream, n: usize| { + let mut b = vec![0u8; n]; + s.read_exact(&mut b).unwrap(); + b + }; + s.write_all(b"RFB 003.008\n").unwrap(); + assert_eq!(read(&mut s, 12), b"RFB 003.008\n"); + s.write_all(&[1, 2]).unwrap(); + assert_eq!(read(&mut s, 1), [2]); + let challenge = *b"0123456789abcdef"; + s.write_all(&challenge).unwrap(); + if read(&mut s, 16) != vnc_auth_response(password, &challenge) { + s.write_all(&[0, 0, 0, 1, 0, 0, 0, 14]).unwrap(); + s.write_all(b"wrong password").unwrap(); + return Vec::new(); + } + s.write_all(&[0, 0, 0, 0]).unwrap(); + assert_eq!(read(&mut s, 1), [1]); // shared + let mut init = vec![0, 4, 0, 4]; + init.extend_from_slice(&[32, 24, 0, 1, 0, 255, 0, 255, 0, 255, 16, 8, 0, 0, 0, 0]); + init.extend_from_slice(&[0, 0, 0, 4]); + init.extend_from_slice(b"test"); + s.write_all(&init).unwrap(); + let pf = read(&mut s, 20); + assert_eq!(pf[0], 0); + let enc_hdr = read(&mut s, 4); + assert_eq!(enc_hdr[0], 2); + read(&mut s, u16::from_be_bytes([enc_hdr[2], enc_hdr[3]]) as usize * 4); + assert_eq!(read(&mut s, 10), update_request(false, 4, 4)); + + let mut m = vec![0, 0, 0, 5]; + rect(&mut m, 0, 0, 2, 2, 0); // Raw: red, green / blue, white + m.extend_from_slice(&[255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0, 255, 255, 255, 0]); + rect(&mut m, 2, 2, 2, 2, 1); // CopyRect from (0,0) + m.extend_from_slice(&[0, 0, 0, 0]); + rect(&mut m, 2, 0, 2, 2, 16); // ZRLE solid yellow + let z = zlib(&[1, 255, 255, 0]); + m.extend_from_slice(&(z.len() as u32).to_be_bytes()); + m.extend_from_slice(&z); + rect(&mut m, 0, 2, 1, 2, 5); // Hextile background cyan + m.extend_from_slice(&[2, 0, 255, 255, 0]); + rect(&mut m, 1, 2, 1, 2, 7); // Tight fill magenta + m.extend_from_slice(&[0x80, 255, 0, 255]); + s.write_all(&m).unwrap(); + assert_eq!(read(&mut s, 10), update_request(true, 4, 4)); + read(&mut s, 8) // the key event + }) + } + + #[test] + fn handshake_auth_and_decoding_against_fake_server() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = fake_server(listener, "secret"); + let (frame_tx, frame_rx) = mpsc::channel(); + let (close_tx, close_rx) = mpsc::channel(); + let client = Client::new(Config { + host: "127.0.0.1".into(), + port, + password: Some("secret".into()), + connect_timeout: Duration::from_secs(5), + on_frame: Arc::new(move || frame_tx.send(()).unwrap()), + on_close: Arc::new(move || close_tx.send(()).unwrap()), + ..Default::default() + }); + client.connect().unwrap(); + assert_eq!(client.desktop_name(), "test"); + frame_rx.recv_timeout(Duration::from_secs(5)).unwrap(); + { + let fb = client.framebuffer(); + let fb = fb.lock().unwrap(); + assert_eq!((fb.width, fb.height), (4, 4)); + assert!(fb.dirty); + let px = |x: usize, y: usize| &fb.rgba[(y * 4 + x) * 4..(y * 4 + x) * 4 + 4]; + assert_eq!(px(0, 0), [255, 0, 0, 255]); + assert_eq!(px(1, 1), [255, 255, 255, 255]); + assert_eq!(px(2, 2), [255, 0, 0, 255]); // copied red + assert_eq!(px(3, 2), [0, 255, 0, 255]); // copied green + assert_eq!(px(2, 3), [0, 0, 255, 255]); // copied blue + assert_eq!(px(3, 1), [255, 255, 0, 255]); // ZRLE + assert_eq!(px(0, 3), [0, 255, 255, 255]); // Hextile + assert_eq!(px(1, 2), [255, 0, 255, 255]); // Tight + } + client.key(0xff0d, true).unwrap(); + assert_eq!(server.join().unwrap(), [4, 1, 0, 0, 0, 0, 0xff, 0x0d]); + client.close(); + client.close(); + close_rx.recv_timeout(Duration::from_secs(5)).unwrap(); + assert!(close_rx.recv_timeout(Duration::from_millis(200)).is_err()); + } + + #[test] + fn wrong_password_reports_reason() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = fake_server(listener, "secret"); + let client = Client::new(Config { + host: "127.0.0.1".into(), + port, + password: Some("nope".into()), + connect_timeout: Duration::from_secs(5), + ..Default::default() + }); + let err = client.connect().unwrap_err().to_string(); + assert_eq!(err, "VNC authentication failed: wrong password"); + server.join().unwrap(); + } + + #[test] + fn zrle_and_tight_palettes_and_gradient() { + let px = |fb: &Framebuffer, x: usize| fb.rgba[x * 4..x * 4 + 3].to_vec(); + let (red, blue) = ([255u8, 0, 0], [0u8, 0, 255]); + + // ZRLE packed palette (1 bit/pixel): blue, red, blue. + let mut fb = Framebuffer::new(3, 1); + zrle(&[2, 255, 0, 0, 0, 0, 255, 0b1010_0000], &mut fb, 0, 0, 3, 1).unwrap(); + assert_eq!([px(&fb, 0), px(&fb, 1), px(&fb, 2)], [blue.to_vec(), red.to_vec(), blue.to_vec()]); + // ZRLE palette RLE: index 0 run of 2, then index 1 once. + zrle(&[130, 255, 0, 0, 0, 0, 255, 0x80, 1, 1], &mut fb, 0, 0, 3, 1).unwrap(); + assert_eq!([px(&fb, 0), px(&fb, 1), px(&fb, 2)], [red.to_vec(), red.to_vec(), blue.to_vec()]); + + // Tight two-colour palette, uncompressed (< 12 bytes): red, blue, red. + let mut dec = Decoders::default(); + let msg = [0x40, 1, 1, 255, 0, 0, 0, 0, 255, 0b0100_0000]; + tight(&mut &msg[..], &mut fb, &mut dec, 0, 0, 3, 1).unwrap(); + assert_eq!([px(&fb, 0), px(&fb, 1), px(&fb, 2)], [red.to_vec(), blue.to_vec(), red.to_vec()]); + + // Tight gradient filter over zlib stream 1: a flat 2x2 colour. + let mut fb = Framebuffer::new(2, 2); + let z = zlib(&[10, 20, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + let mut msg = vec![0x50, 2, z.len() as u8]; + msg.extend_from_slice(&z); + tight(&mut &msg[..], &mut fb, &mut dec, 0, 0, 2, 2).unwrap(); + assert!((0..4).all(|i| px(&fb, i) == [10, 20, 30])); + } + + #[test] + fn apple_only_security_is_explained() { + assert!(unsupported_security(&[30, 33]).contains("Apple Remote Desktop")); + } +}