Skip to main content

go_http/
handler.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::sync::{Arc, RwLock};
4use std::time::Duration;
5
6use crate::error::HttpError;
7use crate::request::Request;
8use crate::response::ResponseWriter;
9
10// ---------------------------------------------------------------------------
11// Handler trait
12// ---------------------------------------------------------------------------
13
14/// The core handler interface.  Port of Go's `http.Handler`.
15pub trait Handler: Send + Sync {
16    fn serve_http(&self, w: &mut dyn ResponseWriter, r: &Request);
17}
18
19// ---------------------------------------------------------------------------
20// HandlerFunc
21// ---------------------------------------------------------------------------
22
23/// Adapter that turns a function into a `Handler`.
24/// Port of Go's `http.HandlerFunc`.
25pub struct HandlerFunc(pub Box<dyn Fn(&mut dyn ResponseWriter, &Request) + Send + Sync>);
26
27impl Handler for HandlerFunc {
28    fn serve_http(&self, w: &mut dyn ResponseWriter, r: &Request) {
29        (self.0)(w, r)
30    }
31}
32
33/// Any `Arc<H>` where `H: Handler` is itself a `Handler`.  This lets you
34/// wrap a mux or other handler in an `Arc` and pass it directly to middleware
35/// functions like `timeout_handler`.
36impl<H: Handler> Handler for Arc<H> {
37    fn serve_http(&self, w: &mut dyn ResponseWriter, r: &Request) {
38        (**self).serve_http(w, r)
39    }
40}
41
42/// Convenience constructor.
43pub fn handler_func<F>(f: F) -> HandlerFunc
44where
45    F: Fn(&mut dyn ResponseWriter, &Request) + Send + Sync + 'static,
46{
47    HandlerFunc(Box::new(f))
48}
49
50// ---------------------------------------------------------------------------
51// ServeMux
52// ---------------------------------------------------------------------------
53
54struct MuxEntry {
55    handler: Arc<dyn Handler>,
56    pattern: String,
57}
58
59/// HTTP request multiplexer.  Port of Go's `http.ServeMux`.
60///
61/// Matching rules (same as Go):
62/// 1. Exact match wins over prefix match.
63/// 2. Among prefix matches, the longest pattern wins.
64/// 3. Patterns ending with `/` are subtree patterns (prefix match).
65/// 4. Patterns not ending with `/` are exact match only.
66pub struct ServeMux {
67    entries: RwLock<Vec<MuxEntry>>,
68}
69
70impl ServeMux {
71    pub fn new() -> Self {
72        Self {
73            entries: RwLock::new(Vec::new()),
74        }
75    }
76
77    /// Register a handler for the given pattern.
78    pub fn handle(&self, pattern: &str, handler: impl Handler + 'static) {
79        self.handle_arc(pattern, Arc::new(handler));
80    }
81
82    /// Register a function as a handler.
83    pub fn handle_func<F>(&self, pattern: &str, f: F)
84    where
85        F: Fn(&mut dyn ResponseWriter, &Request) + Send + Sync + 'static,
86    {
87        self.handle(pattern, handler_func(f));
88    }
89
90    fn handle_arc(&self, pattern: &str, handler: Arc<dyn Handler>) {
91        let mut entries = self.entries.write().unwrap();
92        // Replace existing entry for the same pattern.
93        if let Some(e) = entries.iter_mut().find(|e| e.pattern == pattern) {
94            e.handler = handler;
95            return;
96        }
97        entries.push(MuxEntry { handler, pattern: pattern.to_owned() });
98    }
99
100    /// Find the best matching handler for `path`.
101    pub fn match_handler(&self, path: &str) -> Option<Arc<dyn Handler>> {
102        let entries = self.entries.read().unwrap();
103        let mut best_len = 0usize;
104        let mut best: Option<Arc<dyn Handler>> = None;
105
106        for entry in entries.iter() {
107            let pat = entry.pattern.as_str();
108            if pat.ends_with('/') {
109                // Subtree (prefix) match.
110                if path.starts_with(pat) && pat.len() > best_len {
111                    best_len = pat.len();
112                    best = Some(Arc::clone(&entry.handler));
113                }
114            } else {
115                // Exact match — wins immediately if found.
116                if path == pat {
117                    return Some(Arc::clone(&entry.handler));
118                }
119            }
120        }
121        best
122    }
123}
124
125impl Handler for ServeMux {
126    fn serve_http(&self, w: &mut dyn ResponseWriter, r: &Request) {
127        let path = r.url.path();
128        match self.match_handler(path) {
129            Some(h) => h.serve_http(w, r),
130            None    => not_found_handler().serve_http(w, r),
131        }
132    }
133}
134
135impl Default for ServeMux {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141// ---------------------------------------------------------------------------
142// Default mux — global DefaultServeMux
143// ---------------------------------------------------------------------------
144
145use std::sync::OnceLock;
146
147static DEFAULT_SERVE_MUX: OnceLock<Arc<ServeMux>> = OnceLock::new();
148
149fn default_mux() -> &'static Arc<ServeMux> {
150    DEFAULT_SERVE_MUX.get_or_init(|| Arc::new(ServeMux::new()))
151}
152
153/// Register `handler` on the `DefaultServeMux`.  Port of Go's `http.Handle`.
154pub fn handle(pattern: &str, handler: impl Handler + 'static) {
155    default_mux().handle(pattern, handler);
156}
157
158/// Register a function on the `DefaultServeMux`.  Port of Go's `http.HandleFunc`.
159pub fn handle_func<F>(pattern: &str, f: F)
160where
161    F: Fn(&mut dyn ResponseWriter, &Request) + Send + Sync + 'static,
162{
163    default_mux().handle_func(pattern, f);
164}
165
166/// Return a reference to the global `DefaultServeMux`.
167pub fn default_serve_mux() -> Arc<ServeMux> {
168    Arc::clone(default_mux())
169}
170
171// ---------------------------------------------------------------------------
172// Built-in handler helpers
173// ---------------------------------------------------------------------------
174
175/// Returns a `Handler` that always replies 404.
176pub fn not_found_handler() -> impl Handler {
177    handler_func(|w, _r| {
178        w.write_header(crate::status::NOT_FOUND);
179        let _ = w.write(b"404 page not found\n");
180    })
181}
182
183/// Strips `prefix` from the request path before forwarding to `handler`.
184/// Port of Go's `http.StripPrefix`.
185///
186/// If the path does not start with `prefix` the request is answered with 404.
187/// The forwarded request has its URL path rewritten to the stripped path so
188/// the inner handler sees the correct path.
189pub fn strip_prefix(prefix: String, handler: impl Handler + 'static) -> impl Handler {
190    let handler = Arc::new(handler);
191    handler_func(move |w, r| {
192        let path = r.url.path();
193        match path.strip_prefix(prefix.as_str()) {
194            None => not_found_handler().serve_http(w, r),
195            Some(stripped) => {
196                // Build a new Request with the stripped path.
197                let mut new_url = r.url.clone();
198                new_url.set_path(if stripped.is_empty() { "/" } else { stripped });
199                match rebuild_request(r, new_url) {
200                    Err(_) => crate::util::error(w, "internal error", 500),
201                    Ok(req) => handler.serve_http(w, &req),
202                }
203            }
204        }
205    })
206}
207
208/// Serve files from the filesystem rooted at `root`.
209/// Port of Go's `http.FileServer`.
210///
211/// The URL path is joined to `root` to form the filesystem path.  Directory
212/// listings are not supported — a 403 is returned for directories.  File reads
213/// are performed synchronously within the goroutine (no extra goroutine spawn
214/// needed since each connection already has its own goroutine).
215pub fn file_server(root: String) -> impl Handler {
216    handler_func(move |w, r| {
217        use std::io::Read;
218        use std::path::Path;
219
220        let url_path = r.url.path();
221
222        // Strip the leading `/` and join with root.
223        let rel = url_path.trim_start_matches('/');
224        let fs_path = if rel.is_empty() {
225            Path::new(&root).to_path_buf()
226        } else {
227            Path::new(&root).join(rel)
228        };
229
230        // Guard against path traversal: the canonical path must remain under root.
231        // We canonicalize the root once and compare prefixes.
232        let root_canon = match std::fs::canonicalize(&root) {
233            Ok(p)  => p,
234            Err(_) => {
235                crate::util::error(w, "500 Internal Server Error", crate::status::INTERNAL_SERVER_ERROR);
236                return;
237            }
238        };
239        // For the candidate path, canonicalize if it exists; otherwise check the
240        // parent chain — if the parent is outside root, deny.
241        let candidate_canon = std::fs::canonicalize(&fs_path)
242            .or_else(|_| std::fs::canonicalize(fs_path.parent().unwrap_or(&fs_path)))
243            .unwrap_or_else(|_| fs_path.clone());
244        if !candidate_canon.starts_with(&root_canon) {
245            crate::util::error(w, "403 Forbidden", crate::status::FORBIDDEN);
246            return;
247        }
248
249        // Disallow directory access.
250        match fs_path.metadata() {
251            Err(_) => {
252                crate::util::error(w, "404 Not Found", crate::status::NOT_FOUND);
253                return;
254            }
255            Ok(meta) if meta.is_dir() => {
256                crate::util::error(w, "403 Forbidden", crate::status::FORBIDDEN);
257                return;
258            }
259            Ok(_) => {}
260        }
261
262        // Detect content type from the first 512 bytes.
263        let ct = {
264            let mut probe = [0u8; 512];
265            let n = std::fs::File::open(&fs_path)
266                .and_then(|mut f| f.read(&mut probe))
267                .unwrap_or(0);
268            crate::mime::detect_content_type(&probe[..n]).to_owned()
269        };
270
271        // Read and serve the file.
272        match std::fs::read(&fs_path) {
273            Err(_) => crate::util::error(w, "500 Internal Server Error", crate::status::INTERNAL_SERVER_ERROR),
274            Ok(data) => {
275                w.header().set("Content-Type", &ct);
276                w.header().set("Content-Length", data.len().to_string());
277                w.write_header(crate::status::OK);
278                let _ = w.write(&data);
279            }
280        }
281    })
282}
283
284/// Wraps `handler` with a per-request deadline.
285///
286/// If the handler does not complete within `timeout` the connection receives
287/// `body` with status 503.  The handler runs in a spawned goroutine; the
288/// caller goroutine selects on a done channel vs a timeout context.
289///
290/// Port of Go's `http.TimeoutHandler`.
291pub fn timeout_handler(
292    handler: impl Handler + 'static,
293    timeout: Duration,
294    body:    &'static str,
295) -> impl Handler {
296    let handler = Arc::new(handler);
297    handler_func(move |w, r| {
298        use go_lib::chan::chan;
299        use go_lib::context::with_timeout;
300
301        // BodyCapture collects response data without HTTP framing so we can
302        // replay it through the outer ResponseWriter cleanly.
303        let (done_tx, done_rx) = chan::<BodyCapture>(1);
304
305        let inner_handler = Arc::clone(&handler);
306        let req_url    = r.url.clone();
307        let method     = r.method.clone();
308        let req_header = r.header.clone();
309        let host       = r.host.clone();
310        let remote     = r.remote_addr.clone();
311
312        let (ctx, cancel) = with_timeout(&go_lib::context::background(), timeout);
313
314        go_lib::go!(move || {
315            let mut inner_req = match Request::new(&method, req_url.as_str(), None) {
316                Ok(r)  => r,
317                Err(_) => { done_tx.send(BodyCapture::default()); return; }
318            };
319            inner_req.header      = req_header;
320            inner_req.host        = host;
321            inner_req.remote_addr = remote;
322
323            let mut capture = BodyCapture::default();
324            inner_handler.serve_http(&mut capture, &inner_req);
325            done_tx.send(capture);
326        });
327
328        // Select: timeout fires → 503; inner handler done → replay captured response.
329        go_lib::select! {
330            recv(ctx.done()) -> _v => {
331                cancel.cancel();
332                w.write_header(crate::status::SERVICE_UNAVAILABLE);
333                let _ = w.write(body.as_bytes());
334            }
335            recv(done_rx) -> result => {
336                cancel.cancel();
337                if let Some(capture) = result {
338                    // Replay captured headers.
339                    for (name, values) in capture.header.iter() {
340                        for val in values {
341                            w.header().add(name, val.as_str());
342                        }
343                    }
344                    let status = if capture.status == 0 { 200 } else { capture.status };
345                    w.write_header(status);
346                    let _ = w.write(&capture.body);
347                }
348            }
349        }
350    })
351}
352
353// ---------------------------------------------------------------------------
354// BodyCapture — a ResponseWriter that buffers status + headers + raw body
355// without adding HTTP framing.  Used by timeout_handler.
356// ---------------------------------------------------------------------------
357
358#[derive(Default)]
359struct BodyCapture {
360    status: u16,
361    header: crate::header::Header,
362    body:   Vec<u8>,
363}
364
365impl ResponseWriter for BodyCapture {
366    fn header(&mut self) -> &mut crate::header::Header { &mut self.header }
367    fn write(&mut self, buf: &[u8]) -> Result<usize, crate::error::HttpError> {
368        self.body.extend_from_slice(buf);
369        Ok(buf.len())
370    }
371    fn write_header(&mut self, code: u16) {
372        if self.status == 0 { self.status = code; }
373    }
374}
375
376// BodyCapture must be Send to cross a goroutine boundary through a channel.
377// It only holds Vec<u8> and Header (both Send).
378unsafe impl Send for BodyCapture {}
379
380// ---------------------------------------------------------------------------
381// Internal: rebuild a Request with a new URL (used by strip_prefix)
382// ---------------------------------------------------------------------------
383
384fn rebuild_request(r: &Request, new_url: url::Url) -> Result<Request, HttpError> {
385    let mut req = Request::new_with_context(
386        &r.method,
387        new_url.as_str(),
388        None, // body is not forwarded — it may be consumed; handlers should read from original
389        r.context().clone(),
390    )?;
391    req.proto             = r.proto.clone();
392    req.proto_major       = r.proto_major;
393    req.proto_minor       = r.proto_minor;
394    req.header            = r.header.clone();
395    req.host              = r.host.clone();
396    req.content_length    = r.content_length;
397    req.transfer_encoding = r.transfer_encoding.clone();
398    req.remote_addr       = r.remote_addr.clone();
399    req.trailer           = r.trailer.clone();
400    Ok(req)
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use crate::response::ConnResponseWriter;
407    use crate::request::Request;
408
409    fn dummy_request(path: &str) -> Request {
410        Request::new("GET", &format!("http://example.com{path}"), None).unwrap()
411    }
412
413    struct RecordingWriter {
414        inner: ConnResponseWriter<Vec<u8>>,
415    }
416    impl RecordingWriter {
417        fn new() -> Self { Self { inner: ConnResponseWriter::new(Vec::new()) } }
418        fn bytes(mut self) -> Vec<u8> {
419            let _ = self.inner.finish();
420            self.inner.inner
421        }
422    }
423    impl ResponseWriter for RecordingWriter {
424        fn header(&mut self) -> &mut crate::header::Header { self.inner.header() }
425        fn write(&mut self, buf: &[u8]) -> Result<usize, crate::error::HttpError> { self.inner.write(buf) }
426        fn write_header(&mut self, code: u16) { self.inner.write_header(code) }
427    }
428
429    #[test]
430    fn exact_match() {
431        let mux = ServeMux::new();
432        mux.handle_func("/hello", |w, _| { let _ = w.write(b"hi"); });
433        let r = dummy_request("/hello");
434        let mut w = RecordingWriter::new();
435        mux.serve_http(&mut w, &r);
436        let out = w.bytes();
437        assert!(out.windows(2).any(|w| w == b"hi"), "body should contain 'hi'");
438    }
439
440    #[test]
441    fn prefix_match() {
442        let mux = ServeMux::new();
443        mux.handle_func("/static/", |w, _| { let _ = w.write(b"file"); });
444        let r = dummy_request("/static/foo.js");
445        let mut w = RecordingWriter::new();
446        mux.serve_http(&mut w, &r);
447        let out = w.bytes();
448        assert!(out.windows(4).any(|s| s == b"file"));
449    }
450
451    #[test]
452    fn not_found_fallback() {
453        let mux = ServeMux::new();
454        let r = dummy_request("/nowhere");
455        let mut w = RecordingWriter::new();
456        mux.serve_http(&mut w, &r);
457        let out = String::from_utf8(w.bytes()).unwrap();
458        assert!(out.contains("404"));
459    }
460
461    #[test]
462    fn longer_prefix_wins() {
463        let mux = ServeMux::new();
464        mux.handle_func("/api/", |w, _| { let _ = w.write(b"short"); });
465        mux.handle_func("/api/v2/", |w, _| { let _ = w.write(b"long"); });
466        let r = dummy_request("/api/v2/users");
467        let mut w = RecordingWriter::new();
468        mux.serve_http(&mut w, &r);
469        let out = w.bytes();
470        assert!(out.windows(4).any(|s| s == b"long"));
471    }
472
473    // ── strip_prefix ─────────────────────────────────────────────────────────
474
475    #[test]
476    fn strip_prefix_rewrites_path() {
477        // Inner handler sees the stripped path in the request URL.
478        let inner = handler_func(|w, r| {
479            let _ = w.write(r.url.path().as_bytes());
480        });
481        let h = strip_prefix("/api".to_owned(), inner);
482        let r = dummy_request("/api/users");
483        let mut w = RecordingWriter::new();
484        h.serve_http(&mut w, &r);
485        let body = String::from_utf8(w.bytes()).unwrap();
486        // The body is the raw bytes written; find /users in them.
487        assert!(body.contains("/users"), "stripped path should be /users, got: {body:?}");
488    }
489
490    #[test]
491    fn strip_prefix_no_match_returns_404() {
492        let inner = handler_func(|w, _| { let _ = w.write(b"ok"); });
493        let h = strip_prefix("/api".to_owned(), inner);
494        let r = dummy_request("/other/path");
495        let mut w = RecordingWriter::new();
496        h.serve_http(&mut w, &r);
497        let out = String::from_utf8(w.bytes()).unwrap();
498        assert!(out.contains("404"));
499    }
500
501    // ── file_server ──────────────────────────────────────────────────────────
502
503    #[test]
504    fn file_server_serves_existing_file() {
505        // Write a temp file.
506        let dir  = std::env::temp_dir();
507        let path = dir.join("go_http_test_file.txt");
508        std::fs::write(&path, b"hello file").unwrap();
509
510        let h = file_server(dir.to_str().unwrap().to_owned());
511        let r = dummy_request("/go_http_test_file.txt");
512        let mut w = RecordingWriter::new();
513        h.serve_http(&mut w, &r);
514        let out = w.bytes();
515        assert!(out.windows(10).any(|s| s == b"hello file"), "file content not found");
516
517        let _ = std::fs::remove_file(path);
518    }
519
520    #[test]
521    fn file_server_missing_file_returns_404() {
522        let dir = std::env::temp_dir();
523        let h = file_server(dir.to_str().unwrap().to_owned());
524        let r = dummy_request("/this_file_does_not_exist_xyz.bin");
525        let mut w = RecordingWriter::new();
526        h.serve_http(&mut w, &r);
527        let out = String::from_utf8(w.bytes()).unwrap();
528        assert!(out.contains("404"), "expected 404 in response, got: {}", out);
529    }
530
531    #[test]
532    fn file_server_rejects_path_traversal() {
533        // Create a subdirectory and serve only from it.
534        let root = std::env::temp_dir().join("go_http_test_root");
535        std::fs::create_dir_all(&root).unwrap();
536
537        // Write a sentinel file *outside* the root (in its parent).
538        let outside = std::env::temp_dir().join("go_http_outside.txt");
539        std::fs::write(&outside, b"secret").unwrap();
540
541        // Request a path that resolves to the parent directory's file.
542        // We serve from .../go_http_test_root/ and request ../go_http_outside.txt
543        // which on the filesystem becomes .../go_http_outside.txt (outside root).
544        let h = file_server(root.to_str().unwrap().to_owned());
545        let r = dummy_request("/../go_http_outside.txt");
546        let mut w = RecordingWriter::new();
547        h.serve_http(&mut w, &r);
548        let out = String::from_utf8(w.bytes()).unwrap();
549
550        let _ = std::fs::remove_file(outside);
551        let _ = std::fs::remove_dir(root);
552
553        // URL normalises /../foo to /foo so the path is just the filename —
554        // which doesn't exist in our empty root → 404.  Either 403 or 404 is
555        // acceptable; the important thing is we don't serve secret content.
556        assert!(
557            out.contains("403") || out.contains("404"),
558            "expected 403 or 404, got: {out:?}"
559        );
560        assert!(!out.contains("secret"), "traversal should not expose file content");
561    }
562
563    // timeout_handler is covered by tests/middleware.rs integration tests
564    // (timeout_handler_fast_passes, timeout_handler_slow_503), which carry
565    // `#[go_lib::main]` so each test body runs as the first goroutine on the
566    // shared process-wide scheduler.
567}