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: &Request);
}
pub struct HandlerFunc(pub Box<dyn Fn(&mut dyn ResponseWriter, &Request) + Send + Sync>);
impl Handler for HandlerFunc {
fn serve_http(&self, w: &mut dyn ResponseWriter, r: &Request) {
(self.0)(w, r)
}
}
impl<H: Handler> Handler for Arc<H> {
fn serve_http(&self, w: &mut dyn ResponseWriter, r: &Request) {
(**self).serve_http(w, r)
}
}
pub fn handler_func<F>(f: F) -> HandlerFunc
where
F: Fn(&mut dyn ResponseWriter, &Request) + Send + Sync + 'static,
{
HandlerFunc(Box::new(f))
}
struct MuxEntry {
handler: Arc<dyn Handler>,
pattern: String,
}
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, &Request) + Send + Sync + 'static,
{
self.handle(pattern, handler_func(f));
}
fn handle_arc(&self, pattern: &str, handler: Arc<dyn Handler>) {
let mut entries = self.entries.write().unwrap();
if let Some(e) = entries.iter_mut().find(|e| e.pattern == pattern) {
e.handler = handler;
return;
}
entries.push(MuxEntry { handler, pattern: pattern.to_owned() });
}
pub fn match_handler(&self, path: &str) -> Option<Arc<dyn Handler>> {
let entries = self.entries.read().unwrap();
let mut best_len = 0usize;
let mut best: Option<Arc<dyn Handler>> = None;
for entry in entries.iter() {
let pat = entry.pattern.as_str();
if pat.ends_with('/') {
if path.starts_with(pat) && pat.len() > best_len {
best_len = pat.len();
best = Some(Arc::clone(&entry.handler));
}
} else {
if path == pat {
return Some(Arc::clone(&entry.handler));
}
}
}
best
}
}
impl Handler for ServeMux {
fn serve_http(&self, w: &mut dyn ResponseWriter, r: &Request) {
let path = r.url.path();
match self.match_handler(path) {
Some(h) => h.serve_http(w, r),
None => not_found_handler().serve_http(w, r),
}
}
}
impl Default for ServeMux {
fn default() -> Self {
Self::new()
}
}
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, &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(req) => handler.serve_http(w, &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, &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();
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 r = dummy_request("/hello");
let mut w = RecordingWriter::new();
mux.serve_http(&mut w, &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 r = dummy_request("/static/foo.js");
let mut w = RecordingWriter::new();
mux.serve_http(&mut w, &r);
let out = w.bytes();
assert!(out.windows(4).any(|s| s == b"file"));
}
#[test]
fn not_found_fallback() {
let mux = ServeMux::new();
let r = dummy_request("/nowhere");
let mut w = RecordingWriter::new();
mux.serve_http(&mut w, &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 r = dummy_request("/api/v2/users");
let mut w = RecordingWriter::new();
mux.serve_http(&mut w, &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 r = dummy_request("/api/users");
let mut w = RecordingWriter::new();
h.serve_http(&mut w, &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 r = dummy_request("/other/path");
let mut w = RecordingWriter::new();
h.serve_http(&mut w, &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 r = dummy_request("/go_http_test_file.txt");
let mut w = RecordingWriter::new();
h.serve_http(&mut w, &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 r = dummy_request("/this_file_does_not_exist_xyz.bin");
let mut w = RecordingWriter::new();
h.serve_http(&mut w, &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 r = dummy_request("/../go_http_outside.txt");
let mut w = RecordingWriter::new();
h.serve_http(&mut w, &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");
}
}