From 5026bfed7c0d9506eaf632ad81a0adf4c8c85c48 Mon Sep 17 00:00:00 2001 From: Jose Quintana Date: Sun, 14 Feb 2021 03:30:50 +0100 Subject: [PATCH] Merge pull request #33 from joseluisq/feature/windows_support feat: windows support --- .drone.yml | 1 + Cargo.lock | 9 ++++++++- Cargo.toml | 1 + src/helpers.rs | 16 ++++++++++++++++ src/lib.rs | 3 +++ src/server.rs | 2 +- src/staticfile_middleware/staticfile.rs | 225 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------------------------------------------- src/staticfiles.rs | 28 +++++++++------------------- 8 files changed, 135 insertions(+), 150 deletions(-) diff --git a/.drone.yml b/.drone.yml index b2570f9..b985aef 100644 --- a/.drone.yml +++ b/.drone.yml @@ -48,6 +48,7 @@ trigger: - feature/* - bugfix/* - hotfix/* + - 1.x diff --git a/Cargo.lock b/Cargo.lock index 503e664..cf206cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,6 +501,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" [[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] name = "phf" version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -872,6 +878,7 @@ dependencies = [ "mime_guess", "nix", "openssl", + "percent-encoding 2.1.0", "signal", "structopt", "tempdir", @@ -1055,7 +1062,7 @@ checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" dependencies = [ "idna", "matches", - "percent-encoding", + "percent-encoding 1.0.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0a9c641..fec4ee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ mime_guess = "1.8" structopt = { version = "0.3", default-features = false } time = "0.1" url = "1.4" +percent-encoding = "2.1" [target.'cfg(not(windows))'.dependencies.nix] version = "0.14" diff --git a/src/helpers.rs b/src/helpers.rs index d251a0c..f8b1713 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -33,3 +33,19 @@ where ))), } } + +#[cfg(not(windows))] +pub fn adjust_canonicalization>(p: P) -> String { + p.as_ref().display().to_string() +} + +#[cfg(windows)] +pub fn adjust_canonicalization>(p: P) -> String { + const VERBATIM_PREFIX: &str = r#"\\?\"#; + let p = p.as_ref().display().to_string(); + if p.starts_with(VERBATIM_PREFIX) { + p[VERBATIM_PREFIX.len()..].to_string() + } else { + p + } +} diff --git a/src/lib.rs b/src/lib.rs index 4841d9b..b0eea07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,10 @@ pub mod gzip; pub mod helpers; pub mod logger; pub mod server; + +#[cfg(not(windows))] pub mod signal_manager; + pub mod staticfile_middleware; pub mod staticfiles; diff --git a/src/server.rs b/src/server.rs index 2550d32..2db456b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -109,5 +109,5 @@ fn handle_signals() { #[cfg(windows)] fn handle_signals() { - println!("TODO: Windows signals...") + // TODO: Windows signals... } diff --git a/src/staticfile_middleware/staticfile.rs b/src/staticfile_middleware/staticfile.rs index 06836c9..4df9ff8 100644 --- a/src/staticfile_middleware/staticfile.rs +++ b/src/staticfile_middleware/staticfile.rs @@ -1,56 +1,57 @@ use humansize::{file_size_opts, FileSize}; use iron::headers::{ - AcceptEncoding, AcceptRanges, ContentEncoding, ContentLength, Encoding, HttpDate, - IfModifiedSince, LastModified, Range, RangeUnit, + 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::fs::{File, Metadata}; use std::path::{Path, PathBuf}; -use std::time::UNIX_EPOCH; +use std::time::{SystemTime, UNIX_EPOCH}; use std::{error, io}; -use std::{ffi::OsString, time::SystemTime}; -use crate::staticfile_middleware::helpers; +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_list: bool, + dir_listing: bool, } impl Staticfile { - pub fn new

(root: P, assets: P, dir_list: bool) -> io::Result + pub fn new>( + root_dir: P, + assets_dir: P, + dir_listing: bool, + ) -> io::Result where - P: AsRef, + PathBuf: From

