index : static-web-server.git

ascending towards madness

author Jose Quintana <joseluisquintana20@gmail.com> 2021-02-08 0:24:39.0 +00:00:00
committer Jose Quintana <joseluisquintana20@gmail.com> 2021-02-08 0:24:39.0 +00:00:00
commit
533006fc36d16441e80482f221a01ddbdd134a04 [patch]
tree
5ea66b9cd561bb10d5c1f9ce3fcc041f6a9380c2
parent
e52a0f9012c42eee407bbae38dcde3bc1dd2ec63
download
533006fc36d16441e80482f221a01ddbdd134a04.tar.gz

refactor: structure project modules



Diff

 .gitignore                                          |   1 +-
 Cargo.lock                                          |  70 +---
 Cargo.toml                                          |  22 +-
 src/bin/server.rs                                   | 227 ++++++++++++++-
 src/gzip.rs                                         |  37 +-
 src/lib.rs                                          |  21 +-
 src/main.rs                                         | 320 +-------------------
 src/server.rs                                       |  91 ++++++-
 src/staticfile_middleware/cache.rs                  |  36 ++-
 src/staticfile_middleware/guess_content_type.rs     |  47 +++-
 src/staticfile_middleware/helpers.rs                |  41 ++-
 src/staticfile_middleware/http_to_https_redirect.rs |  56 +++-
 src/staticfile_middleware/mod.rs                    |  17 +-
 src/staticfile_middleware/modify_with.rs            |  24 +-
 src/staticfile_middleware/partial_file.rs           | 138 ++++++++-
 src/staticfile_middleware/prefix.rs                 |  45 +++-
 src/staticfile_middleware/rewrite.rs                |  34 ++-
 src/staticfile_middleware/staticfile.rs             | 346 +++++++++++++++++++++-
 src/staticfiles.rs                                  |  12 +-
 19 files changed, 1198 insertions(+), 387 deletions(-)

diff --git a/.gitignore b/.gitignore
index 820521f..7b070eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,5 +15,6 @@
**/*.env
release
.vscode
TODO

!sample.env
diff --git a/Cargo.lock b/Cargo.lock
index 47e56c2..d7375e4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -163,16 +163,16 @@ checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
dependencies = [
 "atty",
 "humantime",
 "log 0.4.13",
 "log 0.4.14",
 "regex",
 "termcolor",
]

[[package]]
name = "flate2"
version = "1.0.19"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129"
checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0"
dependencies = [
 "cfg-if 1.0.0",
 "crc32fast",
@@ -238,9 +238,9 @@ dependencies = [

[[package]]
name = "httparse"
version = "1.3.4"
version = "1.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691"

[[package]]
name = "humantime"
@@ -315,7 +315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24b02b8856c7f14e443c483e802cf0ce693f3bec19f49d2c9a242b18f88c9b70"
dependencies = [
 "iron",
 "log 0.4.13",
 "log 0.4.14",
]

[[package]]
@@ -332,20 +332,6 @@ dependencies = [
]

[[package]]
name = "iron_staticfile_middleware"
version = "0.4.2"
source = "git+https://github.com/joseluisq/iron-staticfile-middleware.git?tag=v0.4.2#1681b273edd087ff470d77d2c3fa7d64fc547e6f"
dependencies = [
 "iron",
 "log 0.4.13",
 "mime",
 "mime_guess",
 "rustc-serialize",
 "time",
 "url",
]

[[package]]
name = "jemalloc-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -380,9 +366,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"

[[package]]
name = "libc"
version = "0.2.82"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3"

[[package]]
name = "log"
@@ -390,16 +376,16 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
dependencies = [
 "log 0.4.13",
 "log 0.4.14",
]

[[package]]
name = "log"
version = "0.4.13"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
 "cfg-if 0.1.10",
 "cfg-if 1.0.0",
]

[[package]]
@@ -459,7 +445,7 @@ checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
dependencies = [
 "lazy_static",
 "libc",
 "log 0.4.13",
 "log 0.4.14",
 "openssl",
 "openssl-probe",
 "openssl-sys",
@@ -718,9 +704,9 @@ dependencies = [

[[package]]
name = "rand"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18519b42a40024d661e1714153e9ad0c3de27cd495760ceb09710920f1098b1e"
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
dependencies = [
 "libc",
 "rand_chacha 0.3.0",
@@ -961,14 +947,18 @@ dependencies = [
 "iron",
 "iron-cors",
 "iron-test",
 "iron_staticfile_middleware",
 "jemallocator",
 "log 0.4.13",
 "log 0.4.14",
 "mime",
 "mime_guess",
 "nix",
 "openssl",
 "rustc-serialize",
 "signal",
 "structopt",
 "tempdir",
 "time",
 "url",
]

[[package]]
@@ -1003,9 +993,9 @@ dependencies = [

[[package]]
name = "syn"
version = "1.0.59"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07cb8b1b4ebf86a89ee88cbd201b022b94138c623644d035185c84d3f41b7e66"
checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
dependencies = [
 "proc-macro2",
 "quote",
@@ -1030,7 +1020,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
 "cfg-if 1.0.0",
 "libc",
 "rand 0.8.2",
 "rand 0.8.3",
 "redox_syscall",
 "remove_dir_all",
 "winapi",
@@ -1056,9 +1046,9 @@ dependencies = [

[[package]]
name = "thread_local"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8208a331e1cb318dd5bd76951d2b8fc48ca38a69f5f4e4af1b6a9f8c6236915"
checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
dependencies = [
 "once_cell",
]
@@ -1075,9 +1065,9 @@ dependencies = [

[[package]]
name = "tinyvec"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f"
checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023"
dependencies = [
 "tinyvec_macros",
]
@@ -1215,9 +1205,9 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"

[[package]]
name = "wasi"
version = "0.10.1+wasi-snapshot-preview1"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93c6c3420963c5c64bca373b25e77acb562081b9bb4dd5bb864187742186cea9"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"

[[package]]
name = "winapi"
diff --git a/Cargo.toml b/Cargo.toml
index 04a8af3..285f323 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,18 +20,26 @@ categories = ["network-programming", "web-programming::http-server"]
edition = "2018"
include = ["src/**/*", "Cargo.toml", "Cargo.lock"]

