use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use crate::error::HttpError;
use crate::request::Request;
use crate::response::ResponseWriter;
pub trait Handler: Send + Sync {
fn serve_http(&self, w: &mut dyn ResponseWriter, r: &mut Request);
}
type HandlerFn = Box<dyn Fn(&mut dyn ResponseWriter, &mut Request) + Send + Sync>;
pub struct HandlerFunc(pub HandlerFn);
impl Handler for HandlerFunc {
fn serve_http(&self, w: &mut dyn ResponseWriter, r: &mut Request) {
(self.0)(w, r)
}
}
impl<H: Handler> Handler for Arc<H> {
fn serve_http(&self, w: &mut dyn ResponseWriter, r: &mut Request) {
(**self).serve_http(w, r)
}
}
pub fn handler_func<F>(f: F) -> HandlerFunc
where
F: Fn(&mut dyn ResponseWriter, &mut Request) + Send + Sync + 'static,
{
HandlerFunc(Box::new(f))
}
#[derive(Clone, Debug)]
enum Segment {
Literal(String),
Wildcard { name: String },
Tail { name: String },
}
#[derive(Clone, Debug)]
struct ParsedPattern {
method: Option<String>,
host: Option<String>,
segments: Vec<Segment>,
has_trailing_slash: bool,
}
struct MuxEntry {
raw: String,
parsed: ParsedPattern,
handler: Arc<dyn Handler>,
}
pub struct ServeMux {
entries: RwLock<Vec<MuxEntry>>,
}
impl ServeMux {
pub fn new() -> Self {
Self { entries: RwLock::new(Vec::new()) }
}
pub fn handle(&self, pattern: &str, handler: impl Handler + 'static) {
self.handle_arc(pattern, Arc::new(handler));
}
pub fn handle_func<F>(&self, pattern: &str, f: F)
where
F: Fn(&mut dyn ResponseWriter, &mut Request) + Send + Sync + 'static,
{
self.handle(pattern, handler_func(f));
}
fn handle_arc(&self, pattern: &str, handler: Arc<dyn Handler>) {
let parsed = parse_pattern(pattern);
let mut entries = self.entries.write().unwrap();
if let Some(e) = entries.iter_mut().find(|e| e.raw == pattern) {
e.handler = handler;
return;
}
entries.push(MuxEntry { raw: pattern.to_owned(), parsed, handler });
}
pub fn match_handler(&self, path: &str) -> Option<Arc<dyn Handler>> {
let entries = self.entries.read().unwrap();
let (h, _, _) = match_with_params(&entries, "", "", path);
h
}
}
impl Handler for ServeMux {
fn serve_http(&self, w: &mut dyn ResponseWriter, r: &mut Request) {
let entries = self.entries.read().unwrap();
let (handler, params, method_not_allowed) =
match_with_params(&entries, &r.method, &r.host, r.url.path());
drop(entries);
match handler {
Some(h) => {
r.path_params = params;
h.serve_http(w, r);
}
None if method_not_allowed => {
w.write_header(crate::status::METHOD_NOT_ALLOWED);
let _ = w.write(b"405 method not allowed\n");
}
None => not_found_handler().serve_http(w, r),
}
}
}
impl Default for ServeMux {
fn default() -> Self {
Self::new()
}
}
const KNOWN_METHODS: &[&str] = &[
"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE",
];
fn parse_pattern(raw: &str) -> ParsedPattern {
let mut s = raw;
let method = {
let upper = s.to_ascii_uppercase();
let mut found = None;
for &m in KNOWN_METHODS {
if upper.starts_with(m)
&& s[m.len()..].starts_with(' ')
{
found = Some(m.to_owned());
s = s[m.len() + 1..].trim_start();
break;
}
}
found
};
let host = if !s.starts_with('/') {
let slash = s.find('/').unwrap_or(s.len());
let h = s[..slash].to_owned();
s = if slash < s.len() { &s[slash..] } else { "/" };
Some(h)
} else {
None
};
let has_trailing_slash = s.len() > 1 && s.ends_with('/');
let path_body = if has_trailing_slash { &s[..s.len() - 1] } else { s };
let segments: Vec<Segment> = path_body
.split('/')
.filter(|p| !p.is_empty())
.map(parse_segment)
.collect();
ParsedPattern { method, host, segments, has_trailing_slash }
}
fn parse_segment(seg: &str) -> Segment {
if seg.starts_with('{') && seg.ends_with('}') {
let inner = &seg[1..seg.len() - 1];
if let Some(name) = inner.strip_suffix("...") {
return Segment::Tail { name: name.to_owned() };
}
return Segment::Wildcard { name: inner.to_owned() };
}
Segment::Literal(seg.to_owned())
}
fn specificity(p: &ParsedPattern) -> i64 {
let mut score: i64 = 0;
if p.method.is_some() { score += 10_000_000; }
if p.host.is_some() { score += 1_000_000; }
for seg in &p.segments {
score += match seg {
Segment::Literal(_) => 10_000,
Segment::Wildcard { .. } => 1_000,
Segment::Tail { .. } => 100,
};
}
let has_tail = p.segments.iter().any(|s| matches!(s, Segment::Tail { .. }));
if !p.has_trailing_slash && !has_tail { score += 1; }
score
}
fn try_match_path(
segments: &[Segment],
parts: &[&str],
has_trailing_slash: bool,
) -> Option<HashMap<String, String>> {
let mut params = HashMap::new();
for (i, seg) in segments.iter().enumerate() {
match seg {
Segment::Tail { name } => {
let tail = parts[i..].join("/");
if !name.is_empty() {
params.insert(name.clone(), tail);
}
return Some(params);
}
Segment::Wildcard { name } => {
let part = parts.get(i)?;
if !name.is_empty() {
params.insert(name.clone(), (*part).to_owned());
}
}
Segment::Literal(lit) => {
if parts.get(i)? != lit { return None; }
}
}
}
if parts.len() == segments.len() {
return Some(params);
}
if has_trailing_slash && parts.len() > segments.len() {
return Some(params);
}
None
}
fn match_with_params(
entries: &[MuxEntry],
method: &str,
host: &str,
path: &str,
) -> (Option<Arc<dyn Handler>>, HashMap<String, String>, bool) {
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let mut best_handler: Option<Arc<dyn Handler>> = None;
let mut best_params = HashMap::new();
let mut best_score: i64 = i64::MIN;
let mut method_not_allowed = false;
for entry in entries {
let pat = &entry.parsed;
let Some(params) = try_match_path(&pat.segments, &parts, pat.has_trailing_slash)
else { continue };
if !host.is_empty()
&& let Some(ref ph) = pat.host
&& !host.eq_ignore_ascii_case(ph)
{
continue;
}
if !method.is_empty()
&& let Some(ref pm) = pat.method
&& !method.eq_ignore_ascii_case(pm)
{
method_not_allowed = true;
continue;
}
let score = specificity(pat);
if score > best_score {
best_score = score;
best_handler = Some(Arc::clone(&entry.handler));
best_params = params;
}
}
let mna = method_not_allowed && best_handler.is_none();
(best_handler, best_params, mna)
}
use std::sync::OnceLock;
static DEFAULT_SERVE_MUX: OnceLock<Arc<ServeMux>> = OnceLock::new();
fn default_mux() -> &'static Arc<ServeMux> {
DEFAULT_SERVE_MUX.get_or_init(|| Arc::new(ServeMux::new()))
}
pub fn handle(pattern: &str, handler: impl Handler + 'static) {
default_mux().handle(pattern, handler);
}
pub fn handle_func<F>(pattern: &str, f: F)
where
F: Fn(&mut dyn ResponseWriter, &mut Request) + Send + Sync + 'static,
{
default_mux().handle_func(pattern, f);
}
pub fn default_serve_mux() -> Arc<ServeMux> {
Arc::clone(default_mux())
}
pub fn not_found_handler() -> impl Handler {
handler_func(|w, _r| {
w.write_header(crate::status::NOT_FOUND);
let _ = w.write(b"404 page not found\n");
})
}
pub fn strip_prefix(prefix: String, handler: impl Handler + 'static) -> impl Handler {
let handler = Arc::new(handler);
handler_func(move |w, r| {
let path = r.url.path();
match path.strip_prefix(prefix.as_str()) {
None => not_found_handler().serve_http(w, r),
Some(stripped) => {
let mut new_url = r.url.clone();
new_url.set_path(if stripped.is_empty() { "/" } else { stripped });
match rebuild_request(r, new_url) {
Err(_) => crate::util::error(w, "internal error", 500),
Ok(mut req) => handler.serve_http(w, &mut req),
}
}
}
})
}
pub fn file_server(root: String) -> impl Handler {
handler_func(move |w, r| {
use std::io::Read;
use std::path::Path;
let url_path = r.url.path();
let rel = url_path.trim_start_matches('/');
let fs_path = if rel.is_empty() {
Path::new(&root).to_path_buf()
} else {
Path::new(&root).join(rel)
};
let root_canon = match std::fs::canonicalize(&root) {
Ok(p) => p,
Err(_) => {
crate::util::error(w, "500 Internal Server Error", crate::status::INTERNAL_SERVER_ERROR);
return;
}
};
let candidate_canon = std::fs::canonicalize(&fs_path)
.or_else(|_| std::fs::canonicalize(fs_path.parent().unwrap_or(&fs_path)))
.unwrap_or_else(|_| fs_path.clone());
if !candidate_canon.starts_with(&root_canon) {
crate::util::error(w, "403 Forbidden", crate::status::FORBIDDEN);
return;
}
match fs_path.metadata() {
Err(_) => {
crate::util::error(w, "404 Not Found", crate::status::NOT_FOUND);
return;
}
Ok(meta) if meta.is_dir() => {
crate::util::error(w, "403 Forbidden", crate::status::FORBIDDEN);
return;
}
Ok(_) => {}
}
let ct = {
let mut probe = [0u8; 512];
let n = std::fs::File::open(&fs_path)
.and_then(|mut f| f.read(&mut probe))
.unwrap_or(0);
crate::mime::detect_content_type(&probe[..n]).to_owned()
};
match std::fs::read(&fs_path) {
Err(_) => crate::util::error(w, "500 Internal Server Error", crate::status::INTERNAL_SERVER_ERROR),
Ok(data) => {
w.header().set("Content-Type", &ct);
w.header().set("Content-Length", data.len().to_string());
w.write_header(crate::status::OK);
let _ = w.write(&data);
}
}
})
}
pub fn timeout_handler(
handler: impl Handler + 'static,
timeout: Duration,
body: &'static str,
) -> impl Handler {
let handler = Arc::new(handler);
handler_func(move |w, r| {
use go_lib::chan::chan;
use go_lib::context::with_timeout;
let (done_tx, done_rx) = chan::<BodyCapture>(1);
let inner_handler = Arc::clone(&handler);
let req_url = r.url.clone();
let method = r.method.clone();
let req_header = r.header.clone();
let host = r.host.clone();
let remote = r.remote_addr.clone();
let (ctx, cancel) = with_timeout(&go_lib::context::background(), timeout);
go_lib::go!(move || {
let mut inner_req = match Request::new(&method, req_url.as_str(), None) {
Ok(r) => r,
Err(_) => { done_tx.send(BodyCapture::default()); return; }
};
inner_req.header = req_header;
inner_req.host = host;
inner_req.remote_addr = remote;
let mut capture = BodyCapture::default();
inner_handler.serve_http(&mut capture, &mut inner_req);
done_tx.send(capture);
});
go_lib::select! {
recv(ctx.done()) -> _v => {
cancel.cancel();
w.write_header(crate::status::SERVICE_UNAVAILABLE);
let _ = w.write(body.as_bytes());
}
recv(done_rx) -> result => {
cancel.cancel();
if let Some(capture) = result {
for (name, values) in capture.header.iter() {
for val in values {
w.header().add(name, val.as_str());
}
}
let status = if capture.status == 0 { 200 } else { capture.status };
w.write_header(status);
let _ = w.write(&capture.body);
}
}
}
})
}
#[derive(Default)]
struct BodyCapture {
status: u16,
header: crate::header::Header,
body: Vec<u8>,
}
impl ResponseWriter for BodyCapture {
fn header(&mut self) -> &mut crate::header::Header { &mut self.header }
fn write(&mut self, buf: &[u8]) -> Result<usize, crate::error::HttpError> {
self.body.extend_from_slice(buf);
Ok(buf.len())
}
fn write_header(&mut self, code: u16) {
if self.status == 0 { self.status = code; }
}
}
unsafe impl Send for BodyCapture {}
fn rebuild_request(r: &Request, new_url: url::Url) -> Result<Request, HttpError> {
let mut req = Request::new_with_context(
&r.method,
new_url.as_str(),
None, r.context().clone(),
)?;
req.proto = r.proto.clone();
req.proto_major = r.proto_major;
req.proto_minor = r.proto_minor;
req.header = r.header.clone();
req.host = r.host.clone();
req.content_length = r.content_length;
req.transfer_encoding = r.transfer_encoding.clone();
req.remote_addr = r.remote_addr.clone();
req.trailer = r.trailer.clone();
req.path_params = r.path_params.clone();
Ok(req)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::response::ConnResponseWriter;
use crate::request::Request;
fn dummy_request(path: &str) -> Request {
Request::new("GET", &format!("http://example.com{path}"), None).unwrap()
}
struct RecordingWriter {
inner: ConnResponseWriter<Vec<u8>>,
}
impl RecordingWriter {
fn new() -> Self { Self { inner: ConnResponseWriter::new(Vec::new()) } }
fn bytes(mut self) -> Vec<u8> {
let _ = self.inner.finish();
self.inner.inner
}
}
impl ResponseWriter for RecordingWriter {
fn header(&mut self) -> &mut crate::header::Header { self.inner.header() }
fn write(&mut self, buf: &[u8]) -> Result<usize, crate::error::HttpError> { self.inner.write(buf) }
fn write_header(&mut self, code: u16) { self.inner.write_header(code) }
}
#[test]
fn exact_match() {
let mux = ServeMux::new();
mux.handle_func("/hello", |w, _| { let _ = w.write(b"hi"); });
let mut r = dummy_request("/hello");
let mut w = RecordingWriter::new();
mux.serve_http(&mut w, &mut r);
let out = w.bytes();
assert!(out.windows(2).any(|w| w == b"hi"), "body should contain 'hi'");
}
#[test]
fn prefix_match() {
let mux = ServeMux::new();
mux.handle_func("/static/", |w, _| { let _ = w.write(b"file"); });
let mut r = dummy_request("/static/foo.js");
let mut w = RecordingWriter::new();
mux.serve_http(&mut w, &mut r);
let out = w.bytes();
assert!(out.windows(4).any(|s| s == b"file"));
}
#[test]
fn not_found_fallback() {
let mux = ServeMux::new();
let mut r = dummy_request("/nowhere");
let mut w = RecordingWriter::new();
mux.serve_http(&mut w, &mut r);
let out = String::from_utf8(w.bytes()).unwrap();
assert!(out.contains("404"));
}
#[test]
fn longer_prefix_wins() {
let mux = ServeMux::new();
mux.handle_func("/api/", |w, _| { let _ = w.write(b"short"); });
mux.handle_func("/api/v2/", |w, _| { let _ = w.write(b"long"); });
let mut r = dummy_request("/api/v2/users");
let mut w = RecordingWriter::new();
mux.serve_http(&mut w, &mut r);
let out = w.bytes();
assert!(out.windows(4).any(|s| s == b"long"));
}
#[test]
fn strip_prefix_rewrites_path() {
let inner = handler_func(|w, r| {
let _ = w.write(r.url.path().as_bytes());
});
let h = strip_prefix("/api".to_owned(), inner);
let mut r = dummy_request("/api/users");
let mut w = RecordingWriter::new();
h.serve_http(&mut w, &mut r);
let body = String::from_utf8(w.bytes()).unwrap();
assert!(body.contains("/users"), "stripped path should be /users, got: {body:?}");
}
#[test]
fn strip_prefix_no_match_returns_404() {
let inner = handler_func(|w, _| { let _ = w.write(b"ok"); });
let h = strip_prefix("/api".to_owned(), inner);
let mut r = dummy_request("/other/path");
let mut w = RecordingWriter::new();
h.serve_http(&mut w, &mut r);
let out = String::from_utf8(w.bytes()).unwrap();
assert!(out.contains("404"));
}
#[test]
fn file_server_serves_existing_file() {
let dir = std::env::temp_dir();
let path = dir.join("go_http_test_file.txt");
std::fs::write(&path, b"hello file").unwrap();
let h = file_server(dir.to_str().unwrap().to_owned());
let mut r = dummy_request("/go_http_test_file.txt");
let mut w = RecordingWriter::new();
h.serve_http(&mut w, &mut r);
let out = w.bytes();
assert!(out.windows(10).any(|s| s == b"hello file"), "file content not found");
let _ = std::fs::remove_file(path);
}
#[test]
fn file_server_missing_file_returns_404() {
let dir = std::env::temp_dir();
let h = file_server(dir.to_str().unwrap().to_owned());
let mut r = dummy_request("/this_file_does_not_exist_xyz.bin");
let mut w = RecordingWriter::new();
h.serve_http(&mut w, &mut r);
let out = String::from_utf8(w.bytes()).unwrap();
assert!(out.contains("404"), "expected 404 in response, got: {}", out);
}
#[test]
fn file_server_rejects_path_traversal() {
let root = std::env::temp_dir().join("go_http_test_root");
std::fs::create_dir_all(&root).unwrap();
let outside = std::env::temp_dir().join("go_http_outside.txt");
std::fs::write(&outside, b"secret").unwrap();
let h = file_server(root.to_str().unwrap().to_owned());
let mut r = dummy_request("/../go_http_outside.txt");
let mut w = RecordingWriter::new();
h.serve_http(&mut w, &mut r);
let out = String::from_utf8(w.bytes()).unwrap();
let _ = std::fs::remove_file(outside);
let _ = std::fs::remove_dir(root);
assert!(
out.contains("403") || out.contains("404"),
"expected 403 or 404, got: {out:?}"
);
assert!(!out.contains("secret"), "traversal should not expose file content");
}
}