, { - let root = root.as_ref().canonicalize()?; - let assets = assets.as_ref().canonicalize()?; - Ok(Staticfile { - root, - assets, - dir_list, + root: root_dir.into(), + assets: assets_dir.into(), + dir_listing, }) } fn resolve_path(&self, path: &[&str]) -> Result> { - let path_dirname = path[0]; + 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 resolved = if path_dirname == asserts_dirname { + let path_resolved = if current_dirname.as_ref() == asserts_dirname { // Assets path validation resolve is_assets = true; let mut res = self.assets.clone(); for component in path.iter().skip(1) { - res.push(component); + res.push(percent_decode_str(component).decode_utf8()?.as_ref()); } res @@ -58,21 +59,26 @@ impl Staticfile { // Root path validation resolve let mut res = self.root.clone(); for component in path { - res.push(component); + res.push(percent_decode_str(component).decode_utf8()?.as_ref()); } res }; - let resolved = resolved.canonicalize()?; - let path = if is_assets { &self.assets } else { &self.root }; + let base_path = if is_assets { &self.assets } else { &self.root }; + let path_resolved = PathBuf::from(helpers::adjust_canonicalization( + path_resolved.canonicalize()?, + )); // Protect against path/directory traversal - if !resolved.starts_with(&path) { - return Result::Err(From::from(format!("Cannot leave {:?} path", &path))); + if !path_resolved.starts_with(&base_path) { + return Err(From::from(format!( + "Cannot leave {:?} base path", + &base_path + ))); } - Ok(resolved) + Ok(path_resolved) } } @@ -84,17 +90,20 @@ impl Handler for Staticfile { } // Resolve path on file system - let file_path = match self.resolve_path(&req.url.path()) { + let path_resolved = match self.resolve_path(&req.url.path()) { Ok(file_path) => file_path, - Err(_) => return Ok(Response::with(status::NotFound)), + Err(e) => { + trace!("{}", e); + return Ok(Response::with(status::NotFound)); + } }; - // 1. Check if directory listing feature is enabled, + // 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_list && file_path.is_dir() && !file_path.join("index.html").exists() { - let encoding = Encoding::Identity; - let readir = match std::fs::read_dir(file_path) { + 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(dir) => dir, Err(err) => { error!("{}", err); @@ -129,7 +138,7 @@ impl Handler for Staticfile { entries_str = String::from("../"); } - for entry in readir { + 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(); @@ -138,8 +147,9 @@ impl Handler for Staticfile { name = format!("{}/", name); filesize = String::from("-") } + let uri = format!("{}{}", current_path, name); - let modified = get_last_modified(meta.modified().unwrap()).unwrap(); + let modified = parse_last_modified(meta.modified().unwrap()).unwrap(); entries_str = format!( "{}{}{}{}", @@ -152,12 +162,16 @@ impl Handler for Staticfile { ); } - let page = format!( - "Index of {}

Index of {}

{}


", current_path, current_path, entries_str + 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.len() as u64; - let content_encoding = ContentEncoding(vec![encoding]); - let mut resp = Response::with((status::Ok, Header(content_encoding), page)); + 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) @@ -170,53 +184,39 @@ impl Handler for Staticfile { return Ok(resp); } - // 2. Otherwise proceed with the normal file-response process + // 2. Otherwise proceed with the normal file- process - // Get current file metadata - let accept_gz = helpers::accept_gzip(req.headers.get::()); - let file = match StaticFileWithMetadata::search(&file_path, accept_gz) { + // Search a file and its metadata by the resolved path + let static_file = match StaticFileWithMeta::search(path_resolved.clone()) { Ok(f) => f, - Err(_) => return Ok(Response::with(status::NotFound)), + Err(e) => { + trace!("{}", e); + return Ok(Response::with(status::NotFound)); + } }; // Apply last modified date time - let client_last_modified = req.headers.get::(); - let last_modified = file.last_modified().ok().map(HttpDate); + let client_last_mod = req.headers.get::(); + let last_mod = static_file.last_modified().ok().map(HttpDate); - if let (Some(client_last_modified), Some(last_modified)) = - (client_last_modified, last_modified) - { + if let (Some(client_last_mod), Some(last_mod)) = (client_last_mod, last_mod) { trace!( "Comparing {} (file) <= {} (req)", - last_modified, - client_last_modified.0 + last_mod, + client_last_mod.0 ); - if last_modified <= client_last_modified.0 { + if last_mod <= client_last_mod.0 { return Ok(Response::with(status::NotModified)); } } - // Add Encoding Gzip header - let encoding = if file.is_gz { - Encoding::Gzip - } else { - Encoding::Identity - }; - let encoding = ContentEncoding(vec![encoding]); - - // Prepare response - let mut resp = match last_modified { - Some(last_modified) => { - let last_modified = LastModified(last_modified); - Response::with(( - status::Ok, - Header(last_modified), - Header(encoding), - file.file, - )) + // 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, Header(encoding), file.file)), + None => Response::with((status::Ok, static_file.file)), }; // Empty current response body on HEAD requests, @@ -224,7 +224,7 @@ impl Handler for Staticfile { // https://tools.ietf.org/html/rfc7231#section-4.3.2 if req.method == Method::Head { resp.set_mut(vec![]); - resp.set_mut(Header(ContentLength(file.metadata.len()))); + resp.set_mut(Header(ContentLength(static_file.meta.len()))); return Ok(resp); } @@ -236,7 +236,7 @@ impl Handler for Staticfile { None => resp, // Try to deliver partial content Some(Range::Bytes(v)) => { - if let Ok(partial_file) = PartialFile::from_path(&file_path, v) { + if let Ok(partial_file) = PartialFile::from_path(&path_resolved, v) { Response::with(( status::Ok, partial_file, @@ -253,79 +253,46 @@ impl Handler for Staticfile { } } -struct StaticFileWithMetadata { +/// It represents a regular source file in file system with its metadata. +struct StaticFileWithMeta { file: File, - metadata: Metadata, - is_gz: bool, + meta: Metadata, } -impl StaticFileWithMetadata { - pub fn search

( - path: P, - allow_gz: bool, - ) -> Result> - // TODO: unbox - where - P: Into, - { - let mut file_path = path.into(); - trace!("Opening {}", file_path.display()); - let mut file = StaticFileWithMetadata::open(&file_path)?; - - // Look for index.html inside of a directory - if file.metadata.is_dir() { - file_path.push("index.html"); - trace!("Redirecting to index {}", file_path.display()); - file = StaticFileWithMetadata::open(&file_path)?; +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(mut src: PathBuf) -> Result> { + trace!("Opening {}", 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()); } - if file.metadata.is_file() { - if allow_gz { - // Find the foo.gz sibling of foo - let mut side_by_side_path: OsString = file_path.into(); - side_by_side_path.push(".gz"); - file_path = side_by_side_path.into(); - trace!("Attempting to find side-by-side GZ {}", file_path.display()); - - match StaticFileWithMetadata::open(&file_path) { - Ok(mut gz_file) => { - if gz_file.metadata.is_file() { - gz_file.is_gz = true; - Ok(gz_file) - } else { - Ok(file) - } - } - Err(_) => Ok(file), - } - } else { - Ok(file) - } + // 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")) } } - fn open

(path: P) -> Result> - where - P: AsRef, - { - let file = File::open(path)?; - let metadata = file.metadata()?; - - Ok(StaticFileWithMetadata { - file, - metadata, - is_gz: false, - }) - } - + /// Get the last modification time of current file. pub fn last_modified(&self) -> Result> { - get_last_modified(self.metadata.modified()?) + parse_last_modified(self.meta.modified()?) } } -fn get_last_modified(modified: SystemTime) -> Result> { +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. diff --git a/src/staticfiles.rs b/src/staticfiles.rs index 1794385..ac3ef2e 100644 --- a/src/staticfiles.rs +++ b/src/staticfiles.rs @@ -1,8 +1,8 @@ use iron::mime; use iron::prelude::*; use iron_cors::CorsMiddleware; -use std::collections::HashSet; use std::time::Duration; +use std::{collections::HashSet, path::PathBuf}; use crate::error_page::ErrorPage; use crate::gzip::GzipMiddleware; @@ -32,26 +32,16 @@ impl StaticFiles { /// Handle static files for current `StaticFiles` middleware. pub fn handle(&self) -> Chain { - // Check the root directory - let root_dir = &match helpers::get_valid_dirpath(&self.opts.root_dir) { - Err(e) => { - error!("{}", e); - std::process::exit(1) - } - Ok(v) => v, - }; + // Check root directory + let p = PathBuf::from(&self.opts.root_dir).canonicalize().unwrap(); + let root_dir = PathBuf::from(helpers::adjust_canonicalization(p)); - // Check the assets directory - let assets_dir = &match helpers::get_valid_dirpath(&self.opts.assets_dir) { - Err(e) => { - error!("{}", e); - std::process::exit(1) - } - Ok(v) => v, - }; + // Check assets directory + let p = PathBuf::from(&self.opts.assets_dir).canonicalize().unwrap(); + let assets_dir = PathBuf::from(helpers::adjust_canonicalization(p)); - // Get the assets directory name - let assets_dirname = &match helpers::get_dirname(assets_dir) { + // Get assets directory name + let assets_dirname = &match helpers::get_dirname(&assets_dir) { Err(e) => { error!("{}", e); std::process::exit(1) -- libgit2 1.7.2