http_srv/request/handler/
mod.rsmod indexing;
mod ranges;
mod auth;
pub use auth::AuthConfig;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::{self, File, OpenOptions};
use std::io::{self, stdout, BufReader, Read, Seek, SeekFrom, Write};
use std::ops::Range;
use std::path::Path;
use std::sync::Mutex;
use crate::request::{HttpRequest, HttpMethod};
use crate::Result;
use self::indexing::index_of;
use self::ranges::get_range_for;
use mime::Mime;
pub trait Interceptor: Fn(&mut HttpRequest) + Send + Sync + 'static { }
impl<T> Interceptor for T
where T: Fn(&mut HttpRequest) + Send + Sync + 'static { }
pub type HandlerTable = HashMap<HttpMethod,HashMap<String,Box<dyn RequestHandler>>>;
pub trait RequestHandler: Send + Sync + 'static {
fn handle(&self, req: &mut HttpRequest) -> Result<()>;
}
impl <T> RequestHandler for T
where T: Fn(&mut HttpRequest) -> Result<()> + Send + Sync + 'static
{
fn handle(&self, req: &mut HttpRequest) -> Result<()> {
self(req)
}
}
pub struct Handler {
handlers: HandlerTable,
defaults: HashMap<HttpMethod,Box<dyn RequestHandler>>,
pre_interceptors: Vec<Box<dyn Interceptor>>,
post_interceptors: Vec<Box<dyn Interceptor>>,
}
impl Handler {
#[must_use]
pub fn new() -> Self {
Self { handlers: HashMap::new(), defaults: HashMap::new(),
pre_interceptors: Vec::new(), post_interceptors: Vec::new() }
}
#[inline]
pub fn get(&mut self, url: &str, f: impl RequestHandler) {
self.add(HttpMethod::GET,url,f);
}
#[inline]
pub fn post(&mut self, url: &str, f: impl RequestHandler) {
self.add(HttpMethod::POST,url,f);
}
#[inline]
pub fn delete(&mut self, url: &str, f: impl RequestHandler) {
self.add(HttpMethod::DELETE,url,f);
}
#[inline]
pub fn head(&mut self, url: &str, f: impl RequestHandler) {
self.add(HttpMethod::HEAD,url,f);
}
pub fn add(&mut self, method: HttpMethod, url: &str, f: impl RequestHandler) {
let map = self.handlers.entry(method).or_default();
map.insert(url.to_string(), Box::new(f));
}
#[inline]
pub fn add_default(&mut self, method: HttpMethod, f: impl RequestHandler) {
self.defaults.insert(method, Box::new(f));
}
#[inline]
pub fn pre_interceptor(&mut self, f: impl Interceptor) {
self.pre_interceptors.push(Box::new(f));
}
#[inline]
pub fn post_interceptor(&mut self, f: impl Interceptor) {
self.post_interceptors.push(Box::new(f));
}
#[must_use]
pub fn get_handler(&self, method: &HttpMethod, url: &str) -> Option<&dyn RequestHandler> {
match self.handlers.get(method) {
Some(map) => map.get(url).or_else(|| self.defaults.get(method)),
None => self.defaults.get(method),
}.map(|b| &**b)
}
pub fn handle(&self, req: &mut HttpRequest) -> Result<()> {
self.pre_interceptors.iter().for_each(|f| f(req));
let result = match self.get_handler(req.method(), req.url()) {
Some(handler) => handler.handle(req).or_else(|err| {
eprintln!("ERROR: {err}");
req.server_error()
}),
None => req.forbidden(),
};
self.post_interceptors.iter().for_each(|f| f(req));
result
}
}
impl Default for Handler {
fn default() -> Self {
let mut handler = Self::new();
handler.pre_interceptor(suffix_html);
handler.pre_interceptor(|req| {
req.set_header("Accept-Ranges", "bytes");
});
handler.add_default(HttpMethod::GET, cat_handler);
handler.add_default(HttpMethod::POST, post_handler);
handler.add_default(HttpMethod::DELETE, delete_handler);
handler.add_default(HttpMethod::HEAD, head_handler);
handler.get("/", root_handler);
handler.head("/", root_handler);
handler.post_interceptor(log_stdout);
handler
}
}
fn head_headers(req: &mut HttpRequest) -> Result<Option<Range<u64>>> {
let filename = req.filename()?;
if dir_exists(&filename) {
req.set_header("Content-Type", "text/html");
return Ok(None);
}
match File::open(&*filename) {
Ok(file) => {
if let Ok(mime) = Mime::from_filename(&filename) {
req.set_header("Content-Type", mime.to_string());
}
let metadata = file.metadata()?;
let len = metadata.len();
if metadata.is_file() {
req.set_header("Content-Length", len.to_string());
}
let Some(range) = req.header("Range") else { return Ok(None); };
let range = get_range_for(range, len)?;
if range.end > len || range.end <= range.start {
req.set_status(416);
} else {
req.set_status(206);
req.set_header("Content-Length", (range.end - range.start).to_string());
req.set_header("Content-Range", format!("bytes {}-{}/{}", range.start, range.end - 1, len));
}
return Ok(Some(range));
},
Err(err) => {
let status = match err.kind() {
io::ErrorKind::PermissionDenied => 403,
_ => 404,
};
req.set_status(status);
}
};
Ok(None)
}
#[inline]
fn show_hidden(req: &HttpRequest) -> bool {
match req.param("hidden") {
Some(s) => s != "false",
None => true,
}
}
pub fn head_handler(req: &mut HttpRequest) -> Result<()> {
head_headers(req)?;
let filename = req.filename()?;
let len =
if req.is_http_err() {
req.error_page().len()
} else if dir_exists(&filename) {
index_of(&filename, show_hidden(req))?.len()
} else { 0 };
if len > 0 {
req.set_header("Content-Length", len.to_string());
}
req.respond()
}
pub fn cat_handler(req: &mut HttpRequest) -> Result<()> {
let range = head_headers(req)?;
if req.is_http_err() {
return req.respond_error_page();
};
let filename = req.filename()?;
if dir_exists(&filename) {
let page = index_of(&filename, show_hidden(req))?;
return req.respond_str(&page);
}
let mut file = File::open(&*req.filename()?)?;
if let Some(range) = range {
file.seek(SeekFrom::Start(range.start))?;
let mut reader = BufReader::new(file)
.take(range.end - range.start);
req.respond_reader(&mut reader)
} else {
let mut reader = BufReader::new(file);
req.respond_reader(&mut reader)
}
}
pub fn post_handler(req: &mut HttpRequest) -> Result<()> {
let filename = req.filename()?;
match File::create(&*filename) {
Ok(mut file) => {
req.read_body(&mut file)?;
req.ok()
},
Err(err) => {
println!("Error opening {}: {err}", &filename);
match err.kind() {
io::ErrorKind::PermissionDenied => req.forbidden(),
_ => req.not_found(),
}
}
}
}
pub fn delete_handler(req: &mut HttpRequest) -> Result<()> {
match fs::remove_file(&*req.filename()?) {
Ok(()) => req.ok(),
Err(err) =>
match err.kind() {
io::ErrorKind::PermissionDenied => req.forbidden(),
_ => req.not_found(),
}
}
}
#[inline]
fn file_exists(filename: &str) -> bool {
Path::new(filename).is_file()
}
#[inline]
fn dir_exists(filename: &str) -> bool {
Path::new(filename).is_dir()
}
pub fn suffix_html(req: &mut HttpRequest) {
if file_exists(&req.url()[1..]) { return; }
for suffix in [".html",".php"] {
let mut filename = req.url().to_owned();
filename.push_str(suffix);
if file_exists(&filename[1..]) {
req.set_url(filename);
break;
}
}
}
macro_rules! log {
($w:expr , $req:expr) => {
writeln!($w, "{} {} {} {}", $req.method(), $req.url(), $req.status(), $req.status_msg()).unwrap();
};
}
pub fn log_stdout(req: &mut HttpRequest) {
log!(&mut stdout(), req);
}
#[allow(clippy::missing_panics_doc)]
pub fn log_file(filename: &str) -> crate::Result<Box<dyn Interceptor>> {
let file = OpenOptions::new()
.append(true)
.create(true)
.open(filename)
.map_err(|err| Cow::Owned(format!("Error creating file: {filename}: {err}")))?;
let file = Mutex::new(file);
Ok(
Box::new(
move |req: &mut HttpRequest| {
#[allow(clippy::unwrap_used)]
let mut file = file.lock().unwrap();
log!(&mut *file, req);
}
)
)
}
pub fn root_handler(req: &mut HttpRequest) -> Result<()> {
if file_exists("index.html") {
req.set_url("/index.html");
}
cat_handler(req)
}
pub fn redirect(uri: impl Into<Box<str>>) -> impl RequestHandler {
let uri = uri.into();
move |req: &mut HttpRequest| {
req.set_header("Location", &*uri);
req.set_header("Content-Length", "0");
req.set_status(308).respond()
}
}