[[bin]]
name = "static-web-server"
path = "src/bin/server.rs"

[dependencies]
iron = "0.6"
log = "0.4"
chrono = "0.4"
env_logger = "0.7"
structopt = "0.3"
flate2 = "1.0"
iron_staticfile_middleware = { git = "https://github.com/joseluisq/iron-staticfile-middleware.git", tag = "v0.4.2" }
hyper-native-tls = "0.3"
iron = "0.6"
iron-cors = "0.8"
log = "0.4"
mime = "0.2"
mime_guess = "1.8"
nix = "0.14"
rustc-serialize = "0.3"
signal = "0.7"
iron-cors = "0.8"
structopt = "0.3"
time = "0.1"
url = "1.4"

[target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator]
version = "0.3"
@@ -43,6 +51,10 @@ iron-test = "0.6"
tempdir = "0.3"

[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
debug = false
rpath = false
debug-assertions = false
diff --git a/src/bin/server.rs b/src/bin/server.rs
new file mode 100644
index 0000000..c771664
--- /dev/null
+++ b/src/bin/server.rs
@@ -0,0 +1,227 @@
#[cfg(all(target_env = "musl", target_pointer_width = "64"))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

extern crate static_web_server;

use static_web_server::{server, Options};
use structopt::StructOpt;

fn main() {
    server::run(Options::from_args())
}

#[cfg(test)]
mod test {
    extern crate hyper;
    extern crate iron_test;
    extern crate tempdir;

    use super::*;
    use static_web_server::config::Options;
    use static_web_server::staticfiles::*;

    use std::fs::{DirBuilder, File};
    use std::io::Write;
    use std::path::{Path, PathBuf};

    use self::hyper::header::Headers;
    use self::iron_test::{request, response};
    use self::tempdir::TempDir;
    use iron::headers::{ContentLength, ContentType};
    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, body: Vec<u8>) -> PathBuf {
            let p = self.path().join(name);

            let mut file = File::create(&p).expect("Could not create file");
            file.write_all(&body).expect("Could not write to file");

            p
        }
    }

    #[test]
    fn staticfile_allow_request_methods() {
        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: opts.root,
            assets_dir: opts.assets,
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let response = request::head("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::Ok));

        let response = request::get("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::Ok));
    }

    #[test]
    fn staticfile_empty_body_on_head_request() {
        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: opts.root,
            assets_dir: opts.assets,
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let res = request::head("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(res.status, Some(status::Ok));

        let result_body = response::extract_body_to_bytes(res);
        assert_eq!(result_body, vec!());
    }

    #[test]
    fn staticfile_valid_content_length_on_head_request() {
        let root = TestFilesystemSetup::new();
        root.dir("root");
        root.file("index.html", b"<html><h2>hello</h2></html>".to_vec());

        let assets = TestFilesystemSetup::new();
        assets.dir("assets");

        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: root.path().to_str().unwrap().to_string(),
            assets_dir: assets.path().to_str().unwrap().to_string(),
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let res = request::head("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(res.status, Some(status::Ok));

        let content_length = res.headers.get::<ContentLength>().unwrap();

        assert_eq!(content_length.0, 27);
    }

    #[test]
    fn staticfile_zero_content_length_on_404_head_request() {
        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: opts.root,
            assets_dir: opts.assets,
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let res = request::head("http://127.0.0.1/unknown", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(res.status, Some(status::NotFound));

        let content_length = res.headers.get::<ContentLength>().unwrap();

        assert_eq!(content_length.0, 0);
    }

    #[test]
    fn staticfile_disallow_request_methods() {
        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: opts.root,
            assets_dir: opts.assets,
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let response = request::post("http://127.0.0.1/", Headers::new(), "", &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::MethodNotAllowed));

        let response = request::delete("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::MethodNotAllowed));

        let response = request::put("http://127.0.0.1/", Headers::new(), "", &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::MethodNotAllowed));

        let response = request::patch("http://127.0.0.1/", Headers::new(), "", &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::MethodNotAllowed));

        let response = request::options("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::MethodNotAllowed));
    }

    #[test]
    fn staticfile_valid_content_type_for_404() {
        let root = TestFilesystemSetup::new();
        root.dir("root");

        let assets = TestFilesystemSetup::new();
        assets.dir("assets");

        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: root.path().to_str().unwrap().to_string(),
            assets_dir: assets.path().to_str().unwrap().to_string(),
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let res = request::head("http://127.0.0.1/unknown", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(res.status, Some(status::NotFound));

        let content_type = res.headers.get::<ContentType>().unwrap();

        assert_eq!(
            content_type.0,
            "text/html".parse::<iron::mime::Mime>().unwrap()
        );
    }
}
diff --git a/src/gzip.rs b/src/gzip.rs
index 737c72a..ac06955 100644
--- a/src/gzip.rs
+++ b/src/gzip.rs
@@ -3,33 +3,38 @@ use flate2::Compression;
use iron::headers::{AcceptEncoding, ContentEncoding, ContentType, Encoding};
use iron::prelude::*;
use iron::AfterMiddleware;
use iron_staticfile_middleware::helpers;

use crate::staticfile_middleware::helpers;

pub struct GzipMiddleware;

impl AfterMiddleware for GzipMiddleware {
    fn after(&self, req: &mut Request, mut resp: Response) -> IronResult<Response> {
        // Skip Gzip response on HEAD requests
        // Skip Gzip compression for HEAD requests
        if req.method == iron::method::Head {
            return Ok(resp);
        }

        // Enable Gzip compression only for known text-based file types
        let enable_gz = helpers::is_text_mime_type(resp.headers.get::<ContentType>());
        let accept_gz = helpers::accept_gzip(req.headers.get::<AcceptEncoding>());
        // Skip Gzip compression for non-text-based file types
        if !helpers::is_text_mime_type(resp.headers.get::<ContentType>()) {
            return Ok(resp);
        }

        // Skip Gzip compression is there is not gzip accept-encoding value
        if !helpers::accept_gzip(req.headers.get::<AcceptEncoding>()) {
            return Ok(resp);
        }

        if enable_gz && accept_gz {
            let compressed_bytes = resp.body.as_mut().map(|b| {
                let mut encoder = GzEncoder::new(vec![], Compression::fast());
                {
                    let _ = b.write_body(&mut encoder);
                }
                encoder.finish().unwrap()
            });
            if let Some(b) = compressed_bytes {
                resp.headers.set(ContentEncoding(vec![Encoding::Gzip]));
                resp.set_mut(b);
        let compressed_bytes = resp.body.as_mut().map(|b| {
            let mut encoder = GzEncoder::new(vec![], Compression::fast());
            {
                let _ = b.write_body(&mut encoder);
            }
            encoder.finish().unwrap()
        });
        if let Some(b) = compressed_bytes {
            resp.headers.set(ContentEncoding(vec![Encoding::Gzip]));
            resp.set_mut(b);
        }

        Ok(resp)
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..88f30c4
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,21 @@
#[macro_use]
extern crate log;

extern crate iron;
extern crate mime;
extern crate mime_guess;
extern crate rustc_serialize;
extern crate time;
extern crate url;

pub mod config;
pub mod error_page;
pub mod gzip;
pub mod helpers;
pub mod logger;
pub mod server;
pub mod signal_manager;
pub mod staticfile_middleware;
pub mod staticfiles;

pub use config::Options;
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index ab9bbe7..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,320 +0,0 @@
#[cfg(all(target_env = "musl", target_pointer_width = "64"))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

#[macro_use]
extern crate log;

use crate::config::Options;
use hyper_native_tls::NativeTlsServer;
use iron::{prelude::*, Listening};
use iron_staticfile_middleware::HttpToHttpsRedirect;
use staticfiles::*;
use structopt::StructOpt;

mod config;
mod error_page;
mod gzip;
mod helpers;
mod logger;
mod signal_manager;
mod staticfiles;

/// Struct for holding a reference to a running iron server instance
#[derive(Debug)]
struct RunningServer {
    listening: Listening,
    server_type: String,
}

fn on_server_running(server_name: &str, running_servers: &[RunningServer]) {
    // Notify when server is running
    running_servers.iter().for_each(|server| {
        logger::log_server(&format!(
            "{} Server ({}) is listening on {}",
            server.server_type, server_name, server.listening.socket
        ))
    });

    // Wait for incoming signals (E.g Ctrl+C (SIGINT), SIGTERM, etc
    signal_manager::wait_for_signal(|sig: signal::Signal| {
        let code = signal_manager::signal_to_int(sig);

        println!();
        warn!("Signal {} caught. Server execution exited.", code);
        std::process::exit(code)
    })
}

fn main() {
    let opts = Options::from_args();

    logger::init(&opts.log_level);

    let addr = &format!("{}{}{}", opts.host, ":", opts.port);

    // Configure & launch the HTTP server

    let files = StaticFiles::new(StaticFilesOptions {
        root_dir: opts.root,
        assets_dir: opts.assets,
        page_50x_path: opts.page50x,
        page_404_path: opts.page404,
        cors_allow_origins: opts.cors_allow_origins,
    });

    let mut running_servers = Vec::new();
    if opts.tls {
        // Launch static HTTPS server
        let ssl = NativeTlsServer::new(opts.tls_pkcs12, &opts.tls_pkcs12_passwd).unwrap();

        match Iron::new(files.handle()).https(addr, ssl) {
            Ok(listening) => running_servers.push(RunningServer {
                listening,
                server_type: "HTTPS".to_string(),
            }),
            Err(err) => panic!("{:?}", err),
        }

        // Launch redirect HTTP server (if requested)
        if let Some(port_redirect) = opts.tls_redirect_from {
            let addr_redirect = &format!("{}{}{}", opts.host, ":", port_redirect);
            let host_redirect = match opts.tls_redirect_host.as_ref() {
                Some(host) => host,
                None => &opts.host,
            };
            let handler =
                Chain::new(HttpToHttpsRedirect::new(&host_redirect, opts.port).permanent());
            match Iron::new(handler).http(addr_redirect) {
                Ok(listening) => running_servers.push(RunningServer {
                    listening,
                    server_type: "Redirect HTTP".to_string(),
                }),
                Err(err) => panic!("{:?}", err),
            }
        }
    } else {
        // Launch static HTTP server
        match Iron::new(files.handle()).http(addr) {
            Ok(listening) => running_servers.push(RunningServer {
                listening,
                server_type: "HTTP".to_string(),
            }),
            Err(err) => panic!("{:?}", err),
        }
    }
    on_server_running(&opts.name, &running_servers);
}

#[cfg(test)]
mod test {
    extern crate hyper;
    extern crate iron_test;
    extern crate tempdir;

    use super::*;

    use std::fs::{DirBuilder, File};
    use std::io::Write;
    use std::path::{Path, PathBuf};

    use self::hyper::header::Headers;
    use self::iron_test::{request, response};
    use self::tempdir::TempDir;
    use iron::headers::{ContentLength, ContentType};
    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, body: Vec<u8>) -> PathBuf {
            let p = self.path().join(name);

            let mut file = File::create(&p).expect("Could not create file");
            file.write_all(&body).expect("Could not write to file");

            p
        }
    }

    #[test]
    fn staticfile_allow_request_methods() {
        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: opts.root,
            assets_dir: opts.assets,
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let response = request::head("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::Ok));

        let response = request::get("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::Ok));
    }

    #[test]
    fn staticfile_empty_body_on_head_request() {
        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: opts.root,
            assets_dir: opts.assets,
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let res = request::head("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(res.status, Some(status::Ok));

        let result_body = response::extract_body_to_bytes(res);
        assert_eq!(result_body, vec!());
    }

    #[test]
    fn staticfile_valid_content_length_on_head_request() {
        let root = TestFilesystemSetup::new();
        root.dir("root");
        root.file("index.html", b"<html><h2>hello</h2></html>".to_vec());

        let assets = TestFilesystemSetup::new();
        assets.dir("assets");

        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: root.path().to_str().unwrap().to_string(),
            assets_dir: assets.path().to_str().unwrap().to_string(),
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let res = request::head("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(res.status, Some(status::Ok));

        let content_length = res.headers.get::<ContentLength>().unwrap();

        assert_eq!(content_length.0, 27);
    }

    #[test]
    fn staticfile_zero_content_length_on_404_head_request() {
        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: opts.root,
            assets_dir: opts.assets,
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let res = request::head("http://127.0.0.1/unknown", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(res.status, Some(status::NotFound));

        let content_length = res.headers.get::<ContentLength>().unwrap();

        assert_eq!(content_length.0, 0);
    }

    #[test]
    fn staticfile_disallow_request_methods() {
        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: opts.root,
            assets_dir: opts.assets,
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let response = request::post("http://127.0.0.1/", Headers::new(), "", &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::MethodNotAllowed));

        let response = request::delete("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::MethodNotAllowed));

        let response = request::put("http://127.0.0.1/", Headers::new(), "", &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::MethodNotAllowed));

        let response = request::patch("http://127.0.0.1/", Headers::new(), "", &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::MethodNotAllowed));

        let response = request::options("http://127.0.0.1/", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(response.status, Some(status::MethodNotAllowed));
    }

    #[test]
    fn staticfile_valid_content_type_for_404() {
        let root = TestFilesystemSetup::new();
        root.dir("root");

        let assets = TestFilesystemSetup::new();
        assets.dir("assets");

        let opts = Options::from_args();

        let files = StaticFiles::new(StaticFilesOptions {
            root_dir: root.path().to_str().unwrap().to_string(),
            assets_dir: assets.path().to_str().unwrap().to_string(),
            page_50x_path: opts.page50x,
            page_404_path: opts.page404,
            cors_allow_origins: "".to_string(),
        });

        let res = request::head("http://127.0.0.1/unknown", Headers::new(), &files.handle())
            .expect("Response was a http error");

        assert_eq!(res.status, Some(status::NotFound));

        let content_type = res.headers.get::<ContentType>().unwrap();

        assert_eq!(
            content_type.0,
            "text/html".parse::<iron::mime::Mime>().unwrap()
        );
    }
}
diff --git a/src/server.rs b/src/server.rs
new file mode 100644
index 0000000..35202ef
--- /dev/null
+++ b/src/server.rs
@@ -0,0 +1,91 @@
use hyper_native_tls::NativeTlsServer;
use iron::{Chain, Iron, Listening};

use crate::signal_manager;
use crate::staticfile_middleware::HttpToHttpsRedirect;
use crate::staticfiles::*;
use crate::{config::Options, logger};

/// Struct for holding a reference to a running iron server instance
#[derive(Debug)]
struct RunningServer {
    listening: Listening,
    server_type: String,
}

fn on_server_running(server_name: &str, running_servers: &[RunningServer]) {
    // Notify when server is running
    running_servers.iter().for_each(|server| {
        logger::log_server(&format!(
            "{} Server ({}) is listening on {}",
            server.server_type, server_name, server.listening.socket
        ))
    });

    // Wait for incoming signals (E.g Ctrl+C (SIGINT), SIGTERM, etc
    signal_manager::wait_for_signal(|sig: signal::Signal| {
        let code = signal_manager::signal_to_int(sig);

        println!();
        warn!("Signal {} caught. Server execution exited.", code);
        std::process::exit(code)
    })
}

pub fn run(opts: Options) {
    logger::init(&opts.log_level);

    let addr = &format!("{}{}{}", opts.host, ":", opts.port);

    // Configure & launch the HTTP server

    let files = StaticFiles::new(StaticFilesOptions {
        root_dir: opts.root,
        assets_dir: opts.assets,
        page_50x_path: opts.page50x,
        page_404_path: opts.page404,
        cors_allow_origins: opts.cors_allow_origins,
    });

    let mut running_servers = Vec::new();
    if opts.tls {
        // Launch static HTTPS server
        let ssl = NativeTlsServer::new(opts.tls_pkcs12, &opts.tls_pkcs12_passwd).unwrap();

        match Iron::new(files.handle()).https(addr, ssl) {
            Ok(listening) => running_servers.push(RunningServer {
                listening,
                server_type: "HTTPS".to_string(),
            }),
            Err(err) => panic!("{:?}", err),
        }

        // Launch redirect HTTP server (if requested)
        if let Some(port_redirect) = opts.tls_redirect_from {
            let addr_redirect = &format!("{}{}{}", opts.host, ":", port_redirect);
            let host_redirect = match opts.tls_redirect_host.as_ref() {
                Some(host) => host,
                None => &opts.host,
            };
            let handler =
                Chain::new(HttpToHttpsRedirect::new(&host_redirect, opts.port).permanent());
            match Iron::new(handler).http(addr_redirect) {
                Ok(listening) => running_servers.push(RunningServer {
                    listening,
                    server_type: "Redirect HTTP".to_string(),
                }),
                Err(err) => panic!("{:?}", err),
            }
        }
    } else {
        // Launch static HTTP server
        match Iron::new(files.handle()).http(addr) {
            Ok(listening) => running_servers.push(RunningServer {
                listening,
                server_type: "HTTP".to_string(),
            }),
            Err(err) => panic!("{:?}", err),
        }
    }
    on_server_running(&opts.name, &running_servers);
}
diff --git a/src/staticfile_middleware/cache.rs b/src/staticfile_middleware/cache.rs
new file mode 100644
index 0000000..78375bc
--- /dev/null
+++ b/src/staticfile_middleware/cache.rs
@@ -0,0 +1,36 @@
use std::time::Duration;
use std::{cmp, u32};

use iron::headers::{CacheControl, CacheDirective};
use iron::modifier::Modifier;
use iron::modifiers::Header;
use iron::prelude::*;
use iron::status;

/// Sets the Cache-Control header for successful responses.
#[derive(Debug, Copy, Clone)]
pub struct Cache(u32);

impl Cache {
    pub fn new(duration: Duration) -> Cache {
        // Capping the value at ~136 years!
        let duration = cmp::min(duration.as_secs(), u32::MAX as u64) as u32;

        Cache(duration)
    }
}

impl Modifier<Response> for Cache {
    fn modify(self, response: &mut Response) {
        match response.status {
            Some(status::Ok) | Some(status::NotModified) => (),
            _ => return,
        }

        Header(CacheControl(vec![
            CacheDirective::Public,
            CacheDirective::MaxAge(self.0),
        ]))
        .modify(response)
    }
}
diff --git a/src/staticfile_middleware/guess_content_type.rs b/src/staticfile_middleware/guess_content_type.rs
new file mode 100644
index 0000000..2a4ad50
--- /dev/null
+++ b/src/staticfile_middleware/guess_content_type.rs
@@ -0,0 +1,47 @@
use iron::headers::ContentType;
use iron::prelude::*;
use iron::AfterMiddleware;

use mime::Mime;

/// Attempts to guess the content type of the response based on the
/// requested URL. Existing content types will not be modified.
pub struct GuessContentType {
    default: Mime,
}

impl GuessContentType {
    pub fn new(default: Mime) -> GuessContentType {
        GuessContentType { default }
    }
}

impl Default for GuessContentType {
    fn default() -> GuessContentType {
        let default = "application/octet-stream"
            .parse()
            .expect("Unable to create default MIME type");
        GuessContentType::new(default)
    }
}

impl AfterMiddleware for GuessContentType {
    fn after(&self, req: &mut Request, mut res: Response) -> IronResult<Response> {
        match res.headers.get::<ContentType>() {
            Some(_) => {}
            None => {
                let new_content_type = req
                    .url
                    .path()
                    .last()
                    .and_then(mime_guess::guess_mime_type_opt)
                    .unwrap_or_else(|| self.default.clone());

                let header = ContentType(new_content_type);
                res.headers.set(header);
            }
        }

        Ok(res)
    }
}
diff --git a/src/staticfile_middleware/helpers.rs b/src/staticfile_middleware/helpers.rs
new file mode 100644
index 0000000..8a56e42
--- /dev/null
+++ b/src/staticfile_middleware/helpers.rs
@@ -0,0 +1,41 @@
use iron::headers::{AcceptEncoding, ContentType, Encoding};
use mime::Mime;
use std::option::Option;

// Contains a common fixed list of text-based MIME types for Gzip compression
pub const TEXT_MIME_TYPES: [&str; 16] = [
    "text/html",
    "text/css",
    "text/javascript",
    "text/xml",
    "text/plain",
    "text/x-component",
    "application/javascript",
    "application/x-javascript",
    "application/json",
    "application/xml",
    "application/rss+xml",
    "application/atom+xml",
    "font/truetype",
    "font/opentype",
    "application/vnd.ms-fontobject",
    "image/svg+xml",
];

// Checks if a `content-type` header is a common text-based MIME type for Gzip compression
pub fn is_text_mime_type(content_type: Option<&ContentType>) -> bool {
    match content_type {
        Some(content_type) => TEXT_MIME_TYPES
            .iter()
            .any(|h| h.parse::<Mime>().unwrap() == content_type.0),
        None => false,
    }
}

// Checks if an `accept-encoding` header accepts Gzip encoding.
pub fn accept_gzip(accept_encoding: Option<&AcceptEncoding>) -> bool {
    match accept_encoding {
        Some(accept) => accept.0.iter().any(|qi| qi.item == Encoding::Gzip),
        None => false,
    }
}
diff --git a/src/staticfile_middleware/http_to_https_redirect.rs b/src/staticfile_middleware/http_to_https_redirect.rs
new file mode 100644
index 0000000..61e8022
--- /dev/null
+++ b/src/staticfile_middleware/http_to_https_redirect.rs
@@ -0,0 +1,56 @@
use iron::prelude::*;
use iron::{self, status};

#[derive(Debug)]
pub struct HttpToHttpsRedirect {
    permanent: bool,
    host: String,
    port: u16,
}

impl HttpToHttpsRedirect {
    pub fn new(host: &str, port: u16) -> Self {
        HttpToHttpsRedirect {
            permanent: false,
            host: host.into(),
            port,
        }
    }

    pub fn temporary(self) -> Self {
        HttpToHttpsRedirect {
            permanent: false,
            ..self
        }
    }

    pub fn permanent(self) -> Self {
        HttpToHttpsRedirect {
            permanent: true,
            ..self
        }
    }
}

impl iron::Handler for HttpToHttpsRedirect {
    fn handle(&self, req: &mut Request) -> IronResult<Response> {
        let mut url: url::Url = req.url.clone().into();

        url.set_scheme("https")
            .expect("Unable to rewrite URL scheme");
        url.set_host(Some(&self.host))
            .expect("Unable to rewrite URL host");
        url.set_port(Some(self.port))
            .expect("Unable to rewrite URL port");

        let url = iron::Url::from_generic_url(url).expect("Unable to rewrite HTTP URL to HTTPS");

        let status = if self.permanent {
            status::PermanentRedirect
        } else {
            status::TemporaryRedirect
        };

        Ok(Response::with((status, iron::modifiers::Redirect(url))))
    }
}
diff --git a/src/staticfile_middleware/mod.rs b/src/staticfile_middleware/mod.rs
new file mode 100644
index 0000000..7c3ce74
--- /dev/null
+++ b/src/staticfile_middleware/mod.rs
@@ -0,0 +1,17 @@
mod cache;
mod guess_content_type;
pub mod helpers;
mod http_to_https_redirect;
mod modify_with;
mod partial_file;
mod prefix;
mod rewrite;
mod staticfile;

pub use self::cache::Cache;
pub use self::guess_content_type::GuessContentType;
pub use self::http_to_https_redirect::HttpToHttpsRedirect;
pub use self::modify_with::ModifyWith;
pub use self::prefix::Prefix;
pub use self::rewrite::Rewrite;
pub use self::staticfile::Staticfile;
diff --git a/src/staticfile_middleware/modify_with.rs b/src/staticfile_middleware/modify_with.rs
new file mode 100644
index 0000000..04ba9b1
--- /dev/null
+++ b/src/staticfile_middleware/modify_with.rs
@@ -0,0 +1,24 @@
use iron::modifier::Modifier;
use iron::prelude::*;
use iron::AfterMiddleware;

/// Applies a modifier to every request.
pub struct ModifyWith<M> {
    modifier: M,
}

impl<M> ModifyWith<M> {
    pub fn new(modifier: M) -> ModifyWith<M> {
        ModifyWith { modifier }
    }
}

impl<M> AfterMiddleware for ModifyWith<M>
where
    M: Clone + Modifier<Response> + Send + Sync + 'static,
{
    fn after(&self, _req: &mut Request, mut res: Response) -> IronResult<Response> {
        self.modifier.clone().modify(&mut res);
        Ok(res)
    }
}
diff --git a/src/staticfile_middleware/partial_file.rs b/src/staticfile_middleware/partial_file.rs
new file mode 100644
index 0000000..e939017
--- /dev/null
+++ b/src/staticfile_middleware/partial_file.rs
@@ -0,0 +1,138 @@
// NOTE:
// This file implements Partial Content Delivery which is used as part as this middleware.
// Code below was borrowed from one @Cobrand's PR and adapted to this project.
// More details at https://github.com/iron/staticfile/pull/98

use iron::headers::{ByteRangeSpec, ContentLength, ContentRange, ContentRangeSpec};
use iron::modifier::Modifier;
use iron::response::{Response, WriteBody};
use iron::status::Status;
use std::cmp;
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;

pub enum PartialFileRange {
    AllFrom(u64),
    FromTo(u64, u64),
    Last(u64),
}

pub struct PartialFile {
    file: File,
    range: PartialFileRange,
}

struct PartialContentBody {
    pub file: File,
    pub offset: u64,
    pub len: u64,
}

impl PartialFile {
    pub fn new<Range>(file: File, range: Range) -> PartialFile
    where
        Range: Into<PartialFileRange>,
    {
        let range = range.into();
        PartialFile { file, range }
    }

    pub fn from_path<P: AsRef<Path>, Range>(path: P, range: Range) -> Result<PartialFile, io::Error>
    where
        Range: Into<PartialFileRange>,
    {
        let file = File::open(path.as_ref())?;
        Ok(Self::new(file, range))
    }
}

impl From<ByteRangeSpec> for PartialFileRange {
    fn from(b: ByteRangeSpec) -> PartialFileRange {
        match b {
            ByteRangeSpec::AllFrom(from) => PartialFileRange::AllFrom(from),
            ByteRangeSpec::FromTo(from, to) => PartialFileRange::FromTo(from, to),
            ByteRangeSpec::Last(last) => PartialFileRange::Last(last),
        }
    }
}

impl From<Vec<ByteRangeSpec>> for PartialFileRange {
    fn from(v: Vec<ByteRangeSpec>) -> PartialFileRange {
        match v.into_iter().next() {
            // in the case no value is in "Range", return
            // the whole file instead of panicking
            // Note that an empty vec should never happen,
            // but we can never be too sure
            None => PartialFileRange::AllFrom(0),
            Some(byte_range) => PartialFileRange::from(byte_range),
        }
    }
}

impl Modifier<Response> for PartialFile {
    #[inline]
    fn modify(self, res: &mut Response) {
        use self::PartialFileRange::*;

        let metadata: Option<_> = self.file.metadata().ok();
        let file_length: Option<u64> = metadata.map(|m| m.len());
        let range: Option<(u64, u64)> = match (self.range, file_length) {
            (FromTo(from, to), Some(file_length)) => {
                if from <= to && from < file_length {
                    Some((from, cmp::min(to, file_length - 1)))
                } else {
                    None
                }
            }
            (AllFrom(from), Some(file_length)) => {
                if from < file_length {
                    Some((from, file_length - 1))
                } else {
                    None
                }
            }
            (Last(last), Some(file_length)) => {
                if last < file_length {
                    Some((file_length - last, file_length - 1))
                } else {
                    Some((0, file_length - 1))
                }
            }
            (_, None) => None,
        };

        if let Some(range) = range {
            let content_range = ContentRange(ContentRangeSpec::Bytes {
                range: Some(range),
                instance_length: file_length,
            });
            let content_len = range.1 - range.0 + 1;
            res.headers.set(ContentLength(content_len));
            res.headers.set(content_range);
            let partial_content = PartialContentBody {
                file: self.file,
                offset: range.0,
                len: content_len,
            };
            res.status = Some(Status::PartialContent);
            res.body = Some(Box::new(partial_content));
        } else {
            if let Some(file_length) = file_length {
                res.headers.set(ContentRange(ContentRangeSpec::Bytes {
                    range: None,
                    instance_length: Some(file_length),
                }));
            };
            res.status = Some(Status::RangeNotSatisfiable);
        }
    }
}

impl WriteBody for PartialContentBody {
    fn write_body(&mut self, res: &mut dyn Write) -> io::Result<()> {
        self.file.seek(SeekFrom::Start(self.offset))?;
        let mut limiter = <File as Read>::by_ref(&mut self.file).take(self.len);
        io::copy(&mut limiter, res).map(|_| ())
    }
}
diff --git a/src/staticfile_middleware/prefix.rs b/src/staticfile_middleware/prefix.rs
new file mode 100644
index 0000000..ac7d9ae
--- /dev/null
+++ b/src/staticfile_middleware/prefix.rs
@@ -0,0 +1,45 @@
use iron::modifier::Modifier;
use iron::prelude::*;
use iron::AfterMiddleware;

/// Applies a modifier to every request that starts with a given path.
pub struct Prefix<M> {
    prefix: Vec<String>,
    modifier: M,
}

impl<M> Prefix<M> {
    pub fn new<P, S>(prefix: P, modifier: M) -> Prefix<M>
    where
        P: IntoIterator<Item = S>,
        S: AsRef<str>,
    {
        Prefix {
            prefix: prefix.into_iter().map(|s| s.as_ref().into()).collect(),
            modifier,
        }
    }

    fn prefix_matches(&self, path: &[&str]) -> bool {
        if self.prefix.len() > path.len() {
            return false;
        }

        path.iter()
            .zip(self.prefix.iter())
            .all(|(path, prefix)| path == prefix)
    }
}

impl<M> AfterMiddleware for Prefix<M>
where
    M: Clone + Modifier<Response> + Send + Sync + 'static,
{
    fn after(&self, req: &mut Request, mut res: Response) -> IronResult<Response> {
        if self.prefix_matches(&req.url.path()) {
            self.modifier.clone().modify(&mut res);
        }

        Ok(res)
    }
}
diff --git a/src/staticfile_middleware/rewrite.rs b/src/staticfile_middleware/rewrite.rs
new file mode 100644
index 0000000..681173f
--- /dev/null
+++ b/src/staticfile_middleware/rewrite.rs
@@ -0,0 +1,34 @@
use iron::prelude::*;

pub struct Rewrite {
    from: Vec<Vec<String>>,
    to: String,
}

impl Rewrite {
    pub fn new(from_paths: Vec<Vec<String>>, to_path: String) -> Self {
        Rewrite {
            from: from_paths,
            to: to_path,
        }
    }
}

impl iron::BeforeMiddleware for Rewrite {
    fn before(&self, req: &mut Request) -> IronResult<()> {
        let should_rewrite = {
            let request_path = req.url.path();
            self.from
                .iter()
                .any(|rewrite_path| request_path == *rewrite_path)
        };

        if should_rewrite {
            let mut u: url::Url = req.url.clone().into();
            u.set_path(&self.to);
            req.url = iron::Url::from_generic_url(u).expect("Invalid rewritten URL");
        }

        Ok(())
    }
}
diff --git a/src/staticfile_middleware/staticfile.rs b/src/staticfile_middleware/staticfile.rs
new file mode 100644
index 0000000..b8e03fe
--- /dev/null
+++ b/src/staticfile_middleware/staticfile.rs
@@ -0,0 +1,346 @@
use std::ffi::OsString;
use std::fs::{File, Metadata};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use std::{error, io};

use iron::headers::{
    AcceptEncoding, 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 crate::staticfile_middleware::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,
}

impl Staticfile {
    pub fn new<P>(root: P, assets: P) -> io::Result<Staticfile>
    where
        P: AsRef<Path>,
    {
        let root = root.as_ref().canonicalize()?;
        let assets = assets.as_ref().canonicalize()?;

        Ok(Staticfile { root, assets })
    }

    fn resolve_path(&self, path: &[&str]) -> Result<PathBuf, Box<dyn error::Error>> {
        let path_dirname = path[0];
        let asserts_dirname = self.assets.iter().last().unwrap().to_str().unwrap();
        let mut is_assets = false;

        let resolved = if path_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(component);
            }

            res
        } else {
            // Root path validation resolve
            let mut res = self.root.clone();
            for component in path {
                res.push(component);
            }

            res
        };

        let resolved = resolved.canonicalize()?;
        let path = if is_assets { &self.assets } else { &self.root };

        // Protect against path/directory traversal
        if !resolved.starts_with(&path) {
            return Result::Err(From::from(format!("Cannot leave {:?} path", &path)));
        }

        Ok(resolved)
    }
}

impl Handler for Staticfile {
    fn handle(&self, req: &mut Request) -> IronResult<Response> {
        // 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 file_path = match self.resolve_path(&req.url.path()) {
            Ok(file_path) => file_path,
            Err(_) => return Ok(Response::with(status::NotFound)),
        };

        // Get current file metadata
        let accept_gz = helpers::accept_gzip(req.headers.get::<AcceptEncoding>());
        let file = match StaticFileWithMetadata::search(&file_path, accept_gz) {
            Ok(file) => file,
            Err(_) => 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);

        if let (Some(client_last_modified), Some(last_modified)) =
            (client_last_modified, last_modified)
        {
            trace!(
                "Comparing {} (file) <= {} (req)",
                last_modified,
                client_last_modified.0
            );

            if last_modified <= client_last_modified.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]);

        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,
                ))
            }
            None => Response::with((status::Ok, Header(encoding), 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(file.metadata.len())));
            return Ok(resp);
        }

        // Partial Content Delivery response
        // Enable the "Accept-Ranges" header on all files
        resp.set_mut(Header(AcceptRanges(vec![RangeUnit::Bytes])));

        let resp = match req.headers.get::<Range>().cloned() {
            // Deliver the whole file
            None => resp,
            // Try to deliver partial content
            Some(Range::Bytes(v)) => {
                if let Ok(partial_file) = PartialFile::from_path(&file_path, 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 StaticFileWithMetadata {
    file: File,
    metadata: Metadata,
    is_gz: bool,
}

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)?;
        }

        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)
            }
        } 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,
        })
    }

    pub fn last_modified(&self) -> Result<time::Tm, Box<dyn error::Error>> {
        let modified = self.metadata.modified()?;
        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 {
    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()).unwrap();
        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()).unwrap();
        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).unwrap();
        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()).unwrap();

        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));
    }
}
diff --git a/src/staticfiles.rs b/src/staticfiles.rs
index 7649ac8..07de16f 100644
--- a/src/staticfiles.rs
+++ b/src/staticfiles.rs
@@ -1,15 +1,15 @@
use crate::error_page::ErrorPage;
use crate::gzip::GzipMiddleware;
use crate::helpers;
use crate::logger::{log_server, Logger};

use iron::mime;
use iron::prelude::*;
use iron_cors::CorsMiddleware;
use iron_staticfile_middleware::{Cache, GuessContentType, ModifyWith, Prefix, Staticfile};
use std::collections::HashSet;
use std::time::Duration;

use crate::error_page::ErrorPage;
use crate::gzip::GzipMiddleware;
use crate::helpers;
use crate::logger::{log_server, Logger};
use crate::staticfile_middleware::{Cache, GuessContentType, ModifyWith, Prefix, Staticfile};

/// An Iron middleware for static files-serving.
pub struct StaticFiles {
    opts: StaticFilesOptions,