summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/deno_dir.rs55
-rw-r--r--cli/flags.rs118
-rw-r--r--cli/ops.rs140
-rw-r--r--cli/permissions.rs404
-rw-r--r--cli/state.rs9
-rw-r--r--js/read_dir_test.ts8
-rwxr-xr-xtools/complex_permissions_test.py195
-rw-r--r--tools/complex_permissions_test.ts24
-rwxr-xr-xtools/test.py2
9 files changed, 850 insertions, 105 deletions
diff --git a/cli/deno_dir.rs b/cli/deno_dir.rs
index c58a252cb..4bca1117a 100644
--- a/cli/deno_dir.rs
+++ b/cli/deno_dir.rs
@@ -291,36 +291,14 @@ impl DenoDir {
referrer: &str,
) -> Result<Url, url::ParseError> {
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<Url, url::ParseError> {
+ 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 e68b831d5..d90a025a2 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<String>,
pub allow_read: bool,
+ pub read_whitelist: Vec<String>,
pub allow_write: bool,
+ pub write_whitelist: Vec<String>,
pub allow_net: bool,
+ pub net_whitelist: Vec<String>,
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 7b9500ef8..a1d6e0c48 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<OpCreator> {
}
}
+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<OpWithError> {
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<OpWithError> {
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<OpWithError> {
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<OpWithError> {
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<OpWithError> {
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<OpWithError> {
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 84c2f0e17..304b6edfe 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<HashSet<String>>,
pub allow_write: PermissionAccessor,
+ pub write_whitelist: Arc<HashSet<String>>,
pub allow_net: PermissionAccessor,
+ pub net_whitelist: Arc<HashSet<String>>,
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, host_and_port: &str) -> DenoResult<()> {
+ match self.allow_net.get_state() {
+ PermissionAccessorState::Allow => Ok(()),
+ state => {
+ let parts = host_and_port.split(':').collect::<Vec<&str>>();
+ 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(&self, domain_name: &str) -> DenoResult<()> {
+ 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<PromptResult> {
};
}
}
+
+fn check_path_white_list(
+ filename: &str,
+ white_list: &Arc<HashSet<String>>,
+) -> 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<String>
+ 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 8a4f4eaee..f27aa95a4 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 a8466dda9..55badd0db 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 000000000..98eeac013
--- /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 000000000..72377ff93
--- /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 2a59a0c87..0b913ea5b 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)