use tokio::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
use tokio::net::TcpStream;
use super::ServeConfig;
use super::response::{reply, respond};
use crate::http::{Header, HttpRequest};
use crate::serve::{conditional, fs, listing, mime, path};
pub(super) fn path_of(target: &str) -> &str {
target
.split_once(['?', '#'])
.map_or(target, |(path, _rest)| path)
}
const WWW_AUTHENTICATE: Header<'static> = Header {
name: "WWW-Authenticate",
value: "Basic realm=\"shep\"",
};
const ALLOW: Header<'static> = Header {
name: "Allow",
value: "GET, HEAD",
};
pub(super) async fn answer(
stream: &mut TcpStream,
cfg: &ServeConfig,
request: &HttpRequest,
logged_path: &mut String,
) -> (u16, u64) {
let raw_path = path_of(&request.target);
if let Some(creds) = &cfg.auth {
let header = request.headers.get("authorization").map(String::as_str);
if !creds.satisfies(header) {
let body = b"unauthorized\n";
return reply(stream, 401, "text/plain", body, vec![WWW_AUTHENTICATE]).await;
}
}
if request.method != "GET" && request.method != "HEAD" {
let body = b"method not allowed; this server answers GET and HEAD\n";
return reply(stream, 405, "text/plain", body, vec![ALLOW]).await;
}
let has_declared_body = request
.headers
.get("content-length")
.and_then(|value| value.parse::<u64>().ok())
.is_some_and(|len| len > 0)
|| request.headers.contains_key("transfer-encoding");
if has_declared_body {
let body = b"this server does not accept a request body\n";
return reply(stream, 400, "text/plain", body, vec![]).await;
}
let segments = match path::resolve(&request.target) {
Ok(segments) => segments,
Err(refusal) => {
let body = refusal.to_string();
return reply(stream, 400, "text/plain", body.as_bytes(), vec![]).await;
}
};
*logged_path = format!("/{}", segments.join("/"));
if path::is_hidden(&segments) && !cfg.hidden {
return send_not_found(stream, cfg, request).await;
}
let Some(resolved) = fs::contain(&cfg.root, &segments, cfg.follow_symlinks).await else {
return send_not_found(stream, cfg, request).await;
};
let metadata = match tokio::fs::metadata(&resolved).await {
Ok(metadata) => metadata,
Err(_err) => return send_not_found(stream, cfg, request).await,
};
if metadata.is_dir() {
if !raw_path.ends_with('/') {
let mut location = String::from("/");
location.push_str(
&segments
.iter()
.map(|segment| listing::encode_segment(segment))
.collect::<Vec<_>>()
.join("/"),
);
location.push('/');
let header = Header {
name: "Location",
value: location.as_str(),
};
return match respond(stream, 301, "text/plain", 0, vec![header]).await {
Ok(()) => (301, 0),
Err(_err) => {
let body = b"could not build the redirect\n";
reply(stream, 500, "text/plain", body, vec![]).await
}
};
}
if let Some((file, metadata)) = fs::open_regular(&resolved.join("index.html")).await {
return serve_file(
stream,
request,
mime::content_type("index.html"),
file,
&metadata,
)
.await;
}
if cfg.listing {
let entries = read_listing(&resolved, cfg.hidden)
.await
.unwrap_or_default();
let mut prefix = String::from("/");
prefix.push_str(&segments.join("/"));
if !prefix.ends_with('/') {
prefix.push('/');
}
let html = listing::render(&prefix, &entries);
let body = html.as_bytes();
return reply(stream, 200, "text/html; charset=utf-8", body, vec![]).await;
}
return send_not_found(stream, cfg, request).await;
}
let Some((file, metadata)) = fs::open_regular(&resolved).await else {
return send_not_found(stream, cfg, request).await;
};
let content_type = mime::content_type(logged_path.as_str());
serve_file(stream, request, content_type, file, &metadata).await
}
async fn send_not_found(
stream: &mut TcpStream,
cfg: &ServeConfig,
request: &HttpRequest,
) -> (u16, u64) {
if cfg.spa
&& matches!(request.method.as_str(), "GET" | "HEAD")
&& request
.headers
.get("accept")
.is_some_and(|accept| accept.contains("text/html"))
&& let Some((file, metadata)) = fs::open_regular(&cfg.root.join("index.html")).await
{
return serve_file(
stream,
request,
mime::content_type("index.html"),
file,
&metadata,
)
.await;
}
reply(stream, 404, "text/plain", b"not found\n", vec![]).await
}
async fn serve_file(
stream: &mut TcpStream,
request: &HttpRequest,
content_type: &str,
mut file: tokio::fs::File,
metadata: &std::fs::Metadata,
) -> (u16, u64) {
let len = metadata.len();
let validators = conditional::Validators::new(len, metadata.modified().ok());
let outcome = conditional::decide(&request.headers, len, &validators);
let body = match outcome {
conditional::Outcome::Whole => Some((0_u64, len)),
conditional::Outcome::Partial { start, end } => Some((start, end + 1)),
conditional::Outcome::Unsatisfiable | conditional::Outcome::NotModified => None,
};
let status = match outcome {
conditional::Outcome::Whole => 200,
conditional::Outcome::Partial { .. } => 206,
conditional::Outcome::Unsatisfiable => 416,
conditional::Outcome::NotModified => 304,
};
let body_len = body.map_or(0, |(start, stop)| stop - start);
let mut slice = match body {
Some((start, stop)) if request.method == "GET" => {
match file.seek(SeekFrom::Start(start)).await {
Ok(_here) => Some(file.take(stop - start)),
Err(_err) => return (500, 0),
}
}
_ => None,
};
let last_modified = validators.last_modified();
let content_range = match outcome {
conditional::Outcome::Partial { start, end } => Some(format!("bytes {start}-{end}/{len}")),
conditional::Outcome::Unsatisfiable => Some(format!("bytes */{len}")),
_ => None,
};
let mut headers = vec![
Header {
name: "Accept-Ranges",
value: "bytes",
},
Header {
name: "ETag",
value: validators.etag(),
},
];
if let Some(value) = &last_modified {
headers.push(Header {
name: "Last-Modified",
value: value.as_str(),
});
}
if let Some(value) = &content_range {
headers.push(Header {
name: "Content-Range",
value: value.as_str(),
});
}
if respond(stream, status, content_type, body_len, headers)
.await
.is_err()
{
return (500, 0);
}
if let Some(slice) = &mut slice {
let _ = tokio::io::copy(slice, stream).await;
}
(status, body_len)
}
async fn read_listing(dir: &std::path::Path, hidden: bool) -> std::io::Result<Vec<listing::Entry>> {
let mut dirs = Vec::new();
let mut files = Vec::new();
let mut read_dir = tokio::fs::read_dir(dir).await?;
while let Some(entry) = read_dir.next_entry().await? {
let name = entry.file_name().to_string_lossy().into_owned();
if !hidden && path::is_hidden(std::slice::from_ref(&name)) {
continue;
}
if entry.file_type().await?.is_dir() {
dirs.push(name);
} else {
files.push(name);
}
}
dirs.sort();
files.sort();
let mut entries = Vec::with_capacity(dirs.len() + files.len());
entries.extend(dirs.into_iter().map(listing::Entry::dir));
entries.extend(files.into_iter().map(listing::Entry::file));
Ok(entries)
}
#[cfg(all(test, unix))]
mod tests;