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; /// Recursively serves files from the specified root and assets directories. pub struct Staticfile { root: PathBuf, assets: PathBuf, dir_listing: bool, } impl Staticfile { pub fn new>(root_dir: P, assets_dir: P, dir_listing: bool) -> Staticfile where PathBuf: From

, { Staticfile { root: root_dir.into(), assets: assets_dir.into(), dir_listing, } } /// Resolve a specific path which should be relative to base path. fn resolve_path(&self, path: &[&str]) -> Result> { 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 { // Assets path validation resolve 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 { // Root path validation resolve 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 base_path = PathBuf::from(helpers::adjust_canonicalization(base_path.canonicalize()?)); let path_resolved = PathBuf::from(helpers::adjust_canonicalization( path_resolved.canonicalize()?, )); // Protect against path/directory traversal 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 { // Accept only HEAD and GET methods if !(req.method == Method::Head || req.method == Method::Get) { return Ok(Response::with(status::MethodNotAllowed)); } // Resolve path on file system let path_resolved = match self.resolve_path(&req.url.path()) { Ok(p) => p, Err(e) => { trace!("{}", e); return Ok(Response::with(status::NotFound)); } }; // 1. Check if "directory listing" feature is enabled, // if current path is a valid directory and // if it does not contain an `index.html` file 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::(); // Redirect if current path does not end with a slash 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), ))); } // Read current directory and create the index page content let mut entries_str = String::new(); if current_path != "/" { entries_str = String::from("../"); } 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!( "{}{}{}{}", 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!( "Index of {}

Index of {}

{}


", 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)); // Empty current response body on HEAD requests, // just setting up the `content-length` header (size of the file in bytes) // https://tools.ietf.org/html/rfc7231#section-4.3.2 if req.method == Method::Head { resp.set_mut(vec![]); resp.set_mut(Header(ContentLength(len))); } return Ok(resp); } // 2. Otherwise proceed with the normal file- process // Search a file and its metadata by the resolved path let static_file = match StaticFileWithMeta::search(&path_resolved) { Ok(f) => f, Err(e) => { trace!("{}", e); return Ok(Response::with(status::NotFound)); } }; // Apply last modified date time let client_last_mod = req.headers.get::(); 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)); } } // Prepare response object 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)), }; // Empty current response body on HEAD requests, // just setting up the `content-length` header (size of the file in bytes) // https://tools.ietf.org/html/rfc7231#section-4.3.2 if req.method == Method::Head { resp.set_mut(vec![]); resp.set_mut(Header(ContentLength(static_file.meta.len()))); return Ok(resp); } // Partial Content Delivery response // Enable the "Accept-Ranges" header on all files resp.set_mut(Header(AcceptRanges(vec![RangeUnit::Bytes]))); resp = match req.headers.get::().cloned() { // Deliver the whole file None => resp, // Try to deliver partial content 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) } } /// It represents a regular source file in file system with its metadata. struct StaticFileWithMeta { file: File, meta: Metadata, } impl StaticFileWithMeta { /// Search for a regular source file in file system. /// If source file is a directory then it attempts to search for an index.html. pub fn search>(src: P) -> Result> where PathBuf: From

, { let mut src: PathBuf = src.into(); trace!("Opening path {}", src.display()); let mut auto_index = false; let meta = std::fs::metadata(&src)?; // Look for an `index.html` file inside of a directory if meta.is_dir() { src.push("index.html"); auto_index = true; trace!("Redirecting to index {}", src.display()); } // Attempt to open source file in read-only mode 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")) } } /// Get the last modification time of current file. pub fn last_modified(&self) -> Result> { parse_last_modified(self.meta.modified()?) } } fn parse_last_modified(modified: SystemTime) -> Result> { let since_epoch = modified.duration_since(UNIX_EPOCH)?; // HTTP times don't have nanosecond precision, so we truncate // the modification time. // Converting to i64 should be safe until we get beyond the // planned lifetime of the universe // // TODO: Investigate how to write a test for this. Changing // the modification time of a file with greater than second // precision appears to be something that only is possible to // do on Linux. let ts = time::Timespec::new(since_epoch.as_secs() as i64, 0); Ok(time::at_utc(ts)) } #[cfg(test)] mod test { use hyper; use iron_test; use tempfile; use super::*; use std::fs::{DirBuilder, File}; use std::path::{Path, PathBuf}; use self::hyper::header::Headers; use self::iron_test::request; use self::tempfile::TempDir; use iron::status; struct TestFilesystemSetup(TempDir); impl TestFilesystemSetup { fn new() -> Self { TestFilesystemSetup(TempDir::new().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)); } }