diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 546 |
1 files changed, 10 insertions, 536 deletions
diff --git a/src/main.rs b/src/main.rs index d3fb3b1..633ab84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,372 +1,23 @@ -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::{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, -} - -#[derive(Clone, Debug)] -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", - )) -} - -fn is_valid_port(port: String) -> Result<(), String> { - port.parse::<u16>() - .and(Ok(())) - .or_else(|e| Err(e.to_string())) -} - -fn is_valid_interface(interface: String) -> Result<(), String> { - interface - .parse::<IpAddr>() - .and(Ok(())) - .or_else(|e| Err(e.to_string())) -} - -fn is_valid_auth(auth: String) -> Result<(), String> { - auth.find(':') - .ok_or_else(|| "Correct format is username:password".to_owned()) - .map(|_| ()) -} - -pub fn parse_args() -> 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(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(); - - let reverse_sort = matches.is_present("reverse"); - - 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)) - } -} +mod args; +mod auth; +mod config; +mod listing; fn main() { if cfg!(windows) && !Paint::enable_windows_ascii() { Paint::disable(); } - let miniserve_config = parse_args(); + let miniserve_config = args::parse_args(); if miniserve_config.no_symlinks && miniserve_config .path @@ -390,9 +41,9 @@ 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) + .configure(config::configure_app) }) .bind( miniserve_config @@ -487,180 +138,3 @@ 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()); - } - }; - - 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)) -} |