index : static-web-server.git

ascending towards madness

author Jose Quintana <joseluisquintana20@gmail.com> 2021-02-14 2:30:50.0 +00:00:00
committer GitHub <noreply@github.com> 2021-02-14 2:30:50.0 +00:00:00
commit
5026bfed7c0d9506eaf632ad81a0adf4c8c85c48 [patch]
tree
d5527f54248444bf11866bb4b7e32fe955fd6730
parent
252406daf376435013a5597c7c05ffabf3c5c8bc
parent
cfcb6cba91ef7ff84f2f5e3fdb8eaad10991cd3d
download
5026bfed7c0d9506eaf632ad81a0adf4c8c85c48.tar.gz

Merge pull request #33 from joseluisq/feature/windows_support

feat: windows support

Diff

 .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: AsRef<std::path::Path>>(p: P) -> String {
    p.as_ref().display().to_string()
}

#[cfg(windows)]
pub fn adjust_canonicalization<P: AsRef<std::path::Path>>(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<P>(root: P, assets: P, dir_list: bool) -> io::Result<Staticfile>
    pub fn new<P: AsRef<Path>>(
        root_dir: P,
        assets_dir: P,
        dir_listing: bool,
    ) -> io::Result<Staticfile>
    where
        P: AsRef<Path>,
        PathBuf: From<P>,
    {
        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<PathBuf, Box<dyn error::Error>> {
        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("<tr><td colspan=\"3\"><a href=\"../\">../</a></td></tr>");
            }
            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!(
                    "{}<tr><td><a href=\"{}\" title=\"{}\">{}</a></td><td style=\"width: 160px;\">{}</td><td align=\"right\" style=\"width: 140px;\">{}</td></tr>",
@@ -152,12 +162,16 @@ impl Handler for Staticfile {
                );
            }

            let page = format!(
                "<html><head><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 current_path = percent_decode_str(&current_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.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::<AcceptEncoding>());
        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::<IfModifiedSince>();
        let last_modified = file.last_modified().ok().map(HttpDate);
        let client_last_mod = req.headers.get::<IfModifiedSince>();
        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<P>(
        path: P,
        allow_gz: bool,
    ) -> Result<StaticFileWithMetadata, Box<dyn error::Error>>
    // TODO: unbox
    where
        P: Into<PathBuf>,
    {
        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<StaticFileWithMeta, Box<dyn error::Error>> {
        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<P>(path: P) -> Result<StaticFileWithMetadata, Box<dyn error::Error>>
    where
        P: AsRef<Path>,
    {
        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<time::Tm, Box<dyn error::Error>> {
        get_last_modified(self.metadata.modified()?)
        parse_last_modified(self.meta.modified()?)
    }
}

fn get_last_modified(modified: SystemTime) -> Result<time::Tm, Box<dyn error::Error>> {
fn parse_last_modified(modified: SystemTime) -> Result<time::Tm, Box<dyn error::Error>> {
    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)