//! 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")); } }