1use std::{fs, io::ErrorKind, path::PathBuf};
2
3use crate::{config::Config, http::{HttpMethod, StatusCode}, request::HttpRequest, response::HttpResponse, stats::Stats};
4
5#[derive(Default)]
7pub struct Router {
8 config: Config,
9 stats: Stats,
10}
11
12impl Router {
13
14 pub fn new(config: Config, stats: Stats) -> Self {
16 Self { config, stats }
17 }
18
19 pub fn handle_request(&mut self, request: &HttpRequest) -> HttpResponse {
21 let response: HttpResponse = match (request.method, request.path.as_str()) {
22 (_, "/stats") => {
23 self.stats.record(match self.stats.to_json() {
24 Ok(_) => StatusCode::Ok.code(),
25 Err(_) => StatusCode::InternalServerError.code()
26 });
27 self.get_head(&request.path, request.method == HttpMethod::HEAD)
28 }
29 (HttpMethod::GET | HttpMethod::HEAD, _) => {
30 let res = self.get_head(&request.path, request.method == HttpMethod::HEAD);
31 self.stats.record(res.status.code());
32 res
33 }
34 };
35
36 response
37 }
38
39 fn get_head(&self, path: &str, is_head: bool) -> HttpResponse {
41 if path == "/stats" {
42 match self.stats.to_json() {
43 Ok(json) => HttpResponse::ok("application/json").with_body(json.into_bytes()).build().unwrap(),
44 Err(_) => HttpResponse::internal_server_error().build().unwrap(),
45 }
46 } else {
47 self.get_static(path, is_head)
48 }
49 }
50
51 fn get_static(&self, path: &str, is_head: bool) -> HttpResponse {
53 let mut full_path = PathBuf::from(&self.config.root_folder);
54 full_path.push(path.trim_start_matches("/"));
55
56 if full_path.is_dir() {
57 full_path.push(&self.config.index_file);
58 }
59
60 match fs::read(&full_path) {
61 Ok(content) => {
62 let extension = full_path
63 .extension()
64 .and_then(|e| e.to_str())
65 .unwrap_or("");
66 let content_type = self.config.mime_type(extension);
67 if is_head {
68 HttpResponse::builder()
69 .with_status(StatusCode::Ok)
70 .with_content_type(content_type)
71 .with_header("Content-Length", &format!("{}", content.len()))
72 .with_body(vec![])
73 .build().unwrap()
74 } else {
75 HttpResponse::ok(content_type).with_body(content).build().unwrap()
76 }
77 },
78 Err(e) if e.kind() == ErrorKind::PermissionDenied => {
79 HttpResponse::builder().with_status(StatusCode::Forbidden).build().unwrap()
80 },
81 Err(_) => {
82 let p = PathBuf::from(&self.config.root_folder).join(&self.config.not_found_file);
83 match fs::read(&p) {
84 Ok(body) => HttpResponse::not_found().with_body(body).build().unwrap(),
85 Err(_) => HttpResponse::not_found().build().unwrap()
86 }
87 }
88 }
89 }
90
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::config::Config;
97 use crate::http::HttpVersion;
98 use crate::stats::Stats;
99
100 fn mock_config() -> Config {
101 Config {
102 root_folder: "public".into(),
103 index_file: "index.html".into(),
104 log_file: "test.log".into(),
105 mime_types: [("html".into(), "text/html".into())].into_iter().collect(),
106 not_found_file: "404.html".into(),
107 }
108 }
109
110 #[test]
111 fn stats_route_returns_json() {
112 let mut router = Router::new(mock_config(), Stats::default());
113 let req = HttpRequest { method: HttpMethod::GET, path: "/stats".into(), version: HttpVersion::Http1_1, headers: Default::default() };
114 let res = router.handle_request(&req);
115 assert_eq!(res.status.code(), 200);
116 assert_eq!(res.headers.get("Content-Type").unwrap(), "application/json");
117 }
118
119 #[test]
120 fn missing_file_returns_404() {
121 let mut router = Router::new(mock_config(), Stats::default());
122 let req = HttpRequest { method: HttpMethod::GET, path: "/nope.html".into(), version: HttpVersion::Http1_1, headers: Default::default() };
123 assert_eq!(router.handle_request(&req).status.code(), 404);
124 }
125
126 #[test]
127 fn head_request_has_no_body() {
128 let mut router = Router::new(mock_config(), Stats::default());
129 let req = HttpRequest { method: HttpMethod::HEAD, path: "/index.html".into(), version: HttpVersion::Http1_1, headers: Default::default() };
130 let res = router.handle_request(&req);
131 assert_eq!(res.status.code(), 200);
132 assert!(res.body.is_none() || res.body.unwrap().is_empty());
133 }
134}