From 2edee3367dc9003b721cf87ca57e820c7acf7b25 Mon Sep 17 00:00:00 2001 From: andy finch Date: Wed, 8 May 2019 19:20:30 -0400 Subject: [PATCH] First pass at permissions whitelist (#2129) --- cli/deno_dir.rs | 55 ++-- cli/flags.rs | 118 +++++++-- cli/ops.rs | 140 +++++++---- cli/permissions.rs | 404 ++++++++++++++++++++++++++++-- cli/state.rs | 9 +- js/read_dir_test.ts | 8 +- tools/complex_permissions_test.py | 195 ++++++++++++++ tools/complex_permissions_test.ts | 24 ++ tools/test.py | 2 + 9 files changed, 850 insertions(+), 105 deletions(-) create mode 100755 tools/complex_permissions_test.py create mode 100644 tools/complex_permissions_test.ts diff --git a/cli/deno_dir.rs b/cli/deno_dir.rs index c58a252cba..4bca1117a4 100644 --- a/cli/deno_dir.rs +++ b/cli/deno_dir.rs @@ -291,36 +291,14 @@ impl DenoDir { referrer: &str, ) -> Result { let specifier = self.src_file_to_url(specifier); - let mut referrer = self.src_file_to_url(referrer); + let referrer = self.src_file_to_url(referrer); debug!( "resolve_module specifier {} referrer {}", specifier, referrer ); - if referrer.starts_with('.') { - let cwd = std::env::current_dir().unwrap(); - let referrer_path = cwd.join(referrer); - referrer = referrer_path.to_str().unwrap().to_string() + "/"; - } - - let j = if is_remote(&specifier) - || (Path::new(&specifier).is_absolute() && !is_remote(&referrer)) - { - parse_local_or_remote(&specifier)? - } else if referrer.ends_with('/') { - let r = Url::from_directory_path(&referrer); - // TODO(ry) Properly handle error. - if r.is_err() { - error!("Url::from_directory_path error {}", referrer); - } - let base = r.unwrap(); - base.join(specifier.as_ref())? - } else { - let base = parse_local_or_remote(&referrer)?; - base.join(specifier.as_ref())? - }; - Ok(j) + resolve_file_url(specifier, referrer) } /// Returns (module name, local filename) @@ -889,6 +867,35 @@ fn save_source_code_headers( } } +pub fn resolve_file_url( + specifier: String, + mut referrer: String, +) -> Result { + if referrer.starts_with('.') { + let cwd = std::env::current_dir().unwrap(); + let referrer_path = cwd.join(referrer); + referrer = referrer_path.to_str().unwrap().to_string() + "/"; + } + + let j = if is_remote(&specifier) + || (Path::new(&specifier).is_absolute() && !is_remote(&referrer)) + { + parse_local_or_remote(&specifier)? + } else if referrer.ends_with('/') { + let r = Url::from_directory_path(&referrer); + // TODO(ry) Properly handle error. + if r.is_err() { + error!("Url::from_directory_path error {}", referrer); + } + let base = r.unwrap(); + base.join(specifier.as_ref())? + } else { + let base = parse_local_or_remote(&referrer)?; + base.join(specifier.as_ref())? + }; + Ok(j) +} + #[cfg(test)] mod tests { use super::*; diff --git a/cli/flags.rs b/cli/flags.rs index e68b831d50..d90a025a2d 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -16,8 +16,11 @@ pub struct DenoFlags { /// the path passed on the command line, otherwise `None`. pub config_path: Option, pub allow_read: bool, + pub read_whitelist: Vec, pub allow_write: bool, + pub write_whitelist: Vec, pub allow_net: bool, + pub net_whitelist: Vec, pub allow_env: bool, pub allow_run: bool, pub allow_high_precision: bool, @@ -193,17 +196,29 @@ ability to spawn subprocesses. ", ).arg( Arg::with_name("allow-read") - .long("allow-read") - .help("Allow file system read access"), - ).arg( - Arg::with_name("allow-write") - .long("allow-write") - .help("Allow file system write access"), - ).arg( - Arg::with_name("allow-net") - .long("allow-net") - .help("Allow network access"), - ).arg( + .long("allow-read") + .min_values(0) + .takes_value(true) + .use_delimiter(true) + .require_equals(true) + .help("Allow file system read access"), + ).arg( + Arg::with_name("allow-write") + .long("allow-write") + .min_values(0) + .takes_value(true) + .use_delimiter(true) + .require_equals(true) + .help("Allow file system write access"), + ).arg( + Arg::with_name("allow-net") + .long("allow-net") + .min_values(0) + .takes_value(true) + .use_delimiter(true) + .require_equals(true) + .help("Allow network access"), + ).arg( Arg::with_name("allow-env") .long("allow-env") .help("Allow environment access"), @@ -301,13 +316,31 @@ pub fn parse_flags(matches: ArgMatches) -> DenoFlags { // flags specific to "run" subcommand if let Some(run_matches) = matches.subcommand_matches("run") { if run_matches.is_present("allow-read") { - flags.allow_read = true; + if run_matches.value_of("allow-read").is_some() { + let read_wl = run_matches.values_of("allow-read").unwrap(); + flags.read_whitelist = + read_wl.map(std::string::ToString::to_string).collect(); + } else { + flags.allow_read = true; + } } if run_matches.is_present("allow-write") { - flags.allow_write = true; + if run_matches.value_of("allow-write").is_some() { + let write_wl = run_matches.values_of("allow-write").unwrap(); + flags.write_whitelist = + write_wl.map(std::string::ToString::to_string).collect(); + } else { + flags.allow_write = true; + } } if run_matches.is_present("allow-net") { - flags.allow_net = true; + if run_matches.value_of("allow-net").is_some() { + let net_wl = run_matches.values_of("allow-net").unwrap(); + flags.net_whitelist = + net_wl.map(std::string::ToString::to_string).collect(); + } else { + flags.allow_net = true; + } } if run_matches.is_present("allow-env") { flags.allow_env = true; @@ -779,4 +812,61 @@ mod tests { assert_eq!(subcommand, DenoSubcommand::Xeval); assert_eq!(argv, svec!["deno", "console.log(val)"]); } + #[test] + fn test_flags_from_vec_19() { + let (flags, subcommand, argv) = flags_from_vec(svec![ + "deno", + "run", + "--allow-read=/some/test/dir", + "script.ts" + ]); + assert_eq!( + flags, + DenoFlags { + allow_read: false, + read_whitelist: svec!["/some/test/dir"], + ..DenoFlags::default() + } + ); + assert_eq!(subcommand, DenoSubcommand::Run); + assert_eq!(argv, svec!["deno", "script.ts"]); + } + #[test] + fn test_flags_from_vec_20() { + let (flags, subcommand, argv) = flags_from_vec(svec![ + "deno", + "run", + "--allow-write=/some/test/dir", + "script.ts" + ]); + assert_eq!( + flags, + DenoFlags { + allow_write: false, + write_whitelist: svec!["/some/test/dir"], + ..DenoFlags::default() + } + ); + assert_eq!(subcommand, DenoSubcommand::Run); + assert_eq!(argv, svec!["deno", "script.ts"]); + } + #[test] + fn test_flags_from_vec_21() { + let (flags, subcommand, argv) = flags_from_vec(svec![ + "deno", + "run", + "--allow-net=127.0.0.1", + "script.ts" + ]); + assert_eq!( + flags, + DenoFlags { + allow_net: false, + net_whitelist: svec!["127.0.0.1"], + ..DenoFlags::default() + } + ); + assert_eq!(subcommand, DenoSubcommand::Run); + assert_eq!(argv, svec!["deno", "script.ts"]); + } } diff --git a/cli/ops.rs b/cli/ops.rs index 7b9500ef82..a1d6e0c48f 100644 --- a/cli/ops.rs +++ b/cli/ops.rs @@ -2,6 +2,7 @@ use atty; use crate::ansi; use crate::compiler::get_compiler_config; +use crate::deno_dir; use crate::dispatch_minimal::dispatch_minimal; use crate::dispatch_minimal::parse_min_record; use crate::errors; @@ -43,7 +44,6 @@ use std; use std::convert::From; use std::fs; use std::net::Shutdown; -use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::time::{Duration, Instant, UNIX_EPOCH}; @@ -241,6 +241,14 @@ pub fn op_selector_std(inner_type: msg::Any) -> Option { } } +fn resolve_path(path: &str) -> Result<(PathBuf, String), DenoError> { + let url = deno_dir::resolve_file_url(path.to_string(), ".".to_string()) + .map_err(DenoError::from)?; + let path = url.to_file_path().unwrap(); + let path_string = path.to_str().unwrap().to_string(); + Ok((path, path_string)) +} + // Returns a milliseconds and nanoseconds subsec // since the start time of the deno runtime. // If the High precision flag is not set, the @@ -697,7 +705,11 @@ fn op_fetch( } let req = maybe_req.unwrap(); - if let Err(e) = state.check_net(url) { + let url_ = match url::Url::parse(url) { + Err(err) => return odd_future(DenoError::from(err)), + Ok(v) => v, + }; + if let Err(e) = state.check_net_url(url_) { return odd_future(e); } @@ -816,17 +828,20 @@ fn op_mkdir( ) -> Box { assert!(data.is_none()); let inner = base.inner_as_mkdir().unwrap(); - let path = String::from(inner.path().unwrap()); + let (path, path_) = match resolve_path(inner.path().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; let recursive = inner.recursive(); let mode = inner.mode(); - if let Err(e) = state.check_write(&path) { + if let Err(e) = state.check_write(&path_) { return odd_future(e); } blocking(base.sync(), move || { - debug!("op_mkdir {}", path); - deno_fs::mkdir(Path::new(&path), mode, recursive)?; + debug!("op_mkdir {}", path_); + deno_fs::mkdir(&path, mode, recursive)?; Ok(empty_buf()) }) } @@ -839,15 +854,17 @@ fn op_chmod( assert!(data.is_none()); let inner = base.inner_as_chmod().unwrap(); let _mode = inner.mode(); - let path = String::from(inner.path().unwrap()); + let (path, path_) = match resolve_path(inner.path().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; - if let Err(e) = state.check_write(&path) { + if let Err(e) = state.check_write(&path_) { return odd_future(e); } blocking(base.sync(), move || { - debug!("op_chmod {}", &path); - let path = PathBuf::from(&path); + debug!("op_chmod {}", &path_); // Still check file/dir exists on windows let _metadata = fs::metadata(&path)?; // Only work in unix @@ -902,8 +919,10 @@ fn op_open( assert!(data.is_none()); let cmd_id = base.cmd_id(); let inner = base.inner_as_open().unwrap(); - let filename_str = inner.filename().unwrap(); - let filename = PathBuf::from(&filename_str); + let (filename, filename_) = match resolve_path(inner.filename().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; let mode = inner.mode().unwrap(); let mut open_options = tokio::fs::OpenOptions::new(); @@ -944,20 +963,20 @@ fn op_open( match mode { "r" => { - if let Err(e) = state.check_read(&filename_str) { + if let Err(e) = state.check_read(&filename_) { return odd_future(e); } } "w" | "a" | "x" => { - if let Err(e) = state.check_write(&filename_str) { + if let Err(e) = state.check_write(&filename_) { return odd_future(e); } } &_ => { - if let Err(e) = state.check_read(&filename_str) { + if let Err(e) = state.check_read(&filename_) { return odd_future(e); } - if let Err(e) = state.check_write(&filename_str) { + if let Err(e) = state.check_write(&filename_) { return odd_future(e); } } @@ -1146,11 +1165,13 @@ fn op_remove( ) -> Box { assert!(data.is_none()); let inner = base.inner_as_remove().unwrap(); - let path_ = inner.path().unwrap(); - let path = PathBuf::from(path_); + let (path, path_) = match resolve_path(inner.path().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; let recursive = inner.recursive(); - if let Err(e) = state.check_write(path.to_str().unwrap()) { + if let Err(e) = state.check_write(&path_) { return odd_future(e); } @@ -1175,10 +1196,14 @@ fn op_copy_file( ) -> Box { assert!(data.is_none()); let inner = base.inner_as_copy_file().unwrap(); - let from_ = inner.from().unwrap(); - let from = PathBuf::from(from_); - let to_ = inner.to().unwrap(); - let to = PathBuf::from(to_); + let (from, from_) = match resolve_path(inner.from().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; + let (to, to_) = match resolve_path(inner.to().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; if let Err(e) = state.check_read(&from_) { return odd_future(e); @@ -1258,8 +1283,10 @@ fn op_stat( assert!(data.is_none()); let inner = base.inner_as_stat().unwrap(); let cmd_id = base.cmd_id(); - let filename_ = inner.filename().unwrap(); - let filename = PathBuf::from(filename_); + let (filename, filename_) = match resolve_path(inner.filename().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; let lstat = inner.lstat(); if let Err(e) = state.check_read(&filename_) { @@ -1275,6 +1302,8 @@ fn op_stat( fs::metadata(&filename)? }; + let filename_str = builder.create_string(&filename_); + let inner = msg::StatRes::create( builder, &msg::StatResArgs { @@ -1286,6 +1315,7 @@ fn op_stat( created: to_seconds!(metadata.created()), mode: get_mode(&metadata.permissions()), has_mode: cfg!(target_family = "unix"), + path: Some(filename_str), ..Default::default() }, ); @@ -1310,16 +1340,19 @@ fn op_read_dir( assert!(data.is_none()); let inner = base.inner_as_read_dir().unwrap(); let cmd_id = base.cmd_id(); - let path = String::from(inner.path().unwrap()); + let (path, path_) = match resolve_path(inner.path().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; - if let Err(e) = state.check_read(&path) { + if let Err(e) = state.check_read(&path_) { return odd_future(e); } blocking(base.sync(), move || -> OpResult { - debug!("op_read_dir {}", path); + debug!("op_read_dir {}", path.display()); let builder = &mut FlatBufferBuilder::new(); - let entries: Vec<_> = fs::read_dir(Path::new(&path))? + let entries: Vec<_> = fs::read_dir(path)? .map(|entry| { let entry = entry.unwrap(); let metadata = entry.metadata().unwrap(); @@ -1370,9 +1403,15 @@ fn op_rename( ) -> Box { assert!(data.is_none()); let inner = base.inner_as_rename().unwrap(); - let oldpath = PathBuf::from(inner.oldpath().unwrap()); - let newpath_ = inner.newpath().unwrap(); - let newpath = PathBuf::from(newpath_); + let (oldpath, _) = match resolve_path(inner.oldpath().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; + let (newpath, newpath_) = match resolve_path(inner.newpath().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; + if let Err(e) = state.check_write(&newpath_) { return odd_future(e); } @@ -1390,9 +1429,14 @@ fn op_link( ) -> Box { assert!(data.is_none()); let inner = base.inner_as_link().unwrap(); - let oldname = PathBuf::from(inner.oldname().unwrap()); - let newname_ = inner.newname().unwrap(); - let newname = PathBuf::from(newname_); + let (oldname, _) = match resolve_path(inner.oldname().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; + let (newname, newname_) = match resolve_path(inner.newname().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; if let Err(e) = state.check_write(&newname_) { return odd_future(e); @@ -1412,9 +1456,14 @@ fn op_symlink( ) -> Box { assert!(data.is_none()); let inner = base.inner_as_symlink().unwrap(); - let oldname = PathBuf::from(inner.oldname().unwrap()); - let newname_ = inner.newname().unwrap(); - let newname = PathBuf::from(newname_); + let (oldname, _) = match resolve_path(inner.oldname().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; + let (newname, newname_) = match resolve_path(inner.newname().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; if let Err(e) = state.check_write(&newname_) { return odd_future(e); @@ -1442,8 +1491,10 @@ fn op_read_link( assert!(data.is_none()); let inner = base.inner_as_readlink().unwrap(); let cmd_id = base.cmd_id(); - let name_ = inner.name().unwrap(); - let name = PathBuf::from(name_); + let (name, name_) = match resolve_path(inner.name().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; if let Err(e) = state.check_read(&name_) { return odd_future(e); @@ -1547,15 +1598,18 @@ fn op_truncate( assert!(data.is_none()); let inner = base.inner_as_truncate().unwrap(); - let filename = String::from(inner.name().unwrap()); + let (filename, filename_) = match resolve_path(inner.name().unwrap()) { + Err(err) => return odd_future(err), + Ok(v) => v, + }; let len = inner.len(); - if let Err(e) = state.check_write(&filename) { + if let Err(e) = state.check_write(&filename_) { return odd_future(e); } blocking(base.sync(), move || { - debug!("op_truncate {} {}", filename, len); + debug!("op_truncate {} {}", filename_, len); let f = fs::OpenOptions::new().write(true).open(&filename)?; f.set_len(u64::from(len))?; Ok(empty_buf()) diff --git a/cli/permissions.rs b/cli/permissions.rs index 84c2f0e178..304b6edfef 100644 --- a/cli/permissions.rs +++ b/cli/permissions.rs @@ -6,8 +6,10 @@ use crate::flags::DenoFlags; use ansi_term::Style; use crate::errors::permission_denied; use crate::errors::DenoResult; +use std::collections::HashSet; use std::fmt; use std::io; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; @@ -127,8 +129,11 @@ impl Default for PermissionAccessor { pub struct DenoPermissions { // Keep in sync with src/permissions.ts pub allow_read: PermissionAccessor, + pub read_whitelist: Arc>, pub allow_write: PermissionAccessor, + pub write_whitelist: Arc>, pub allow_net: PermissionAccessor, + pub net_whitelist: Arc>, pub allow_env: PermissionAccessor, pub allow_run: PermissionAccessor, pub allow_high_precision: PermissionAccessor, @@ -139,9 +144,14 @@ impl DenoPermissions { pub fn from_flags(flags: &DenoFlags) -> Self { Self { allow_read: PermissionAccessor::from(flags.allow_read), + read_whitelist: Arc::new(flags.read_whitelist.iter().cloned().collect()), allow_write: PermissionAccessor::from(flags.allow_write), - allow_env: PermissionAccessor::from(flags.allow_env), + write_whitelist: Arc::new( + flags.write_whitelist.iter().cloned().collect(), + ), allow_net: PermissionAccessor::from(flags.allow_net), + net_whitelist: Arc::new(flags.net_whitelist.iter().cloned().collect()), + allow_env: PermissionAccessor::from(flags.allow_env), allow_run: PermissionAccessor::from(flags.allow_run), allow_high_precision: PermissionAccessor::from( flags.allow_high_precision, @@ -170,42 +180,115 @@ impl DenoPermissions { pub fn check_read(&self, filename: &str) -> DenoResult<()> { match self.allow_read.get_state() { PermissionAccessorState::Allow => Ok(()), - PermissionAccessorState::Ask => match self - .try_permissions_prompt(&format!("read access to \"{}\"", filename)) - { - Err(e) => Err(e), - Ok(v) => { - self.allow_read.update_with_prompt_result(&v); - v.check()?; + state => { + if check_path_white_list(filename, &self.read_whitelist) { Ok(()) + } else { + match state { + PermissionAccessorState::Ask => match self.try_permissions_prompt( + &format!("read access to \"{}\"", filename), + ) { + Err(e) => Err(e), + Ok(v) => { + self.allow_read.update_with_prompt_result(&v); + v.check()?; + Ok(()) + } + }, + PermissionAccessorState::Deny => Err(permission_denied()), + _ => unreachable!(), + } } - }, - PermissionAccessorState::Deny => Err(permission_denied()), + } } } pub fn check_write(&self, filename: &str) -> DenoResult<()> { match self.allow_write.get_state() { PermissionAccessorState::Allow => Ok(()), - PermissionAccessorState::Ask => match self - .try_permissions_prompt(&format!("write access to \"{}\"", filename)) - { - Err(e) => Err(e), - Ok(v) => { - self.allow_write.update_with_prompt_result(&v); - v.check()?; + state => { + if check_path_white_list(filename, &self.write_whitelist) { Ok(()) + } else { + match state { + PermissionAccessorState::Ask => match self.try_permissions_prompt( + &format!("write access to \"{}\"", filename), + ) { + Err(e) => Err(e), + Ok(v) => { + self.allow_write.update_with_prompt_result(&v); + v.check()?; + Ok(()) + } + }, + PermissionAccessorState::Deny => Err(permission_denied()), + _ => unreachable!(), + } } - }, - PermissionAccessorState::Deny => Err(permission_denied()), + } } } - pub fn check_net(&self, domain_name: &str) -> DenoResult<()> { + pub fn check_net(&self, host_and_port: &str) -> DenoResult<()> { match self.allow_net.get_state() { PermissionAccessorState::Allow => Ok(()), + state => { + let parts = host_and_port.split(':').collect::>(); + if match parts.len() { + 2 => { + if self.net_whitelist.contains(parts[0]) { + true + } else { + self + .net_whitelist + .contains(&format!("{}:{}", parts[0], parts[1])) + } + } + 1 => self.net_whitelist.contains(parts[0]), + _ => panic!("Failed to parse origin string: {}", host_and_port), + } { + Ok(()) + } else { + self.check_net_inner(state, host_and_port) + } + } + } + } + + pub fn check_net_url(&self, url: url::Url) -> DenoResult<()> { + match self.allow_net.get_state() { + PermissionAccessorState::Allow => Ok(()), + state => { + let host = url.host().unwrap(); + let whitelist_result = { + if self.net_whitelist.contains(&format!("{}", host)) { + true + } else { + match url.port() { + Some(port) => { + self.net_whitelist.contains(&format!("{}:{}", host, port)) + } + None => false, + } + } + }; + if whitelist_result { + Ok(()) + } else { + self.check_net_inner(state, &url.to_string()) + } + } + } + } + + fn check_net_inner( + &self, + state: PermissionAccessorState, + prompt_str: &str, + ) -> DenoResult<()> { + match state { PermissionAccessorState::Ask => match self.try_permissions_prompt( - &format!("network access to \"{}\"", domain_name), + &format!("network access to \"{}\"", prompt_str), ) { Err(e) => Err(e), Ok(v) => { @@ -215,6 +298,7 @@ impl DenoPermissions { } }, PermissionAccessorState::Deny => Err(permission_denied()), + _ => unreachable!(), } } @@ -354,3 +438,281 @@ fn permission_prompt(message: &str) -> DenoResult { }; } } + +fn check_path_white_list( + filename: &str, + white_list: &Arc>, +) -> bool { + let mut path_buf = PathBuf::from(filename); + + loop { + if white_list.contains(path_buf.to_str().unwrap()) { + return true; + } + if !path_buf.pop() { + break; + } + } + false +} + +#[cfg(test)] +mod tests { + #![allow(clippy::cyclomatic_complexity)] + use super::*; + + // Creates vector of strings, Vec + macro_rules! svec { + ($($x:expr),*) => (vec![$($x.to_string()),*]); + } + + #[test] + fn check_paths() { + let whitelist = svec!["/a/specific/dir/name", "/a/specific", "/b/c"]; + + let perms = DenoPermissions::from_flags(&DenoFlags { + read_whitelist: whitelist.clone(), + write_whitelist: whitelist.clone(), + no_prompts: true, + ..Default::default() + }); + + // Inside of /a/specific and /a/specific/dir/name + assert!(perms.check_read("/a/specific/dir/name").is_ok()); + assert!(perms.check_write("/a/specific/dir/name").is_ok()); + + // Inside of /a/specific but outside of /a/specific/dir/name + assert!(perms.check_read("/a/specific/dir").is_ok()); + assert!(perms.check_write("/a/specific/dir").is_ok()); + + // Inside of /a/specific and /a/specific/dir/name + assert!(perms.check_read("/a/specific/dir/name/inner").is_ok()); + assert!(perms.check_write("/a/specific/dir/name/inner").is_ok()); + + // Inside of /a/specific but outside of /a/specific/dir/name + assert!(perms.check_read("/a/specific/other/dir").is_ok()); + assert!(perms.check_write("/a/specific/other/dir").is_ok()); + + // Exact match with /b/c + assert!(perms.check_read("/b/c").is_ok()); + assert!(perms.check_write("/b/c").is_ok()); + + // Sub path within /b/c + assert!(perms.check_read("/b/c/sub/path").is_ok()); + assert!(perms.check_write("/b/c/sub/path").is_ok()); + + // Inside of /b but outside of /b/c + assert!(perms.check_read("/b/e").is_err()); + assert!(perms.check_write("/b/e").is_err()); + + // Inside of /a but outside of /a/specific + assert!(perms.check_read("/a/b").is_err()); + assert!(perms.check_write("/a/b").is_err()); + } + + #[test] + fn check_net() { + let perms = DenoPermissions::from_flags(&DenoFlags { + net_whitelist: svec![ + "localhost", + "deno.land", + "github.com:3000", + "127.0.0.1", + "172.16.0.2:8000" + ], + no_prompts: true, + ..Default::default() + }); + + // Any protocol + port for localhost should be ok, since we don't specify + assert!( + perms + .check_net_url(url::Url::parse("http://localhost").unwrap()) + .is_ok() + ); + assert!( + perms + .check_net_url(url::Url::parse("http://localhost:8080").unwrap()) + .is_ok() + ); + assert!( + perms + .check_net_url(url::Url::parse("https://localhost").unwrap()) + .is_ok() + ); + assert!( + perms + .check_net_url(url::Url::parse("https://localhost:4443").unwrap()) + .is_ok() + ); + assert!( + perms + .check_net_url(url::Url::parse("tcp://localhost:5000").unwrap()) + .is_ok() + ); + assert!( + perms + .check_net_url(url::Url::parse("udp://localhost:6000").unwrap()) + .is_ok() + ); + assert!(perms.check_net("localhost:1234").is_ok()); + + // Correct domain + any port and protocol should be ok incorrect shouldn't + assert!(perms.check_net("deno.land").is_ok()); + assert!( + perms + .check_net_url( + url::Url::parse("https://deno.land/std/example/welcome.ts").unwrap() + ).is_ok() + ); + assert!(perms.check_net("deno.land:3000").is_ok()); + assert!( + perms + .check_net_url( + url::Url::parse("https://deno.land:3000/std/example/welcome.ts") + .unwrap() + ).is_ok() + ); + assert!(perms.check_net("deno.lands").is_err()); + assert!( + perms + .check_net_url( + url::Url::parse("https://deno.lands/std/example/welcome.ts").unwrap() + ).is_err() + ); + assert!(perms.check_net("deno.lands:3000").is_err()); + assert!( + perms + .check_net_url( + url::Url::parse("https://deno.lands:3000/std/example/welcome.ts") + .unwrap() + ).is_err() + ); + + // Correct domain + port should be ok all other combinations should err + assert!(perms.check_net("github.com:3000").is_ok()); + assert!( + perms + .check_net_url( + url::Url::parse("https://github.com:3000/denoland/deno").unwrap() + ).is_ok() + ); + assert!(perms.check_net("github.com").is_err()); + assert!( + perms + .check_net_url( + url::Url::parse("https://github.com/denoland/deno").unwrap() + ).is_err() + ); + assert!(perms.check_net("github.com:2000").is_err()); + assert!( + perms + .check_net_url( + url::Url::parse("https://github.com:2000/denoland/deno").unwrap() + ).is_err() + ); + assert!(perms.check_net("github.net:3000").is_err()); + assert!( + perms + .check_net_url( + url::Url::parse("https://github.net:3000/denoland/deno").unwrap() + ).is_err() + ); + + // Correct ipv4 address + any port should be ok others should err + assert!(perms.check_net("127.0.0.1").is_ok()); + assert!( + perms + .check_net_url(url::Url::parse("tcp://127.0.0.1").unwrap()) + .is_ok() + ); + assert!( + perms + .check_net_url(url::Url::parse("https://127.0.0.1").unwrap()) + .is_ok() + ); + assert!(perms.check_net("127.0.0.1:3000").is_ok()); + assert!( + perms + .check_net_url(url::Url::parse("tcp://127.0.0.1:3000").unwrap()) + .is_ok() + ); + assert!( + perms + .check_net_url(url::Url::parse("https://127.0.0.1:3000").unwrap()) + .is_ok() + ); + assert!(perms.check_net("127.0.0.2").is_err()); + assert!( + perms + .check_net_url(url::Url::parse("tcp://127.0.0.2").unwrap()) + .is_err() + ); + assert!( + perms + .check_net_url(url::Url::parse("https://127.0.0.2").unwrap()) + .is_err() + ); + assert!(perms.check_net("127.0.0.2:3000").is_err()); + assert!( + perms + .check_net_url(url::Url::parse("tcp://127.0.0.2:3000").unwrap()) + .is_err() + ); + assert!( + perms + .check_net_url(url::Url::parse("https://127.0.0.2:3000").unwrap()) + .is_err() + ); + + // Correct address + port should be ok all other combinations should err + assert!(perms.check_net("172.16.0.2:8000").is_ok()); + assert!( + perms + .check_net_url(url::Url::parse("tcp://172.16.0.2:8000").unwrap()) + .is_ok() + ); + assert!( + perms + .check_net_url(url::Url::parse("https://172.16.0.2:8000").unwrap()) + .is_ok() + ); + assert!(perms.check_net("172.16.0.2").is_err()); + assert!( + perms + .check_net_url(url::Url::parse("tcp://172.16.0.2").unwrap()) + .is_err() + ); + assert!( + perms + .check_net_url(url::Url::parse("https://172.16.0.2").unwrap()) + .is_err() + ); + assert!(perms.check_net("172.16.0.2:6000").is_err()); + assert!( + perms + .check_net_url(url::Url::parse("tcp://172.16.0.2:6000").unwrap()) + .is_err() + ); + assert!( + perms + .check_net_url(url::Url::parse("https://172.16.0.2:6000").unwrap()) + .is_err() + ); + assert!(perms.check_net("172.16.0.1:8000").is_err()); + assert!( + perms + .check_net_url(url::Url::parse("tcp://172.16.0.1:8000").unwrap()) + .is_err() + ); + assert!( + perms + .check_net_url(url::Url::parse("https://172.16.0.1:8000").unwrap()) + .is_err() + ); + + // Just some random hosts that should err + assert!(perms.check_net("somedomain").is_err()); + assert!(perms.check_net("192.168.0.1").is_err()); + } +} diff --git a/cli/state.rs b/cli/state.rs index 8a4f4eaee0..f27aa95a4d 100644 --- a/cli/state.rs +++ b/cli/state.rs @@ -189,8 +189,13 @@ impl ThreadSafeState { } #[inline] - pub fn check_net(&self, filename: &str) -> DenoResult<()> { - self.permissions.check_net(filename) + pub fn check_net(&self, host_and_port: &str) -> DenoResult<()> { + self.permissions.check_net(host_and_port) + } + + #[inline] + pub fn check_net_url(&self, url: url::Url) -> DenoResult<()> { + self.permissions.check_net_url(url) } #[inline] diff --git a/js/read_dir_test.ts b/js/read_dir_test.ts index a8466dda9c..55badd0dba 100644 --- a/js/read_dir_test.ts +++ b/js/read_dir_test.ts @@ -3,6 +3,8 @@ import { testPerm, assert, assertEquals } from "./test_util.ts"; type FileInfo = Deno.FileInfo; +const isWin = Deno.build.os === "win"; + function assertSameContent(files: FileInfo[]): void { let counter = 0; @@ -13,7 +15,11 @@ function assertSameContent(files: FileInfo[]): void { } if (file.name === "002_hello.ts") { - assertEquals(file.path, `tests/${file.name}`); + if (isWin) { + assert(file.path.endsWith(`tests\\${file.name}`)); + } else { + assert(file.path.endsWith(`tests/${file.name}`)); + } assertEquals(file.mode!, Deno.statSync(`tests/${file.name}`).mode!); counter++; } diff --git a/tools/complex_permissions_test.py b/tools/complex_permissions_test.py new file mode 100755 index 0000000000..98eeac013d --- /dev/null +++ b/tools/complex_permissions_test.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import os +import pty +import select +import subprocess +import sys +import time + +from util import build_path, root_path, executable_suffix, green_ok, red_failed + +PERMISSIONS_PROMPT_TEST_TS = "tools/complex_permissions_test.ts" + +PROMPT_PATTERN = b'⚠️' +PERMISSION_DENIED_PATTERN = b'PermissionDenied: permission denied' + + +# This function is copied from: +# https://gist.github.com/hayd/4f46a68fc697ba8888a7b517a414583e +# https://stackoverflow.com/q/52954248/1240268 +def tty_capture(cmd, bytes_input, timeout=5): + """Capture the output of cmd with bytes_input to stdin, + with stdin, stdout and stderr as TTYs.""" + mo, so = pty.openpty() # provide tty to enable line-buffering + me, se = pty.openpty() + mi, si = pty.openpty() + fdmap = {mo: 'stdout', me: 'stderr', mi: 'stdin'} + + timeout_exact = time.time() + timeout + p = subprocess.Popen( + cmd, bufsize=1, stdin=si, stdout=so, stderr=se, close_fds=True) + os.write(mi, bytes_input) + + select_timeout = .04 #seconds + res = {'stdout': b'', 'stderr': b''} + while True: + ready, _, _ = select.select([mo, me], [], [], select_timeout) + if ready: + for fd in ready: + data = os.read(fd, 512) + if not data: + break + res[fdmap[fd]] += data + elif p.poll() is not None or time.time( + ) > timeout_exact: # select timed-out + break # p exited + for fd in [si, so, se, mi, mo, me]: + os.close(fd) # can't do it sooner: it leads to errno.EIO error + p.wait() + return p.returncode, res['stdout'], res['stderr'] + + +# Wraps a test in debug printouts +# so we have visual indicator of what test failed +def wrap_test(test_name, test_method, *argv): + sys.stdout.write(test_name + " ... ") + try: + test_method(*argv) + print green_ok() + except AssertionError: + print red_failed() + raise + + +class Prompt(object): + def __init__(self, deno_exe, test_types): + self.deno_exe = deno_exe + self.test_types = test_types + + def run(self, flags, args, bytes_input): + "Returns (return_code, stdout, stderr)." + cmd = [self.deno_exe, "run"] + flags + [PERMISSIONS_PROMPT_TEST_TS + ] + args + print " ".join(cmd) + return tty_capture(cmd, bytes_input) + + def warm_up(self): + # ignore the ts compiling message + self.run(["--allow-read"], ["read", "package.json"], b'') + + def test(self): + for test_type in ["read", "write"]: + test_name_base = "test_" + test_type + wrap_test(test_name_base + "_inside_project_dir", + self.test_inside_project_dir, test_type) + wrap_test(test_name_base + "_outside_tests_dir", + self.test_outside_test_dir, test_type) + wrap_test(test_name_base + "_inside_tests_dir", + self.test_inside_test_dir, test_type) + wrap_test(test_name_base + "_outside_tests_and_js_dir", + self.test_outside_test_and_js_dir, test_type) + wrap_test(test_name_base + "_inside_tests_and_js_dir", + self.test_inside_test_and_js_dir, test_type) + wrap_test(test_name_base + "_allow_localhost_4545", + self.test_allow_localhost_4545) + wrap_test(test_name_base + "_allow_deno_land", + self.test_allow_deno_land) + wrap_test(test_name_base + "_allow_localhost_4545_fail", + self.test_allow_localhost_4545_fail) + wrap_test(test_name_base + "_allow_localhost", + self.test_allow_localhost) + + def test_inside_project_dir(self, test_type): + code, _stdout, stderr = self.run( + ["--no-prompt", "--allow-" + test_type + "=" + root_path], + [test_type, "package.json", "tests/subdir/config.json"], b'') + assert code == 0 + assert not PROMPT_PATTERN in stderr + assert not PERMISSION_DENIED_PATTERN in stderr + + def test_outside_test_dir(self, test_type): + code, _stdout, stderr = self.run([ + "--no-prompt", + "--allow-" + test_type + "=" + os.path.join(root_path, "tests") + ], [test_type, "package.json"], b'') + assert code == 1 + assert not PROMPT_PATTERN in stderr + assert PERMISSION_DENIED_PATTERN in stderr + + def test_inside_test_dir(self, test_type): + code, _stdout, stderr = self.run([ + "--no-prompt", + "--allow-" + test_type + "=" + os.path.join(root_path, "tests") + ], [test_type, "tests/subdir/config.json"], b'') + assert code == 0 + assert not PROMPT_PATTERN in stderr + assert not PERMISSION_DENIED_PATTERN in stderr + + def test_outside_test_and_js_dir(self, test_type): + code, _stdout, stderr = self.run([ + "--no-prompt", "--allow-" + test_type + "=" + os.path.join( + root_path, "tests") + "," + os.path.join(root_path, "js") + ], [test_type, "package.json"], b'') + assert code == 1 + assert not PROMPT_PATTERN in stderr + assert PERMISSION_DENIED_PATTERN in stderr + + def test_inside_test_and_js_dir(self, test_type): + code, _stdout, stderr = self.run([ + "--no-prompt", "--allow-" + test_type + "=" + os.path.join( + root_path, "tests") + "," + os.path.join(root_path, "js") + ], [test_type, "js/dir_test.ts", "tests/subdir/config.json"], b'') + assert code == 0 + assert not PROMPT_PATTERN in stderr + assert not PERMISSION_DENIED_PATTERN in stderr + + def test_allow_localhost_4545(self): + code, _stdout, stderr = self.run( + ["--no-prompt", "--allow-net=localhost:4545"], + ["net", "http://localhost:4545"], b'') + assert code == 0 + assert not PROMPT_PATTERN in stderr + assert not PERMISSION_DENIED_PATTERN in stderr + + def test_allow_deno_land(self): + code, _stdout, stderr = self.run( + ["--no-prompt", "--allow-net=deno.land"], + ["net", "http://localhost:4545"], b'') + assert code == 1 + assert not PROMPT_PATTERN in stderr + assert PERMISSION_DENIED_PATTERN in stderr + + def test_allow_localhost_4545_fail(self): + code, _stdout, stderr = self.run( + ["--no-prompt", "--allow-net=localhost:4545"], + ["net", "http://localhost:4546"], b'') + assert code == 1 + assert not PROMPT_PATTERN in stderr + assert PERMISSION_DENIED_PATTERN in stderr + + def test_allow_localhost(self): + code, _stdout, stderr = self.run( + ["--no-prompt", "--allow-net=localhost"], [ + "net", "http://localhost:4545", "http://localhost:4546", + "http://localhost:4547" + ], b'') + assert code == 0 + assert not PROMPT_PATTERN in stderr + assert not PERMISSION_DENIED_PATTERN in stderr + + +def complex_permissions_test(deno_exe): + p = Prompt(deno_exe, ["read", "write", "net"]) + p.test() + + +def main(): + print "Permissions prompt tests" + deno_exe = os.path.join(build_path(), "deno" + executable_suffix) + complex_permissions_test(deno_exe) + + +if __name__ == "__main__": + main() diff --git a/tools/complex_permissions_test.ts b/tools/complex_permissions_test.ts new file mode 100644 index 0000000000..72377ff93a --- /dev/null +++ b/tools/complex_permissions_test.ts @@ -0,0 +1,24 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +const { args, readFileSync, writeFileSync, exit, dial } = Deno; + +const name = args[1]; +const test: (args: string[]) => void = { + read: (files: string[]): void => { + files.forEach((file): any => readFileSync(file)); + }, + write: (files: string[]): void => { + files.forEach( + (file): any => writeFileSync(file, new Uint8Array(), { append: true }) + ); + }, + net: (hosts: string[]): void => { + hosts.forEach((host): any => fetch(host)); + } +}[name]; + +if (!test) { + console.log("Unknown test:", name); + exit(1); +} + +test(args.slice(2)); diff --git a/tools/test.py b/tools/test.py index 2a59a0c879..0b913ea5b6 100755 --- a/tools/test.py +++ b/tools/test.py @@ -105,7 +105,9 @@ def main(argv): if os.name != 'nt': from is_tty_test import is_tty_test from permission_prompt_test import permission_prompt_test + from complex_permissions_test import complex_permissions_test permission_prompt_test(deno_exe) + complex_permissions_test(deno_exe) is_tty_test(deno_exe) repl_tests(deno_exe)