diff options
author | Sven-Hendrik Haase <svenstaro@gmail.com> | 2021-08-27 02:51:15 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-27 02:51:15 +0000 |
commit | 96f68cfb7156ad1fccdb8718706401443fe5d03a (patch) | |
tree | 1633c19d861995145adccebf89cbd14889a6e967 | |
parent | Golf tests a bit more (diff) | |
parent | Fix accidental formatting (diff) | |
download | miniserve-96f68cfb7156ad1fccdb8718706401443fe5d03a.tar.gz miniserve-96f68cfb7156ad1fccdb8718706401443fe5d03a.zip |
Merge pull request #576 from svenstaro/add-rustls-support
Add TLS support via rustls (fixes #18)
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | Cargo.lock | 99 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | README.md | 19 | ||||
-rw-r--r-- | src/args.rs | 8 | ||||
-rw-r--r-- | src/config.rs | 181 | ||||
-rw-r--r-- | src/main.rs | 187 | ||||
-rw-r--r-- | tests/data/cert.pem | 29 | ||||
-rwxr-xr-x | tests/data/generate_tls_certs.sh | 2 | ||||
-rw-r--r-- | tests/data/key.pem | 52 | ||||
-rw-r--r-- | tests/fixtures/mod.rs | 28 | ||||
-rw-r--r-- | tests/tls.rs | 53 |
12 files changed, 485 insertions, 179 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0a1f9..d730d6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Add hardened systemd template unit file to `packaging/miniserve@.service` - Fix qrcodegen dependency problem [#568](https://github.com/svenstaro/miniserve/issues/568) - Remove animation on QR code hover (it was kind of annoying as it makes things less snappy) +- Add TLS support [#576](https://github.com/svenstaro/miniserve/pull/576) ## [0.14.0] - 2021-04-18 - Fix breadcrumbs for right-to-left languages [#489](https://github.com/svenstaro/miniserve/pull/489) (thanks @aliemjay) @@ -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" @@ -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" @@ -49,6 +49,10 @@ Sometimes this is just a more practical and quick way than doing things properly miniserve -i 192.168.0.1 -i 10.13.37.10 -i ::1 /tmp/myshare +### Start with TLS: + + miniserve --tls-cert my.cert --tls-key my.key /tmp/myshare + ### Upload a file using `curl`: # in one terminal @@ -74,7 +78,7 @@ Sometimes this is just a more practical and quick way than doing things properly ## Usage - miniserve 0.14.0 + miniserve 0.14.1-alpha.0 Sven-Hendrik Haase <svenstaro@gmail.com>, Boastful Squirrel <boastful.squirrel@gmail.com> For when you really just want to serve some files over HTTP right now! @@ -102,6 +106,12 @@ Sometimes this is just a more practical and quick way than doing things properly -h, --help Prints help information + -H, --hidden + Show hidden files + + -F, --hide-version-footer + Hide version footer + -P, --no-symlinks Do not follow symbolic links @@ -134,6 +144,7 @@ Sometimes this is just a more practical and quick way than doing things properly zenburn, monokai] --header <header>... Set custom header for responses + --index <index_file> The name of a directory index file to serve, like "index.html" @@ -151,6 +162,12 @@ Sometimes this is just a more practical and quick way than doing things properly -t, --title <title> Shown instead of host in page title and heading + --tls-cert <tls-cert> + TLS certificate to use + + --tls-key <tls-key> + TLS private key to use + ARGS: <PATH> diff --git a/src/args.rs b/src/args.rs index 819618f..c2b2bf2 100644 --- a/src/args.rs +++ b/src/args.rs @@ -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 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----- +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 +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(()) +} |