use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::net::TcpListener;
use std::net::TcpStream;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
pub struct Reply {
pub status: u16,
pub headers: Vec<(&'static str, String)>,
pub body: Vec<u8>,
}
impl Reply {
pub fn new(status: u16, body: impl Into<Vec<u8>>) -> Self {
Self {
status,
headers: Vec::new(),
body: body.into(),
}
}
pub fn header(mut self, name: &'static str, value: impl Into<String>) -> Self {
self.headers.push((name, value.into()));
self
}
}
pub struct Server {
pub url: String,
requests: Arc<Mutex<Vec<String>>>,
}
type Route = dyn Fn(&str, &str) -> Reply + Send + Sync;
impl Server {
pub fn start(route: impl Fn(&str, &str) -> Reply + Send + Sync + 'static) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let requests = Arc::new(Mutex::new(Vec::new()));
let seen = Arc::clone(&requests);
let route: Arc<Route> = Arc::new(route);
thread::spawn(move || {
for stream in listener.incoming() {
let Ok(stream) = stream else { continue };
let (route, seen) = (Arc::clone(&route), Arc::clone(&seen));
thread::spawn(move || serve(stream, &*route, &seen));
}
});
Self { url, requests }
}
pub fn requests(&self) -> Vec<String> {
self.requests.lock().unwrap().clone()
}
}
fn serve(mut stream: TcpStream, route: &Route, seen: &Mutex<Vec<String>>) {
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
if reader.read_line(&mut line).is_err() {
return;
}
let mut header = String::new();
while reader.read_line(&mut header).is_ok_and(|n| n > 2) {
header.clear();
}
let mut parts = line.split_whitespace();
let (method, path) = (parts.next().unwrap_or(""), parts.next().unwrap_or(""));
seen.lock().unwrap().push(format!("{method} {path}"));
let reply = route(method, path);
let mut head = format!(
"HTTP/1.1 {} Reply\r\nContent-Length: {}\r\nConnection: close\r\n",
reply.status,
reply.body.len()
);
for (name, value) in &reply.headers {
head.push_str(&format!("{name}: {value}\r\n"));
}
head.push_str("\r\n");
let _ = stream.write_all(head.as_bytes());
if method != "HEAD" {
let _ = stream.write_all(&reply.body);
}
}
pub fn closed_url() -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
format!("http://{}", listener.local_addr().unwrap())
}