1use std::{
2 io::{self, ErrorKind},
3 net::SocketAddr,
4 path::Path,
5 sync::Arc,
6};
7
8use color_eyre::{eyre::Result, owo_colors::OwoColorize};
9use http_body_util::{combinators::BoxBody, BodyExt, Full, StreamBody};
10use hyper::{
11 body::{Bytes, Frame, Incoming},
12 header::CONTENT_TYPE,
13 service::service_fn,
14 Request, Response, StatusCode,
15};
16use hyper_util::{
17 rt::{TokioExecutor, TokioIo},
18 server::conn::auto::Builder,
19};
20use tokio::{io::BufReader, net::TcpListener};
21use tokio_stream::StreamExt;
22use tokio_util::io::ReaderStream;
23
24pub async fn serve(
25 root_path: Arc<Path>,
26 listen_addr: SocketAddr,
27 no_index_convenience: bool,
28) -> Result<()> {
29 let tcp_listener = TcpListener::bind(listen_addr).await?;
30 println!(
31 "HTTP server listening on {}",
32 format!("http://{listen_addr}").cyan().underline()
33 );
34
35 loop {
36 let root_path = root_path.clone();
37 let (stream, addr) = match tcp_listener.accept().await {
38 Ok(x) => x,
39 Err(e) => {
40 eprintln!("Failed to accept connection: {e}");
41 continue;
42 }
43 };
44
45 let serve_connection = async move {
46 let result = Builder::new(TokioExecutor::new())
47 .serve_connection(
48 TokioIo::new(stream),
49 service_fn({
50 move |req: hyper::Request<Incoming>| {
51 handle_request(root_path.clone(), no_index_convenience, req)
52 }
53 }),
54 )
55 .await;
56
57 if let Err(e) = result {
58 eprintln!("Error serving {addr}: {:?}", e.source());
59 }
60 };
61
62 tokio::spawn(serve_connection);
63 }
64}
65
66pub async fn handle_request(
67 root_path: Arc<Path>,
68 no_index_convenience: bool,
69 req: Request<Incoming>,
70) -> Result<Response<BoxBody<Bytes, io::Error>>> {
71 let path = req.uri().path();
72 tracing::debug!("Path: {}", path);
73
74 let raw_file_path = root_path.join(&path[1..]);
75
76 tracing::debug!("Root + path: {}", raw_file_path.display());
77
78 let mut file_path = match raw_file_path.canonicalize() {
79 Ok(file_path) => file_path,
80 Err(err) => match err.kind() {
81 ErrorKind::NotFound => {
82 println!("{} {}", "404".yellow().bold(), path);
83 return Ok(not_found(path));
84 }
85 _ => {
86 if let Some(err_no) = err.raw_os_error() {
87 if err_no == 20 || err_no == 21 {
88 println!("{} {}", "404".yellow().bold(), path);
90 return Ok(not_found(path));
91 }
92 }
93
94 return Err(err.into());
95 }
96 },
97 };
98
99 tracing::debug!("Canonicalized req path: {}", raw_file_path.display());
100
101 if !file_path.starts_with(root_path.as_ref()) {
102 tracing::warn!("Malicious request path: {}", path);
105 return Ok(not_found(path));
106 }
107
108 let mut file = tokio::fs::OpenOptions::new()
109 .read(true)
110 .open(&file_path)
111 .await?;
112
113 let meta = file.metadata().await?;
114
115 if meta.is_dir() {
116 if no_index_convenience {
117 return Ok(not_found(path));
118 } else {
119 let new_file_path = file_path.join("index.html");
121 file = match tokio::fs::OpenOptions::new()
122 .read(true)
123 .open(&new_file_path)
124 .await
125 {
126 Ok(file) => file,
127 Err(_) => {
128 println!("{} {}", "404".yellow().bold(), path);
129 return Ok(not_found(path));
130 }
131 };
132
133 if file.metadata().await?.is_dir() {
134 println!("{} {}", "404".yellow().bold(), path);
135 return Ok(not_found(path));
136 }
137
138 file_path = new_file_path;
139 tracing::debug!("Remapped to {}", file_path.display());
140 }
141 }
142
143 let stream = ReaderStream::new(BufReader::with_capacity(16 * 1024, file))
144 .map(|read_res| read_res.map(Frame::data));
145 let mime = mime_guess::from_path(&file_path).first_or_text_plain();
146
147 println!("{} {}", "200".green().bold(), path);
148
149 let response = Response::builder()
150 .header(CONTENT_TYPE, mime.essence_str())
151 .body(StreamBody::new(stream).boxed())
152 .expect("values provided to the builder should be valid");
153
154 Ok(response)
155}
156
157fn not_found<E>(path: &str) -> Response<BoxBody<Bytes, E>> {
158 Response::builder()
159 .status(StatusCode::NOT_FOUND)
160 .header(CONTENT_TYPE, "text/plain")
161 .body(
162 Full::new(Bytes::from(format!("Not found: {path}")))
163 .map_err(|_| unreachable!("Creating not found body cannot fail."))
164 .boxed(),
165 )
166 .unwrap()
167}