use std::fs::File;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use http;
#[cfg(feature = "mime_guess")]
use mime_guess;
use middlewares;
use {Error, Handler, ResponseBuilder, Result};
pub fn static_files_handler<P>(root_path: P) -> Result<impl Handler<()>>
where
P: AsRef<Path> + Clone + Send + Sync + 'static,
{
let root_path = root_path.as_ref().to_path_buf().canonicalize()?;
let handler = middlewares::with_context(root_path, |root_path, req, resp| {
let path = match_file(root_path, req.uri().path())?;
serve_file(path, resp)
});
let handler = middlewares::with_error_handling(handler, handle_io_errors);
Ok(handler)
}
fn handle_io_errors(error: Error) -> Result<http::Response<String>> {
match error.downcast::<io::Error>() {
Ok(_) => {
let resp = http::Response::builder()
.status(http::StatusCode::NOT_FOUND)
.body("404 - not found".to_owned())?;
Ok(resp)
}
Err(error) => Err(error),
}
}
fn match_file(root: PathBuf, path: &str) -> Result<impl AsRef<Path>> {
let path = Path::new(path).strip_prefix("/")?;
let potential = root.join(path).canonicalize()?;
if !potential.starts_with(root) {
return Err(io::Error::from(io::ErrorKind::NotFound).into());
}
Ok(potential)
}
pub fn static_file_handler<P>(path: P) -> impl Handler<Vec<u8>>
where
P: AsRef<Path> + Clone + Send + Sync + 'static,
{
let handler = middlewares::with_context(path, |path, _, resp| serve_file(path, resp));
middlewares::with_error_handling(handler, handle_io_errors)
}
fn serve_file<P>(path: P, mut resp: ResponseBuilder) -> Result<http::Response<Vec<u8>>>
where
P: AsRef<Path>,
{
let content = read_file(&path)?;
let resp = resp.header(http::header::CONTENT_TYPE, mime_type(path).as_str())
.body(content)?;
Ok(resp)
}
fn read_file<P>(path: P) -> Result<Vec<u8>>
where
P: AsRef<Path>,
{
let mut file = File::open(path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
Ok(buffer)
}
#[cfg(feature = "mime_guess")]
fn mime_type<P>(path: P) -> String
where
P: AsRef<Path>,
{
let mime = mime_guess::guess_mime_type(path);
format!("{}", mime)
}
#[cfg(not(feature = "mime_guess"))]
fn mime_type<P>(_: P) -> String
where
P: AsRef<Path>,
{
"application/octet-stream".to_owned()
}
#[cfg(test)]
mod test {
use std::path::Path;
use super::match_file;
#[test]
fn match_file_in_directory() {
let root = Path::new("examples").to_path_buf().canonicalize().unwrap();
let path = match_file(root.clone(), "/static-files").unwrap();
let expected = root.join("static-files");
assert_eq!(path.as_ref().to_str().unwrap(), expected.to_str().unwrap());
}
#[test]
fn match_file_directory_traversal_attack() {
let path = Path::new("examples").to_path_buf().canonicalize().unwrap();
let result = match_file(path, "../Cargo.toml");
assert!(result.is_err());
}
}