// (Shared with relay/, which uses the parts the app doesn't.) #![allow(dead_code)] // A very small HTTP/1.1 server (one thread per connection), shared by the phone page on this // computer and by the relay (the thin backend). No keep-alive: every answer closes the connection. use std::collections::HashMap; use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; use std::sync::Arc; use std::time::Duration; /// Largest request body accepted (a phone photo fits). pub const MAX_BODY: usize = 30 * 1024 * 1024; #[derive(Clone, Debug, Default)] pub struct Req { pub method: String, pub path: String, pub query: HashMap, /// Header names in lower case. pub headers: HashMap, pub body: Vec, pub peer: String, } impl Req { pub fn header(&self, name: &str) -> &str { self.headers.get(&name.to_ascii_lowercase()).map(|s| s.as_str()).unwrap_or("") } pub fn bearer(&self) -> String { let a = self.header("authorization"); if let Some(t) = a.strip_prefix("Bearer ") { return t.trim().to_string(); } // EventSource / can't send headers: a token in the query works too. self.query.get("t").cloned().unwrap_or_default() } pub fn json(&self) -> serde_json::Value { serde_json::from_slice(&self.body).unwrap_or(serde_json::Value::Null) } /// Path segments, e.g. "/api/bots/3/send" → ["api", "bots", "3", "send"]. pub fn parts(&self) -> Vec<&str> { self.path.split('/').filter(|s| !s.is_empty()).collect() } } #[derive(Clone, Debug)] pub struct Resp { pub status: u16, pub ctype: String, pub body: Vec, pub headers: Vec<(String, String)>, } impl Resp { pub fn json(v: &serde_json::Value) -> Resp { Resp { status: 200, ctype: "application/json; charset=utf-8".into(), body: v.to_string().into_bytes(), headers: vec![] } } pub fn bytes(ctype: &str, body: Vec) -> Resp { Resp { status: 200, ctype: ctype.into(), body, headers: vec![] } } pub fn error(status: u16, msg: &str) -> Resp { Resp { status, ..Resp::json(&serde_json::json!({ "error": msg })) } } pub fn empty(status: u16) -> Resp { Resp { status, ctype: "text/plain".into(), body: vec![], headers: vec![] } } pub fn with_header(mut self, k: &str, v: &str) -> Resp { self.headers.push((k.into(), v.into())); self } } fn reason(status: u16) -> &'static str { match status { 200 => "OK", 204 => "No Content", 400 => "Bad Request", 401 => "Unauthorized", 403 => "Forbidden", 404 => "Not Found", 409 => "Conflict", 413 => "Payload Too Large", 429 => "Too Many Requests", 502 => "Bad Gateway", 503 => "Service Unavailable", 504 => "Gateway Timeout", _ => "OK", } } fn decode(s: &str) -> String { // Work on bytes only: slicing the &str at byte offsets can land inside a multi-byte // character and panic (an unauthenticated request like "/%)…" used to kill its thread). let b = s.as_bytes(); let mut out = Vec::with_capacity(b.len()); let hex = |c: u8| -> Option { match c { b'0'..=b'9' => Some(c - b'0'), b'a'..=b'f' => Some(c - b'a' + 10), b'A'..=b'F' => Some(c - b'A' + 10), _ => None, } }; let mut i = 0; while i < b.len() { match b[i] { b'+' => { out.push(b' '); i += 1; } b'%' if i + 2 < b.len() && hex(b[i + 1]).is_some() && hex(b[i + 2]).is_some() => { out.push(hex(b[i + 1]).unwrap() * 16 + hex(b[i + 2]).unwrap()); i += 3; } c => { out.push(c); i += 1; } } } String::from_utf8_lossy(&out).to_string() } pub fn parse_query(q: &str) -> HashMap { q.split('&').filter(|p| !p.is_empty()).map(|p| { let (k, v) = p.split_once('=').unwrap_or((p, "")); (decode(k), decode(v)) }).collect() } /// Read one request (head, then exactly Content-Length bytes of body). pub fn read_request(c: &mut TcpStream) -> Result { c.set_read_timeout(Some(Duration::from_secs(30))).ok(); let mut got: Vec = vec![]; let mut buf = [0u8; 16384]; let head_end = loop { if let Some(p) = got.windows(4).position(|w| w == b"\r\n\r\n") { break p; } if got.len() > 64 * 1024 { return Err(400); } let n = c.read(&mut buf).map_err(|_| 400u16)?; if n == 0 { return Err(400); } got.extend_from_slice(&buf[..n]); }; let head = String::from_utf8_lossy(&got[..head_end]).to_string(); let mut lines = head.split("\r\n"); let first = lines.next().unwrap_or(""); let mut fp = first.split_whitespace(); let method = fp.next().unwrap_or("").to_string(); let target = fp.next().unwrap_or("/").to_string(); let headers: HashMap = lines.filter_map(|l| l.split_once(':')).map(|(k, v)| (k.trim().to_ascii_lowercase(), v.trim().to_string())).collect(); let chunked = headers.get("transfer-encoding").map(|v| v.to_ascii_lowercase().contains("chunked")).unwrap_or(false); let mut body = got[head_end + 4..].to_vec(); if chunked { // Some phone browsers send POST bodies in chunks (no Content-Length): read chunks // until the zero-size terminator, reassembling the body. let mut out: Vec = vec![]; let mut pos = 0usize; loop { let size_end = loop { if let Some(p) = body[pos..].windows(2).position(|w| w == b"\r\n") { break pos + p; } if body.len() > MAX_BODY + 64 * 1024 { return Err(413); } let n = c.read(&mut buf).map_err(|_| 400u16)?; if n == 0 { return Err(400); } body.extend_from_slice(&buf[..n]); }; let hex = String::from_utf8_lossy(&body[pos..size_end]).split(';').next().unwrap_or("0").trim().to_string(); let size = usize::from_str_radix(&hex, 16).map_err(|_| 400u16)?; if size == 0 { break; } if out.len() + size > MAX_BODY { return Err(413); } let end = size_end + 2 + size; while body.len() < end + 2 { let n = c.read(&mut buf).map_err(|_| 400u16)?; if n == 0 { return Err(400); } body.extend_from_slice(&buf[..n]); } out.extend_from_slice(&body[size_end + 2..end]); pos = end + 2; } body = out; } else { let len: usize = headers.get("content-length").and_then(|v| v.parse().ok()).unwrap_or(0); if len > MAX_BODY { return Err(413); } while body.len() < len { let n = c.read(&mut buf).map_err(|_| 400u16)?; if n == 0 { break; } body.extend_from_slice(&buf[..n]); } body.truncate(len); } let (path, q) = target.split_once('?').unwrap_or((&target, "")); Ok(Req { method, path: decode(path), query: parse_query(q), headers, body, peer: c.peer_addr().map(|a| a.ip().to_string()).unwrap_or_default() }) } pub fn write_response(c: &mut TcpStream, r: &Resp) { let mut head = format!( "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\n", r.status, reason(r.status), r.ctype, r.body.len() ); for (k, v) in &r.headers { head.push_str(&format!("{k}: {v}\r\n")); } head.push_str("\r\n"); let _ = c.write_all(head.as_bytes()); let _ = c.write_all(&r.body); let _ = c.flush(); } pub type Handler = Arc Resp + Send + Sync>; /// Takes over a connection that asked to become a WebSocket (it answers the handshake itself). pub type Upgrade = Arc; /// Answer every connection on this listener with `handler` (each on its own thread). pub fn serve(listener: TcpListener, handler: Handler, alive: Arc bool + Send + Sync>) { serve_upgrade(listener, handler, None, alive) } /// Like `serve`, and connections asking for a WebSocket go to `upgrade`. pub fn serve_upgrade(listener: TcpListener, handler: Handler, upgrade: Option, alive: Arc bool + Send + Sync>) { listener.set_nonblocking(true).ok(); loop { if !alive() { return; } match listener.accept() { Ok((mut c, _)) => { c.set_nonblocking(false).ok(); let h = handler.clone(); let up = upgrade.clone(); std::thread::spawn(move || match read_request(&mut c) { Ok(req) if req.header("upgrade").eq_ignore_ascii_case("websocket") && up.is_some() => { if let Some(u) = up { u(req, c) } } Ok(req) => { let resp = h(req); write_response(&mut c, &resp); } Err(code) => write_response(&mut c, &Resp::error(code, "bad request")), }); } Err(_) => std::thread::sleep(Duration::from_millis(50)), } } } // ---------- secrets ---------- pub fn random_hex(bytes: usize) -> String { use ring::rand::SecureRandom; let mut b = vec![0u8; bytes]; ring::rand::SystemRandom::new().fill(&mut b).expect("random"); b.iter().map(|x| format!("{x:02x}")).collect() } pub fn sha256_hex(s: &str) -> String { ring::digest::digest(&ring::digest::SHA256, s.as_bytes()).as_ref().iter().map(|x| format!("{x:02x}")).collect() } const PBKDF2_ROUNDS: u32 = 120_000; /// A password stored as "salt$hash" (PBKDF2-HMAC-SHA256). pub fn hash_password(pw: &str) -> String { let salt = random_hex(16); format!("{salt}${}", pbkdf2_hex(pw, &salt)) } fn pbkdf2_hex(pw: &str, salt: &str) -> String { let mut out = [0u8; 32]; ring::pbkdf2::derive(ring::pbkdf2::PBKDF2_HMAC_SHA256, std::num::NonZeroU32::new(PBKDF2_ROUNDS).unwrap(), salt.as_bytes(), pw.as_bytes(), &mut out); out.iter().map(|x| format!("{x:02x}")).collect() } pub fn check_password(pw: &str, stored: &str) -> bool { let Some((salt, hash)) = stored.split_once('$') else { return false }; let mine = pbkdf2_hex(pw, salt); // Compare without stopping at the first difference. mine.len() == hash.len() && mine.bytes().zip(hash.bytes()).fold(0u8, |acc, (a, b)| acc | (a ^ b)) == 0 } /// A 256-bit key from a password (for the encrypted backup kept by the relay). pub fn derive_key(pw: &str, user: &str) -> [u8; 32] { let mut out = [0u8; 32]; ring::pbkdf2::derive(ring::pbkdf2::PBKDF2_HMAC_SHA256, std::num::NonZeroU32::new(PBKDF2_ROUNDS).unwrap(), format!("anybot-backup:{user}").as_bytes(), pw.as_bytes(), &mut out); out } /// AES-256-GCM; the 12-byte nonce goes first. pub fn seal(key: &[u8; 32], plain: &[u8]) -> Vec { use ring::aead::*; use ring::rand::SecureRandom; let k = LessSafeKey::new(UnboundKey::new(&AES_256_GCM, key).expect("key")); let mut nonce = [0u8; 12]; ring::rand::SystemRandom::new().fill(&mut nonce).expect("random"); let mut data = plain.to_vec(); k.seal_in_place_append_tag(Nonce::assume_unique_for_key(nonce), Aad::empty(), &mut data).expect("seal"); let mut out = nonce.to_vec(); out.extend(data); out } pub fn open(key: &[u8; 32], sealed: &[u8]) -> Option> { use ring::aead::*; if sealed.len() < 12 + 16 { return None; } let k = LessSafeKey::new(UnboundKey::new(&AES_256_GCM, key).ok()?); let nonce = Nonce::try_assume_unique_for_key(&sealed[..12]).ok()?; let mut data = sealed[12..].to_vec(); let plain = k.open_in_place(nonce, Aad::empty(), &mut data).ok()?; Some(plain.to_vec()) } #[cfg(test)] mod tests { use super::*; #[test] fn passwords_and_sealing() { let h = hash_password("貓咪 123"); assert!(check_password("貓咪 123", &h) && !check_password("貓咪 124", &h)); let k = derive_key("pw", "daniel"); let s = seal(&k, "秘密".as_bytes()); assert_eq!(open(&k, &s).unwrap(), "秘密".as_bytes()); assert!(open(&derive_key("pw2", "daniel"), &s).is_none()); assert_eq!(parse_query("a=1&b=%E4%BD%A0+x")["b"], "你 x"); } #[test] fn serves_a_request() { let l = TcpListener::bind("127.0.0.1:0").unwrap(); let port = l.local_addr().unwrap().port(); let h: Handler = Arc::new(|r: Req| Resp::json(&serde_json::json!({"p": r.path, "n": r.body.len(), "q": r.query.get("x")}))); std::thread::spawn(move || serve(l, h, Arc::new(|| true))); let body = vec![7u8; 100_000]; let mut resp = ureq::post(&format!("http://127.0.0.1:{port}/a/b?x=1")).send(&body[..]).unwrap(); let v: serde_json::Value = serde_json::from_str(&resp.body_mut().read_to_string().unwrap()).unwrap(); assert_eq!(v["n"], 100_000); assert_eq!(v["p"], "/a/b"); } }