use humansize::{file_size_opts, FileSize};
use iron::headers::{
AcceptRanges, ContentEncoding, ContentLength, Encoding, HttpDate, IfModifiedSince,
LastModified, Range, RangeUnit,
};
use iron::method::Method;
use iron::middleware::Handler;
use iron::modifiers::Header;
use iron::prelude::*;
use iron::status;
use percent_encoding::percent_decode_str;
use std::error;
use std::fs::{File, Metadata};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::helpers;
use crate::staticfile_middleware::partial_file::PartialFile;
pub struct Staticfile {
root: PathBuf,
assets: PathBuf,
dir_listing: bool,
}
impl Staticfile {
pub fn new<P: AsRef<Path>>(root_dir: P, assets_dir: P, dir_listing: bool) -> Staticfile
where
PathBuf: From<P>,
{
Staticfile {
root: root_dir.into(),
assets: assets_dir.into(),
dir_listing,
}
}
fn resolve_path(&self, path: &[&str]) -> Result<PathBuf, Box<dyn error::Error>> {
let current_dirname = percent_decode_str(path[0]).decode_utf8()?;
let asserts_dirname = self.assets.iter().last().unwrap().to_str().unwrap();
let mut is_assets = false;
let path_resolved = if current_dirname == asserts_dirname {
is_assets = true;
let mut res = self.assets.clone();
for component in path.iter().skip(1) {
res.push(percent_decode_str(component).decode_utf8()?.as_ref());
}
res
} else {
let mut res = self.root.clone();
for component in path {
res.push(percent_decode_str(component).decode_utf8()?.as_ref());
}
res
};
let base_path = if is_assets { &self.assets } else { &self.root };
let path_resolved = PathBuf::from(helpers::adjust_canonicalization(
path_resolved.canonicalize()?,
));
if !path_resolved.starts_with(&base_path) {
return Err(From::from(format!(
"Cannot leave {:?} base path",
&base_path
)));
}
Ok(path_resolved)
}
}
impl Handler for Staticfile {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
if !(req.method == Method::Head || req.method == Method::Get) {
return Ok(Response::with(status::MethodNotAllowed));
}
let path_resolved = match self.resolve_path(&req.url.path()) {
Ok(p) => p,
Err(e) => {
trace!("{}", e);
return Ok(Response::with(status::NotFound));
}
};
if self.dir_listing && path_resolved.is_dir() && !path_resolved.join("index.html").exists()
{
let read_dir = match std::fs::read_dir(&path_resolved) {
Ok(d) => d,
Err(e) => {
error!("{}", e);
return Ok(Response::with(status::InternalServerError));
}
};
let mut current_path = req
.url
.path()
.into_iter()
.map(|i| format!("/{}", i))
.collect::<String>();
if !current_path.ends_with('/') {
let mut u: url::Url = req.url.clone().into();
current_path.push('/');
u.set_path(¤t_path);
let url = match iron::Url::from_generic_url(u) {
Ok(u) => u,
Err(e) => {
error!("Unable to parse redirection url: {}", e);
return Ok(Response::with(status::BadRequest));
}
};
return Ok(Response::with((
status::PermanentRedirect,
iron::modifiers::Redirect(url),
)));
}
let mut entries_str = String::new();
if current_path != "/" {
entries_str =
String::from("<tr><td colspan=\"3\"><a href=\"../\">../</a></td></tr>");
}
for entry in read_dir {
let entry = entry.unwrap();
let meta = entry.metadata().unwrap();
let mut filesize = meta.len().file_size(file_size_opts::DECIMAL).unwrap();
let mut name = entry.file_name().into_string().unwrap();
if meta.is_dir() {
name = format!("{}/", name);
filesize = String::from("-")
}
let uri = format!("{}{}", current_path, name);
let modified = parse_last_modified(meta.modified().unwrap()).unwrap();
entries_str = format!(
"{}<tr><td><a href=\"{}\" title=\"{}\">{}</a></td><td style=\"width: 160px;\">{}</td><td align=\"right\" style=\"width: 140px;\">{}</td></tr>",
entries_str,
uri,
name,
name,
modified.to_local().strftime("%F %T").unwrap(),
filesize
);
}
let current_path = percent_decode_str(¤t_path)
.decode_utf8()
.unwrap()
.to_string();
let page_str = format!(
"<html><head><meta charset=\"utf-8\"><title>Index of {}</title></head><body><h1>Index of {}</h1><table style=\"min-width:680px;\"><tr><th colspan=\"3\"><hr></th></tr>{}<tr><th colspan=\"3\"><hr></th></tr></table></body></html>", current_path, current_path, entries_str
);
let len = page_str.len() as u64;
let content_encoding = ContentEncoding(vec![Encoding::Identity]);
let mut resp = Response::with((status::Ok, Header(content_encoding), page_str));
if req.method == Method::Head {
resp.set_mut(vec![]);
resp.set_mut(Header(ContentLength(len)));
}
return Ok(resp);
}
let static_file = match StaticFileWithMeta::search(&path_resolved) {
Ok(f) => f,
Err(e) => {
trace!("{}", e);
return Ok(Response::with(status::NotFound));
}
};
let client_last_mod = req.headers.get::<IfModifiedSince>();
let last_mod = static_file.last_modified().ok().map(HttpDate);
if let (Some(client_last_mod), Some(last_mod)) = (client_last_mod, last_mod) {
trace!(
"Comparing {} (file) <= {} (req)",
last_mod,
client_last_mod.0
);
if last_mod <= client_last_mod.0 {
return Ok(Response::with(status::NotModified));
}
}
let mut resp = match last_mod {
Some(last_mod) => {
Response::with((status::Ok, Header(LastModified(last_mod)), static_file.file))
}
None => Response::with((status::Ok, static_file.file)),
};
if req.method == Method::Head {
resp.set_mut(vec![]);
resp.set_mut(Header(ContentLength(static_file.meta.len())));
return Ok(resp);
}
resp.set_mut(Header(AcceptRanges(vec![RangeUnit::Bytes])));
resp = match req.headers.get::<Range>().cloned() {
None => resp,
Some(Range::Bytes(v)) => {
if let Ok(partial_file) = PartialFile::from_path(&path_resolved, v) {
Response::with((
status::Ok,
partial_file,
Header(AcceptRanges(vec![RangeUnit::Bytes])),
))
} else {
Response::with(status::NotFound)
}
}
Some(_) => Response::with(status::RangeNotSatisfiable),
};
Ok(resp)
}
}
struct StaticFileWithMeta {
file: File,
meta: Metadata,
}
impl StaticFileWithMeta {
pub fn search<P: AsRef<Path>>(src: P) -> Result<StaticFileWithMeta, Box<dyn error::Error>>
where
PathBuf: From<P>,
{
let mut src: PathBuf = src.into();
trace!("Opening path {}", src.display());
let mut auto_index = false;
let meta = std::fs::metadata(&src)?;
if meta.is_dir() {
src.push("index.html");
auto_index = true;
trace!("Redirecting to index {}", src.display());
}
let file = File::open(src)?;
let meta = if auto_index { file.metadata()? } else { meta };
if meta.is_file() {
Ok(StaticFileWithMeta { file, meta })
} else {
Err(From::from("Requested path was not a regular file"))
}
}
pub fn last_modified(&self) -> Result<time::Tm, Box<dyn error::Error>> {
parse_last_modified(self.meta.modified()?)
}
}
fn parse_last_modified(modified: SystemTime) -> Result<time::Tm, Box<dyn error::Error>> {
let since_epoch = modified.duration_since(UNIX_EPOCH)?;
let ts = time::Timespec::new(since_epoch.as_secs() as i64, 0);
Ok(time::at_utc(ts))
}
#[cfg(test)]
mod test {
extern crate hyper;
extern crate iron_test;
extern crate tempdir;
use super::*;
use std::fs::{DirBuilder, File};
use std::path::{Path, PathBuf};
use self::hyper::header::Headers;
use self::iron_test::request;
use self::tempdir::TempDir;
use iron::status;
struct TestFilesystemSetup(TempDir);
impl TestFilesystemSetup {
fn new() -> Self {
TestFilesystemSetup(TempDir::new("test").expect("Could not create test directory"))
}
fn path(&self) -> &Path {
self.0.path()
}
fn dir(&self, name: &str) -> PathBuf {
let p = self.path().join(name);
DirBuilder::new()
.recursive(true)
.create(&p)
.expect("Could not create directory");
p
}
fn file(&self, name: &str) -> PathBuf {
let p = self.path().join(name);
File::create(&p).expect("Could not create file");
p
}
}
#[test]
fn staticfile_resolves_paths() {
let fs = TestFilesystemSetup::new();
fs.file("index.html");
let fs2 = TestFilesystemSetup::new();
fs2.dir("assets");
let sf = Staticfile::new(fs.path(), fs2.path(), false);
let path = sf.resolve_path(&["index.html"]);
assert!(path.unwrap().ends_with("index.html"));
}
#[test]
fn staticfile_resolves_nested_paths() {
let fs = TestFilesystemSetup::new();
fs.dir("dir");
fs.file("dir/index.html");
let fs2 = TestFilesystemSetup::new();
fs2.file("assets");
let sf = Staticfile::new(fs.path(), fs2.path(), false);
let path = sf.resolve_path(&["dir", "index.html"]);
assert!(path.unwrap().ends_with("dir/index.html"));
}
#[test]
fn staticfile_disallows_resolving_out_of_root() {
let fs = TestFilesystemSetup::new();
fs.file("naughty.txt");
let dir = fs.dir("dir");
let fs2 = TestFilesystemSetup::new();
let dir2 = fs2.file("assets");
let sf = Staticfile::new(dir, dir2, false);
let path = sf.resolve_path(&["..", "naughty.txt"]);
assert!(path.is_err());
}
#[test]
fn staticfile_disallows_post_requests() {
let fs = TestFilesystemSetup::new();
let fs2 = TestFilesystemSetup::new();
let sf = Staticfile::new(fs.path(), fs2.path(), false);
let response = request::post("http://127.0.0.1/", Headers::new(), "", &sf);
let response = response.expect("Response was an error");
assert_eq!(response.status, Some(status::MethodNotAllowed));
}
}