aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSven-Hendrik Haase <svenstaro@gmail.com>2021-08-27 02:11:10 +0000
committerSven-Hendrik Haase <svenstaro@gmail.com>2021-08-27 02:11:10 +0000
commitaad1c0ae685865e7e65ef194f49d6020c5e9e65f (patch)
treee49a7a0930778acd38e19f2b716d2254336be4ea /src
parentGolf tests a bit more (diff)
downloadminiserve-aad1c0ae685865e7e65ef194f49d6020c5e9e65f.tar.gz
miniserve-aad1c0ae685865e7e65ef194f49d6020c5e9e65f.zip
Add TLS support via rustls (fixes #18)
Diffstat (limited to 'src')
-rw-r--r--src/args.rs156
-rw-r--r--src/config.rs181
-rw-r--r--src/main.rs187
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}",