AnyBot/src/httpd.rs

367 lines
13 KiB
Rust
Raw Normal View History

// (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<String, String>,
/// Header names in lower case.
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
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 / <img> 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<u8>,
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<u8>) -> 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<u8> {
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<String, String> {
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<Req, u16> {
c.set_read_timeout(Some(Duration::from_secs(30))).ok();
let mut got: Vec<u8> = 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<String, String> = 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<u8> = 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<dyn Fn(Req) -> Resp + Send + Sync>;
/// Takes over a connection that asked to become a WebSocket (it answers the handshake itself).
pub type Upgrade = Arc<dyn Fn(Req, TcpStream) + Send + Sync>;
/// Answer every connection on this listener with `handler` (each on its own thread).
pub fn serve(listener: TcpListener, handler: Handler, alive: Arc<dyn Fn() -> 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<Upgrade>, alive: Arc<dyn Fn() -> 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<u8> {
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<Vec<u8>> {
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");
}
}