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(-)
@@ -15,5 +15,6 @@
**/*.env
release
.vscode
TODO
!sample.env
@@ -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"
@@ -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
@@ -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()
);
}
}
@@ -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> {
if req.method == iron::method::Head {
return Ok(resp);
}
let enable_gz = helpers::is_text_mime_type(resp.headers.get::<ContentType>());
let accept_gz = helpers::accept_gzip(req.headers.get::<AcceptEncoding>());
if !helpers::is_text_mime_type(resp.headers.get::<ContentType>()) {
return Ok(resp);
}
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)
@@ -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;
@@ -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;
#[derive(Debug)]
struct RunningServer {
listening: Listening,
server_type: String,
}
fn on_server_running(server_name: &str, running_servers: &[RunningServer]) {
running_servers.iter().for_each(|server| {
logger::log_server(&format!(
"{} Server ({}) is listening on {}",
server.server_type, server_name, server.listening.socket
))
});
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);
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 {
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),
}
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 {
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()
);
}
}
@@ -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};
#[derive(Debug)]
struct RunningServer {
listening: Listening,
server_type: String,
}
fn on_server_running(server_name: &str, running_servers: &[RunningServer]) {
running_servers.iter().for_each(|server| {
logger::log_server(&format!(
"{} Server ({}) is listening on {}",
server.server_type, server_name, server.listening.socket
))
});
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);
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 {
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),
}
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 {
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);
}
@@ -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;
#[derive(Debug, Copy, Clone)]
pub struct Cache(u32);
impl Cache {
pub fn new(duration: Duration) -> Cache {
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)
}
}
@@ -0,0 +1,47 @@
use iron::headers::ContentType;
use iron::prelude::*;
use iron::AfterMiddleware;
use mime::Mime;
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)
}
}
@@ -0,0 +1,41 @@
use iron::headers::{AcceptEncoding, ContentType, Encoding};
use mime::Mime;
use std::option::Option;
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",
];
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,
}
}
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,
}
}
@@ -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))))
}
}
@@ -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;
@@ -0,0 +1,24 @@
use iron::modifier::Modifier;
use iron::prelude::*;
use iron::AfterMiddleware;
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)
}
}
@@ -0,0 +1,138 @@
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() {
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(|_| ())
}
}
@@ -0,0 +1,45 @@
use iron::modifier::Modifier;
use iron::prelude::*;
use iron::AfterMiddleware;
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)
}
}
@@ -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(())
}
}
@@ -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;
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 {
is_assets = true;
let mut res = self.assets.clone();
for component in path.iter().skip(1) {
res.push(component);
}
res
} else {
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 };
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> {
if !(req.method == Method::Head || req.method == Method::Get) {
return Ok(Response::with(status::MethodNotAllowed));
}
let file_path = match self.resolve_path(&req.url.path()) {
Ok(file_path) => file_path,
Err(_) => return Ok(Response::with(status::NotFound)),
};
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)),
};
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));
}
}
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)),
};
if req.method == Method::Head {
resp.set_mut(vec![]);
resp.set_mut(Header(ContentLength(file.metadata.len())));
return Ok(resp);
}
resp.set_mut(Header(AcceptRanges(vec![RangeUnit::Bytes])));
let resp = match req.headers.get::<Range>().cloned() {
None => resp,
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>>
where
P: Into<PathBuf>,
{
let mut file_path = path.into();
trace!("Opening {}", file_path.display());
let mut file = StaticFileWithMetadata::open(&file_path)?;
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 {
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)?;
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));
}
}
@@ -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};
pub struct StaticFiles {
opts: StaticFilesOptions,