char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
4.5 KiB136 linesraw
1use std::collections::BTreeMap;
2use std::io::{Read, Write};
3use std::net::TcpListener;
4use std::sync::{
5    Arc, Mutex,
6    atomic::{AtomicBool, Ordering},
7};
8use std::time::Duration;
9
10#[derive(Clone)]
11pub struct Response {
12    pub status: u16,
13    pub headers: String,
14    pub body: Vec<u8>,
15}
16
17type ResponseHook = Box<dyn FnOnce() + Send>;
18
19pub struct Http {
20    pub url: url::Url,
21    pub routes: Arc<Mutex<BTreeMap<String, Response>>>,
22    pub requests: Arc<Mutex<Vec<String>>>,
23    #[allow(dead_code)]
24    pub hook: Arc<Mutex<Option<ResponseHook>>>,
25    stop: Arc<AtomicBool>,
26    worker: Option<std::thread::JoinHandle<()>>,
27}
28
29impl Http {
30    pub fn new() -> Self {
31        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
32        listener.set_nonblocking(true).unwrap();
33        let url = url::Url::parse(&format!("http://{}/", listener.local_addr().unwrap())).unwrap();
34        let routes = Arc::new(Mutex::new(BTreeMap::<String, Response>::new()));
35        let requests = Arc::new(Mutex::new(Vec::new()));
36        let hook = Arc::new(Mutex::new(None::<ResponseHook>));
37        let stop = Arc::new(AtomicBool::new(false));
38        let state = (routes.clone(), requests.clone(), hook.clone(), stop.clone());
39        let worker = std::thread::spawn(move || {
40            while !state.3.load(Ordering::Relaxed) {
41                let mut stream = match listener.accept() {
42                    Ok((stream, _)) => stream,
43                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
44                        std::thread::sleep(Duration::from_millis(1));
45                        continue;
46                    }
47                    Err(error) => panic!("{error}"),
48                };
49                stream
50                    .set_read_timeout(Some(Duration::from_secs(3)))
51                    .unwrap();
52                let mut request = Vec::new();
53                let mut buf = [0; 1024];
54                while !request.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
55                    match stream.read(&mut buf) {
56                        Ok(0) | Err(_) => break,
57                        Ok(len) => request.extend_from_slice(&buf[..len]),
58                    }
59                }
60                let request = String::from_utf8_lossy(&request);
61                let Some(path) = request
62                    .lines()
63                    .next()
64                    .and_then(|line| line.split_whitespace().nth(1))
65                else {
66                    continue;
67                };
68                state.1.lock().unwrap().push(path.to_owned());
69                let response = state
70                    .0
71                    .lock()
72                    .unwrap()
73                    .get(path)
74                    .cloned()
75                    .unwrap_or(Response {
76                        status: 404,
77                        headers: String::new(),
78                        body: Vec::new(),
79                    });
80                if let Some(hook) = state.2.lock().unwrap().take() {
81                    hook();
82                }
83                let headers = format!(
84                    "HTTP/1.1 {} Test\r\n{}Content-Length: {}\r\nConnection: close\r\n\r\n",
85                    response.status,
86                    response.headers,
87                    response.body.len()
88                );
89                let _ = stream
90                    .write_all(headers.as_bytes())
91                    .and_then(|_| stream.write_all(&response.body));
92            }
93        });
94        Self {
95            url,
96            routes,
97            requests,
98            hook,
99            stop,
100            worker: Some(worker),
101        }
102    }
103
104    pub fn name(&self, path: &str) -> String {
105        self.url.join(path).unwrap().to_string()
106    }
107
108    pub fn source(&self, path: &str, source: &str) {
109        self.routes.lock().unwrap().insert(
110            format!("/{path}"),
111            Response {
112                status: 200,
113                headers: "Content-Type: application/javascript\r\n".into(),
114                body: source.as_bytes().to_vec(),
115            },
116        );
117    }
118
119    pub fn redirect(&self, path: &str, location: &str) {
120        self.routes.lock().unwrap().insert(
121            format!("/{path}"),
122            Response {
123                status: 302,
124                headers: format!("Location: {location}\r\n"),
125                body: Vec::new(),
126            },
127        );
128    }
129}
130
131impl Drop for Http {
132    fn drop(&mut self) {
133        self.stop.store(true, Ordering::Relaxed);
134        self.worker.take().unwrap().join().unwrap();
135    }
136}