diff options
-rw-r--r-- | Cargo.lock | 84 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/main.rs | 193 | ||||
-rw-r--r-- | tests/bind.rs | 88 |
4 files changed, 249 insertions, 118 deletions
@@ -303,7 +303,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -312,7 +312,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -363,7 +363,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -506,6 +506,12 @@ dependencies = [ ] [[package]] +name = "c_linked_list" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" + +[[package]] name = "cc" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -530,7 +536,7 @@ dependencies = [ "num-integer", "num-traits", "time 0.1.43", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -735,7 +741,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -880,6 +886,12 @@ dependencies = [ ] [[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] name = "generic-array" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -890,6 +902,28 @@ dependencies = [ ] [[package]] +name = "get_if_addrs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7" +dependencies = [ + "c_linked_list", + "get_if_addrs-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "get_if_addrs-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48" +dependencies = [ + "gcc", + "libc", +] + +[[package]] name = "getrandom" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1377,6 +1411,7 @@ dependencies = [ "clap 3.0.0-beta.4", "clap_generate", "futures", + "get_if_addrs", "grass", "hex", "http", @@ -1399,6 +1434,7 @@ dependencies = [ "serde", "sha2", "simplelog", + "socket2", "strum", "strum_macros", "tar", @@ -1428,7 +1464,7 @@ dependencies = [ "log", "miow", "ntapi", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1437,7 +1473,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1467,7 +1503,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1546,7 +1582,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1571,7 +1607,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1964,7 +2000,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2015,7 +2051,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2278,7 +2314,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2433,7 +2469,7 @@ dependencies = [ "rand 0.8.4", "redox_syscall", "remove_dir_all", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2463,7 +2499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2521,7 +2557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2536,7 +2572,7 @@ dependencies = [ "stdweb", "time-macros", "version_check", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2593,7 +2629,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2781,7 +2817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", - "winapi", + "winapi 0.3.9", "winapi-util", ] @@ -2906,6 +2942,12 @@ dependencies = [ [[package]] name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" @@ -2926,7 +2968,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2941,7 +2983,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -53,6 +53,8 @@ http = "0.2" bytes = "1" atty = "0.2" rustls = { version = "0.19", optional = true } +socket2 = "0.4" +get_if_addrs = "0.5" [features] default = ["tls"] diff --git a/src/main.rs b/src/main.rs index c1d17b4..882fd08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use std::io; use std::io::Write; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, SocketAddr, TcpListener}; use std::thread; use std::time::Duration; @@ -52,10 +52,11 @@ fn main() -> Result<()> { let miniserve_config = MiniserveConfig::try_from_args(args)?; - match run(miniserve_config) { - Ok(()) => (), - Err(e) => errors::log_error_chain(e.to_string()), - } + run(miniserve_config).map_err(|e| { + errors::log_error_chain(e.to_string()); + e + })?; + Ok(()) } @@ -102,23 +103,6 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { let inside_config = miniserve_config.clone(); - let interfaces = miniserve_config - .interfaces - .iter() - .map(|&interface| { - if interface == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) { - // If the interface is 0.0.0.0, we'll change it to 127.0.0.1 so that clicking the link will - // also work on Windows. Why can't Windows interpret 0.0.0.0? - "127.0.0.1".to_string() - } else if interface.is_ipv6() { - // If the interface is IPv6 then we'll print it with brackets so that it is clickable. - format!("[{}]", interface) - } else { - format!("{}", interface) - } - }) - .collect::<Vec<String>>(); - let canon_path = miniserve_config.path.canonicalize().map_err(|e| { ContextualError::IoError("Failed to resolve path to be served".to_string(), e) })?; @@ -164,63 +148,59 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { thread::sleep(Duration::from_millis(500)); } } - let mut addresses = String::new(); - for interface in &interfaces { - 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!( - "{protocol}://{interface}:{port}", - protocol = protocol, - interface = &interface, - port = miniserve_config.port - )) - .bold() - )); - - if let Some(random_route) = miniserve_config.clone().random_route { - addresses.push_str(&format!( - "{}", - Color::Green - .paint(format!("/{random_route}", random_route = random_route,)) - .bold() - )); - } - } - let socket_addresses = interfaces - .iter() - .map(|interface| { - format!( - "{interface}:{port}", - interface = &interface, - port = miniserve_config.port, - ) - .parse::<SocketAddr>() - }) - .collect::<Result<Vec<SocketAddr>, _>>(); - - let socket_addresses = match socket_addresses { - Ok(addresses) => addresses, - Err(e) => { - // Note that this should never fail, since CLI parsing succeeded - // This means the format of each IP address is valid, and so is the port - // Valid IpAddr + valid port == valid SocketAddr - return Err(ContextualError::ParseError( - "string as socket address".to_string(), - e.to_string(), - )); + let display_urls = { + let (mut ifaces, wildcard): (Vec<_>, Vec<_>) = miniserve_config + .interfaces + .clone() + .into_iter() + .partition(|addr| !addr.is_unspecified()); + + // Replace wildcard addresses with local interface addresses + if !wildcard.is_empty() { + let all_ipv4 = wildcard.iter().any(|addr| addr.is_ipv4()); + let all_ipv6 = wildcard.iter().any(|addr| addr.is_ipv6()); + ifaces = get_if_addrs::get_if_addrs() + .unwrap_or_else(|e| { + error!("Failed to get local interface addresses: {}", e); + Default::default() + }) + .into_iter() + .map(|iface| iface.ip()) + .filter(|ip| (all_ipv4 && ip.is_ipv4()) || (all_ipv6 && ip.is_ipv6())) + .collect(); + ifaces.sort(); } + + ifaces + .into_iter() + .map(|addr| match addr { + IpAddr::V4(_) => format!("{}:{}", addr, miniserve_config.port), + IpAddr::V6(_) => format!("[{}]:{}", addr, miniserve_config.port), + }) + .map(|addr| match miniserve_config.tls_rustls_config { + Some(_) => format!("https://{}", addr), + None => format!("http://{}", addr), + }) + .map(|url| match miniserve_config.random_route { + Some(ref random_route) => format!("{}/{}", url, random_route), + None => url, + }) + .map(|url| Color::Green.paint(url).bold().to_string()) + .collect::<Vec<_>>() }; + let socket_addresses = miniserve_config + .interfaces + .iter() + .map(|&interface| SocketAddr::new(interface, miniserve_config.port)) + .collect::<Vec<_>>(); + + let display_sockets = socket_addresses + .iter() + .map(|sock| Color::Green.paint(sock.to_string()).bold().to_string()) + .collect::<Vec<_>>(); + let srv = actix_web::HttpServer::new(move || { App::new() .wrap(configure_header(&inside_config.clone())) @@ -240,40 +220,59 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { .default_service(web::get().to(error_404)) }); - #[cfg(feature = "tls")] - 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() - }; + let srv = socket_addresses.iter().try_fold(srv, |srv, addr| { + let listener = create_tcp_listener(*addr).map_err(|e| { + ContextualError::IoError(format!("Failed to bind server to {}", addr), e) + })?; + + #[cfg(feature = "tls")] + let srv = match &miniserve_config.tls_rustls_config { + Some(tls_config) => srv.listen_rustls(listener, tls_config.clone()), + None => srv.listen(listener), + }; - #[cfg(not(feature = "tls"))] - let srv = srv - .bind(socket_addresses.as_slice()) - .map_err(|e| ContextualError::IoError("Failed to bind server".to_string(), e))? - .shutdown_timeout(0) - .run(); + #[cfg(not(feature = "tls"))] + let srv = srv.listen(listener); + + srv.map_err(|e| ContextualError::IoError(format!("Failed to bind server to {}", addr), e)) + })?; + + let srv = srv.shutdown_timeout(0).run(); + + println!("Bound to {}", display_sockets.join(", ")); + + println!("Serving path {}", Color::Yellow.paint(path_string).bold()); println!( - "Serving path {path} at {addresses}", - path = Color::Yellow.paint(path_string).bold(), - addresses = addresses, + "Availabe at (non-exhaustive list):\n {}\n", + display_urls.join("\n "), ); if atty::is(atty::Stream::Stdout) { - println!("\nQuit by pressing CTRL-C"); + println!("Quit by pressing CTRL-C"); } srv.await .map_err(|e| ContextualError::IoError("".to_owned(), e)) } +/// Allows us to set low-level socket options +/// +/// This mainly used to set `set_only_v6` socket option +/// to get a consistent behavior across platforms. +/// see: https://github.com/svenstaro/miniserve/pull/500 +fn create_tcp_listener(addr: SocketAddr) -> io::Result<TcpListener> { + use socket2::{Domain, Protocol, Socket, Type}; + let socket = Socket::new(Domain::for_address(addr), Type::STREAM, Some(Protocol::TCP))?; + if addr.is_ipv6() { + socket.set_only_v6(true)?; + } + socket.set_reuse_address(true)?; + socket.bind(&addr.into())?; + socket.listen(1024 /* Default backlog */)?; + Ok(TcpListener::from(socket)) +} + fn configure_header(conf: &MiniserveConfig) -> middleware::DefaultHeaders { let headers = conf.clone().header; diff --git a/tests/bind.rs b/tests/bind.rs new file mode 100644 index 0000000..1c816f0 --- /dev/null +++ b/tests/bind.rs @@ -0,0 +1,88 @@ +mod fixtures; + +use assert_cmd::prelude::*; +use assert_fs::fixture::TempDir; +use fixtures::{port, server, tmpdir, Error, TestServer}; +use regex::Regex; +use rstest::rstest; +use std::io::{BufRead, BufReader}; +use std::process::{Command, Stdio}; + +#[rstest] +#[case(&["-i", "12.123.234.12"])] +#[case(&["-i", "::", "-i", "12.123.234.12"])] +fn bind_fails(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> { + Command::cargo_bin("miniserve")? + .arg(tmpdir.path()) + .arg("-p") + .arg(port.to_string()) + .args(args) + .assert() + .stderr(predicates::str::contains("Failed to bind server to")) + .failure(); + + Ok(()) +} + +#[rstest] +#[case(server(&[] as &[&str]), true, true)] +#[case(server(&["-i", "::"]), false, true)] +#[case(server(&["-i", "0.0.0.0"]), true, false)] +#[case(server(&["-i", "::", "-i", "0.0.0.0"]), true, true)] +fn bind_ipv4_ipv6( + #[case] server: TestServer, + #[case] bind_ipv4: bool, + #[case] bind_ipv6: bool, +) -> Result<(), Error> { + assert_eq!( + reqwest::blocking::get(format!("http://127.0.0.1:{}", server.port()).as_str()).is_ok(), + bind_ipv4 + ); + assert_eq!( + reqwest::blocking::get(format!("http://[::1]:{}", server.port()).as_str()).is_ok(), + bind_ipv6 + ); + + Ok(()) +} + +#[rstest] +#[case(&[] as &[&str])] +#[case(&["-i", "::"])] +#[case(&["-i", "127.0.0.1"])] +#[case(&["-i", "0.0.0.0"])] +#[case(&["-i", "::", "-i", "0.0.0.0"])] +#[case(&["--random-route"])] +fn validate_printed_urls(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> { + let mut child = Command::cargo_bin("miniserve")? + .arg(tmpdir.path()) + .arg("-p") + .arg(port.to_string()) + .args(args) + .stdout(Stdio::piped()) + .spawn()?; + + // WARN assumes urls list is terminated by an empty line + let url_lines = BufReader::new(child.stdout.take().unwrap()) + .lines() + .map(|line| line.expect("Error reading stdout")) + .take_while(|line| !line.is_empty()) /* non-empty lines */ + .collect::<Vec<_>>(); + let url_lines = url_lines.join("\n"); + + let urls = Regex::new(r"http://[a-zA-Z0-9\.\[\]:/]+") + .unwrap() + .captures_iter(url_lines.as_str()) + .map(|caps| caps.get(0).unwrap().as_str()) + .collect::<Vec<_>>(); + + assert!(!urls.is_empty()); + + for url in urls { + reqwest::blocking::get(url)?.error_for_status()?; + } + + child.kill()?; + + Ok(()) +} |