aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSven-Hendrik Haase <svenstaro@gmail.com>2021-08-30 04:49:35 +0000
committerGitHub <noreply@github.com>2021-08-30 04:49:35 +0000
commitc758993260d315c2d0be95c9cebd67713a3f5b8c (patch)
tree6302c540464bd3b8d26f290365d38daaf7b9fea5
parentMerge pull request #587 from svenstaro/switch-structopt-to-clap (diff)
parentaddress review comment (diff)
downloadminiserve-c758993260d315c2d0be95c9cebd67713a3f5b8c.tar.gz
miniserve-c758993260d315c2d0be95c9cebd67713a3f5b8c.zip
Merge pull request #500 from aliemjay/interfaces
Interfaces
-rw-r--r--Cargo.lock84
-rw-r--r--Cargo.toml2
-rw-r--r--src/main.rs193
-rw-r--r--tests/bind.rs88
4 files changed, 249 insertions, 118 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2be4620..45ce326 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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]]
diff --git a/Cargo.toml b/Cargo.toml
index 4efc015..a40aad5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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(())
+}