use std::collections::BTreeMap; use std::io::{Read, Write}; use std::net::TcpListener; use std::sync::{ Arc, Mutex, atomic::{AtomicBool, Ordering}, }; use std::time::Duration; #[derive(Clone)] pub struct Response { pub status: u16, pub headers: String, pub body: Vec, } type ResponseHook = Box; pub struct Http { pub url: url::Url, pub routes: Arc>>, pub requests: Arc>>, #[allow(dead_code)] pub hook: Arc>>, stop: Arc, worker: Option>, } impl Http { pub fn new() -> Self { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); listener.set_nonblocking(true).unwrap(); let url = url::Url::parse(&format!("http://{}/", listener.local_addr().unwrap())).unwrap(); let routes = Arc::new(Mutex::new(BTreeMap::::new())); let requests = Arc::new(Mutex::new(Vec::new())); let hook = Arc::new(Mutex::new(None::)); let stop = Arc::new(AtomicBool::new(false)); let state = (routes.clone(), requests.clone(), hook.clone(), stop.clone()); let worker = std::thread::spawn(move || { while !state.3.load(Ordering::Relaxed) { let mut stream = match listener.accept() { Ok((stream, _)) => stream, Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { std::thread::sleep(Duration::from_millis(1)); continue; } Err(error) => panic!("{error}"), }; stream .set_read_timeout(Some(Duration::from_secs(3))) .unwrap(); let mut request = Vec::new(); let mut buf = [0; 1024]; while !request.windows(4).any(|bytes| bytes == b"\r\n\r\n") { match stream.read(&mut buf) { Ok(0) | Err(_) => break, Ok(len) => request.extend_from_slice(&buf[..len]), } } let request = String::from_utf8_lossy(&request); let Some(path) = request .lines() .next() .and_then(|line| line.split_whitespace().nth(1)) else { continue; }; state.1.lock().unwrap().push(path.to_owned()); let response = state .0 .lock() .unwrap() .get(path) .cloned() .unwrap_or(Response { status: 404, headers: String::new(), body: Vec::new(), }); if let Some(hook) = state.2.lock().unwrap().take() { hook(); } let headers = format!( "HTTP/1.1 {} Test\r\n{}Content-Length: {}\r\nConnection: close\r\n\r\n", response.status, response.headers, response.body.len() ); let _ = stream .write_all(headers.as_bytes()) .and_then(|_| stream.write_all(&response.body)); } }); Self { url, routes, requests, hook, stop, worker: Some(worker), } } pub fn name(&self, path: &str) -> String { self.url.join(path).unwrap().to_string() } pub fn source(&self, path: &str, source: &str) { self.routes.lock().unwrap().insert( format!("/{path}"), Response { status: 200, headers: "Content-Type: application/javascript\r\n".into(), body: source.as_bytes().to_vec(), }, ); } pub fn redirect(&self, path: &str, location: &str) { self.routes.lock().unwrap().insert( format!("/{path}"), Response { status: 302, headers: format!("Location: {location}\r\n"), body: Vec::new(), }, ); } } impl Drop for Http { fn drop(&mut self) { self.stop.store(true, Ordering::Relaxed); self.worker.take().unwrap().join().unwrap(); } }