use std::{io::{self, ErrorKind}, path::Path};
use http_range::{HttpRange, HttpRangeParseError};
use hyper::{Body, Request, Response, header, StatusCode};
use tokio::{fs, io::{AsyncSeekExt, AsyncReadExt}};
use tokio_util::codec::{FramedRead, BytesCodec};
use crate::{inject, Config};
use super::{bad_request, not_found, SERVER_HEADER};
pub(crate) async fn try_serve(
req: &Request<Body>,
config: &Config,
) -> Option<Response<Body>> {
let (subpath, mount) = config.mounts.iter()
.filter_map(|mount| {
req.uri()
.path()
.strip_prefix(&mount.uri_path)
.map(|subpath| {
(subpath.trim_start_matches('/').to_owned(), mount)
})
})
.max_by_key(|(_, mount)| mount.uri_path.len())?;
Some(serve(req, &subpath, &mount.fs_path, config).await)
}
async fn serve(
req: &Request<Body>,
subpath: &str,
fs_root: &Path,
config: &Config,
) -> Response<Body> {
log::trace!("Serving request from file server...");
let subpath = Path::new(subpath);
let path = fs_root.join(subpath);
macro_rules! canonicalize {
($path:expr) => {
match fs::canonicalize($path).await {
Ok(v) => v,
Err(e) if e.kind() == ErrorKind::NotFound => return not_found(config),
Err(e) => panic!(
"unhandled error: could not canonicalize path '{}': {}",
$path.display(),
e,
),
}
};
}
let canonical_req = canonicalize!(&path);
let canonical_root = canonicalize!(fs_root);
if !canonical_req.starts_with(canonical_root) {
log::warn!(
"Directory traversal attack detected ({:?} {}) -> responding BAD REQUEST",
req.method(),
req.uri().path(),
);
return bad_request("Bad request: requested file outside of served directory\n");
}
if !path.exists() {
not_found(config)
} else if path.is_file() {
log::trace!("Serving requested file");
serve_file(&path, req, config).await
} else if path.join("index.html").is_file() {
log::trace!("Serving 'index.html' file in requested directory");
serve_file(&path.join("index.html"), req, config).await
} else {
log::trace!("Listing contents of directory...");
serve_dir(req.uri().path(), &path, config)
.await
.expect("failed to read directory contents due to IO error")
}
}
async fn serve_dir(
uri_path: &str,
path: &Path,
config: &Config,
) -> Result<Response<Body>, io::Error> {
const DIR_LISTING_HTML: &str = include_str!("../assets/dir-listing.html");
let mut folders = Vec::new();
let mut files = Vec::new();
let mut it = fs::read_dir(path).await?;
while let Some(entry) = it.next_entry().await? {
let name = entry.file_name().to_string_lossy().into_owned();
if entry.file_type().await?.is_file() {
files.push((name, false));
} else {
folders.push((name + "/", false));
}
}
for sd in config.mounts.iter().filter(|sd| sd.fs_path.exists()) {
if let Some(rest) = sd.uri_path.strip_prefix(uri_path) {
if rest.is_empty() {
continue;
}
let name = rest.find('/')
.map(|pos| &rest[..pos])
.unwrap_or(rest)
.to_owned();
if sd.fs_path.is_dir() {
folders.push((name + "/", true));
} else {
files.push((name, true));
}
}
}
folders.sort();
files.sort();
let mut entries = String::from("\n");
for (name, is_mount) in folders.into_iter().chain(files) {
entries.push_str(&format!(
"<li><a href=\"{0}\" class=\"{1}\"><code>{0}</code></a></li>\n",
name,
if is_mount { "mount" } else { "real" },
));
}
let html = DIR_LISTING_HTML
.replace("{{ uri_path }}", uri_path)
.replace("{{ entries }}", &entries)
.replace("{{ control_path }}", config.control_path());
Ok(
Response::builder()
.header("Content-Type", "text/html; charset=utf-8")
.header("Server", SERVER_HEADER)
.body(html.into())
.expect("bug: invalid response")
)
}
async fn serve_file(
path: &Path,
req: &Request<Body>,
config: &Config,
) -> Response<Body> {
let mime = mime_guess::from_path(&path).first();
if mime.as_ref().map_or(false, |mime| mime.as_ref().starts_with("text/html")) {
let raw = fs::read(path).await.expect("failed to read file");
let html = inject::into(&raw, &config);
Response::builder()
.header("Content-Type", "text/html")
.header("Content-Length", html.len().to_string())
.header("Server", SERVER_HEADER)
.body(html.into())
.expect("bug: invalid response")
} else {
let mut file = fs::File::open(path).await.expect("failed to open file");
let file_size = file.metadata().await.expect("failed to read file metadata").len();
let mut response = Response::builder()
.header("Server", SERVER_HEADER)
.header(header::ACCEPT_RANGES, "bytes");
if let Some(mime) = mime {
response = response.header("Content-Type", mime.to_string());
}
if let Some(range_header) = req.headers().get(header::RANGE) {
let range = match HttpRange::parse_bytes(range_header.as_bytes(), file_size) {
Ok(ranges) if ranges.len() == 1 => ranges[0],
Ok(_) => {
return Response::builder()
.status(StatusCode::BAD_REQUEST)
.header("Server", SERVER_HEADER)
.body("multiple ranges in 'Range' header not supported".into())
.expect("bug: invalid response")
}
Err(HttpRangeParseError::InvalidRange) => todo!(),
Err(HttpRangeParseError::NoOverlap) => {
return Response::builder()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header("Server", SERVER_HEADER)
.body("".into())
.expect("bug: invalid response");
}
};
file.seek(io::SeekFrom::Start(range.start)).await.unwrap();
let reader = FramedRead::new(file.take(range.length), BytesCodec::new());
let body = Body::wrap_stream(reader);
response
.status(StatusCode::PARTIAL_CONTENT)
.header(header::CONTENT_LENGTH, range.length)
.header(header::CONTENT_RANGE, format!(
"bytes {}-{}/{}",
range.start,
range.start + range.length - 1,
file_size,
))
.body(body)
.expect("bug: invalid response")
} else {
let body = Body::wrap_stream(FramedRead::new(file, BytesCodec::new()));
response
.header(header::CONTENT_LENGTH, file_size)
.body(body)
.expect("bug: invalid response")
}
}
}