From 702547d65a9cb60ca6672792c08225b1c3099bad Mon Sep 17 00:00:00 2001 From: Colin Harrington Date: Wed, 24 Jun 2020 08:43:29 -0500 Subject: [PATCH] fix(cli): ipv6 parsing for --allow-net params (#6453) Co-authored-by: Liming Jin --- cli/flags.rs | 69 +++++----- cli/main.rs | 1 + cli/resolve_hosts.rs | 188 ++++++++++++++++++++++++++ cli/tests/complex_permissions_test.ts | 12 +- 4 files changed, 232 insertions(+), 38 deletions(-) create mode 100644 cli/resolve_hosts.rs diff --git a/cli/flags.rs b/cli/flags.rs index 4226bb5c50cd3..8093f55c65585 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -1,4 +1,5 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use crate::resolve_hosts::resolve_hosts; use clap::App; use clap::AppSettings; use clap::Arg; @@ -1053,7 +1054,7 @@ Grant permission to read from disk and listen to network: Grant permission to read allow-listed files from disk: deno run --allow-read=/etc https://deno.land/std/http/file_server.ts - + Deno allows specifying the filename '-' to read the file from stdin. curl https://deno.land/std/examples/welcome.ts | target/debug/deno run -", ) @@ -1375,41 +1376,6 @@ pub fn resolve_urls(urls: Vec) -> Vec { out } -/// Expands "bare port" paths (eg. ":8080") into full paths with hosts. It -/// expands to such paths into 3 paths with following hosts: `0.0.0.0:port`, -/// `127.0.0.1:port` and `localhost:port`. -fn resolve_hosts(paths: Vec) -> Vec { - let mut out: Vec = vec![]; - for host_and_port in paths.iter() { - let parts = host_and_port.split(':').collect::>(); - - match parts.len() { - // host only - 1 => { - out.push(host_and_port.to_owned()); - } - // host and port (NOTE: host might be empty string) - 2 => { - let host = parts[0]; - let port = parts[1]; - - if !host.is_empty() { - out.push(host_and_port.to_owned()); - continue; - } - - // we got bare port, let's add default hosts - for host in ["0.0.0.0", "127.0.0.1", "localhost"].iter() { - out.push(format!("{}:{}", host, port)); - } - } - _ => panic!("Bad host:port pair: {}", host_and_port), - } - } - - out -} - #[cfg(test)] mod tests { use super::*; @@ -2481,6 +2447,37 @@ mod tests { ); } + #[test] + fn allow_net_allowlist_with_ipv6_address() { + let r = flags_from_vec_safe(svec![ + "deno", + "run", + "--allow-net=deno.land,deno.land:80,::,127.0.0.1,[::1],1.2.3.4:5678,:5678,[::1]:8080", + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run { + script: "script.ts".to_string(), + }, + net_allowlist: svec![ + "deno.land", + "deno.land:80", + "::", + "127.0.0.1", + "[::1]", + "1.2.3.4:5678", + "0.0.0.0:5678", + "127.0.0.1:5678", + "localhost:5678", + "[::1]:8080" + ], + ..Flags::default() + } + ); + } + #[test] fn lock_write() { let r = flags_from_vec_safe(svec![ diff --git a/cli/main.rs b/cli/main.rs index e20435a0079cf..4f327b08efed9 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -52,6 +52,7 @@ pub mod ops; pub mod permissions; mod repl; pub mod resolve_addr; +pub mod resolve_hosts; pub mod signal; pub mod source_maps; mod startup_data; diff --git a/cli/resolve_hosts.rs b/cli/resolve_hosts.rs new file mode 100644 index 0000000000000..d562af4d5d474 --- /dev/null +++ b/cli/resolve_hosts.rs @@ -0,0 +1,188 @@ +use std::net::IpAddr; +use std::str::FromStr; +use url::Url; + +#[derive(Debug, PartialEq, Eq)] +pub struct ParsePortError(String); + +#[derive(Debug, PartialEq, Eq)] +pub struct BarePort(u16); + +impl FromStr for BarePort { + type Err = ParsePortError; + fn from_str(s: &str) -> Result { + if s.starts_with(':') { + match s.split_at(1).1.parse::() { + Ok(port) => Ok(BarePort(port)), + Err(e) => Err(ParsePortError(e.to_string())), + } + } else { + Err(ParsePortError( + "Bare Port doesn't start with ':'".to_string(), + )) + } + } +} + +/// Expands "bare port" paths (eg. ":8080") into full paths with hosts. It +/// expands to such paths into 3 paths with following hosts: `0.0.0.0:port`, +/// `127.0.0.1:port` and `localhost:port`. +pub fn resolve_hosts(paths: Vec) -> Vec { + let mut out: Vec = vec![]; + for host_and_port in paths.iter() { + if Url::parse(&format!("deno://{}", host_and_port)).is_ok() + || host_and_port.parse::().is_ok() + { + out.push(host_and_port.to_owned()) + } else if let Ok(port) = host_and_port.parse::() { + // we got bare port, let's add default hosts + for host in ["0.0.0.0", "127.0.0.1", "localhost"].iter() { + out.push(format!("{}:{}", host, port.0)); + } + } else { + panic!("Bad host:port pair: {}", host_and_port) + } + } + out +} + +#[cfg(test)] +mod bare_port_tests { + use super::{BarePort, ParsePortError}; + + #[test] + fn bare_port_parsed() { + let expected = BarePort(8080); + let actual = ":8080".parse::(); + assert_eq!(actual, Ok(expected)); + } + + #[test] + fn bare_port_parse_error1() { + let expected = + ParsePortError("Bare Port doesn't start with ':'".to_string()); + let actual = "8080".parse::(); + assert_eq!(actual, Err(expected)); + } + + #[test] + fn bare_port_parse_error2() { + let actual = ":65536".parse::(); + assert!(actual.is_err()); + } + + #[test] + fn bare_port_parse_error3() { + let actual = ":14u16".parse::(); + assert!(actual.is_err()); + } + + #[test] + fn bare_port_parse_error4() { + let actual = "Deno".parse::(); + assert!(actual.is_err()); + } + + #[test] + fn bare_port_parse_error5() { + let actual = "deno.land:8080".parse::(); + assert!(actual.is_err()); + } +} + +#[cfg(test)] +mod tests { + use super::resolve_hosts; + + // Creates vector of strings, Vec + macro_rules! svec { + ($($x:expr),*) => (vec![$($x.to_string()),*]); + } + + #[test] + fn resolve_hosts_() { + let entries = svec![ + "deno.land", + "deno.land:80", + "::", + "::1", + "127.0.0.1", + "[::1]", + "1.2.3.4:5678", + "0.0.0.0:5678", + "127.0.0.1:5678", + "[::]:5678", + "[::1]:5678", + "localhost:5678", + "[::1]:8080", + "[::]:8000", + "[::1]:8000", + "localhost:8000", + "0.0.0.0:4545", + "127.0.0.1:4545", + "999.0.88.1:80" + ]; + let expected = svec![ + "deno.land", + "deno.land:80", + "::", + "::1", + "127.0.0.1", + "[::1]", + "1.2.3.4:5678", + "0.0.0.0:5678", + "127.0.0.1:5678", + "[::]:5678", + "[::1]:5678", + "localhost:5678", + "[::1]:8080", + "[::]:8000", + "[::1]:8000", + "localhost:8000", + "0.0.0.0:4545", + "127.0.0.1:4545", + "999.0.88.1:80" + ]; + let actual = resolve_hosts(entries); + assert_eq!(actual, expected); + } + + #[test] + fn resolve_hosts_expansion() { + let entries = svec![":8080"]; + let expected = svec!["0.0.0.0:8080", "127.0.0.1:8080", "localhost:8080"]; + let actual = resolve_hosts(entries); + assert_eq!(actual, expected); + } + + #[test] + fn resolve_hosts_ipv6() { + let entries = + svec!["::", "::1", "[::1]", "[::]:5678", "[::1]:5678", "::cafe"]; + let expected = + svec!["::", "::1", "[::1]", "[::]:5678", "[::1]:5678", "::cafe"]; + let actual = resolve_hosts(entries); + assert_eq!(actual, expected); + } + + #[test] + #[should_panic] + fn resolve_hosts_ipv6_error1() { + let entries = svec![":::"]; + resolve_hosts(entries); + } + + #[test] + #[should_panic] + fn resolve_hosts_ipv6_error2() { + let entries = svec!["0123:4567:890a:bcde:fg::"]; + resolve_hosts(entries); + } + + #[test] + #[should_panic] + fn resolve_hosts_ipv6_error3() { + let entries = svec!["[::q]:8080"]; + resolve_hosts(entries); + } +} diff --git a/cli/tests/complex_permissions_test.ts b/cli/tests/complex_permissions_test.ts index ad8b5302ca21c..f4b8e6c730655 100644 --- a/cli/tests/complex_permissions_test.ts +++ b/cli/tests/complex_permissions_test.ts @@ -14,7 +14,11 @@ const test: { [key: string]: Function } = { }, netListen(endpoints: string[]): void { endpoints.forEach((endpoint) => { - const [hostname, port] = endpoint.split(":"); + const index = endpoint.lastIndexOf(":"); + const [hostname, port] = [ + endpoint.substr(0, index), + endpoint.substr(index + 1), + ]; const listener = Deno.listen({ transport: "tcp", hostname, @@ -25,7 +29,11 @@ const test: { [key: string]: Function } = { }, async netConnect(endpoints: string[]): Promise { for (const endpoint of endpoints) { - const [hostname, port] = endpoint.split(":"); + const index = endpoint.lastIndexOf(":"); + const [hostname, port] = [ + endpoint.substr(0, index), + endpoint.substr(index + 1), + ]; const listener = await Deno.connect({ transport: "tcp", hostname,