diff options
author | Sven-Hendrik Haase <svenstaro@gmail.com> | 2019-02-13 21:34:57 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-02-13 21:34:57 +0000 |
commit | e543c5a7c3a1e9de0dcf92ab33dc237b85a8a632 (patch) | |
tree | ae9e1c915d1f88c4248d9ae62a5be9acd03fdcf5 | |
parent | This is v0.3.0 (diff) | |
parent | Added some docstrings (diff) | |
download | miniserve-e543c5a7c3a1e9de0dcf92ab33dc237b85a8a632.tar.gz miniserve-e543c5a7c3a1e9de0dcf92ab33dc237b85a8a632.zip |
Merge pull request #32 from boastful-squirrel/split-files
Split project into multiple files
Diffstat (limited to '')
-rw-r--r-- | src/args.rs | 173 | ||||
-rw-r--r-- | src/auth.rs | 77 | ||||
-rw-r--r-- | src/listing.rs | 272 | ||||
-rw-r--r-- | src/main.rs | 578 |
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(); + // " -- " & -- & ' -- ' < -- < > -- > + 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(); - // " -- " & -- & ' -- ' < -- < > -- > - 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)) } |