301 lines
10 KiB
Rust
301 lines
10 KiB
Rust
|
|
// (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 {
|
||
|
|
let b = s.as_bytes();
|
||
|
|
let mut out = Vec::with_capacity(b.len());
|
||
|
|
let mut i = 0;
|
||
|
|
while i < b.len() {
|
||
|
|
match b[i] {
|
||
|
|
b'+' => out.push(b' '),
|
||
|
|
b'%' if i + 2 < b.len() => {
|
||
|
|
if let Ok(v) = u8::from_str_radix(&s[i + 1..i + 3], 16) {
|
||
|
|
out.push(v);
|
||
|
|
i += 2;
|
||
|
|
} else {
|
||
|
|
out.push(b'%');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
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 len: usize = headers.get("content-length").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||
|
|
if len > MAX_BODY {
|
||
|
|
return Err(413);
|
||
|
|
}
|
||
|
|
let mut body = got[head_end + 4..].to_vec();
|
||
|
|
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>;
|
||
|
|
|
||
|
|
/// 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>) {
|
||
|
|
listener.set_nonblocking(true).ok();
|
||
|
|
loop {
|
||
|
|
if !alive() {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
match listener.accept() {
|
||
|
|
Ok((mut c, _)) => {
|
||
|
|
c.set_nonblocking(false).ok();
|
||
|
|
let h = handler.clone();
|
||
|
|
std::thread::spawn(move || match read_request(&mut 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");
|
||
|
|
}
|
||
|
|
}
|