AnyBot/src/vnc.rs

264 lines
10 KiB
Rust

// 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<Option<Instant>> = 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<u8>,
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<Session, String> {
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<Vec<u8>, 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 {
let rest = &self.pending[at..];
let Some(&kind) = rest.first() else { break };
let len = match kind {
0 => 20,
2 if rest.len() >= 4 => 4 + 4 * u16::from_be_bytes([rest[2], rest[3]]) as usize,
3 => 10,
4 => 8,
5 => 6,
6 if rest.len() >= 8 => 8 + u32::from_be_bytes([rest[4], rest[5], rest[6], rest[7]]) as usize,
2 | 6 => break,
_ => {
self.sniffing = false;
self.pending.clear();
return;
}
};
if rest.len() < len {
break;
}
match kind {
4 if rest[1] == 1 => person_acted(),
5 if rest[1] != 0 => person_acted(),
_ => {}
}
at += len;
}
self.pending.drain(..at);
}
}
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 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);
}
}