diff options
author | Sven-Hendrik Haase <svenstaro@gmail.com> | 2021-08-27 02:11:10 +0000 |
---|---|---|
committer | Sven-Hendrik Haase <svenstaro@gmail.com> | 2021-08-27 02:11:10 +0000 |
commit | aad1c0ae685865e7e65ef194f49d6020c5e9e65f (patch) | |
tree | e49a7a0930778acd38e19f2b716d2254336be4ea /src | |
parent | Golf tests a bit more (diff) | |
download | miniserve-aad1c0ae685865e7e65ef194f49d6020c5e9e65f.tar.gz miniserve-aad1c0ae685865e7e65ef194f49d6020c5e9e65f.zip |
Add TLS support via rustls (fixes #18)
Diffstat (limited to 'src')
-rw-r--r-- | src/args.rs | 156 | ||||
-rw-r--r-- | src/config.rs | 181 | ||||
-rw-r--r-- | src/main.rs | 187 |
3 files changed, 292 insertions, 232 deletions
diff --git a/src/args.rs b/src/args.rs index 819618f..4fd933a 100644 --- a/src/args.rs +++ b/src/args.rs @@ -48,11 +48,11 @@ pub struct CliArgs { /// username:password, username:sha256:hash, username:sha512:hash /// (e.g. joe:123, joe:sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3) #[structopt( - short = "a", - long = "auth", - parse(try_from_str = parse_auth), - number_of_values = 1, - )] + short = "a", + long = "auth", + parse(try_from_str = parse_auth), + number_of_values = 1, + )] pub auth: Vec<auth::RequiredAuth>, /// Generate a random 6-hexdigit route @@ -69,22 +69,22 @@ pub struct CliArgs { /// Default color scheme #[structopt( - short = "c", - long = "color-scheme", - default_value = "squirrel", - possible_values = &renderer::THEME_SLUGS, - case_insensitive = true, - )] + short = "c", + long = "color-scheme", + default_value = "squirrel", + possible_values = &renderer::THEME_SLUGS, + case_insensitive = true, + )] pub color_scheme: String, /// Default color scheme #[structopt( - short = "d", - long = "color-scheme-dark", - default_value = "archlinux", - possible_values = &renderer::THEME_SLUGS, - case_insensitive = true, - )] + short = "d", + long = "color-scheme-dark", + default_value = "archlinux", + possible_values = &renderer::THEME_SLUGS, + case_insensitive = true, + )] pub color_scheme_dark: String, /// Enable QR code display @@ -133,6 +133,14 @@ pub struct CliArgs { /// Generate completion file for a shell #[structopt(long = "print-completions", value_name = "shell", possible_values = &structopt::clap::Shell::variants())] pub print_completions: Option<structopt::clap::Shell>, + + /// TLS certificate to use + #[structopt(long = "tls-cert", requires = "tls-key")] + pub tls_cert: Option<PathBuf>, + + /// TLS private key to use + #[structopt(long = "tls-key", requires = "tls-cert")] + pub tls_key: Option<PathBuf>, } /// Checks wether an interface is valid, i.e. it can be parsed into an IP address @@ -208,63 +216,63 @@ pub fn parse_header(src: &str) -> Result<HeaderMap, httparse::Error> { #[rustfmt::skip] #[cfg(test)] -mod tests { - use super::*; - use rstest::rstest; - use pretty_assertions::assert_eq; - - /// Helper function that creates a `RequiredAuth` structure - fn create_required_auth(username: &str, password: &str, encrypt: &str) -> auth::RequiredAuth { - use auth::*; - use RequiredAuthPassword::*; - - let password = match encrypt { - "plain" => Plain(password.to_owned()), - "sha256" => Sha256(hex::decode(password.to_owned()).unwrap()), - "sha512" => Sha512(hex::decode(password.to_owned()).unwrap()), - _ => panic!("Unknown encryption type"), - }; - - auth::RequiredAuth { - username: username.to_owned(), - password, + mod tests { + use super::*; + use rstest::rstest; + use pretty_assertions::assert_eq; + + /// Helper function that creates a `RequiredAuth` structure + fn create_required_auth(username: &str, password: &str, encrypt: &str) -> auth::RequiredAuth { + use auth::*; + use RequiredAuthPassword::*; + + let password = match encrypt { + "plain" => Plain(password.to_owned()), + "sha256" => Sha256(hex::decode(password.to_owned()).unwrap()), + "sha512" => Sha512(hex::decode(password.to_owned()).unwrap()), + _ => panic!("Unknown encryption type"), + }; + + auth::RequiredAuth { + username: username.to_owned(), + password, + } } - } - #[rstest( - auth_string, username, password, encrypt, - case("username:password", "username", "password", "plain"), - case("username:sha256:abcd", "username", "abcd", "sha256"), - case("username:sha512:abcd", "username", "abcd", "sha512") - )] - fn parse_auth_valid(auth_string: &str, username: &str, password: &str, encrypt: &str) { - assert_eq!( - parse_auth(auth_string).unwrap(), - create_required_auth(username, password, encrypt), - ); + #[rstest( + auth_string, username, password, encrypt, + case("username:password", "username", "password", "plain"), + case("username:sha256:abcd", "username", "abcd", "sha256"), + case("username:sha512:abcd", "username", "abcd", "sha512") + )] + fn parse_auth_valid(auth_string: &str, username: &str, password: &str, encrypt: &str) { + assert_eq!( + parse_auth(auth_string).unwrap(), + create_required_auth(username, password, encrypt), + ); + } + + #[rstest( + auth_string, err_msg, + case( + "foo", + "Invalid format for credentials string. Expected username:password, username:sha256:hash or username:sha512:hash" + ), + case( + "username:blahblah:abcd", + "blahblah is not a valid hashing method. Expected sha256 or sha512" + ), + case( + "username:sha256:invalid", + "Invalid format for password hash. Expected hex code" + ), + case( + "username:sha512:invalid", + "Invalid format for password hash. Expected hex code" + ), + )] + fn parse_auth_invalid(auth_string: &str, err_msg: &str) { + let err = parse_auth(auth_string).unwrap_err(); + assert_eq!(format!("{}", err), err_msg.to_owned()); + } } - - #[rstest( - auth_string, err_msg, - case( - "foo", - "Invalid format for credentials string. Expected username:password, username:sha256:hash or username:sha512:hash" - ), - case( - "username:blahblah:abcd", - "blahblah is not a valid hashing method. Expected sha256 or sha512" - ), - case( - "username:sha256:invalid", - "Invalid format for password hash. Expected hex code" - ), - case( - "username:sha512:invalid", - "Invalid format for password hash. Expected hex code" - ), - )] - fn parse_auth_invalid(auth_string: &str, err_msg: &str) { - let err = parse_auth(auth_string).unwrap_err(); - assert_eq!(format!("{}", err), err_msg.to_owned()); - } -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..6eeafef --- /dev/null +++ b/src/config.rs @@ -0,0 +1,181 @@ +use std::{ + fs::File, + io::BufReader, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + path::PathBuf, +}; + +use anyhow::{anyhow, Context, Result}; +use http::HeaderMap; +use rustls::internal::pemfile::{certs, pkcs8_private_keys}; + +use crate::{args::CliArgs, auth::RequiredAuth}; + +/// 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', +]; + +#[derive(Clone)] +/// Configuration of the Miniserve application +pub struct MiniserveConfig { + /// Enable verbose mode + pub verbose: bool, + + /// Path to be served by miniserve + pub path: std::path::PathBuf, + + /// Port on which miniserve will be listening + pub port: u16, + + /// IP address(es) on which miniserve will be available + pub interfaces: Vec<IpAddr>, + + /// Enable HTTP basic authentication + pub auth: Vec<RequiredAuth>, + + /// If false, miniserve will serve the current working directory + pub path_explicitly_chosen: bool, + + /// Enable symlink resolution + pub no_symlinks: bool, + + /// Show hidden files + pub show_hidden: bool, + + /// Enable random route generation + pub random_route: Option<String>, + + /// Randomly generated favicon route + pub favicon_route: String, + + /// Randomly generated css route + pub css_route: String, + + /// Default color scheme + pub default_color_scheme: String, + + /// Default dark mode color scheme + pub default_color_scheme_dark: String, + + /// The name of a directory index file to serve, like "index.html" + /// + /// Normally, when miniserve serves a directory, it creates a listing for that directory. + /// However, if a directory contains this file, miniserve will serve that file instead. + pub index: Option<std::path::PathBuf>, + + /// Enable QR code display + pub show_qrcode: bool, + + /// Enable file upload + pub file_upload: bool, + + /// Enable upload to override existing files + pub overwrite_files: bool, + + /// If false, creation of uncompressed tar archives is disabled + pub tar_enabled: bool, + + /// If false, creation of gz-compressed tar archives is disabled + pub tar_gz_enabled: bool, + + /// If false, creation of zip archives is disabled + pub zip_enabled: bool, + + /// If enabled, directories are listed first + pub dirs_first: bool, + + /// Shown instead of host in page title and heading + pub title: Option<String>, + + /// If specified, header will be added + pub header: Vec<HeaderMap>, + + /// If enabled, version footer is hidden + pub hide_version_footer: bool, + + /// If set, use provided rustls config for TLS + pub tls_rustls_config: Option<rustls::ServerConfig>, +} + +impl MiniserveConfig { + /// Parses the command line arguments + pub fn try_from_args(args: CliArgs) -> Result<Self> { + let interfaces = if !args.interfaces.is_empty() { + args.interfaces + } else { + vec![ + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + ] + }; + + let random_route = if args.random_route { + Some(nanoid::nanoid!(6, &ROUTE_ALPHABET)) + } else { + None + }; + + // Generate some random routes for the favicon and css so that they are very unlikely to conflict with + // real files. + let favicon_route = nanoid::nanoid!(10, &ROUTE_ALPHABET); + let css_route = nanoid::nanoid!(10, &ROUTE_ALPHABET); + + let default_color_scheme = args.color_scheme; + let default_color_scheme_dark = args.color_scheme_dark; + + let path_explicitly_chosen = args.path.is_some() || args.index.is_some(); + + let port = match args.port { + 0 => port_check::free_local_port().context("No free ports available")?, + _ => args.port, + }; + + let tls_rustls_server_config = if let (Some(tls_cert), Some(tls_key)) = + (args.tls_cert, args.tls_key) + { + let mut server_config = rustls::ServerConfig::new(rustls::NoClientAuth::new()); + let cert_file = &mut BufReader::new( + File::open(&tls_cert) + .context(format!("Couldn't access TLS certificate {:?}", tls_cert))?, + ); + let key_file = &mut BufReader::new( + File::open(&tls_key).context(format!("Couldn't access TLS key {:?}", tls_key))?, + ); + let cert_chain = certs(cert_file).map_err(|_| anyhow!("Couldn't load certificates"))?; + let mut keys = + pkcs8_private_keys(key_file).map_err(|_| anyhow!("Couldn't load private key"))?; + server_config.set_single_cert(cert_chain, keys.remove(0))?; + Some(server_config) + } else { + None + }; + Ok(MiniserveConfig { + verbose: args.verbose, + path: args.path.unwrap_or_else(|| PathBuf::from(".")), + port, + interfaces, + auth: args.auth, + path_explicitly_chosen, + no_symlinks: args.no_symlinks, + show_hidden: args.hidden, + random_route, + favicon_route, + css_route, + default_color_scheme, + default_color_scheme_dark, + index: args.index, + overwrite_files: args.overwrite_files, + show_qrcode: args.qrcode, + file_upload: args.file_upload, + tar_enabled: args.enable_tar, + tar_gz_enabled: args.enable_tar_gz, + zip_enabled: args.enable_zip, + dirs_first: args.dirs_first, + title: args.title, + header: args.header, + hide_version_footer: args.hide_version_footer, + tls_rustls_config: tls_rustls_server_config, + }) + } +} diff --git a/src/main.rs b/src/main.rs index b6dd856..1432a1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ use std::io; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::io::Write; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::thread; use std::time::Duration; -use std::{io::Write, path::PathBuf}; use actix_web::web; use actix_web::{ @@ -11,7 +11,7 @@ use actix_web::{ }; use actix_web::{middleware, App, HttpRequest, HttpResponse}; use actix_web_httpauth::middleware::HttpAuthentication; -use http::header::HeaderMap; +use anyhow::Result; use log::{error, warn}; use structopt::clap::crate_version; use structopt::StructOpt; @@ -20,174 +20,31 @@ use yansi::{Color, Paint}; mod archive; mod args; mod auth; +mod config; mod errors; mod file_upload; mod listing; mod pipe; mod renderer; +use crate::config::MiniserveConfig; use crate::errors::ContextualError; -/// 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', -]; - -#[derive(Clone)] -/// Configuration of the Miniserve application -pub struct MiniserveConfig { - /// Enable verbose mode - pub verbose: bool, - - /// Path to be served by miniserve - pub path: std::path::PathBuf, - - /// Port on which miniserve will be listening - pub port: u16, - - /// IP address(es) on which miniserve will be available - pub interfaces: Vec<IpAddr>, - - /// Enable HTTP basic authentication - pub auth: Vec<auth::RequiredAuth>, - - /// If false, miniserve will serve the current working directory - pub path_explicitly_chosen: bool, - - /// Enable symlink resolution - pub no_symlinks: bool, - - /// Show hidden files - pub show_hidden: bool, - - /// Enable random route generation - pub random_route: Option<String>, - - /// Randomly generated favicon route - pub favicon_route: String, - - /// Randomly generated css route - pub css_route: String, - - /// Default color scheme - pub default_color_scheme: String, - - /// Default dark mode color scheme - pub default_color_scheme_dark: String, - - /// The name of a directory index file to serve, like "index.html" - /// - /// Normally, when miniserve serves a directory, it creates a listing for that directory. - /// However, if a directory contains this file, miniserve will serve that file instead. - pub index: Option<std::path::PathBuf>, - - /// Enable QR code display - pub show_qrcode: bool, - - /// Enable file upload - pub file_upload: bool, - - /// Enable upload to override existing files - pub overwrite_files: bool, - - /// If false, creation of uncompressed tar archives is disabled - pub tar_enabled: bool, - - /// If false, creation of gz-compressed tar archives is disabled - pub tar_gz_enabled: bool, - - /// If false, creation of zip archives is disabled - pub zip_enabled: bool, - - /// If enabled, directories are listed first - pub dirs_first: bool, - - /// Shown instead of host in page title and heading - pub title: Option<String>, - - /// If specified, header will be added - pub header: Vec<HeaderMap>, - - /// If enabled, version footer is hidden - pub hide_version_footer: bool, -} - -impl MiniserveConfig { - /// Parses the command line arguments - fn from_args(args: args::CliArgs) -> Self { - let interfaces = if !args.interfaces.is_empty() { - args.interfaces - } else { - vec![ - IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), - IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - ] - }; - - let random_route = if args.random_route { - Some(nanoid::nanoid!(6, &ROUTE_ALPHABET)) - } else { - None - }; - - // Generate some random routes for the favicon and css so that they are very unlikely to conflict with - // real files. - let favicon_route = nanoid::nanoid!(10, &ROUTE_ALPHABET); - let css_route = nanoid::nanoid!(10, &ROUTE_ALPHABET); - - let default_color_scheme = args.color_scheme; - let default_color_scheme_dark = args.color_scheme_dark; - - let path_explicitly_chosen = args.path.is_some() || args.index.is_some(); - - let port = match args.port { - 0 => port_check::free_local_port().expect("no free ports available"), - _ => args.port, - }; - - crate::MiniserveConfig { - verbose: args.verbose, - path: args.path.unwrap_or_else(|| PathBuf::from(".")), - port, - interfaces, - auth: args.auth, - path_explicitly_chosen, - no_symlinks: args.no_symlinks, - show_hidden: args.hidden, - random_route, - favicon_route, - css_route, - default_color_scheme, - default_color_scheme_dark, - index: args.index, - overwrite_files: args.overwrite_files, - show_qrcode: args.qrcode, - file_upload: args.file_upload, - tar_enabled: args.enable_tar, - tar_gz_enabled: args.enable_tar_gz, - zip_enabled: args.enable_zip, - dirs_first: args.dirs_first, - title: args.title, - header: args.header, - hide_version_footer: args.hide_version_footer, - } - } -} - -fn main() { +fn main() -> Result<()> { let args = args::CliArgs::from_args(); if let Some(shell) = args.print_completions { args::CliArgs::clap().gen_completions_to("miniserve", shell, &mut std::io::stdout()); - return; + return Ok(()); } - let miniserve_config = MiniserveConfig::from_args(args); + let miniserve_config = MiniserveConfig::try_from_args(args)?; match run(miniserve_config) { Ok(()) => (), Err(e) => errors::log_error_chain(e.to_string()), } + Ok(()) } #[actix_web::main(miniserve)] @@ -300,11 +157,17 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { if !addresses.is_empty() { addresses.push_str(", "); } + let protocol = if miniserve_config.tls_rustls_config.is_some() { + "https" + } else { + "http" + }; addresses.push_str(&format!( "{}", Color::Green .paint(format!( - "http://{interface}:{port}", + "{protocol}://{interface}:{port}", + protocol = protocol, interface = &interface, port = miniserve_config.port )) @@ -362,11 +225,19 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { .route(&format!("/{}", inside_config.css_route), web::get().to(css)) .configure(|c| configure_app(c, &inside_config)) .default_service(web::get().to(error_404)) - }) - .bind(socket_addresses.as_slice()) - .map_err(|e| ContextualError::IoError("Failed to bind server".to_string(), e))? - .shutdown_timeout(0) - .run(); + }); + + let srv = if let Some(tls_config) = miniserve_config.tls_rustls_config { + srv.bind_rustls(socket_addresses.as_slice(), tls_config) + .map_err(|e| ContextualError::IoError("Failed to bind server".to_string(), e))? + .shutdown_timeout(0) + .run() + } else { + srv.bind(socket_addresses.as_slice()) + .map_err(|e| ContextualError::IoError("Failed to bind server".to_string(), e))? + .shutdown_timeout(0) + .run() + }; println!( "Serving path {path} at {addresses}", |