feat: directory listing support
Diff
Cargo.lock | 15 ++-
Cargo.toml | 1 +-
README.md | 11 +-
src/bin/server.rs | 6 +-
src/config.rs | 8 ++-
src/server.rs | 3 +-
src/staticfile_middleware/staticfile.rs | 149 ++++++++++++++++++++++++++-------
src/staticfiles.rs | 4 +-
8 files changed, 158 insertions(+), 39 deletions(-)
@@ -243,6 +243,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691"
[[package]]
name = "humansize"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
[[package]]
name = "humantime"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -366,9 +372,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.85"
version = "0.2.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3"
checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c"
[[package]]
name = "log"
@@ -942,6 +948,7 @@ dependencies = [
"chrono",
"env_logger",
"flate2",
"humansize",
"hyper",
"hyper-native-tls",
"iron",
@@ -1119,9 +1126,9 @@ dependencies = [
[[package]]
name = "unicode-normalization"
version = "0.1.16"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606"
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
dependencies = [
"tinyvec",
]
@@ -28,6 +28,7 @@ path = "src/bin/server.rs"
chrono = "0.4"
env_logger = "0.7"
flate2 = "1.0"
humansize = "1.1"
hyper-native-tls = "0.3"
iron = "0.6"
iron-cors = "0.8"
@@ -19,6 +19,7 @@
- [HEAD](https://tools.ietf.org/html/rfc7231#section-4.3.2) responses support.
- [TLS](https://www.openssl.org/) support via [Rust Native TLS](https://docs.rs/native-tls/0.2.3/native_tls/) crate.
- Lightweight and configurable logging.
- Directory listing support.
- First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch) and latest [Alpine Linux](https://hub.docker.com/_/alpine) Docker images available.
- Server configurable via environment variables or CLI arguments.
- MacOs binary support (`x86_64-apple-darwin`) thanks to [Rust Linux / Darwin Builder](https://github.com/joseluisq/rust-linux-darwin-builder).
@@ -52,7 +53,8 @@ Server can be configured either via environment variables or their equivalent co
### Command-line arguments
@@ -74,8 +76,11 @@ OPTIONS:
Assets directory path for add cache headers functionality [env: SERVER_ASSETS=] [default: ./public/assets]
-c, --cors-allow-origins <cors-allow-origins>
Specify a CORS list of allowed origin hosts separated by comas with no whitespaces. Host ports or protocols
aren't being checked. Use an asterisk (*) to allow any host [env: SERVER_CORS_ALLOW_ORIGINS=] [default: ]
Specify a CORS list of allowed origin hosts separated by comas. Host ports or protocols aren't being
checked. Use an asterisk (*) to allow any host [env: SERVER_CORS_ALLOW_ORIGINS=] [default: ]
-i, --directory-listing <directory-listing>
Enable directory listing for all requests ending with the slash character (‘/’) [env:
SERVER_DIRECTORY_LISTING=]
-a, --host <host> Host address (E.g 127.0.0.1) [env: SERVER_HOST=] [default: [::]]
-g, --log-level <log-level>
Specify a logging level in lower case [env: SERVER_LOG_LEVEL=] [default: error]
@@ -71,6 +71,7 @@ mod test {
page_50x_path: opts.page50x,
page_404_path: opts.page404,
cors_allow_origins: "".to_string(),
directory_listing: false,
});
let response = request::head("http://127.0.0.1/", Headers::new(), &files.handle())
@@ -94,6 +95,7 @@ mod test {
page_50x_path: opts.page50x,
page_404_path: opts.page404,
cors_allow_origins: "".to_string(),
directory_listing: false,
});
let res = request::head("http://127.0.0.1/", Headers::new(), &files.handle())
@@ -122,6 +124,7 @@ mod test {
page_50x_path: opts.page50x,
page_404_path: opts.page404,
cors_allow_origins: "".to_string(),
directory_listing: false,
});
let res = request::head("http://127.0.0.1/", Headers::new(), &files.handle())
@@ -144,6 +147,7 @@ mod test {
page_50x_path: opts.page50x,
page_404_path: opts.page404,
cors_allow_origins: "".to_string(),
directory_listing: false,
});
let res = request::head("http://127.0.0.1/unknown", Headers::new(), &files.handle())
@@ -166,6 +170,7 @@ mod test {
page_50x_path: opts.page50x,
page_404_path: opts.page404,
cors_allow_origins: "".to_string(),
directory_listing: false,
});
let response = request::post("http://127.0.0.1/", Headers::new(), "", &files.handle())
@@ -210,6 +215,7 @@ mod test {
page_50x_path: opts.page50x,
page_404_path: opts.page404,
cors_allow_origins: "".to_string(),
directory_listing: false,
});
let res = request::head("http://127.0.0.1/unknown", Headers::new(), &files.handle())
@@ -81,4 +81,12 @@ pub struct Options {
)]
pub cors_allow_origins: String,
#[structopt(
long,
short = "i",
env = "SERVER_DIRECTORY_LISTING"
)]
pub directory_listing: bool,
}
@@ -32,19 +32,20 @@ fn on_server_running(server_name: &str, running_servers: &[RunningServer]) {
})
}
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,
directory_listing: opts.directory_listing,
});
let mut running_servers = Vec::new();
@@ -1,19 +1,18 @@
use std::ffi::OsString;
use std::fs::{File, Metadata};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use std::{error, io};
use humansize::{file_size_opts, FileSize};
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 std::fs::{File, Metadata};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use std::{error, io};
use std::{ffi::OsString, time::SystemTime};
use crate::staticfile_middleware::helpers;
use crate::staticfile_middleware::partial_file::PartialFile;
@@ -22,17 +21,22 @@ use crate::staticfile_middleware::partial_file::PartialFile;
pub struct Staticfile {
root: PathBuf,
assets: PathBuf,
dir_list: bool,
}
impl Staticfile {
pub fn new<P>(root: P, assets: P) -> io::Result<Staticfile>
pub fn new<P>(root: P, assets: P, dir_list: bool) -> io::Result<Staticfile>
where
P: AsRef<Path>,
{
let root = root.as_ref().canonicalize()?;
let assets = assets.as_ref().canonicalize()?;
Ok(Staticfile { root, assets })
Ok(Staticfile {
root,
assets,
dir_list,
})
}
fn resolve_path(&self, path: &[&str]) -> Result<PathBuf, Box<dyn error::Error>> {
@@ -85,10 +89,93 @@ impl Handler for Staticfile {
Err(_) => return Ok(Response::with(status::NotFound)),
};
if self.dir_list && file_path.is_dir() && !file_path.join("index.html").exists() {
let encoding = Encoding::Identity;
let readir = match std::fs::read_dir(file_path) {
Ok(dir) => dir,
Err(err) => {
error!("{}", err);
return Ok(Response::with(status::InternalServerError));
}
};
let mut current_path = req
.url
.path()
.into_iter()
.map(|i| format!("/{}", i))
.collect::<String>();
if !current_path.ends_with('/') {
let mut u: url::Url = req.url.clone().into();
current_path.push('/');
u.set_path(¤t_path);
let url = iron::Url::from_generic_url(u).expect("Unable to parse redirect url");
return Ok(Response::with((
status::PermanentRedirect,
iron::modifiers::Redirect(url),
)));
}
let mut entries_str = String::new();
if current_path != "/" {
entries_str =
String::from("<tr><td colspan=\"3\"><a href=\"../\">../</a></td></tr>");
}
for entry in readir {
let entry = entry.unwrap();
let meta = entry.metadata().unwrap();
let mut filesize = meta.len().file_size(file_size_opts::DECIMAL).unwrap();
let mut name = entry.file_name().into_string().unwrap();
if meta.is_dir() {
name = format!("{}/", name);
filesize = String::from("-")
}
let uri = format!("{}{}", current_path, name);
let modified = get_last_modified(meta.modified().unwrap()).unwrap();
entries_str = format!(
"{}<tr><td><a href=\"{}\" title=\"{}\">{}</a></td><td style=\"width: 160px;\">{}</td><td align=\"right\" style=\"width: 140px;\">{}</td></tr>",
entries_str,
uri,
name,
name,
modified.to_local().strftime("%F %T").unwrap(),
filesize
);
}
let page = format!(
"<html><head><title>Index of {}</title></head><body><h1>Index of {}</h1><table style=\"min-width:680px;\"><tr><th colspan=\"3\"><hr></th></tr>{}<tr><th colspan=\"3\"><hr></th></tr></table></body></html>", current_path, current_path, entries_str
);
let len = page.len() as u64;
let content_encoding = ContentEncoding(vec![encoding]);
let mut resp = Response::with((status::Ok, Header(content_encoding), page));
if req.method == Method::Head {
resp.set_mut(vec![]);
resp.set_mut(Header(ContentLength(len)));
}
return Ok(resp);
}
let accept_gz = helpers::accept_gzip(req.headers.get::<AcceptEncoding>());
let file = match StaticFileWithMetadata::search(&file_path, accept_gz) {
Ok(file) => file,
Ok(f) => f,
Err(_) => return Ok(Response::with(status::NotFound)),
};
@@ -118,6 +205,7 @@ impl Handler for Staticfile {
};
let encoding = ContentEncoding(vec![encoding]);
let mut resp = match last_modified {
Some(last_modified) => {
let last_modified = LastModified(last_modified);
@@ -143,8 +231,7 @@ impl Handler for Staticfile {
resp.set_mut(Header(AcceptRanges(vec![RangeUnit::Bytes])));
let resp = match req.headers.get::<Range>().cloned() {
resp = match req.headers.get::<Range>().cloned() {
None => resp,
@@ -234,23 +321,25 @@ impl StaticFileWithMetadata {
}
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))
get_last_modified(self.metadata.modified()?)
}
}
fn get_last_modified(modified: SystemTime) -> Result<time::Tm, Box<dyn error::Error>> {
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;
@@ -301,7 +390,7 @@ mod test {
let fs2 = TestFilesystemSetup::new();
fs2.dir("assets");
let sf = Staticfile::new(fs.path(), fs2.path()).unwrap();
let sf = Staticfile::new(fs.path(), fs2.path(), false).unwrap();
let path = sf.resolve_path(&["index.html"]);
assert!(path.unwrap().ends_with("index.html"));
}
@@ -314,7 +403,7 @@ mod test {
let fs2 = TestFilesystemSetup::new();
fs2.file("assets");
let sf = Staticfile::new(fs.path(), fs2.path()).unwrap();
let sf = Staticfile::new(fs.path(), fs2.path(), false).unwrap();
let path = sf.resolve_path(&["dir", "index.html"]);
assert!(path.unwrap().ends_with("dir/index.html"));
}
@@ -327,7 +416,7 @@ mod test {
let fs2 = TestFilesystemSetup::new();
let dir2 = fs2.file("assets");
let sf = Staticfile::new(dir, dir2).unwrap();
let sf = Staticfile::new(dir, dir2, false).unwrap();
let path = sf.resolve_path(&["..", "naughty.txt"]);
assert!(path.is_err());
}
@@ -336,7 +425,7 @@ mod test {
fn staticfile_disallows_post_requests() {
let fs = TestFilesystemSetup::new();
let fs2 = TestFilesystemSetup::new();
let sf = Staticfile::new(fs.path(), fs2.path()).unwrap();
let sf = Staticfile::new(fs.path(), fs2.path(), false).unwrap();
let response = request::post("http://127.0.0.1/", Headers::new(), "", &sf);
@@ -21,6 +21,7 @@ pub struct StaticFilesOptions {
pub page_50x_path: String,
pub page_404_path: String,
pub cors_allow_origins: String,
pub directory_listing: bool,
}
impl StaticFiles {
@@ -60,7 +61,8 @@ impl StaticFiles {
let mut chain = Chain::new(
Staticfile::new(root_dir, assets_dir).expect("Directory to serve files was not found"),
Staticfile::new(root_dir, assets_dir, self.opts.directory_listing)
.expect("Directory to serve files was not found"),
);
let one_day = Duration::new(60 * 60 * 24, 0);
let one_year = Duration::new(60 * 60 * 24 * 365, 0);