aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock99
-rw-r--r--Cargo.toml5
-rw-r--r--src/args.rs156
-rw-r--r--src/config.rs181
-rw-r--r--src/main.rs187
-rw-r--r--tests/data/cert.pem29
-rwxr-xr-xtests/data/generate_tls_certs.sh2
-rw-r--r--tests/data/key.pem52
-rw-r--r--tests/fixtures/mod.rs28
-rw-r--r--tests/tls.rs53
10 files changed, 540 insertions, 252 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e557c3e..ec74ccb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -33,8 +33,11 @@ dependencies = [
"futures-util",
"http",
"log",
+ "rustls 0.18.1",
+ "tokio-rustls 0.14.1",
"trust-dns-proto",
"trust-dns-resolver",
+ "webpki",
]
[[package]]
@@ -68,8 +71,9 @@ dependencies = [
"actix-rt",
"actix-service",
"actix-threadpool",
+ "actix-tls",
"actix-utils",
- "base64",
+ "base64 0.13.0",
"bitflags",
"brotli2",
"bytes 0.5.6",
@@ -229,6 +233,10 @@ dependencies = [
"actix-service",
"actix-utils",
"futures-util",
+ "rustls 0.18.1",
+ "tokio-rustls 0.14.1",
+ "webpki",
+ "webpki-roots 0.20.0",
]
[[package]]
@@ -281,6 +289,7 @@ dependencies = [
"mime",
"pin-project 1.0.8",
"regex",
+ "rustls 0.18.1",
"serde",
"serde_json",
"serde_urlencoded",
@@ -308,7 +317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3b11a07a3df3f7970fd8bd38cc66998b5549f507c54cc64c6e843bc82d6358"
dependencies = [
"actix-web",
- "base64",
+ "base64 0.13.0",
"futures-util",
]
@@ -369,6 +378,12 @@ dependencies = [
]
[[package]]
+name = "anyhow"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
+
+[[package]]
name = "assert_cmd"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -434,7 +449,7 @@ dependencies = [
"actix-http",
"actix-rt",
"actix-service",
- "base64",
+ "base64 0.13.0",
"bytes 0.5.6",
"cfg-if 1.0.0",
"derive_more",
@@ -443,6 +458,7 @@ dependencies = [
"mime",
"percent-encoding",
"rand 0.7.3",
+ "rustls 0.18.1",
"serde",
"serde_json",
"serde_urlencoded",
@@ -456,6 +472,12 @@ checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
[[package]]
name = "base64"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+
+[[package]]
+name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
@@ -822,6 +844,15 @@ dependencies = [
]
[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1205,9 +1236,9 @@ dependencies = [
"futures-util",
"hyper",
"log",
- "rustls",
+ "rustls 0.19.1",
"tokio 1.10.1",
- "tokio-rustls",
+ "tokio-rustls 0.22.0",
"webpki",
]
@@ -1505,6 +1536,7 @@ dependencies = [
"actix-web",
"actix-web-httpauth",
"alphanumeric-sort",
+ "anyhow",
"assert_cmd",
"assert_fs",
"atty",
@@ -1524,11 +1556,13 @@ dependencies = [
"nanoid",
"percent-encoding",
"port_check",
+ "predicates",
"pretty_assertions",
"qrcodegen",
"regex",
"reqwest",
"rstest",
+ "rustls 0.18.1",
"select",
"serde",
"sha2",
@@ -1654,6 +1688,12 @@ dependencies = [
]
[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1946,8 +1986,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c143348f141cc87aab5b950021bac6145d0e5ae754b0591de23244cee42c9308"
dependencies = [
"difflib",
+ "float-cmp",
"itertools",
+ "normalize-line-endings",
"predicates-core",
+ "regex",
]
[[package]]
@@ -2182,7 +2225,7 @@ version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22"
dependencies = [
- "base64",
+ "base64 0.13.0",
"bytes 1.1.0",
"encoding_rs",
"futures-core",
@@ -2199,16 +2242,16 @@ dependencies = [
"mime_guess",
"percent-encoding",
"pin-project-lite 0.2.7",
- "rustls",
+ "rustls 0.19.1",
"serde",
"serde_urlencoded",
"tokio 1.10.1",
- "tokio-rustls",
+ "tokio-rustls 0.22.0",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
- "webpki-roots",
+ "webpki-roots 0.21.1",
"winreg 0.7.0",
]
@@ -2285,11 +2328,24 @@ dependencies = [
[[package]]
name = "rustls"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81"
+dependencies = [
+ "base64 0.12.3",
+ "log",
+ "ring",
+ "sct",
+ "webpki",
+]
+
+[[package]]
+name = "rustls"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
dependencies = [
- "base64",
+ "base64 0.13.0",
"log",
"ring",
"sct",
@@ -2852,11 +2908,23 @@ dependencies = [
[[package]]
name = "tokio-rustls"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a"
+dependencies = [
+ "futures-core",
+ "rustls 0.18.1",
+ "tokio 0.2.25",
+ "webpki",
+]
+
+[[package]]
+name = "tokio-rustls"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
dependencies = [
- "rustls",
+ "rustls 0.19.1",
"tokio 1.10.1",
"webpki",
]
@@ -3253,6 +3321,15 @@ dependencies = [
[[package]]
name = "webpki-roots"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f"
+dependencies = [
+ "webpki",
+]
+
+[[package]]
+name = "webpki-roots"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
diff --git a/Cargo.toml b/Cargo.toml
index 384b61a..ba2d606 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,7 +17,7 @@ codegen-units = 1
panic = 'abort'
[dependencies]
-actix-web = "3"
+actix-web = { version = "3", features = ["rustls"] }
actix-files = "0.5"
actix-multipart = "0.3"
actix-web-httpauth = "0.5"
@@ -37,6 +37,7 @@ tar = "0.4"
futures = "0.3"
libflate = "1"
thiserror = "1"
+anyhow = "1"
log = "0.4"
strum = "0.21"
strum_macros = "0.21"
@@ -49,6 +50,7 @@ httparse = "1"
http = "0.2"
bytes = "1"
atty = "0.2"
+rustls = "0.18"
[dev-dependencies]
assert_cmd = "2"
@@ -59,6 +61,7 @@ rstest = "0.11"
regex = "1"
pretty_assertions = "0.7"
url = "2"
+predicates = "2"
[build-dependencies]
grass = "0.10"
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}",
diff --git a/tests/data/cert.pem b/tests/data/cert.pem
new file mode 100644
index 0000000..c907ef2
--- /dev/null
+++ b/tests/data/cert.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIFCTCCAvGgAwIBAgIUJUf2QS/pOdHEW4EHTfdXxeTvtM8wDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIxMDgyNzAwMzEyOFoXDTMxMDgy
+NTAwMzEyOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEAwmYOqToI0R30lPyYtF9bSuhIOCp9cp0jl2nuHaO8mpr1
+gMiJKKN4HjAdgac+3hYkTRFqK2mKKpV9QdVKR24Ib7mC45Ek7BlLw3VbxPRKrK/j
+rKW3M3ui+453B24yf6K8dH36x9gZo4glzghFxuodFakIX2zNKo6tEx0XVkbhsu/w
+vj2s+0L3oToPAYZaiOB/7xYU6Yu9n7Tn6rE9/orDfK1DlrZDP3hzyxLzuf6tqXCh
+66cgaPQTh+xyyWZcvl60kbB4H3bdhqbYGMMQO8bUxXTQXjwvUsvl0yn9qCpMIn99
+Pm9xhfDQSF3zawM3CQ/lmn9uFQzdOEfYlO6oaidTqxLtBhVUcEutIcmoW9nmmv2g
+Ei49/3OmvWQcEdMWt8xwxSrMvKDSeUdF3rbalTHBFQHJlJiKRX9wTNtSZ5T8FTU7
+4Ip4EzAtP8wY5NDv253mddANoyKsVRGytS35LDFkCS/TxuVDZrjluc86yqUId/jf
+HZAzQ7ifpC890aG0JOq/0mmVDvbn7MzdTsTWwhE8UaOiFljTiNQX3QjX3TaEu32M
+XHKo5nebNqDVRGnFMFmfXw2ZP8lgQCWk1HxLr0qhRxIy8XmIK1ZUz7Uc4Cba73XB
+pSxcIPytpDuuKotslBjoIYu9DY07n1Hu4zYPvpP9DnaunEW6zmANEtjSyrE/TQ0C
+AwEAAaNTMFEwHQYDVR0OBBYEFH0VzGnFqGVB+11uyvqea2qXYxQKMB8GA1UdIwQY
+MBaAFH0VzGnFqGVB+11uyvqea2qXYxQKMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
+hvcNAQELBQADggIBAA4FqzX34FCU1WYzmBRcq7QHSrc7LcuTbxhESB7mYbI8IPFt
+cgrtXL1mTP+nz5nN+E6fyA8Y9zIyXm/6svYpJzXgUTtbdgDW22v5iN+YZvOaQ3Jt
+/0eEtkx7wdNjLsN0aM6OjPXDw0mAVFDdevE7wgnra6x6/VHOt6pksNJa76ZVPX5X
+dlLj+OU4eQPPMVxhL7p3xdSPFDZzXY7mNfVycO3tK5Fzrwko7OQKqEBMtc0oZxLd
+m/FvqcJveHYHfXZl5XKMcsCNO8bG0XXDhwg0CLTf1p0hmp1oLieqplekOWs54Alo
+FF4EBNdDaIFdQ4FAYaAU+9KLoPstorTl+3Owj/k3xhDB+0sGwGeX/e88nhs/ppEy
+bxOt0j4AruwapkcvkwhQeMpQJRYyOrcvlbUEZqFABozZ9gbGRQvnConDNg7tz5zc
+nVUupszA7zs0Vn9b1zVLOcOcS2ziQvoCyh687MsVbjw65Y6tkhvLI35G68zrFKsl
+MS5mqnK4DZYFc1gGGI/rjsFUf3dD4ww6PTnwv3Ga2yBvXi7EckEeEqB+dRlVdvob
+cH/grVUum3s5Y4PTnxyNAUFZlFNZ8jlOcgXtAFuTnJ/jcvboZdE7Oja2OIMJo53d
+rbkqAPNGhQ98QDuTwWjHUq/Th1CQK4ALI/wqoc22TJpSh/mme5Dj4HhB7LWl
+-----END CERTIFICATE-----
diff --git a/tests/data/generate_tls_certs.sh b/tests/data/generate_tls_certs.sh
new file mode 100755
index 0000000..969a38c
--- /dev/null
+++ b/tests/data/generate_tls_certs.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -days 3650
diff --git a/tests/data/key.pem b/tests/data/key.pem
new file mode 100644
index 0000000..4263815
--- /dev/null
+++ b/tests/data/key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDCZg6pOgjRHfSU
+/Ji0X1tK6Eg4Kn1ynSOXae4do7yamvWAyIkoo3geMB2Bpz7eFiRNEWoraYoqlX1B
+1UpHbghvuYLjkSTsGUvDdVvE9Eqsr+Ospbcze6L7jncHbjJ/orx0ffrH2BmjiCXO
+CEXG6h0VqQhfbM0qjq0THRdWRuGy7/C+Paz7QvehOg8BhlqI4H/vFhTpi72ftOfq
+sT3+isN8rUOWtkM/eHPLEvO5/q2pcKHrpyBo9BOH7HLJZly+XrSRsHgfdt2GptgY
+wxA7xtTFdNBePC9Sy+XTKf2oKkwif30+b3GF8NBIXfNrAzcJD+Waf24VDN04R9iU
+7qhqJ1OrEu0GFVRwS60hyahb2eaa/aASLj3/c6a9ZBwR0xa3zHDFKsy8oNJ5R0Xe
+ttqVMcEVAcmUmIpFf3BM21JnlPwVNTvgingTMC0/zBjk0O/bneZ10A2jIqxVEbK1
+LfksMWQJL9PG5UNmuOW5zzrKpQh3+N8dkDNDuJ+kLz3RobQk6r/SaZUO9ufszN1O
+xNbCETxRo6IWWNOI1BfdCNfdNoS7fYxccqjmd5s2oNVEacUwWZ9fDZk/yWBAJaTU
+fEuvSqFHEjLxeYgrVlTPtRzgJtrvdcGlLFwg/K2kO64qi2yUGOghi70NjTufUe7j
+Ng++k/0Odq6cRbrOYA0S2NLKsT9NDQIDAQABAoICAQC9eAEEGQcs4fhXGZav/lyZ
+Nqnk7CzWf6eH1Pv6sXKKcUukmE9uZ10UdyrbCimxBX2eC8Ihy7yZYpfxiTPbSLg6
+RGH48Kc+4izAtWqbHMqHYusRg3Z6XB9u9Ny4RkQ7uF3bYEoDa3EZvQGzvMZdaCKu
+0M/TSdTxjJvNjEYJlg42e7t1f+FQB2YZIuArSUqGK+ElIq2BLuzDcuuzB8r3g0Gj
+C7BbfQswGnMpUzBvcHTMN3Xpmztwb6t1iBQcjYMJHH77nDaH3C9vJMBr6fqxeEo6
+pW7M2fX5ybcXR87tj0QjP4TPTIkl1Z77WW59N2X1lCPhoB+nrqESUJwcFDvbMrdM
+yUZoDTdGui/fGa+91Yl2wn7IIB5AzSH3Vkb6Z2cEFKuKCNcfobfNSOVrrnFBj52u
+IJGJhOj8FZz0HTYnIBQDpjVE92/+2CaCms5thlihm2ccG6jG2KGyjyNXRw548q/K
+NVr65VG3B9IS0PVQ5z5ue5pt36ig057OtmCBGx23fqKwprzSTWQhsgZoUKSH4+UN
+aBjqwcuhQJVPf4In6eJtW9Gf04cLDUMAWiaCHUTLVci3kauGqoxjj0Y4ysYbq16j
+di51k+XVwas0LLjFf2+eaGfd0nMFHPoXXlfaPGJSl2QAnIQGJ2d1b+/EYHlhswr3
+EPO+V22U5aiAXjBYGEb9IQKCAQEA+GlAJzigOc8F9A1HPXuDYAFZxDYF8TEjQOrW
+btFek55RTqO3pPgi5XR7gQLsqpno+3IMwkJPi9LE4HJ04dGlIiLAFsWWWsB7eshE
+E/dm9ddfcvUlcMQ5vT0Z8r6kyLZR5NG+x5KMZ5AB4Uv8PLLaXQd2LyXQzysfPX00
+tWs0/6/DA99uVNiAi9K6ebV5ZJxgbEqiDd0wn1W0MH0lvxIJumTKLiWpOR7FlXlL
+c+xRmyC0YqcX8LpI880GdnRI13SGPD9cu+/nivgjl18UC+j+WapeBLDCQHRcjaVy
+UBKGwOTla3mdx6//jWxFQzZNsapR8dxIxuxY5b+cNUhV7W2LNQKCAQEAyFZkJxQf
+Of7fHyBCL9ldzxgu4MHtGIeH/z+0nnJj+/b5HFlWAK9A1lSmTEMwNZ7tSQWAhmQb
+lS3DEA4a12JKvqtpamYyj17hdx3dCGe9wU8z2aS08wDuM9L1g5xqhhrn2ewcpJNp
+9cqoBKoqLjk9yA2X4GEUCaKuHbOMrkfQAq8jQBgYe3aPWZjMuDAI2RNpGxGwyslR
+AM+LnWFskGtgB5q1rBNQduNXEKVqNd5Zsqztd/tkcLmgqLZHbpUWwqYYTaVE3wW9
+cir+9VGTuOi9kKEjZ2P35f4VP2GNA7V3fgU/LYv81V2osQPsngheL5RVA1RVvCqo
+XTpsEUKie9WdeQKCAQEAhry34k4xggmLRhupp2yGDp3M7cMLqA4p+/0kgAkqDlGR
+8mCUrHM2olRy5MAMVGCU4UW0K+3BraqNxNvwD8ghlIlavT9A1UqP70IOwvGvM+s0
+x2q2exrD4qPwnhzPzlotwzoNC7yuUUHn8ya+0sGD9W+lp98QCj5ufHCcFUboAUN5
+OHGJK5Ye6zhKktde17aGClbU3UY7KEFZMe+/eIq1IhenHi6pQeUx8GhRB7iHbufn
+T5coQhcYmLx9I+Tg2ZRHdwg7KWjvow4CaAlXGzquMz5YLp0dT86NoPq7LTlPQ/Mj
+iQ73CKeqqi+uxcz/iT1DozcDdnodocgzVyc8DEMdfQKCAQAQ37Xv1LIMoHsKlBz/
+Cr/sAY1xQORHfKLnzOXZsqjZQCQbTyr/Q8OiSd737XDSE2DJFb2NlED+f6w+XfHE
+0nKZPLbUT2dSzBsRfWJwosxIy/MCEe1rylhF5S7otvQB96IvqMOA2SnDmh4sxmhn
+HEsn3n08WPDnHtyrg8QFqebLUxUVAPKO851/Xm9f1CvqnMftj7/kVLCN8O1BhEMw
+ptqfyVgj9jyAxwU+UbBweRn1Aru9r172X6w4iaHanpQcMQE7CQCUCFe8lgKDhyt6
+F6Bf3jKtMq5eoNgJTp4iAdbetnJr066oCgt7XWlAplPIjiXa8e+GudEUiScxDPvC
+kmuBAoIBAH/+OwEZBEnMb5c+aWTgxUh3NGtAy6LgD+1cj5IT2s215si6ea4QzPM0
+Ddht4nbs2oPML5kwpym4IKI1OY+07haZJoxt14xo7eLhO8+7+t6yka/Y6SlIuSqP
+Cku40Ok2JyWRQxe3n371wmEKKuJyqVMDR1bY5tkwhh1MYUO8xkGbbZcbrUx6GLuJ
+E53ybjzdaxFYbHO2ZvqQALZq5h8mb+5IFFhIQjM2PLXHY3Ok3xNgrR9Se1Bb6tZY
+x+j6/xEBVw1Yg2J/UgMjRQDzax/wTBzTlUkc1kpp0Xi2iQcxVQSZZ1nQU3opcmwJ
+UbBWUEN95O4LnWFuyhEIOIEmp5JtGks=
+-----END PRIVATE KEY-----
diff --git a/tests/fixtures/mod.rs b/tests/fixtures/mod.rs
index a227f84..9f3560d 100644
--- a/tests/fixtures/mod.rs
+++ b/tests/fixtures/mod.rs
@@ -88,7 +88,7 @@ pub fn port() -> u16 {
#[allow(dead_code)]
pub fn server<I>(#[default(&[] as &[&str])] args: I) -> TestServer
where
- I: IntoIterator,
+ I: IntoIterator + Clone,
I::Item: AsRef<std::ffi::OsStr>,
{
let port = port();
@@ -98,13 +98,16 @@ where
.arg(tmpdir.path())
.arg("-p")
.arg(port.to_string())
- .args(args)
+ .args(args.clone())
.stdout(Stdio::null())
.spawn()
.expect("Couldn't run test binary");
+ let is_tls = args
+ .into_iter()
+ .any(|x| x.as_ref().to_str().unwrap().contains("tls"));
wait_for_port(port);
- TestServer::new(port, tmpdir, child)
+ TestServer::new(port, tmpdir, child, is_tls)
}
/// Same as `server()` but ignore stderr
@@ -112,7 +115,7 @@ where
#[allow(dead_code)]
pub fn server_no_stderr<I>(#[default(&[] as &[&str])] args: I) -> TestServer
where
- I: IntoIterator,
+ I: IntoIterator + Clone,
I::Item: AsRef<std::ffi::OsStr>,
{
let port = port();
@@ -122,14 +125,17 @@ where
.arg(tmpdir.path())
.arg("-p")
.arg(port.to_string())
- .args(args)
+ .args(args.clone())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("Couldn't run test binary");
+ let is_tls = args
+ .into_iter()
+ .any(|x| x.as_ref().to_str().unwrap().contains("tls"));
wait_for_port(port);
- TestServer::new(port, tmpdir, child)
+ TestServer::new(port, tmpdir, child, is_tls)
}
/// Wait a max of 1s for the port to become available.
@@ -150,23 +156,29 @@ pub struct TestServer {
port: u16,
tmpdir: TempDir,
child: Child,
+ is_tls: bool,
}
#[allow(dead_code)]
impl TestServer {
- pub fn new(port: u16, tmpdir: TempDir, child: Child) -> Self {
+ pub fn new(port: u16, tmpdir: TempDir, child: Child, is_tls: bool) -> Self {
Self {
port,
tmpdir,
child,
+ is_tls,
}
}
+
pub fn url(&self) -> Url {
- Url::parse(&format!("http://localhost:{}", self.port)).unwrap()
+ let protocol = if self.is_tls { "https" } else { "http" };
+ Url::parse(&format!("{}://localhost:{}", protocol, self.port)).unwrap()
}
+
pub fn path(&self) -> &std::path::Path {
self.tmpdir.path()
}
+
pub fn port(&self) -> u16 {
self.port
}
diff --git a/tests/tls.rs b/tests/tls.rs
new file mode 100644
index 0000000..2464e1f
--- /dev/null
+++ b/tests/tls.rs
@@ -0,0 +1,53 @@
+mod fixtures;
+
+use assert_cmd::Command;
+use fixtures::{server, Error, TestServer, FILES};
+use predicates::str::contains;
+use reqwest::blocking::ClientBuilder;
+use rstest::rstest;
+use select::{document::Document, node::Node};
+
+/// Can start the server with TLS and receive encrypted responses.
+#[rstest]
+fn tls_works(
+ #[with(&[
+ "--tls-cert", "tests/data/cert.pem",
+ "--tls-key", "tests/data/key.pem"
+ ])]
+ server: TestServer,
+) -> Result<(), Error> {
+ let client = ClientBuilder::new()
+ .danger_accept_invalid_certs(true)
+ .build()?;
+ let body = client.get(server.url()).send()?.error_for_status()?;
+ let parsed = Document::from_read(body)?;
+ for &file in FILES {
+ assert!(parsed.find(|x: &Node| x.text() == file).next().is_some());
+ }
+
+ Ok(())
+}
+
+/// Wrong path for cert throws error.
+#[rstest]
+fn wrong_path_cert() -> Result<(), Error> {
+ Command::cargo_bin("miniserve")?
+ .args(&["--tls-cert", "wrong", "--tls-key", "tests/data/key.pem"])
+ .assert()
+ .failure()
+ .stderr(contains("Error: Couldn't access TLS certificate \"wrong\""));
+
+ Ok(())
+}
+
+/// Wrong paths for key throws errors.
+#[rstest]
+fn wrong_path_key() -> Result<(), Error> {
+ Command::cargo_bin("miniserve")?
+ .args(&["--tls-cert", "tests/data/cert.pem", "--tls-key", "wrong"])
+ .assert()
+ .failure()
+ .stderr(contains("Error: Couldn't access TLS key \"wrong\""));
+
+ Ok(())
+}