1use std::collections::BTreeMap;
32use std::io::{BufRead, BufReader, Read, Write};
33use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream};
34use std::sync::atomic::{AtomicUsize, Ordering};
35use std::sync::Arc;
36use std::time::Duration;
37
38pub const MAX_HEADER_BYTES: usize = 16 * 1024;
40pub const MAX_BODY_BYTES: usize = 8 * 1024 * 1024;
42pub const MAX_CONNECTIONS: usize = 32;
46pub const IO_TIMEOUT: Duration = Duration::from_secs(15);
49
50#[derive(Debug, Clone)]
51pub struct Request {
52 pub method: String,
53 pub path: String,
55 pub query: BTreeMap<String, String>,
56 pub headers: BTreeMap<String, String>,
58 pub body: Vec<u8>,
59}
60
61impl Request {
62 pub fn header(&self, name: &str) -> Option<&str> {
63 self.headers.get(&name.to_lowercase()).map(|s| s.as_str())
64 }
65
66 pub fn segments(&self) -> Vec<&str> {
68 self.path.split('/').filter(|s| !s.is_empty()).collect()
69 }
70}
71
72#[derive(Debug, Clone)]
73pub struct Response {
74 pub status: u16,
75 pub content_type: String,
76 pub body: Vec<u8>,
77 pub extra: Vec<(String, String)>,
79}
80
81impl Response {
82 pub fn json(status: u16, body: impl Into<Vec<u8>>) -> Self {
83 Response {
84 status,
85 content_type: "application/json; charset=utf-8".into(),
86 body: body.into(),
87 extra: vec![],
88 }
89 }
90
91 pub fn text(status: u16, body: impl Into<String>) -> Self {
92 Response {
93 status,
94 content_type: "text/plain; charset=utf-8".into(),
95 body: body.into().into_bytes(),
96 extra: vec![],
97 }
98 }
99
100 pub fn error(status: u16, code: &str, message: impl std::fmt::Display) -> Self {
102 let body = serde_json::json!({ "error": code, "message": message.to_string() });
103 Response::json(status, body.to_string())
104 }
105}
106
107fn reason(status: u16) -> &'static str {
108 match status {
109 200 => "OK",
110 400 => "Bad Request",
111 401 => "Unauthorized",
112 403 => "Forbidden",
113 404 => "Not Found",
114 405 => "Method Not Allowed",
115 413 => "Payload Too Large",
116 421 => "Misdirected Request",
117 500 => "Internal Server Error",
118 503 => "Service Unavailable",
119 _ => "Unknown",
120 }
121}
122
123fn percent_decode(s: &str) -> String {
124 let bytes = s.as_bytes();
125 let mut out = Vec::with_capacity(bytes.len());
126 let mut i = 0;
127 while i < bytes.len() {
128 match bytes[i] {
129 b'%' if i + 2 < bytes.len() => {
130 let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("");
131 match u8::from_str_radix(hex, 16) {
132 Ok(b) => {
133 out.push(b);
134 i += 3;
135 }
136 Err(_) => {
139 out.push(bytes[i]);
140 i += 1;
141 }
142 }
143 }
144 b'+' => {
145 out.push(b' ');
146 i += 1;
147 }
148 b => {
149 out.push(b);
150 i += 1;
151 }
152 }
153 }
154 String::from_utf8_lossy(&out).into_owned()
155}
156
157fn parse_query(raw: &str) -> BTreeMap<String, String> {
158 raw.split('&')
159 .filter(|p| !p.is_empty())
160 .map(|p| match p.split_once('=') {
161 Some((k, v)) => (percent_decode(k), percent_decode(v)),
162 None => (percent_decode(p), String::new()),
163 })
164 .collect()
165}
166
167fn read_request(stream: &mut BufReader<&TcpStream>) -> Result<Option<Request>, Response> {
169 let mut head = Vec::new();
170 let mut line = Vec::new();
171
172 loop {
173 line.clear();
174 let n = stream
175 .read_until(b'\n', &mut line)
176 .map_err(|e| Response::error(400, "read_failed", e))?;
177 if n == 0 {
178 if head.is_empty() {
179 return Ok(None);
180 }
181 return Err(Response::error(400, "truncated", "connection closed mid-headers"));
182 }
183 head.extend_from_slice(&line);
184 if head.len() > MAX_HEADER_BYTES {
185 return Err(Response::error(
186 413,
187 "headers_too_large",
188 format!("request head exceeds {MAX_HEADER_BYTES} bytes"),
189 ));
190 }
191 if line == b"\r\n" || line == b"\n" {
193 break;
194 }
195 }
196
197 let text = String::from_utf8_lossy(&head).into_owned();
198 let mut lines = text.lines();
199 let request_line = lines
200 .next()
201 .ok_or_else(|| Response::error(400, "empty_request", "no request line"))?;
202 let mut parts = request_line.split_whitespace();
203 let method = parts
204 .next()
205 .ok_or_else(|| Response::error(400, "bad_request_line", request_line))?
206 .to_string();
207 let target = parts
208 .next()
209 .ok_or_else(|| Response::error(400, "bad_request_line", request_line))?;
210
211 let (raw_path, raw_query) = match target.split_once('?') {
212 Some((p, q)) => (p, q),
213 None => (target, ""),
214 };
215
216 let mut headers = BTreeMap::new();
217 for l in lines {
218 if l.is_empty() {
219 continue;
220 }
221 if let Some((k, v)) = l.split_once(':') {
222 headers.insert(k.trim().to_lowercase(), v.trim().to_string());
223 }
224 }
225
226 let len: usize = headers
227 .get("content-length")
228 .and_then(|v| v.parse().ok())
229 .unwrap_or(0);
230 if len > MAX_BODY_BYTES {
231 return Err(Response::error(
232 413,
233 "body_too_large",
234 format!("{len} bytes exceeds the {MAX_BODY_BYTES} byte limit"),
235 ));
236 }
237 let mut body = vec![0u8; len];
238 if len > 0 {
239 stream
240 .read_exact(&mut body)
241 .map_err(|e| Response::error(400, "body_read_failed", e))?;
242 }
243
244 Ok(Some(Request {
245 method,
246 path: percent_decode(raw_path),
247 query: parse_query(raw_query),
248 headers,
249 body,
250 }))
251}
252
253fn write_response(stream: &mut TcpStream, r: &Response) -> std::io::Result<()> {
254 let mut head = format!(
255 "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n",
256 r.status,
257 reason(r.status),
258 r.content_type,
259 r.body.len()
260 );
261 head.push_str("X-Content-Type-Options: nosniff\r\n");
266 for (k, v) in &r.extra {
267 head.push_str(&format!("{k}: {v}\r\n"));
268 }
269 head.push_str("\r\n");
270 stream.write_all(head.as_bytes())?;
271 stream.write_all(&r.body)?;
272 stream.flush()
273}
274
275pub fn loopback(port: u16) -> SocketAddr {
283 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)
284}
285
286pub fn serve<H>(listener: TcpListener, handler: H) -> std::io::Result<()>
291where
292 H: Fn(Request) -> Response + Send + Sync + 'static,
293{
294 let handler = Arc::new(handler);
295 let live = Arc::new(AtomicUsize::new(0));
296
297 for incoming in listener.incoming() {
298 let mut stream = match incoming {
299 Ok(s) => s,
300 Err(_) => continue,
301 };
302 let _ = stream.set_read_timeout(Some(IO_TIMEOUT));
303 let _ = stream.set_write_timeout(Some(IO_TIMEOUT));
304
305 if live.load(Ordering::SeqCst) >= MAX_CONNECTIONS {
306 let _ = write_response(
307 &mut stream,
308 &Response::error(503, "busy", "too many concurrent connections"),
309 );
310 continue;
311 }
312
313 let handler = Arc::clone(&handler);
314 let live = Arc::clone(&live);
315 live.fetch_add(1, Ordering::SeqCst);
316 std::thread::spawn(move || {
317 handle_connection(&mut stream, handler.as_ref());
318 live.fetch_sub(1, Ordering::SeqCst);
319 });
320 }
321 Ok(())
322}
323
324fn handle_connection<H>(stream: &mut TcpStream, handler: &H)
325where
326 H: Fn(Request) -> Response,
327{
328 let response = {
329 let mut reader = BufReader::new(&*stream);
330 match read_request(&mut reader) {
331 Ok(None) => return,
332 Ok(Some(req)) => std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(req)))
333 .unwrap_or_else(|_| {
334 Response::error(500, "handler_panicked", "the request handler panicked")
335 }),
336 Err(e) => e,
337 }
338 };
339 let _ = write_response(stream, &response);
340 let _ = stream.shutdown(std::net::Shutdown::Both);
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn a_query_string_is_decoded_not_passed_through() {
349 let q = parse_query("a=one%20two&b&c=%2Fpath");
350 assert_eq!(q.get("a").map(String::as_str), Some("one two"));
351 assert_eq!(q.get("b").map(String::as_str), Some(""));
352 assert_eq!(q.get("c").map(String::as_str), Some("/path"));
353 }
354
355 #[test]
356 fn a_malformed_escape_is_kept_literally_not_dropped() {
357 assert_eq!(percent_decode("%zz"), "%zz");
360 assert_eq!(percent_decode("100%"), "100%");
361 }
362
363 #[test]
364 fn segments_ignore_empty_components() {
365 let r = Request {
366 method: "GET".into(),
367 path: "//decisions//abc/verify/".into(),
368 query: BTreeMap::new(),
369 headers: BTreeMap::new(),
370 body: vec![],
371 };
372 assert_eq!(r.segments(), vec!["decisions", "abc", "verify"]);
373 }
374
375 #[test]
376 fn header_lookup_is_case_insensitive() {
377 let mut headers = BTreeMap::new();
378 headers.insert("authorization".to_string(), "Bearer x".to_string());
379 let r = Request {
380 method: "GET".into(),
381 path: "/".into(),
382 query: BTreeMap::new(),
383 headers,
384 body: vec![],
385 };
386 assert_eq!(r.header("Authorization"), Some("Bearer x"));
387 }
388
389 #[test]
390 fn the_bind_address_is_always_loopback() {
391 assert!(loopback(7842).ip().is_loopback());
394 }
395
396 #[test]
397 fn a_response_never_carries_a_cors_allow_header() {
398 let listener = TcpListener::bind(loopback(0)).unwrap();
401 let port = listener.local_addr().unwrap().port();
402 std::thread::spawn(move || {
403 serve(listener, |_req| Response::json(200, "{}")).unwrap();
404 });
405 let mut s = TcpStream::connect(loopback(port)).unwrap();
406 s.write_all(b"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n").unwrap();
407 let mut buf = String::new();
408 s.read_to_string(&mut buf).unwrap();
409 assert!(buf.starts_with("HTTP/1.1 200"), "{buf}");
410 assert!(
411 !buf.to_lowercase().contains("access-control-allow"),
412 "a CORS allow header would let any web page read this: {buf}"
413 );
414 }
415
416 #[test]
417 fn an_oversized_body_is_refused_by_the_declared_length() {
418 let listener = TcpListener::bind(loopback(0)).unwrap();
420 let port = listener.local_addr().unwrap().port();
421 std::thread::spawn(move || {
422 serve(listener, |_req| Response::json(200, "{}")).unwrap();
423 });
424 let mut s = TcpStream::connect(loopback(port)).unwrap();
425 let head = format!(
426 "POST /observe HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: {}\r\n\r\n",
427 MAX_BODY_BYTES + 1
428 );
429 s.write_all(head.as_bytes()).unwrap();
430 let mut buf = String::new();
431 s.read_to_string(&mut buf).unwrap();
432 assert!(buf.starts_with("HTTP/1.1 413"), "{buf}");
433 }
434
435 #[test]
436 fn a_panicking_handler_answers_500_rather_than_killing_the_daemon() {
437 let listener = TcpListener::bind(loopback(0)).unwrap();
438 let port = listener.local_addr().unwrap().port();
439 std::thread::spawn(move || {
440 serve(listener, |_req| panic!("boom")).unwrap();
441 });
442 let prev = std::panic::take_hook();
444 std::panic::set_hook(Box::new(|_| {}));
445 let mut s = TcpStream::connect(loopback(port)).unwrap();
446 s.write_all(b"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n").unwrap();
447 let mut buf = String::new();
448 s.read_to_string(&mut buf).unwrap();
449 std::panic::set_hook(prev);
450 assert!(buf.starts_with("HTTP/1.1 500"), "{buf}");
451
452 let mut s2 = TcpStream::connect(loopback(port)).unwrap();
454 s2.write_all(b"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n").unwrap();
455 let mut buf2 = String::new();
456 s2.read_to_string(&mut buf2).unwrap();
457 assert!(buf2.starts_with("HTTP/1.1 500"), "{buf2}");
458 }
459
460 #[test]
461 fn a_body_is_read_in_full_before_the_handler_sees_it() {
462 let listener = TcpListener::bind(loopback(0)).unwrap();
463 let port = listener.local_addr().unwrap().port();
464 std::thread::spawn(move || {
465 serve(listener, |req| {
466 Response::text(200, String::from_utf8_lossy(&req.body).to_string())
467 })
468 .unwrap();
469 });
470 let mut s = TcpStream::connect(loopback(port)).unwrap();
471 let payload = r#"{"locator":"."}"#;
472 s.write_all(
473 format!(
474 "POST /observe HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: {}\r\n\r\n{}",
475 payload.len(),
476 payload
477 )
478 .as_bytes(),
479 )
480 .unwrap();
481 let mut buf = String::new();
482 s.read_to_string(&mut buf).unwrap();
483 assert!(buf.ends_with(payload), "{buf}");
484 }
485}