soft_serve/
http.rs

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                        // Is/Not a dir
89                        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        // Someone is trying to access files outside of the root directory
103
104        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            // Convenience for serving websites
120            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}