// People watching and helping on the bots' computer: VNC, carried to them as a stream. // // The box already runs a VNC server (x11vnc, inside the container, with a password). A person's // viewer (noVNC in a browser: the phone, or any computer through the account server) speaks VNC // to this proxy, which reaches the box's server through one `docker exec` (a few lines of // Python in the box joining its stdin/stdout to 127.0.0.1:5900). VNC sends only what changed on // the screen and every mouse move and key as it happens, so watching is live and helping is // like sitting at it — unlike pictures sent one by one. // // The proxy signs in to the box with its password itself and offers the viewer "no password", // so the password never leaves this AnyBot; whoever reaches the proxy was already let in (an // account session or one-time ticket at the account server, or the phone's password here). // While the person clicks or types, the bot's own steps wait (HELD_BY_PERSON), and go on a // little after they stop (person_active). use std::io::{Read, Write}; use std::process::{Child, ChildStdin}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Mutex; use std::time::{Duration, Instant}; /// The bot waits this long after the person's last click or key before it goes on. const HELD_AFTER_INPUT: Duration = Duration::from_secs(8); /// Joins the box's VNC server to stdin/stdout, and ends when either side does (a loop left in /// the box would hold the connection open). const BRIDGE: &str = r#"import os,select,socket s=socket.create_connection(('127.0.0.1',5900)) while True: r=select.select([s,0],[],[])[0] if s in r: d=s.recv(65536) if not d: break os.write(1,d) if 0 in r: d=os.read(0,65536) if not d: break s.sendall(d) "#; /// Viewers connected now (the box stays awake while there is one). static OPEN: AtomicUsize = AtomicUsize::new(0); /// When the person last clicked or typed through a viewer. static LAST_INPUT: Mutex> = Mutex::new(None); /// VNC authentication: the 16-byte challenge encrypted with DES, the key being the password's /// first 8 bytes with each byte's bits reversed (RFC 6143 §7.2.2, as every VNC server does it). pub fn vnc_response(password: &str, challenge: &[u8; 16]) -> [u8; 16] { use des::cipher::{BlockEncrypt, KeyInit}; let mut key = [0u8; 8]; for (k, b) in key.iter_mut().zip(password.bytes()) { *k = b.reverse_bits(); } let c = des::Des::new_from_slice(&key).expect("8-byte key"); let mut out = *challenge; for block in out.chunks_mut(8) { c.encrypt_block(des::cipher::generic_array::GenericArray::from_mut_slice(block)); } out } /// This proxy's first words to a viewer, as a VNC server: version 3.8 (then "no password"; see /// Session::feed). pub const GREETING: &[u8] = b"RFB 003.008\n"; /// One person's viewer joined to the box. pub struct Session { child: Child, stdin: ChildStdin, /// The viewer's side of the handshake still to read: its version (12 bytes) and its choice /// of security type (1 byte). greeting: usize, /// Viewer → box messages not yet complete (to notice clicks and keys). pending: Vec, sniffing: bool, /// The viewer's ClientInit (one byte) is still to come. init: bool, } /// Open a viewer's way to the box: sign in to its VNC server, and start sending what the server /// says to `out` (from a thread of its own; `out` returns false when the viewer is gone, and /// gets an empty slice when the box's side ended). The caller then sends the viewer GREETING /// (once it is ready for the viewer's answer). pub fn open(out: impl Fn(&[u8]) -> bool + Send + 'static) -> Result { crate::cloudbox::ensure_running()?; let mut child = crate::cloudbox::exec_piped(&["python3", "-c", BRIDGE])?; let mut stdin = child.stdin.take().ok_or("連不上這台電腦的畫面")?; let mut stdout = child.stdout.take().ok_or("連不上這台電腦的畫面")?; let fail = |child: &mut Child, why: &str| -> String { let _ = child.kill(); let _ = child.wait(); format!("連不上這台電腦的畫面:{why}") }; let mut read = |n: usize| -> Result, String> { let mut b = vec![0u8; n]; stdout.read_exact(&mut b).map(|_| b).map_err(|_| "畫面伺服器沒有回應".to_string()) }; // The box's side, as a VNC client: version 3.8, then its password. let signed_in = (|| -> Result<(), String> { let v = read(12)?; if !v.starts_with(b"RFB 003.") { return Err("不是 VNC 伺服器".into()); } stdin.write_all(b"RFB 003.008\n").map_err(|e| e.to_string())?; let n = read(1)?[0] as usize; if n == 0 { return Err("畫面伺服器拒絕連線".into()); } let types = read(n)?; if types.contains(&2) { stdin.write_all(&[2]).map_err(|e| e.to_string())?; let c: [u8; 16] = read(16)?.try_into().map_err(|_| "認證失敗".to_string())?; stdin.write_all(&vnc_response(&crate::cloudbox::box_vnc_password(), &c)).map_err(|e| e.to_string())?; } else if types.contains(&1) { stdin.write_all(&[1]).map_err(|e| e.to_string())?; } else { return Err("畫面伺服器要的登入方式不支援".into()); } if read(4)? != [0, 0, 0, 0] { return Err("畫面伺服器的密碼不對".into()); } Ok(()) })(); if let Err(e) = signed_in { return Err(fail(&mut child, &e)); } OPEN.fetch_add(1, Ordering::SeqCst); crate::cloudbox::set_viewer_open(true); std::thread::spawn(move || { let mut buf = vec![0u8; 64 * 1024]; loop { match stdout.read(&mut buf) { Ok(0) | Err(_) => break, Ok(n) => { if !out(&buf[..n]) { return; } } } } // The box's side ended: an empty send says so. out(&[]); }); Ok(Session { child, stdin, greeting: 13, pending: Vec::new(), sniffing: true, init: true }) } impl Session { /// Bytes from the viewer. False when the box's side is gone. pub fn feed(&mut self, mut b: &[u8], reply: impl Fn(&[u8]) -> bool) -> bool { if self.greeting > 0 { let take = self.greeting.min(b.len()); let before = self.greeting; self.greeting -= take; b = &b[take..]; // After the version (12 bytes): the one security type, "None". if before > 1 && self.greeting <= 1 && !reply(&[1, 1]) { return false; } // After the choice: OK. if self.greeting == 0 && !reply(&[0, 0, 0, 0]) { return false; } if b.is_empty() { return true; } } self.notice_input(b); self.stdin.write_all(b).and_then(|_| self.stdin.flush()).is_ok() } /// Follow the viewer's messages (RFC 6143 §7.5) to see clicks and keys. Anything it doesn't /// know stops the watching, never the forwarding. fn notice_input(&mut self, b: &[u8]) { if !self.sniffing { return; } self.pending.extend_from_slice(b); // The first byte after the handshake is ClientInit (the shared flag). if self.init && !self.pending.is_empty() { self.pending.remove(0); self.init = false; } let mut at = 0; loop { match next_message(&self.pending[at..]) { Msg::Need => break, Msg::Unknown => { self.sniffing = false; self.pending.clear(); return; } Msg::Is { len, input } => { if input { person_acted(); } at += len; } } } self.pending.drain(..at); } } enum Msg { /// Not all of it has arrived yet. Need, /// A message this doesn't know (watching stops; forwarding goes on). Unknown, /// A whole message of `len` bytes; `input` when it is the person pressing a key or button. Is { len: usize, input: bool }, } /// The viewer's next message (RFC 6143 §7.5 and the extensions noVNC uses with TigerVNC: /// continuous updates, fences, desktop size, xvp and QEMU key events — without them the watching /// stopped at the first fence, and the bot went on clicking while the person worked). fn next_message(rest: &[u8]) -> Msg { let Some(&kind) = rest.first() else { return Msg::Need }; let need = |n: usize| rest.len() >= n; let len = match kind { 0 => 20, 2 if need(4) => 4 + 4 * u16::from_be_bytes([rest[2], rest[3]]) as usize, 3 => 10, 4 => 8, 5 => 6, 6 if need(8) => 8 + u32::from_be_bytes([rest[4], rest[5], rest[6], rest[7]]) as usize, 150 => 10, 248 if need(9) => 9 + rest[8] as usize, 250 => 4, 251 if need(7) => 8 + 16 * rest[6] as usize, // QEMU extended key event (submessage 0). 255 if need(2) && rest[1] == 0 => 12, 2 | 6 | 248 | 251 | 255 if !need(9) => return Msg::Need, _ => return Msg::Unknown, }; if !need(len) { return Msg::Need; } let input = match kind { 4 => rest[1] == 1, 5 => rest[1] != 0, 255 => u16::from_be_bytes([rest[2], rest[3]]) != 0, _ => false, }; Msg::Is { len, input } } impl Drop for Session { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); if OPEN.fetch_sub(1, Ordering::SeqCst) == 1 { crate::cloudbox::set_viewer_open(false); } } } /// The person clicked or typed: the bot's steps wait until a little after they stop. pub fn person_acted() { *LAST_INPUT.lock().unwrap_or_else(|e| e.into_inner()) = Some(Instant::now()); } /// The person is using the box through a viewer right now (input in the last few seconds). pub fn person_active() -> bool { LAST_INPUT.lock().unwrap_or_else(|e| e.into_inner()).map(|t| t.elapsed() < HELD_AFTER_INPUT).unwrap_or(false) } /// Text the person typed in the viewer's own box (any language): pasted into the box. pub fn person_typed(text: &str) { if !text.is_empty() { person_acted(); crate::cloudbox::viewer_text(text); } } #[cfg(test)] mod tests { use super::*; #[test] fn a_persons_keys_are_seen_past_novncs_extra_messages() { let walk = |b: &[u8]| -> (usize, bool, bool) { let (mut at, mut input) = (0, false); loop { match next_message(&b[at..]) { Msg::Need => return (at, input, false), Msg::Unknown => return (at, input, true), Msg::Is { len, input: i } => { input |= i; at += len; } } } }; let mut b = vec![150, 1, 0, 0, 0, 0, 5, 0, 3, 32]; // EnableContinuousUpdates b.extend([248, 0, 0, 0, 0, 0, 0, 1, 2, 7, 7]); // ClientFence with 2 bytes of payload b.extend([3, 1, 0, 0, 0, 0, 5, 0, 3, 32]); // FramebufferUpdateRequest let (at, input, stopped) = walk(&b); assert_eq!((at, input, stopped), (b.len(), false, false)); // A QEMU key press (noVNC's keys with TigerVNC), then its release. let mut k = b.clone(); k.extend([255, 0, 0, 1, 0, 0, 0, 0x61, 0, 0, 0, 30]); k.extend([255, 0, 0, 0, 0, 0, 0, 0x61, 0, 0, 0, 30]); assert_eq!(walk(&k), (k.len(), true, false)); // A plain pointer click still counts; a half-arrived message waits for the rest. assert_eq!(walk(&[5, 1, 0, 10, 0, 20]), (6, true, false)); assert_eq!(walk(&[248, 0, 0]), (0, false, false)); // Something unknown stops the watching. assert!(walk(&[99, 0, 0, 0]).2); } #[test] fn the_bridge_keeps_its_indentation() { // Python needs its indentation (a Rust "\" line continuation would strip it). assert!(BRIDGE.contains("\n r=select") && BRIDGE.contains("\n if not d: break")); } #[test] fn vnc_password_answer_matches_the_standard() { // Password "password", challenge 00..0f: DES-ECB with the bit-reversed key 0e86ceceeef64e26 // (worked out with `openssl enc -des-ecb -nopad`). let c: [u8; 16] = core::array::from_fn(|i| i as u8); let want = [0xb8, 0x66, 0x92, 0x41, 0x25, 0xc8, 0xee, 0xbb, 0x9d, 0xeb, 0xc1, 0xdb, 0x61, 0xc5, 0x38, 0xe2]; assert_eq!(vnc_response("password", &c), want); // Only the first 8 bytes of a password count. assert_eq!(vnc_response("password-and-more", &c), want); } }