1mod auth;
2mod indexing;
3mod ranges;
4use std::{
5 borrow::Cow,
6 collections::HashMap,
7 fs::{self, File, OpenOptions},
8 io::{self, BufReader, Read, Seek, SeekFrom, Write, stdout},
9 ops::Range,
10 path::Path,
11 sync::Mutex,
12};
13
14pub use auth::AuthConfig;
15use http::HttpMethod;
16use mime::Mime;
17
18use self::{indexing::index_of, ranges::get_range_for};
19use crate::{
20 Result,
21 log::{self, LogLevel},
22 request::HttpRequest,
23};
24
25pub trait Interceptor: Fn(&mut HttpRequest) + Send + Sync + 'static {}
38impl<T> Interceptor for T where T: Fn(&mut HttpRequest) + Send + Sync + 'static {}
39
40#[derive(Default)]
41struct HandlerMethodAssoc {
42 exact: HashMap<Box<str>, Box<dyn RequestHandler>>,
43 #[cfg(feature = "regex")]
44 regex: Vec<(regexpr::Regex, Box<dyn RequestHandler>)>,
45 def: Option<Box<dyn RequestHandler>>,
46}
47
48pub trait RequestHandler: Send + Sync + 'static {
49 fn handle(&self, req: &mut HttpRequest) -> Result<()>;
54}
55
56impl<T> RequestHandler for T
57where
58 T: Fn(&mut HttpRequest) -> Result<()> + Send + Sync + 'static,
59{
60 fn handle(&self, req: &mut HttpRequest) -> Result<()> {
61 self(req)
62 }
63}
64
65enum UrlMatcherInner {
66 Literal(Box<str>),
67 #[cfg(feature = "regex")]
68 Regex(regexpr::Regex),
69}
70
71pub struct UrlMatcher(UrlMatcherInner);
72
73impl UrlMatcher {
74 #[cfg(feature = "regex")]
75 pub fn regex(src: &str) -> Result<Self> {
76 let regex = regexpr::Regex::compile(src).map_err(|err| err.to_string())?;
77 Ok(UrlMatcher(UrlMatcherInner::Regex(regex)))
78 }
79 #[must_use]
80 pub fn literal(src: impl Into<Box<str>>) -> Self {
81 UrlMatcher(UrlMatcherInner::Literal(src.into()))
82 }
83}
84
85impl<T> From<T> for UrlMatcher
86where
87 T: Into<Box<str>>,
88{
89 fn from(value: T) -> Self {
90 Self::literal(value)
91 }
92}
93
94pub struct Handler {
113 handlers: HashMap<HttpMethod, HandlerMethodAssoc>,
114 pre_interceptors: Vec<Box<dyn Interceptor>>,
115 post_interceptors: Vec<Box<dyn Interceptor>>,
116}
117
118impl Handler {
119 #[must_use]
120 pub fn new() -> Self {
121 Self {
122 handlers: HashMap::new(),
123 pre_interceptors: Vec::new(),
124 post_interceptors: Vec::new(),
125 }
126 }
127 #[inline]
129 pub fn get(&mut self, url: impl Into<UrlMatcher>, f: impl RequestHandler) {
130 self.add(HttpMethod::GET, url, f);
131 }
132 #[inline]
134 pub fn post(&mut self, url: impl Into<UrlMatcher>, f: impl RequestHandler) {
135 self.add(HttpMethod::POST, url, f);
136 }
137 #[inline]
139 pub fn delete(&mut self, url: impl Into<UrlMatcher>, f: impl RequestHandler) {
140 self.add(HttpMethod::DELETE, url, f);
141 }
142 #[inline]
144 pub fn head(&mut self, url: impl Into<UrlMatcher>, f: impl RequestHandler) {
145 self.add(HttpMethod::HEAD, url, f);
146 }
147 pub fn add(&mut self, method: HttpMethod, url: impl Into<UrlMatcher>, f: impl RequestHandler) {
154 let map = self.handlers.entry(method).or_default();
155 match url.into().0 {
156 UrlMatcherInner::Literal(lit) => {
157 map.exact.insert(lit, Box::new(f));
158 }
159 #[cfg(feature = "regex")]
160 UrlMatcherInner::Regex(regex) => {
161 map.regex.push((regex, Box::new(f)));
162 }
163 }
164 }
165 #[inline]
171 pub fn add_default(&mut self, method: HttpMethod, f: impl RequestHandler) {
172 self.handlers.entry(method).or_default().def = Some(Box::new(f));
173 }
174 #[inline]
176 pub fn pre_interceptor(&mut self, f: impl Interceptor) {
177 self.pre_interceptors.push(Box::new(f));
178 }
179 #[inline]
181 pub fn post_interceptor(&mut self, f: impl Interceptor) {
182 self.post_interceptors.push(Box::new(f));
183 }
184 #[must_use]
186 pub fn get_handler(&self, method: &HttpMethod, url: &str) -> Option<&dyn RequestHandler> {
187 let handler_table = self.handlers.get(method)?;
188
189 let handler = handler_table.exact.get(url);
190
191 #[cfg(feature = "regex")]
192 let handler = handler.or_else(|| {
193 handler_table
194 .regex
195 .iter()
196 .find(|(r, _)| r.test(url))
197 .map(|(_, f)| f)
198 });
199
200 handler.or(handler_table.def.as_ref()).map(|b| &**b)
201 }
202 pub fn handle(&self, req: &mut HttpRequest) -> Result<()> {
205 self.pre_interceptors.iter().for_each(|f| f(req));
206 let result = match self.get_handler(req.method(), req.url()) {
207 Some(handler) => handler.handle(req).or_else(|err| {
208 eprintln!("ERROR: {err}");
209 req.server_error()
210 }),
211 None => req.forbidden(),
212 };
213 self.post_interceptors.iter().for_each(|f| f(req));
214 result
215 }
216}
217
218impl Default for Handler {
219 fn default() -> Self {
238 let mut handler = Self::new();
239 handler.pre_interceptor(suffix_html);
240 handler.pre_interceptor(|req| {
241 req.set_header("Accept-Ranges", "bytes");
242 });
243
244 handler.add_default(HttpMethod::GET, cat_handler);
245 handler.add_default(HttpMethod::POST, post_handler);
246 handler.add_default(HttpMethod::DELETE, delete_handler);
247 handler.add_default(HttpMethod::HEAD, head_handler);
248
249 handler.get("/", root_handler);
250 handler.head("/", root_handler);
251
252 if log::get_level() >= LogLevel::Info {
253 handler.post_interceptor(log_stdout);
254 }
255 handler
256 }
257}
258
259fn head_headers(req: &mut HttpRequest) -> Result<Option<Range<u64>>> {
260 let filename = req.filename()?;
261 if dir_exists(&filename) {
262 req.set_header("Content-Type", "text/html");
263 return Ok(None);
264 }
265 match File::open(&*filename) {
266 Ok(file) => {
267 if let Ok(mime) = Mime::from_filename(&filename) {
268 req.set_header("Content-Type", mime.to_string());
269 }
270 let metadata = file.metadata()?;
271 let len = metadata.len();
272 if metadata.is_file() {
273 req.set_header("Content-Length", len.to_string());
274 }
275 let Some(range) = req.header("Range") else {
276 return Ok(None);
277 };
278 let range = get_range_for(range, len)?;
279 if range.end > len || range.end <= range.start {
280 req.set_status(416);
281 } else {
282 req.set_status(206);
283 req.set_header("Content-Length", (range.end - range.start).to_string());
284 req.set_header(
285 "Content-Range",
286 format!("bytes {}-{}/{}", range.start, range.end - 1, len),
287 );
288 }
289 return Ok(Some(range));
290 }
291 Err(err) => {
292 let status = match err.kind() {
293 io::ErrorKind::PermissionDenied => 403,
294 _ => 404,
295 };
296 req.set_status(status);
297 }
298 }
299 Ok(None)
300}
301
302#[inline]
303fn show_hidden(req: &HttpRequest) -> bool {
304 match req.param("hidden") {
305 Some(s) => s != "false",
306 None => true,
307 }
308}
309
310pub fn head_handler(req: &mut HttpRequest) -> Result<()> {
313 head_headers(req)?;
314 let filename = req.filename()?;
315 let len = if req.is_http_err() {
316 req.error_page().len()
317 } else if dir_exists(&filename) {
318 index_of(&filename, show_hidden(req))?.len()
319 } else {
320 0
321 };
322
323 if len > 0 {
324 req.set_header("Content-Length", len.to_string());
325 }
326 req.respond()
327}
328
329pub fn cat_handler(req: &mut HttpRequest) -> Result<()> {
334 let range = head_headers(req)?;
335 if req.is_http_err() {
336 return req.respond_error_page();
337 }
338 let filename = req.filename()?;
339 if dir_exists(&filename) {
340 let page = index_of(&filename, show_hidden(req))?;
341 return req.respond_str(&page);
342 }
343 let mut file = File::open(&*req.filename()?)?;
344 if let Some(range) = range {
345 file.seek(SeekFrom::Start(range.start))?;
346 let mut reader = BufReader::new(file).take(range.end - range.start);
347 req.respond_reader(&mut reader)
348 } else {
349 let mut reader = BufReader::new(file);
350 req.respond_reader(&mut reader)
351 }
352}
353
354pub fn post_handler(req: &mut HttpRequest) -> Result<()> {
359 let filename = req.filename()?;
360 match File::create(&*filename) {
361 Ok(mut file) => {
362 req.read_body(&mut file)?;
363 req.ok()
364 }
365 Err(err) => {
366 println!("Error opening {}: {err}", &filename);
367 match err.kind() {
368 io::ErrorKind::PermissionDenied => req.forbidden(),
369 _ => req.not_found(),
370 }
371 }
372 }
373}
374
375pub fn delete_handler(req: &mut HttpRequest) -> Result<()> {
380 match fs::remove_file(&*req.filename()?) {
381 Ok(()) => req.ok(),
382 Err(err) => match err.kind() {
383 io::ErrorKind::PermissionDenied => req.forbidden(),
384 _ => req.not_found(),
385 },
386 }
387}
388
389#[inline]
390fn file_exists(filename: &str) -> bool {
391 Path::new(filename).is_file()
392}
393
394#[inline]
395fn dir_exists(filename: &str) -> bool {
396 Path::new(filename).is_dir()
397}
398
399pub fn suffix_html(req: &mut HttpRequest) {
405 if file_exists(&req.url()[1..]) {
406 return;
407 }
408 for suffix in [".html", ".php"] {
409 let mut filename = req.url().to_owned();
410 filename.push_str(suffix);
411 if file_exists(&filename[1..]) {
412 req.set_url(filename);
413 break;
414 }
415 }
416}
417
418macro_rules! log {
419 ($w:expr , $req:expr) => {
420 writeln!(
421 $w,
422 "{} {} {} {}",
423 $req.method(),
424 $req.url(),
425 $req.status(),
426 $req.status_msg()
427 )
428 .unwrap();
429 };
430}
431
432pub fn log_stdout(req: &mut HttpRequest) {
434 log!(&mut stdout(), req);
435}
436
437#[allow(clippy::missing_panics_doc)]
444pub fn log_file(filename: &str) -> crate::Result<Box<dyn Interceptor>> {
445 let file = OpenOptions::new()
446 .append(true)
447 .create(true)
448 .open(filename)
449 .map_err(|err| Cow::Owned(format!("Error creating file: {filename}: {err}")))?;
450 let file = Mutex::new(file);
451 Ok(Box::new(move |req: &mut HttpRequest| {
452 #[allow(clippy::unwrap_used)]
453 let mut file = file.lock().unwrap();
454 log!(&mut *file, req);
455 }))
456}
457
458pub fn root_handler(req: &mut HttpRequest) -> Result<()> {
463 if file_exists("index.html") {
464 req.set_url("/index.html");
465 }
466 cat_handler(req)
467}
468
469pub fn redirect(uri: impl Into<Box<str>>) -> impl RequestHandler {
470 let uri = uri.into();
471 move |req: &mut HttpRequest| {
472 req.set_header("Location", &*uri);
473 req.set_header("Content-Length", "0");
474 req.set_status(308).respond()
475 }
476}