aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSven-Hendrik Haase <svenstaro@gmail.com>2019-02-13 21:34:57 +0000
committerGitHub <noreply@github.com>2019-02-13 21:34:57 +0000
commite543c5a7c3a1e9de0dcf92ab33dc237b85a8a632 (patch)
treeae9e1c915d1f88c4248d9ae62a5be9acd03fdcf5
parentThis is v0.3.0 (diff)
parentAdded some docstrings (diff)
downloadminiserve-e543c5a7c3a1e9de0dcf92ab33dc237b85a8a632.tar.gz
miniserve-e543c5a7c3a1e9de0dcf92ab33dc237b85a8a632.zip
Merge pull request #32 from boastful-squirrel/split-files
Split project into multiple files
-rw-r--r--src/args.rs173
-rw-r--r--src/auth.rs77
-rw-r--r--src/listing.rs272
-rw-r--r--src/main.rs578
4 files changed, 584 insertions, 516 deletions
diff --git a/src/args.rs b/src/args.rs
new file mode 100644
index 0000000..ae86108
--- /dev/null
+++ b/src/args.rs
@@ -0,0 +1,173 @@
+use crate::auth;
+use crate::listing;
+use clap::{crate_authors, crate_description, crate_name, crate_version};
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+use std::path::PathBuf;
+
+/// Possible characters for random routes
+const ROUTE_ALPHABET: [char; 16] = [
+ '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f',
+];
+
+/// Checks wether a path is valid, i.e. it exists on the system and points to a file/directory
+fn is_valid_path(path: String) -> Result<(), String> {
+ let path_to_check = PathBuf::from(path);
+ if path_to_check.is_file() || path_to_check.is_dir() {
+ return Ok(());
+ }
+ Err(String::from(
+ "Path either doesn't exist or is not a regular file or a directory",
+ ))
+}
+
+/// Checks wether a port is valid
+fn is_valid_port(port: String) -> Result<(), String> {
+ port.parse::<u16>()
+ .and(Ok(()))
+ .or_else(|e| Err(e.to_string()))
+}
+
+
+/// Checks wether an interface is valid, i.e. it can be parsed into an IP address
+fn is_valid_interface(interface: String) -> Result<(), String> {
+ interface
+ .parse::<IpAddr>()
+ .and(Ok(()))
+ .or_else(|e| Err(e.to_string()))
+}
+
+/// Checks wether the auth string is valid, i.e. it follows the syntax username:password
+fn is_valid_auth(auth: String) -> Result<(), String> {
+ auth.find(':')
+ .ok_or_else(|| "Correct format is username:password".to_owned())
+ .map(|_| ())
+}
+
+/// Parses the command line arguments
+pub fn parse_args() -> crate::MiniserveConfig {
+ use clap::{App, AppSettings, Arg};
+
+ let matches = App::new(crate_name!())
+ .version(crate_version!())
+ .author(crate_authors!())
+ .about(crate_description!())
+ .global_setting(AppSettings::ColoredHelp)
+ .arg(
+ Arg::with_name("verbose")
+ .short("v")
+ .long("verbose")
+ .help("Be verbose, includes emitting access logs"),
+ )
+ .arg(
+ Arg::with_name("PATH")
+ .required(false)
+ .validator(is_valid_path)
+ .help("Which path to serve"),
+ )
+ .arg(
+ Arg::with_name("port")
+ .short("p")
+ .long("port")
+ .help("Port to use")
+ .validator(is_valid_port)
+ .required(false)
+ .default_value("8080")
+ .takes_value(true),
+ )
+ .arg(
+ Arg::with_name("interfaces")
+ .short("i")
+ .long("if")
+ .help("Interface to listen on")
+ .validator(is_valid_interface)
+ .required(false)
+ .takes_value(true)
+ .multiple(true),
+ )
+ .arg(
+ Arg::with_name("auth")
+ .short("a")
+ .long("auth")
+ .validator(is_valid_auth)
+ .help("Set authentication (username:password)")
+ .takes_value(true),
+ )
+ .arg(
+ Arg::with_name("random-route")
+ .long("random-route")
+ .help("Generate a random 6-hexdigit route"),
+ )
+ .arg(
+ Arg::with_name("sort")
+ .short("s")
+ .long("sort")
+ .possible_values(&["natural", "alpha", "dirsfirst"])
+ .default_value("natural")
+ .help("Sort files"),
+ )
+ .arg(
+ Arg::with_name("reverse")
+ .long("reverse")
+ .help("Reverse sorting order"),
+ )
+ .arg(
+ Arg::with_name("no-symlinks")
+ .short("P")
+ .long("no-symlinks")
+ .help("Do not follow symbolic links"),
+ )
+ .get_matches();
+
+ let verbose = matches.is_present("verbose");
+ let no_symlinks = matches.is_present("no-symlinks");
+ let path = matches.value_of("PATH");
+ let port = matches.value_of("port").unwrap().parse().unwrap();
+ let interfaces = if let Some(interfaces) = matches.values_of("interfaces") {
+ interfaces.map(|x| x.parse().unwrap()).collect()
+ } else {
+ vec![
+ IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)),
+ IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
+ ]
+ };
+ let auth = if let Some(auth_split) = matches.value_of("auth").map(|x| x.splitn(2, ':')) {
+ let auth_vec = auth_split.collect::<Vec<&str>>();
+ if auth_vec.len() == 2 {
+ Some(auth::BasicAuthParams {
+ username: auth_vec[0].to_owned(),
+ password: auth_vec[1].to_owned(),
+ })
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ let random_route = if matches.is_present("random-route") {
+ Some(nanoid::custom(6, &ROUTE_ALPHABET))
+ } else {
+ None
+ };
+
+ let sort_method = matches
+ .value_of("sort")
+ .unwrap()
+ .parse::<listing::SortingMethods>()
+ .unwrap();
+
+ let reverse_sort = matches.is_present("reverse");
+
+ crate::MiniserveConfig {
+ verbose,
+ path: PathBuf::from(path.unwrap_or(".")),
+ port,
+ interfaces,
+ auth,
+ path_explicitly_chosen: path.is_some(),
+ no_symlinks,
+ random_route,
+ sort_method,
+ reverse_sort,
+ }
+}
diff --git a/src/auth.rs b/src/auth.rs
new file mode 100644
index 0000000..7b72b21
--- /dev/null
+++ b/src/auth.rs
@@ -0,0 +1,77 @@
+use actix_web::http::header;
+use actix_web::middleware::{Middleware, Response};
+use actix_web::{HttpRequest, HttpResponse, Result};
+
+pub struct Auth;
+
+/// HTTP Basic authentication errors
+pub enum BasicAuthError {
+ Base64DecodeError,
+ InvalidUsernameFormat,
+}
+
+#[derive(Clone, Debug)]
+/// HTTP Basic authentication parameters
+pub struct BasicAuthParams {
+ pub username: String,
+ pub password: String,
+}
+
+/// Decode a HTTP basic auth string into a tuple of username and password.
+pub fn parse_basic_auth(
+ authorization_header: &header::HeaderValue,
+) -> Result<BasicAuthParams, BasicAuthError> {
+ let basic_removed = authorization_header.to_str().unwrap().replace("Basic ", "");
+ let decoded = base64::decode(&basic_removed).map_err(|_| BasicAuthError::Base64DecodeError)?;
+ let decoded_str = String::from_utf8_lossy(&decoded);
+ let strings: Vec<&str> = decoded_str.splitn(2, ':').collect();
+ if strings.len() != 2 {
+ return Err(BasicAuthError::InvalidUsernameFormat);
+ }
+ Ok(BasicAuthParams {
+ username: strings[0].to_owned(),
+ password: strings[1].to_owned(),
+ })
+}
+
+impl Middleware<crate::MiniserveConfig> for Auth {
+ fn response(
+ &self,
+ req: &HttpRequest<crate::MiniserveConfig>,
+ resp: HttpResponse,
+ ) -> Result<Response> {
+ if let Some(ref required_auth) = req.state().auth {
+ if let Some(auth_headers) = req.headers().get(header::AUTHORIZATION) {
+ let auth_req = match parse_basic_auth(auth_headers) {
+ Ok(auth_req) => auth_req,
+ Err(BasicAuthError::Base64DecodeError) => {
+ return Ok(Response::Done(HttpResponse::BadRequest().body(format!(
+ "Error decoding basic auth base64: '{}'",
+ auth_headers.to_str().unwrap()
+ ))));
+ }
+ Err(BasicAuthError::InvalidUsernameFormat) => {
+ return Ok(Response::Done(
+ HttpResponse::BadRequest().body("Invalid basic auth format"),
+ ));
+ }
+ };
+ if auth_req.username != required_auth.username
+ || auth_req.password != required_auth.password
+ {
+ let new_resp = HttpResponse::Forbidden().finish();
+ return Ok(Response::Done(new_resp));
+ }
+ } else {
+ let new_resp = HttpResponse::Unauthorized()
+ .header(
+ header::WWW_AUTHENTICATE,
+ header::HeaderValue::from_static("Basic realm=\"miniserve\""),
+ )
+ .finish();
+ return Ok(Response::Done(new_resp));
+ }
+ }
+ Ok(Response::Done(resp))
+ }
+}
diff --git a/src/listing.rs b/src/listing.rs
new file mode 100644
index 0000000..f0662ef
--- /dev/null
+++ b/src/listing.rs
@@ -0,0 +1,272 @@
+use actix_web::{fs, HttpRequest, HttpResponse, Result};
+use bytesize::ByteSize;
+use htmlescape::encode_minimal as escape_html_entity;
+use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
+use std::cmp::Ordering;
+use std::fmt::Write as FmtWrite;
+use std::io;
+use std::path::Path;
+use std::str::FromStr;
+
+#[derive(Clone, Copy, Debug)]
+/// Available sorting methods
+pub enum SortingMethods {
+ /// Natural sorting method
+ /// 1 -> 2 -> 3 -> 11
+ Natural,
+
+ /// Pure alphabetical sorting method
+ /// 1 -> 11 -> 2 -> 3
+ Alpha,
+
+ /// Directories are listed first, alphabetical sorting is also applied
+ /// 1/ -> 2/ -> 3/ -> 11 -> 12
+ DirsFirst,
+}
+
+#[derive(PartialEq)]
+/// Possible entry types
+enum EntryType {
+ /// Entry is a directory
+ Directory,
+
+ /// Entry is a file
+ File,
+}
+
+impl PartialOrd for EntryType {
+ fn partial_cmp(&self, other: &EntryType) -> Option<Ordering> {
+ match (self, other) {
+ (EntryType::Directory, EntryType::File) => Some(Ordering::Less),
+ (EntryType::File, EntryType::Directory) => Some(Ordering::Greater),
+ _ => Some(Ordering::Equal),
+ }
+ }
+}
+
+/// Entry
+struct Entry {
+ /// Name of the entry
+ name: String,
+
+ /// Type of the entry
+ entry_type: EntryType,
+
+ /// URL of the entry
+ link: String,
+
+ /// Size in byte of the entry. Only available for EntryType::File
+ size: Option<bytesize::ByteSize>,
+}
+
+impl Entry {
+ fn new(
+ name: String,
+ entry_type: EntryType,
+ link: String,
+ size: Option<bytesize::ByteSize>,
+ ) -> Self {
+ Entry {
+ name,
+ entry_type,
+ link,
+ size,
+ }
+ }
+}
+
+impl FromStr for SortingMethods {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<SortingMethods, ()> {
+ match s {
+ "natural" => Ok(SortingMethods::Natural),
+ "alpha" => Ok(SortingMethods::Alpha),
+ "dirsfirst" => Ok(SortingMethods::DirsFirst),
+ _ => Err(()),
+ }
+ }
+}
+
+pub fn file_handler(req: &HttpRequest<crate::MiniserveConfig>) -> Result<fs::NamedFile> {
+ let path = &req.state().path;
+ Ok(fs::NamedFile::open(path)?)
+}
+
+/// List a directory and renders a HTML file accordingly
+/// Adapted from https://docs.rs/actix-web/0.7.13/src/actix_web/fs.rs.html#564
+pub fn directory_listing<S>(
+ dir: &fs::Directory,
+ req: &HttpRequest<S>,
+ skip_symlinks: bool,
+ random_route: Option<String>,
+ sort_method: SortingMethods,
+ reverse_sort: bool,
+) -> Result<HttpResponse, io::Error> {
+ let index_of = format!("Index of {}", req.path());
+ let mut body = String::new();
+ let base = Path::new(req.path());
+ let random_route = format!("/{}", random_route.unwrap_or_default());
+
+ if let Some(parent) = base.parent() {
+ if req.path() != random_route {
+ let _ = write!(
+ body,
+ "<tr><td><a class=\"root\" href=\"{}\">..</a></td><td></td></tr>",
+ parent.display()
+ );
+ }
+ }
+
+ let mut entries: Vec<Entry> = Vec::new();
+
+ for entry in dir.path.read_dir()? {
+ if dir.is_visible(&entry) {
+ let entry = entry.unwrap();
+ let p = match entry.path().strip_prefix(&dir.path) {
+ Ok(p) => base.join(p),
+ Err(_) => continue,
+ };
+ // show file url as relative to static path
+ let file_url =
+ utf8_percent_encode(&p.to_string_lossy(), DEFAULT_ENCODE_SET).to_string();
+ // " -- &quot; & -- &amp; ' -- &#x27; < -- &lt; > -- &gt;
+ let file_name = escape_html_entity(&entry.file_name().to_string_lossy());
+
+ // if file is a directory, add '/' to the end of the name
+ if let Ok(metadata) = entry.metadata() {
+ if skip_symlinks && metadata.file_type().is_symlink() {
+ continue;
+ }
+ if metadata.is_dir() {
+ entries.push(Entry::new(file_name, EntryType::Directory, file_url, None));
+ } else {
+ entries.push(Entry::new(
+ file_name,
+ EntryType::File,
+ file_url,
+ Some(ByteSize::b(metadata.len())),
+ ));
+ }
+ } else {
+ continue;
+ }
+ }
+ }
+
+ match sort_method {
+ SortingMethods::Natural => entries
+ .sort_by(|e1, e2| alphanumeric_sort::compare_str(e1.name.clone(), e2.name.clone())),
+ SortingMethods::Alpha => {
+ entries.sort_by(|e1, e2| e1.entry_type.partial_cmp(&e2.entry_type).unwrap());
+ entries.sort_by_key(|e| e.name.clone())
+ }
+ SortingMethods::DirsFirst => {
+ entries.sort_by_key(|e| e.name.clone());
+ entries.sort_by(|e1, e2| e1.entry_type.partial_cmp(&e2.entry_type).unwrap());
+ }
+ };
+
+ if reverse_sort {
+ entries.reverse();
+ }
+
+ for entry in entries {
+ match entry.entry_type {
+ EntryType::Directory => {
+ let _ = write!(
+ body,
+ "<tr><td><a class=\"directory\" href=\"{}\">{}/</a></td><td></td></tr>",
+ entry.link, entry.name
+ );
+ }
+ EntryType::File => {
+ let _ = write!(
+ body,
+ "<tr><td><a class=\"file\" href=\"{}\">{}</a></td><td>{}</td></tr>",
+ entry.link,
+ entry.name,
+ entry.size.unwrap()
+ );
+ }
+ }
+ }
+
+ let html = format!(
+ "<html>\
+ <head>\
+ <title>{}</title>\
+ <style>\
+ body {{\
+ margin: 0;\
+ font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\"Helvetica Neue\", Helvetica, Arial, sans-serif;\
+ font-weight: 300;\
+ color: #444444;\
+ padding: 0.125rem;\
+ }}\
+ table {{\
+ width: 100%;\
+ background: white;\
+ border: 0;\
+ table-layout: auto;\
+ }}\
+ table thead {{\
+ background: #efefef;\
+ }}\
+ table tr th,\
+ table tr td {{\
+ padding: 0.5625rem 0.625rem;\
+ font-size: 0.875rem;\
+ color: #777c82;\
+ text-align: left;\
+ line-height: 1.125rem;\
+ }}\
+ table thead tr th {{\
+ padding: 0.5rem 0.625rem 0.625rem;\
+ font-weight: bold;\
+ color: #444444;\
+ }}\
+ table tr:nth-child(even) {{\
+ background: #f6f6f6;\
+ }}\
+ a {{\
+ text-decoration: none;\
+ color: #3498db;\
+ }}\
+ a.root, a.root:visited {{\
+ font-weight: bold;\
+ color: #777c82;\
+ }}\
+ a.directory {{\
+ font-weight: bold;\
+ }}\
+ a:hover {{\
+ text-decoration: underline;\
+ }}\
+ a:visited {{\
+ color: #8e44ad;\
+ }}\
+ @media (max-width: 600px) {{\
+ h1 {{\
+ font-size: 1.375em;\
+ }}\
+ }}\
+ @media (max-width: 400px) {{\
+ h1 {{\
+ font-size: 1.375em;\
+ }}\
+ }}\
+ </style>\
+ </head>\
+ <body><h1>{}</h1>\
+ <table>\
+ <thead><th>Name</th><th>Size</th></thead>\
+ <tbody>\
+ {}\
+ </tbody></table></body>\n</html>",
+ index_of, index_of, body
+ );
+ Ok(HttpResponse::Ok()
+ .content_type("text/html; charset=utf-8")
+ .body(html))
+}
diff --git a/src/main.rs b/src/main.rs
index d3fb3b1..7c31976 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,364 +1,48 @@
-use actix_web::http::header;
-use actix_web::middleware::{Middleware, Response};
-use actix_web::{fs, middleware, server, App, HttpMessage, HttpRequest, HttpResponse, Result};
-use bytesize::ByteSize;
-use clap::{crate_authors, crate_description, crate_name, crate_version};
-use htmlescape::encode_minimal as escape_html_entity;
-use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
+use actix_web::{fs, middleware, server, App};
+use clap::crate_version;
use simplelog::{Config, LevelFilter, TermLogger};
-use std::cmp::Ordering;
-use std::fmt::Write as FmtWrite;
use std::io::{self, Write};
-use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs};
-use std::path::{Path, PathBuf};
-use std::str::FromStr;
+use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs};
use std::thread;
use std::time::Duration;
use yansi::{Color, Paint};
-const ROUTE_ALPHABET: [char; 16] = [
- '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f',
-];
-
-enum BasicAuthError {
- Base64DecodeError,
- InvalidUsernameFormat,
-}
-
-#[derive(Clone, Debug)]
-struct BasicAuthParams {
- username: String,
- password: String,
-}
-
-#[derive(Clone, Debug)]
-enum SortingMethods {
- Natural,
- Alpha,
- DirsFirst,
-}
+mod args;
+mod auth;
+mod listing;
#[derive(Clone, Debug)]
+/// Configuration of the Miniserve application
pub struct MiniserveConfig {
- verbose: bool,
- path: std::path::PathBuf,
- port: u16,
- interfaces: Vec<IpAddr>,
- auth: Option<BasicAuthParams>,
- path_explicitly_chosen: bool,
- no_symlinks: bool,
- random_route: Option<String>,
- sort_method: SortingMethods,
- reverse_sort: bool,
-}
-
-#[derive(PartialEq)]
-enum EntryType {
- Directory,
- File,
-}
-
-impl PartialOrd for EntryType {
- fn partial_cmp(&self, other: &EntryType) -> Option<Ordering> {
- match (self, other) {
- (EntryType::Directory, EntryType::File) => Some(Ordering::Less),
- (EntryType::File, EntryType::Directory) => Some(Ordering::Greater),
- _ => Some(Ordering::Equal),
- }
- }
-}
-
-struct Entry {
- name: String,
- entry_type: EntryType,
- link: String,
- size: Option<bytesize::ByteSize>,
-}
-
-impl Entry {
- fn new(
- name: String,
- entry_type: EntryType,
- link: String,
- size: Option<bytesize::ByteSize>,
- ) -> Self {
- Entry {
- name,
- entry_type,
- link,
- size,
- }
- }
-}
-
-impl FromStr for SortingMethods {
- type Err = ();
-
- fn from_str(s: &str) -> Result<SortingMethods, ()> {
- match s {
- "natural" => Ok(SortingMethods::Natural),
- "alpha" => Ok(SortingMethods::Alpha),
- "dirsfirst" => Ok(SortingMethods::DirsFirst),
- _ => Err(()),
- }
- }
-}
-
-/// Decode a HTTP basic auth string into a tuple of username and password.
-fn parse_basic_auth(
- authorization_header: &header::HeaderValue,
-) -> Result<BasicAuthParams, BasicAuthError> {
- let basic_removed = authorization_header.to_str().unwrap().replace("Basic ", "");
- let decoded = base64::decode(&basic_removed).map_err(|_| BasicAuthError::Base64DecodeError)?;
- let decoded_str = String::from_utf8_lossy(&decoded);
- let strings: Vec<&str> = decoded_str.splitn(2, ':').collect();
- if strings.len() != 2 {
- return Err(BasicAuthError::InvalidUsernameFormat);
- }
- Ok(BasicAuthParams {
- username: strings[0].to_owned(),
- password: strings[1].to_owned(),
- })
-}
-
-fn is_valid_path(path: String) -> Result<(), String> {
- let path_to_check = PathBuf::from(path);
- if path_to_check.is_file() || path_to_check.is_dir() {
- return Ok(());
- }
- Err(String::from(
- "Path either doesn't exist or is not a regular file or a directory",
- ))
-}
+ /// Enable verbose mode
+ pub verbose: bool,
+
+ /// Path to be served by miniserve
+ pub path: std::path::PathBuf,
-fn is_valid_port(port: String) -> Result<(), String> {
- port.parse::<u16>()
- .and(Ok(()))
- .or_else(|e| Err(e.to_string()))
-}
+ /// Port on which miniserve will be listening
+ pub port: u16,
-fn is_valid_interface(interface: String) -> Result<(), String> {
- interface
- .parse::<IpAddr>()
- .and(Ok(()))
- .or_else(|e| Err(e.to_string()))
-}
+ /// IP address(es) on which miniserve will be available
+ pub interfaces: Vec<IpAddr>,
-fn is_valid_auth(auth: String) -> Result<(), String> {
- auth.find(':')
- .ok_or_else(|| "Correct format is username:password".to_owned())
- .map(|_| ())
-}
+ /// Enable HTTP basic authentication
+ pub auth: Option<auth::BasicAuthParams>,
-pub fn parse_args() -> MiniserveConfig {
- use clap::{App, AppSettings, Arg};
+ /// If false, miniserve will serve the current working directory
+ pub path_explicitly_chosen: bool,
- let matches = App::new(crate_name!())
- .version(crate_version!())
- .author(crate_authors!())
- .about(crate_description!())
- .global_setting(AppSettings::ColoredHelp)
- .arg(
- Arg::with_name("verbose")
- .short("v")
- .long("verbose")
- .help("Be verbose, includes emitting access logs"),
- )
- .arg(
- Arg::with_name("PATH")
- .required(false)
- .validator(is_valid_path)
- .help("Which path to serve"),
- )
- .arg(
- Arg::with_name("port")
- .short("p")
- .long("port")
- .help("Port to use")
- .validator(is_valid_port)
- .required(false)
- .default_value("8080")
- .takes_value(true),
- )
- .arg(
- Arg::with_name("interfaces")
- .short("i")
- .long("if")
- .help("Interface to listen on")
- .validator(is_valid_interface)
- .required(false)
- .takes_value(true)
- .multiple(true),
- )
- .arg(
- Arg::with_name("auth")
- .short("a")
- .long("auth")
- .validator(is_valid_auth)
- .help("Set authentication (username:password)")
- .takes_value(true),
- )
- .arg(
- Arg::with_name("random-route")
- .long("random-route")
- .help("Generate a random 6-hexdigit route"),
- )
- .arg(
- Arg::with_name("sort")
- .short("s")
- .long("sort")
- .possible_values(&["natural", "alpha", "dirsfirst"])
- .default_value("natural")
- .help("Sort files"),
- )
- .arg(
- Arg::with_name("reverse")
- .long("reverse")
- .help("Reverse sorting order"),
- )
- .arg(
- Arg::with_name("no-symlinks")
- .short("P")
- .long("no-symlinks")
- .help("Do not follow symbolic links"),
- )
- .get_matches();
+ /// Enable symlink resolution
+ pub no_symlinks: bool,
- let verbose = matches.is_present("verbose");
- let no_symlinks = matches.is_present("no-symlinks");
- let path = matches.value_of("PATH");
- let port = matches.value_of("port").unwrap().parse().unwrap();
- let interfaces = if let Some(interfaces) = matches.values_of("interfaces") {
- interfaces.map(|x| x.parse().unwrap()).collect()
- } else {
- vec![
- IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)),
- IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
- ]
- };
- let auth = if let Some(auth_split) = matches.value_of("auth").map(|x| x.splitn(2, ':')) {
- let auth_vec = auth_split.collect::<Vec<&str>>();
- if auth_vec.len() == 2 {
- Some(BasicAuthParams {
- username: auth_vec[0].to_owned(),
- password: auth_vec[1].to_owned(),
- })
- } else {
- None
- }
- } else {
- None
- };
-
- let random_route = if matches.is_present("random-route") {
- Some(nanoid::custom(6, &ROUTE_ALPHABET))
- } else {
- None
- };
-
- let sort_method = matches
- .value_of("sort")
- .unwrap()
- .parse::<SortingMethods>()
- .unwrap();
+ /// Enable random route generation
+ pub random_route: Option<String>,
- let reverse_sort = matches.is_present("reverse");
+ /// Sort files/directories
+ pub sort_method: listing::SortingMethods,
- MiniserveConfig {
- verbose,
- path: PathBuf::from(path.unwrap_or(".")),
- port,
- interfaces,
- auth,
- path_explicitly_chosen: path.is_some(),
- no_symlinks,
- random_route,
- sort_method,
- reverse_sort,
- }
-}
-
-fn file_handler(req: &HttpRequest<MiniserveConfig>) -> Result<fs::NamedFile> {
- let path = &req.state().path;
- Ok(fs::NamedFile::open(path)?)
-}
-
-fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> {
- let s = {
- let path = &app.state().path;
- let no_symlinks = app.state().no_symlinks;
- let random_route = app.state().random_route.clone();
- let sort_method = app.state().sort_method.clone();
- let reverse_sort = app.state().reverse_sort;
- if path.is_file() {
- None
- } else {
- Some(
- fs::StaticFiles::new(path)
- .expect("Couldn't create path")
- .show_files_listing()
- .files_listing_renderer(move |dir, req| {
- directory_listing(
- dir,
- req,
- no_symlinks,
- random_route.clone(),
- sort_method.clone(),
- reverse_sort,
- )
- }),
- )
- }
- };
-
- let random_route = app.state().random_route.clone().unwrap_or_default();
- let full_route = format!("/{}", random_route);
-
- if let Some(s) = s {
- app.handler(&full_route, s)
- } else {
- app.resource(&full_route, |r| r.f(file_handler))
- }
-}
-
-struct Auth;
-
-impl Middleware<MiniserveConfig> for Auth {
- fn response(&self, req: &HttpRequest<MiniserveConfig>, resp: HttpResponse) -> Result<Response> {
- if let Some(ref required_auth) = req.state().auth {
- if let Some(auth_headers) = req.headers().get(header::AUTHORIZATION) {
- let auth_req = match parse_basic_auth(auth_headers) {
- Ok(auth_req) => auth_req,
- Err(BasicAuthError::Base64DecodeError) => {
- return Ok(Response::Done(HttpResponse::BadRequest().body(format!(
- "Error decoding basic auth base64: '{}'",
- auth_headers.to_str().unwrap()
- ))));
- }
- Err(BasicAuthError::InvalidUsernameFormat) => {
- return Ok(Response::Done(
- HttpResponse::BadRequest().body("Invalid basic auth format"),
- ));
- }
- };
- if auth_req.username != required_auth.username
- || auth_req.password != required_auth.password
- {
- let new_resp = HttpResponse::Forbidden().finish();
- return Ok(Response::Done(new_resp));
- }
- } else {
- let new_resp = HttpResponse::Unauthorized()
- .header(
- header::WWW_AUTHENTICATE,
- header::HeaderValue::from_static("Basic realm=\"miniserve\""),
- )
- .finish();
- return Ok(Response::Done(new_resp));
- }
- }
- Ok(Response::Done(resp))
- }
+ /// Enable inverse sorting
+ pub reverse_sort: bool,
}
fn main() {
@@ -366,7 +50,7 @@ fn main() {
Paint::disable();
}
- let miniserve_config = parse_args();
+ let miniserve_config = args::parse_args();
if miniserve_config.no_symlinks
&& miniserve_config
.path
@@ -390,7 +74,7 @@ fn main() {
let inside_config = miniserve_config.clone();
server::new(move || {
App::with_state(inside_config.clone())
- .middleware(Auth)
+ .middleware(auth::Auth)
.middleware(middleware::Logger::default())
.configure(configure_app)
})
@@ -488,179 +172,41 @@ fn main() {
let _ = sys.run();
}
-// ↓ Adapted from https://docs.rs/actix-web/0.7.13/src/actix_web/fs.rs.html#564
-fn directory_listing<S>(
- dir: &fs::Directory,
- req: &HttpRequest<S>,
- skip_symlinks: bool,
- random_route: Option<String>,
- sort_method: SortingMethods,
- reverse_sort: bool,
-) -> Result<HttpResponse, io::Error> {
- let index_of = format!("Index of {}", req.path());
- let mut body = String::new();
- let base = Path::new(req.path());
- let random_route = format!("/{}", random_route.unwrap_or_default());
-
- if let Some(parent) = base.parent() {
- if req.path() != random_route {
- let _ = write!(
- body,
- "<tr><td><a class=\"root\" href=\"{}\">..</a></td><td></td></tr>",
- parent.display()
- );
- }
- }
-
- let mut entries: Vec<Entry> = Vec::new();
-
- for entry in dir.path.read_dir()? {
- if dir.is_visible(&entry) {
- let entry = entry.unwrap();
- let p = match entry.path().strip_prefix(&dir.path) {
- Ok(p) => base.join(p),
- Err(_) => continue,
- };
- // show file url as relative to static path
- let file_url =
- utf8_percent_encode(&p.to_string_lossy(), DEFAULT_ENCODE_SET).to_string();
- // " -- &quot; & -- &amp; ' -- &#x27; < -- &lt; > -- &gt;
- let file_name = escape_html_entity(&entry.file_name().to_string_lossy());
-
- // if file is a directory, add '/' to the end of the name
- if let Ok(metadata) = entry.metadata() {
- if skip_symlinks && metadata.file_type().is_symlink() {
- continue;
- }
- if metadata.is_dir() {
- entries.push(Entry::new(file_name, EntryType::Directory, file_url, None));
- } else {
- entries.push(Entry::new(
- file_name,
- EntryType::File,
- file_url,
- Some(ByteSize::b(metadata.len())),
- ));
- }
- } else {
- continue;
- }
- }
- }
-
- match sort_method {
- SortingMethods::Natural => entries
- .sort_by(|e1, e2| alphanumeric_sort::compare_str(e1.name.clone(), e2.name.clone())),
- SortingMethods::Alpha => {
- entries.sort_by(|e1, e2| e1.entry_type.partial_cmp(&e2.entry_type).unwrap());
- entries.sort_by_key(|e| e.name.clone())
- }
- SortingMethods::DirsFirst => {
- entries.sort_by_key(|e| e.name.clone());
- entries.sort_by(|e1, e2| e1.entry_type.partial_cmp(&e2.entry_type).unwrap());
+/// Configures the Actix application
+fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> {
+ let s = {
+ let path = &app.state().path;
+ let no_symlinks = app.state().no_symlinks;
+ let random_route = app.state().random_route.clone();
+ let sort_method = app.state().sort_method;
+ let reverse_sort = app.state().reverse_sort;
+ if path.is_file() {
+ None
+ } else {
+ Some(
+ fs::StaticFiles::new(path)
+ .expect("Couldn't create path")
+ .show_files_listing()
+ .files_listing_renderer(move |dir, req| {
+ listing::directory_listing(
+ dir,
+ req,
+ no_symlinks,
+ random_route.clone(),
+ sort_method,
+ reverse_sort,
+ )
+ }),
+ )
}
};
- if reverse_sort {
- entries.reverse();
- }
+ let random_route = app.state().random_route.clone().unwrap_or_default();
+ let full_route = format!("/{}", random_route);
- for entry in entries {
- match entry.entry_type {
- EntryType::Directory => {
- let _ = write!(
- body,
- "<tr><td><a class=\"directory\" href=\"{}\">{}/</a></td><td></td></tr>",
- entry.link, entry.name
- );
- }
- EntryType::File => {
- let _ = write!(
- body,
- "<tr><td><a class=\"file\" href=\"{}\">{}</a></td><td>{}</td></tr>",
- entry.link,
- entry.name,
- entry.size.unwrap()
- );
- }
- }
+ if let Some(s) = s {
+ app.handler(&full_route, s)
+ } else {
+ app.resource(&full_route, |r| r.f(listing::file_handler))
}
-
- let html = format!(
- "<html>\
- <head>\
- <title>{}</title>\
- <style>\
- body {{\
- margin: 0;\
- font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\"Helvetica Neue\", Helvetica, Arial, sans-serif;\
- font-weight: 300;\
- color: #444444;\
- padding: 0.125rem;\
- }}\
- table {{\
- width: 100%;\
- background: white;\
- border: 0;\
- table-layout: auto;\
- }}\
- table thead {{\
- background: #efefef;\
- }}\
- table tr th,\
- table tr td {{\
- padding: 0.5625rem 0.625rem;\
- font-size: 0.875rem;\
- color: #777c82;\
- text-align: left;\
- line-height: 1.125rem;\
- }}\
- table thead tr th {{\
- padding: 0.5rem 0.625rem 0.625rem;\
- font-weight: bold;\
- color: #444444;\
- }}\
- table tr:nth-child(even) {{\
- background: #f6f6f6;\
- }}\
- a {{\
- text-decoration: none;\
- color: #3498db;\
- }}\
- a.root, a.root:visited {{\
- font-weight: bold;\
- color: #777c82;\
- }}\
- a.directory {{\
- font-weight: bold;\
- }}\
- a:hover {{\
- text-decoration: underline;\
- }}\
- a:visited {{\
- color: #8e44ad;\
- }}\
- @media (max-width: 600px) {{\
- h1 {{\
- font-size: 1.375em;\
- }}\
- }}\
- @media (max-width: 400px) {{\
- h1 {{\
- font-size: 1.375em;\
- }}\
- }}\
- </style>\
- </head>\
- <body><h1>{}</h1>\
- <table>\
- <thead><th>Name</th><th>Size</th></thead>\
- <tbody>\
- {}\
- </tbody></table></body>\n</html>",
- index_of, index_of, body
- );
- Ok(HttpResponse::Ok()
- .content_type("text/html; charset=utf-8")
- .body(html))
}