1use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5use std::time::Duration;
6
7use crate::error::HttpError;
8use crate::request::Request;
9use crate::response::ResponseWriter;
10
11pub trait Handler: Send + Sync {
17 fn serve_http(&self, w: &mut dyn ResponseWriter, r: &mut Request);
18}
19
20type HandlerFn = Box<dyn Fn(&mut dyn ResponseWriter, &mut Request) + Send + Sync>;
27pub struct HandlerFunc(pub HandlerFn);
28
29impl Handler for HandlerFunc {
30 fn serve_http(&self, w: &mut dyn ResponseWriter, r: &mut Request) {
31 (self.0)(w, r)
32 }
33}
34
35impl<H: Handler> Handler for Arc<H> {
39 fn serve_http(&self, w: &mut dyn ResponseWriter, r: &mut Request) {
40 (**self).serve_http(w, r)
41 }
42}
43
44pub fn handler_func<F>(f: F) -> HandlerFunc
46where
47 F: Fn(&mut dyn ResponseWriter, &mut Request) + Send + Sync + 'static,
48{
49 HandlerFunc(Box::new(f))
50}
51
52#[derive(Clone, Debug)]
58enum Segment {
59 Literal(String),
60 Wildcard { name: String },
62 Tail { name: String },
64}
65
66#[derive(Clone, Debug)]
74struct ParsedPattern {
75 method: Option<String>,
76 host: Option<String>,
77 segments: Vec<Segment>,
78 has_trailing_slash: bool,
79}
80
81struct MuxEntry {
82 raw: String,
83 parsed: ParsedPattern,
84 handler: Arc<dyn Handler>,
85}
86
87pub struct ServeMux {
96 entries: RwLock<Vec<MuxEntry>>,
97}
98
99impl ServeMux {
100 pub fn new() -> Self {
101 Self { entries: RwLock::new(Vec::new()) }
102 }
103
104 pub fn handle(&self, pattern: &str, handler: impl Handler + 'static) {
118 self.handle_arc(pattern, Arc::new(handler));
119 }
120
121 pub fn handle_func<F>(&self, pattern: &str, f: F)
123 where
124 F: Fn(&mut dyn ResponseWriter, &mut Request) + Send + Sync + 'static,
125 {
126 self.handle(pattern, handler_func(f));
127 }
128
129 fn handle_arc(&self, pattern: &str, handler: Arc<dyn Handler>) {
130 let parsed = parse_pattern(pattern);
131 let mut entries = self.entries.write().unwrap();
132 if let Some(e) = entries.iter_mut().find(|e| e.raw == pattern) {
133 e.handler = handler;
134 return;
135 }
136 entries.push(MuxEntry { raw: pattern.to_owned(), parsed, handler });
137 }
138
139 pub fn match_handler(&self, path: &str) -> Option<Arc<dyn Handler>> {
143 let entries = self.entries.read().unwrap();
144 let (h, _, _) = match_with_params(&entries, "", "", path);
145 h
146 }
147}
148
149impl Handler for ServeMux {
150 fn serve_http(&self, w: &mut dyn ResponseWriter, r: &mut Request) {
151 let entries = self.entries.read().unwrap();
152 let (handler, params, method_not_allowed) =
153 match_with_params(&entries, &r.method, &r.host, r.url.path());
154 drop(entries);
155
156 match handler {
157 Some(h) => {
158 r.path_params = params;
159 h.serve_http(w, r);
160 }
161 None if method_not_allowed => {
162 w.write_header(crate::status::METHOD_NOT_ALLOWED);
163 let _ = w.write(b"405 method not allowed\n");
164 }
165 None => not_found_handler().serve_http(w, r),
166 }
167 }
168}
169
170impl Default for ServeMux {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176const KNOWN_METHODS: &[&str] = &[
182 "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE",
183];
184
185fn parse_pattern(raw: &str) -> ParsedPattern {
186 let mut s = raw;
187
188 let method = {
190 let upper = s.to_ascii_uppercase();
191 let mut found = None;
192 for &m in KNOWN_METHODS {
193 if upper.starts_with(m)
194 && s[m.len()..].starts_with(' ')
195 {
196 found = Some(m.to_owned());
197 s = s[m.len() + 1..].trim_start();
198 break;
199 }
200 }
201 found
202 };
203
204 let host = if !s.starts_with('/') {
206 let slash = s.find('/').unwrap_or(s.len());
207 let h = s[..slash].to_owned();
208 s = if slash < s.len() { &s[slash..] } else { "/" };
209 Some(h)
210 } else {
211 None
212 };
213
214 let has_trailing_slash = s.len() > 1 && s.ends_with('/');
216 let path_body = if has_trailing_slash { &s[..s.len() - 1] } else { s };
217
218 let segments: Vec<Segment> = path_body
219 .split('/')
220 .filter(|p| !p.is_empty())
221 .map(parse_segment)
222 .collect();
223
224 ParsedPattern { method, host, segments, has_trailing_slash }
225}
226
227fn parse_segment(seg: &str) -> Segment {
228 if seg.starts_with('{') && seg.ends_with('}') {
229 let inner = &seg[1..seg.len() - 1];
230 if let Some(name) = inner.strip_suffix("...") {
231 return Segment::Tail { name: name.to_owned() };
232 }
233 return Segment::Wildcard { name: inner.to_owned() };
234 }
235 Segment::Literal(seg.to_owned())
236}
237
238fn specificity(p: &ParsedPattern) -> i64 {
244 let mut score: i64 = 0;
245 if p.method.is_some() { score += 10_000_000; }
246 if p.host.is_some() { score += 1_000_000; }
247 for seg in &p.segments {
248 score += match seg {
249 Segment::Literal(_) => 10_000,
250 Segment::Wildcard { .. } => 1_000,
251 Segment::Tail { .. } => 100,
252 };
253 }
254 let has_tail = p.segments.iter().any(|s| matches!(s, Segment::Tail { .. }));
256 if !p.has_trailing_slash && !has_tail { score += 1; }
257 score
258}
259
260fn try_match_path(
262 segments: &[Segment],
263 parts: &[&str],
264 has_trailing_slash: bool,
265) -> Option<HashMap<String, String>> {
266 let mut params = HashMap::new();
267
268 for (i, seg) in segments.iter().enumerate() {
269 match seg {
270 Segment::Tail { name } => {
271 let tail = parts[i..].join("/");
273 if !name.is_empty() {
274 params.insert(name.clone(), tail);
275 }
276 return Some(params);
277 }
278 Segment::Wildcard { name } => {
279 let part = parts.get(i)?;
280 if !name.is_empty() {
281 params.insert(name.clone(), (*part).to_owned());
282 }
283 }
284 Segment::Literal(lit) => {
285 if parts.get(i)? != lit { return None; }
286 }
287 }
288 }
289
290 if parts.len() == segments.len() {
291 return Some(params);
292 }
293 if has_trailing_slash && parts.len() > segments.len() {
295 return Some(params);
296 }
297 None
298}
299
300fn match_with_params(
304 entries: &[MuxEntry],
305 method: &str,
306 host: &str,
307 path: &str,
308) -> (Option<Arc<dyn Handler>>, HashMap<String, String>, bool) {
309 let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
310
311 let mut best_handler: Option<Arc<dyn Handler>> = None;
312 let mut best_params = HashMap::new();
313 let mut best_score: i64 = i64::MIN;
314 let mut method_not_allowed = false;
315
316 for entry in entries {
317 let pat = &entry.parsed;
318
319 let Some(params) = try_match_path(&pat.segments, &parts, pat.has_trailing_slash)
320 else { continue };
321
322 if !host.is_empty()
324 && let Some(ref ph) = pat.host
325 && !host.eq_ignore_ascii_case(ph)
326 {
327 continue;
328 }
329
330 if !method.is_empty()
332 && let Some(ref pm) = pat.method
333 && !method.eq_ignore_ascii_case(pm)
334 {
335 method_not_allowed = true;
336 continue;
337 }
338
339 let score = specificity(pat);
340 if score > best_score {
341 best_score = score;
342 best_handler = Some(Arc::clone(&entry.handler));
343 best_params = params;
344 }
345 }
346
347 let mna = method_not_allowed && best_handler.is_none();
349 (best_handler, best_params, mna)
350}
351
352use std::sync::OnceLock;
357
358static DEFAULT_SERVE_MUX: OnceLock<Arc<ServeMux>> = OnceLock::new();
359
360fn default_mux() -> &'static Arc<ServeMux> {
361 DEFAULT_SERVE_MUX.get_or_init(|| Arc::new(ServeMux::new()))
362}
363
364pub fn handle(pattern: &str, handler: impl Handler + 'static) {
366 default_mux().handle(pattern, handler);
367}
368
369pub fn handle_func<F>(pattern: &str, f: F)
371where
372 F: Fn(&mut dyn ResponseWriter, &mut Request) + Send + Sync + 'static,
373{
374 default_mux().handle_func(pattern, f);
375}
376
377pub fn default_serve_mux() -> Arc<ServeMux> {
379 Arc::clone(default_mux())
380}
381
382pub fn not_found_handler() -> impl Handler {
388 handler_func(|w, _r| {
389 w.write_header(crate::status::NOT_FOUND);
390 let _ = w.write(b"404 page not found\n");
391 })
392}
393
394pub fn strip_prefix(prefix: String, handler: impl Handler + 'static) -> impl Handler {
401 let handler = Arc::new(handler);
402 handler_func(move |w, r| {
403 let path = r.url.path();
404 match path.strip_prefix(prefix.as_str()) {
405 None => not_found_handler().serve_http(w, r),
406 Some(stripped) => {
407 let mut new_url = r.url.clone();
409 new_url.set_path(if stripped.is_empty() { "/" } else { stripped });
410 match rebuild_request(r, new_url) {
411 Err(_) => crate::util::error(w, "internal error", 500),
412 Ok(mut req) => handler.serve_http(w, &mut req),
413 }
414 }
415 }
416 })
417}
418
419pub fn file_server(root: String) -> impl Handler {
427 handler_func(move |w, r| {
428 use std::io::Read;
429 use std::path::Path;
430
431 let url_path = r.url.path();
432
433 let rel = url_path.trim_start_matches('/');
435 let fs_path = if rel.is_empty() {
436 Path::new(&root).to_path_buf()
437 } else {
438 Path::new(&root).join(rel)
439 };
440
441 let root_canon = match std::fs::canonicalize(&root) {
444 Ok(p) => p,
445 Err(_) => {
446 crate::util::error(w, "500 Internal Server Error", crate::status::INTERNAL_SERVER_ERROR);
447 return;
448 }
449 };
450 let candidate_canon = std::fs::canonicalize(&fs_path)
453 .or_else(|_| std::fs::canonicalize(fs_path.parent().unwrap_or(&fs_path)))
454 .unwrap_or_else(|_| fs_path.clone());
455 if !candidate_canon.starts_with(&root_canon) {
456 crate::util::error(w, "403 Forbidden", crate::status::FORBIDDEN);
457 return;
458 }
459
460 match fs_path.metadata() {
462 Err(_) => {
463 crate::util::error(w, "404 Not Found", crate::status::NOT_FOUND);
464 return;
465 }
466 Ok(meta) if meta.is_dir() => {
467 crate::util::error(w, "403 Forbidden", crate::status::FORBIDDEN);
468 return;
469 }
470 Ok(_) => {}
471 }
472
473 let ct = {
475 let mut probe = [0u8; 512];
476 let n = std::fs::File::open(&fs_path)
477 .and_then(|mut f| f.read(&mut probe))
478 .unwrap_or(0);
479 crate::mime::detect_content_type(&probe[..n]).to_owned()
480 };
481
482 match std::fs::read(&fs_path) {
484 Err(_) => crate::util::error(w, "500 Internal Server Error", crate::status::INTERNAL_SERVER_ERROR),
485 Ok(data) => {
486 w.header().set("Content-Type", &ct);
487 w.header().set("Content-Length", data.len().to_string());
488 w.write_header(crate::status::OK);
489 let _ = w.write(&data);
490 }
491 }
492 })
493}
494
495pub fn timeout_handler(
503 handler: impl Handler + 'static,
504 timeout: Duration,
505 body: &'static str,
506) -> impl Handler {
507 let handler = Arc::new(handler);
508 handler_func(move |w, r| {
509 use go_lib::chan::chan;
510 use go_lib::context::with_timeout;
511
512 let (done_tx, done_rx) = chan::<BodyCapture>(1);
515
516 let inner_handler = Arc::clone(&handler);
517 let req_url = r.url.clone();
518 let method = r.method.clone();
519 let req_header = r.header.clone();
520 let host = r.host.clone();
521 let remote = r.remote_addr.clone();
522
523 let (ctx, cancel) = with_timeout(&go_lib::context::background(), timeout);
524
525 go_lib::go!(move || {
526 let mut inner_req = match Request::new(&method, req_url.as_str(), None) {
527 Ok(r) => r,
528 Err(_) => { done_tx.send(BodyCapture::default()); return; }
529 };
530 inner_req.header = req_header;
531 inner_req.host = host;
532 inner_req.remote_addr = remote;
533
534 let mut capture = BodyCapture::default();
535 inner_handler.serve_http(&mut capture, &mut inner_req);
536 done_tx.send(capture);
537 });
538
539 go_lib::select! {
541 recv(ctx.done()) -> _v => {
542 cancel.cancel();
543 w.write_header(crate::status::SERVICE_UNAVAILABLE);
544 let _ = w.write(body.as_bytes());
545 }
546 recv(done_rx) -> result => {
547 cancel.cancel();
548 if let Some(capture) = result {
549 for (name, values) in capture.header.iter() {
551 for val in values {
552 w.header().add(name, val.as_str());
553 }
554 }
555 let status = if capture.status == 0 { 200 } else { capture.status };
556 w.write_header(status);
557 let _ = w.write(&capture.body);
558 }
559 }
560 }
561 })
562}
563
564#[derive(Default)]
570struct BodyCapture {
571 status: u16,
572 header: crate::header::Header,
573 body: Vec<u8>,
574}
575
576impl ResponseWriter for BodyCapture {
577 fn header(&mut self) -> &mut crate::header::Header { &mut self.header }
578 fn write(&mut self, buf: &[u8]) -> Result<usize, crate::error::HttpError> {
579 self.body.extend_from_slice(buf);
580 Ok(buf.len())
581 }
582 fn write_header(&mut self, code: u16) {
583 if self.status == 0 { self.status = code; }
584 }
585}
586
587unsafe impl Send for BodyCapture {}
590
591fn rebuild_request(r: &Request, new_url: url::Url) -> Result<Request, HttpError> {
596 let mut req = Request::new_with_context(
597 &r.method,
598 new_url.as_str(),
599 None, r.context().clone(),
601 )?;
602 req.proto = r.proto.clone();
603 req.proto_major = r.proto_major;
604 req.proto_minor = r.proto_minor;
605 req.header = r.header.clone();
606 req.host = r.host.clone();
607 req.content_length = r.content_length;
608 req.transfer_encoding = r.transfer_encoding.clone();
609 req.remote_addr = r.remote_addr.clone();
610 req.trailer = r.trailer.clone();
611 req.path_params = r.path_params.clone();
612 Ok(req)
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use crate::response::ConnResponseWriter;
619 use crate::request::Request;
620
621 fn dummy_request(path: &str) -> Request {
622 Request::new("GET", &format!("http://example.com{path}"), None).unwrap()
623 }
624
625 struct RecordingWriter {
626 inner: ConnResponseWriter<Vec<u8>>,
627 }
628 impl RecordingWriter {
629 fn new() -> Self { Self { inner: ConnResponseWriter::new(Vec::new()) } }
630 fn bytes(mut self) -> Vec<u8> {
631 let _ = self.inner.finish();
632 self.inner.inner
633 }
634 }
635 impl ResponseWriter for RecordingWriter {
636 fn header(&mut self) -> &mut crate::header::Header { self.inner.header() }
637 fn write(&mut self, buf: &[u8]) -> Result<usize, crate::error::HttpError> { self.inner.write(buf) }
638 fn write_header(&mut self, code: u16) { self.inner.write_header(code) }
639 }
640
641 #[test]
642 fn exact_match() {
643 let mux = ServeMux::new();
644 mux.handle_func("/hello", |w, _| { let _ = w.write(b"hi"); });
645 let mut r = dummy_request("/hello");
646 let mut w = RecordingWriter::new();
647 mux.serve_http(&mut w, &mut r);
648 let out = w.bytes();
649 assert!(out.windows(2).any(|w| w == b"hi"), "body should contain 'hi'");
650 }
651
652 #[test]
653 fn prefix_match() {
654 let mux = ServeMux::new();
655 mux.handle_func("/static/", |w, _| { let _ = w.write(b"file"); });
656 let mut r = dummy_request("/static/foo.js");
657 let mut w = RecordingWriter::new();
658 mux.serve_http(&mut w, &mut r);
659 let out = w.bytes();
660 assert!(out.windows(4).any(|s| s == b"file"));
661 }
662
663 #[test]
664 fn not_found_fallback() {
665 let mux = ServeMux::new();
666 let mut r = dummy_request("/nowhere");
667 let mut w = RecordingWriter::new();
668 mux.serve_http(&mut w, &mut r);
669 let out = String::from_utf8(w.bytes()).unwrap();
670 assert!(out.contains("404"));
671 }
672
673 #[test]
674 fn longer_prefix_wins() {
675 let mux = ServeMux::new();
676 mux.handle_func("/api/", |w, _| { let _ = w.write(b"short"); });
677 mux.handle_func("/api/v2/", |w, _| { let _ = w.write(b"long"); });
678 let mut r = dummy_request("/api/v2/users");
679 let mut w = RecordingWriter::new();
680 mux.serve_http(&mut w, &mut r);
681 let out = w.bytes();
682 assert!(out.windows(4).any(|s| s == b"long"));
683 }
684
685 #[test]
688 fn strip_prefix_rewrites_path() {
689 let inner = handler_func(|w, r| {
691 let _ = w.write(r.url.path().as_bytes());
692 });
693 let h = strip_prefix("/api".to_owned(), inner);
694 let mut r = dummy_request("/api/users");
695 let mut w = RecordingWriter::new();
696 h.serve_http(&mut w, &mut r);
697 let body = String::from_utf8(w.bytes()).unwrap();
698 assert!(body.contains("/users"), "stripped path should be /users, got: {body:?}");
700 }
701
702 #[test]
703 fn strip_prefix_no_match_returns_404() {
704 let inner = handler_func(|w, _| { let _ = w.write(b"ok"); });
705 let h = strip_prefix("/api".to_owned(), inner);
706 let mut r = dummy_request("/other/path");
707 let mut w = RecordingWriter::new();
708 h.serve_http(&mut w, &mut r);
709 let out = String::from_utf8(w.bytes()).unwrap();
710 assert!(out.contains("404"));
711 }
712
713 #[test]
716 fn file_server_serves_existing_file() {
717 let dir = std::env::temp_dir();
719 let path = dir.join("go_http_test_file.txt");
720 std::fs::write(&path, b"hello file").unwrap();
721
722 let h = file_server(dir.to_str().unwrap().to_owned());
723 let mut r = dummy_request("/go_http_test_file.txt");
724 let mut w = RecordingWriter::new();
725 h.serve_http(&mut w, &mut r);
726 let out = w.bytes();
727 assert!(out.windows(10).any(|s| s == b"hello file"), "file content not found");
728
729 let _ = std::fs::remove_file(path);
730 }
731
732 #[test]
733 fn file_server_missing_file_returns_404() {
734 let dir = std::env::temp_dir();
735 let h = file_server(dir.to_str().unwrap().to_owned());
736 let mut r = dummy_request("/this_file_does_not_exist_xyz.bin");
737 let mut w = RecordingWriter::new();
738 h.serve_http(&mut w, &mut r);
739 let out = String::from_utf8(w.bytes()).unwrap();
740 assert!(out.contains("404"), "expected 404 in response, got: {}", out);
741 }
742
743 #[test]
744 fn file_server_rejects_path_traversal() {
745 let root = std::env::temp_dir().join("go_http_test_root");
747 std::fs::create_dir_all(&root).unwrap();
748
749 let outside = std::env::temp_dir().join("go_http_outside.txt");
751 std::fs::write(&outside, b"secret").unwrap();
752
753 let h = file_server(root.to_str().unwrap().to_owned());
757 let mut r = dummy_request("/../go_http_outside.txt");
758 let mut w = RecordingWriter::new();
759 h.serve_http(&mut w, &mut r);
760 let out = String::from_utf8(w.bytes()).unwrap();
761
762 let _ = std::fs::remove_file(outside);
763 let _ = std::fs::remove_dir(root);
764
765 assert!(
769 out.contains("403") || out.contains("404"),
770 "expected 403 or 404, got: {out:?}"
771 );
772 assert!(!out.contains("secret"), "traversal should not expose file content");
773 }
774
775 }