diff --git a/BUILD.gn b/BUILD.gn index cfd7227bf1..68cb2c6db7 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -44,24 +44,25 @@ config("deno_config") { } main_extern = [ + "$rust_build:atty", + "$rust_build:dirs", + "$rust_build:futures", + "$rust_build:getopts", "$rust_build:hyper", "$rust_build:hyper_rustls", - "$rust_build:futures", "$rust_build:lazy_static", "$rust_build:libc", "$rust_build:log", + "$rust_build:rand", + "$rust_build:remove_dir_all", "$rust_build:ring", "$rust_build:tempfile", - "$rust_build:rand", "$rust_build:tokio", - "$rust_build:tokio_io", - "$rust_build:tokio_fs", "$rust_build:tokio_executor", + "$rust_build:tokio_fs", + "$rust_build:tokio_io", "$rust_build:tokio_threadpool", "$rust_build:url", - "$rust_build:remove_dir_all", - "$rust_build:dirs", - "$rust_build:getopts", "//build_extra/flatbuffers/rust:flatbuffers", ":msg_rs", ] diff --git a/Cargo.toml b/Cargo.toml index 3172ba3c10..96499adbd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ name = "deno" version = "0.0.0" [dependencies] +atty = "0.2.11" dirs = "1.0.4" flatbuffers = { path = "third_party/flatbuffers/rust/flatbuffers/" } futures = "0.1.25" diff --git a/build_extra/rust/BUILD.gn b/build_extra/rust/BUILD.gn index e40a19b03d..56e0aaa96f 100644 --- a/build_extra/rust/BUILD.gn +++ b/build_extra/rust/BUILD.gn @@ -113,14 +113,15 @@ rust_crate("winapi") { "basetsd", "cfg", "cfgmgr32", + "consoleapi", "combaseapi", "errhandlingapi", "excpt", "fileapi", "guiddef", "handleapi", - "inaddr", "in6addr", + "inaddr", "knownfolders", "ktmtypes", "libloaderapi", @@ -134,6 +135,7 @@ rust_crate("winapi") { "objbase", "objidl", "objidlbase", + "processenv", "processthreadsapi", "profileapi", "propidl", @@ -152,8 +154,10 @@ rust_crate("winapi") { "vadefs", "vcruntime", "winbase", + "wincon", "wincred", "windef", + "wingdi", "winerror", "winnt", "winreg", @@ -865,6 +869,14 @@ rust_crate("sct") { ] } +rust_crate("atty") { + source_root = "$registry_github/atty-0.2.11/src/lib.rs" + extern = [ + ":libc", + ":winapi", + ] +} + rust_crate("base64") { source_root = "$registry_github/base64-0.9.2/src/lib.rs" extern = [ diff --git a/src/isolate.rs b/src/isolate.rs index e222d280fa..6530c396f5 100644 --- a/src/isolate.rs +++ b/src/isolate.rs @@ -6,8 +6,10 @@ use deno_dir; use errors::DenoError; +use errors::DenoResult; use flags; use libdeno; +use permissions::DenoPermissions; use snapshot; use futures::Future; @@ -56,6 +58,7 @@ pub struct Isolate { pub struct IsolateState { pub dir: deno_dir::DenoDir, pub argv: Vec, + pub permissions: Mutex, pub flags: flags::DenoFlags, tx: Mutex>>, pub metrics: Mutex, @@ -71,6 +74,21 @@ impl IsolateState { tx.send((req_id, buf)).expect("tx.send error"); } + pub fn check_write(&self, filename: &str) -> DenoResult<()> { + let mut perm = self.permissions.lock().unwrap(); + perm.check_write(filename) + } + + pub fn check_env(&self) -> DenoResult<()> { + let mut perm = self.permissions.lock().unwrap(); + perm.check_env() + } + + pub fn check_net(&self, filename: &str) -> DenoResult<()> { + let mut perm = self.permissions.lock().unwrap(); + perm.check_net(filename) + } + fn metrics_op_dispatched( &self, bytes_sent_control: u64, @@ -143,6 +161,7 @@ impl Isolate { state: Arc::new(IsolateState { dir: deno_dir::DenoDir::new(flags.reload, custom_root).unwrap(), argv: argv_rest, + permissions: Mutex::new(DenoPermissions::new(&flags)), flags, tx: Mutex::new(Some(tx)), metrics: Mutex::new(Metrics::default()), diff --git a/src/main.rs b/src/main.rs index 91f5665230..d524b94ed5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ mod http_util; mod isolate; mod libdeno; pub mod ops; +mod permissions; mod resources; mod snapshot; mod tokio_util; diff --git a/src/ops.rs b/src/ops.rs index 37cbd68267..b7a20a46ea 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -1,6 +1,5 @@ // Copyright 2018 the Deno authors. All rights reserved. MIT license. use errors; -use errors::permission_denied; use errors::{DenoError, DenoResult, ErrorKind}; use fs as deno_fs; use http_util; @@ -333,11 +332,9 @@ fn op_set_env( let inner = base.inner_as_set_env().unwrap(); let key = inner.key().unwrap(); let value = inner.value().unwrap(); - - if !state.flags.allow_env { - return odd_future(permission_denied()); + if let Err(e) = state.check_env() { + return odd_future(e); } - std::env::set_var(key, value); ok_future(empty_buf()) } @@ -350,8 +347,8 @@ fn op_env( assert_eq!(data.len(), 0); let cmd_id = base.cmd_id(); - if !state.flags.allow_env { - return odd_future(permission_denied()); + if let Err(e) = state.check_env() { + return odd_future(e); } let builder = &mut FlatBufferBuilder::new(); @@ -399,8 +396,9 @@ fn op_fetch_req( let id = inner.id(); let url = inner.url().unwrap(); - if !state.flags.allow_net { - return odd_future(permission_denied()); + // FIXME use domain (or use this inside check_net) + if let Err(e) = state.check_net(url) { + return odd_future(e); } let url = url.parse::().unwrap(); @@ -513,8 +511,9 @@ fn op_make_temp_dir( let inner = base.inner_as_make_temp_dir().unwrap(); let cmd_id = base.cmd_id(); - if !state.flags.allow_write { - return odd_future(permission_denied()); + // FIXME + if let Err(e) = state.check_write("make_temp") { + return odd_future(e); } let dir = inner.dir().map(PathBuf::from); @@ -562,10 +561,9 @@ fn op_mkdir( let mode = inner.mode(); let path = String::from(inner.path().unwrap()); - if !state.flags.allow_write { - return odd_future(permission_denied()); + if let Err(e) = state.check_write(&path) { + return odd_future(e); } - blocking!(base.sync(), || { debug!("op_mkdir {}", path); deno_fs::mkdir(Path::new(&path), mode)?; @@ -583,8 +581,8 @@ fn op_chmod( let _mode = inner.mode(); let path = String::from(inner.path().unwrap()); - if !state.flags.allow_write { - return odd_future(permission_denied()); + if let Err(e) = state.check_write(&path) { + return odd_future(e); } blocking!(base.sync(), || { @@ -766,11 +764,14 @@ fn op_remove( ) -> Box { assert_eq!(data.len(), 0); let inner = base.inner_as_remove().unwrap(); - let path = PathBuf::from(inner.path().unwrap()); + let path_ = inner.path().unwrap(); + let path = PathBuf::from(path_); let recursive = inner.recursive(); - if !state.flags.allow_write { - return odd_future(permission_denied()); + + if let Err(e) = state.check_write(path.to_str().unwrap()) { + return odd_future(e); } + blocking!(base.sync(), || { debug!("op_remove {}", path.display()); let metadata = fs::metadata(&path)?; @@ -831,10 +832,11 @@ fn op_copy_file( assert_eq!(data.len(), 0); let inner = base.inner_as_copy_file().unwrap(); let from = PathBuf::from(inner.from().unwrap()); - let to = PathBuf::from(inner.to().unwrap()); + let to_ = inner.to().unwrap(); + let to = PathBuf::from(to_); - if !state.flags.allow_write { - return odd_future(permission_denied()); + if let Err(e) = state.check_write(&to_) { + return odd_future(e); } debug!("op_copy_file {} {}", from.display(), to.display()); @@ -1015,14 +1017,13 @@ fn op_write_file( data: &'static mut [u8], ) -> Box { let inner = base.inner_as_write_file().unwrap(); - - if !state.flags.allow_write { - return odd_future(permission_denied()); - } - let filename = String::from(inner.filename().unwrap()); let perm = inner.perm(); + if let Err(e) = state.check_write(&filename) { + return odd_future(e); + } + blocking!(base.sync(), || -> OpResult { debug!("op_write_file {} {}", filename, data.len()); deno_fs::write_file(Path::new(&filename), data, perm)?; @@ -1036,12 +1037,13 @@ fn op_rename( data: &'static mut [u8], ) -> Box { assert_eq!(data.len(), 0); - if !state.flags.allow_write { - return odd_future(permission_denied()); - } let inner = base.inner_as_rename().unwrap(); let oldpath = PathBuf::from(inner.oldpath().unwrap()); - let newpath = PathBuf::from(inner.newpath().unwrap()); + let newpath_ = inner.newpath().unwrap(); + let newpath = PathBuf::from(newpath_); + if let Err(e) = state.check_write(&newpath_) { + return odd_future(e); + } blocking!(base.sync(), || -> OpResult { debug!("op_rename {} {}", oldpath.display(), newpath.display()); fs::rename(&oldpath, &newpath)?; @@ -1055,8 +1057,13 @@ fn op_symlink( data: &'static mut [u8], ) -> Box { assert_eq!(data.len(), 0); - if !state.flags.allow_write { - return odd_future(permission_denied()); + let inner = base.inner_as_symlink().unwrap(); + let oldname = PathBuf::from(inner.oldname().unwrap()); + let newname_ = inner.newname().unwrap(); + let newname = PathBuf::from(newname_); + + if let Err(e) = state.check_write(&newname_) { + return odd_future(e); } // TODO Use type for Windows. if cfg!(windows) { @@ -1065,10 +1072,6 @@ fn op_symlink( "Not implemented".to_string(), )); } - - let inner = base.inner_as_symlink().unwrap(); - let oldname = PathBuf::from(inner.oldname().unwrap()); - let newname = PathBuf::from(inner.newname().unwrap()); blocking!(base.sync(), || -> OpResult { debug!("op_symlink {} {}", oldname.display(), newname.display()); #[cfg(any(unix))] @@ -1118,13 +1121,14 @@ fn op_truncate( ) -> Box { assert_eq!(data.len(), 0); - if !state.flags.allow_write { - return odd_future(permission_denied()); - } - let inner = base.inner_as_truncate().unwrap(); let filename = String::from(inner.name().unwrap()); let len = inner.len(); + + if let Err(e) = state.check_write(&filename) { + return odd_future(e); + } + blocking!(base.sync(), || { debug!("op_truncate {} {}", filename, len); let f = fs::OpenOptions::new().write(true).open(&filename)?; @@ -1139,8 +1143,8 @@ fn op_listen( data: &'static mut [u8], ) -> Box { assert_eq!(data.len(), 0); - if !state.flags.allow_net { - return odd_future(permission_denied()); + if let Err(e) = state.check_net("listen") { + return odd_future(e); } let cmd_id = base.cmd_id(); @@ -1205,10 +1209,9 @@ fn op_accept( data: &'static mut [u8], ) -> Box { assert_eq!(data.len(), 0); - if !state.flags.allow_net { - return odd_future(permission_denied()); + if let Err(e) = state.check_net("accept") { + return odd_future(e); } - let cmd_id = base.cmd_id(); let inner = base.inner_as_accept().unwrap(); let server_rid = inner.rid(); @@ -1232,10 +1235,9 @@ fn op_dial( data: &'static mut [u8], ) -> Box { assert_eq!(data.len(), 0); - if !state.flags.allow_net { - return odd_future(permission_denied()); + if let Err(e) = state.check_net("dial") { + return odd_future(e); } - let cmd_id = base.cmd_id(); let inner = base.inner_as_dial().unwrap(); let network = inner.network().unwrap(); diff --git a/src/permissions.rs b/src/permissions.rs new file mode 100644 index 0000000000..aeeb7df9ae --- /dev/null +++ b/src/permissions.rs @@ -0,0 +1,86 @@ +extern crate atty; + +use flags::DenoFlags; + +use errors::permission_denied; +use errors::DenoResult; +use std::io; + +#[derive(Debug, Default, PartialEq)] +pub struct DenoPermissions { + pub allow_write: bool, + pub allow_net: bool, + pub allow_env: bool, +} + +impl DenoPermissions { + pub fn new(flags: &DenoFlags) -> DenoPermissions { + DenoPermissions { + allow_write: flags.allow_write, + allow_env: flags.allow_env, + allow_net: flags.allow_net, + } + } + + pub fn check_write(&mut self, filename: &str) -> DenoResult<()> { + if self.allow_write { + return Ok(()); + }; + // TODO get location (where access occurred) + let r = permission_prompt(format!( + "Deno requests write access to \"{}\".", + filename + ));; + if r.is_ok() { + self.allow_write = true; + } + r + } + + pub fn check_net(&mut self, domain_name: &str) -> DenoResult<()> { + if self.allow_net { + return Ok(()); + }; + // TODO get location (where access occurred) + let r = permission_prompt(format!( + "Deno requests network access to \"{}\".", + domain_name + )); + if r.is_ok() { + self.allow_net = true; + } + r + } + + pub fn check_env(&mut self) -> DenoResult<()> { + if self.allow_env { + return Ok(()); + }; + // TODO get location (where access occurred) + let r = permission_prompt( + "Deno requests access to environment variables.".to_string(), + ); + if r.is_ok() { + self.allow_env = true; + } + r + } +} + +fn permission_prompt(message: String) -> DenoResult<()> { + if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) { + return Err(permission_denied()); + }; + // print to stderr so that if deno is > to a file this is still displayed. + eprint!("{} Grant? [yN] ", message); + let mut input = String::new(); + let stdin = io::stdin(); + let _nread = stdin.read_line(&mut input)?; + let ch = input.chars().next().unwrap(); + let is_yes = ch == 'y' || ch == 'Y'; + if is_yes { + Ok(()) + } else { + Err(permission_denied()) + } +} diff --git a/third_party b/third_party index b93f9c8bd3..56c4acce2e 160000 --- a/third_party +++ b/third_party @@ -1 +1 @@ -Subproject commit b93f9c8bd39a2548d60167043da6b947c023a830 +Subproject commit 56c4acce2e8ffe979b2e7d52d2b3e6f613ed492e diff --git a/tools/permission_prompt_test.py b/tools/permission_prompt_test.py new file mode 100755 index 0000000000..2bc24d12ca --- /dev/null +++ b/tools/permission_prompt_test.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python +import os +import pty +import select +import subprocess + +from util import build_path, executable_suffix + +PERMISSIONS_PROMPT_TEST_TS = "tools/permission_prompt_test.ts" + + +# This function is copied from: +# https://gist.github.com/hayd/4f46a68fc697ba8888a7b517a414583e +# https://stackoverflow.com/q/52954248/1240268 +def tty_capture(cmd, bytes_input): + """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'} + + p = subprocess.Popen( + cmd, bufsize=1, stdin=si, stdout=so, stderr=se, close_fds=True) + os.write(mi, bytes_input) + + timeout = .04 # seconds + res = {'stdout': b'', 'stderr': b''} + while True: + ready, _, _ = select.select([mo, me], [], [], 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: # 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'] + + +class Prompt(object): + def __init__(self, deno_exe): + self.deno_exe = deno_exe + + def run(self, + arg, + bytes_input, + allow_write=False, + allow_net=False, + allow_env=False): + "Returns (return_code, stdout, stderr)." + cmd = [self.deno_exe, PERMISSIONS_PROMPT_TEST_TS, arg] + if allow_write: + cmd.append("--allow-write") + if allow_net: + cmd.append("--allow-net") + if allow_env: + cmd.append("--allow-env") + return tty_capture(cmd, bytes_input) + + def warm_up(self): + # ignore the ts compiling message + self.run('needsWrite', b'', allow_write=True) + + def test_write_yes(self): + code, stdout, stderr = self.run('needsWrite', b'y\n') + assert code == 0 + assert stdout == b'' + assert b'Deno requests write access' in stderr + + def test_write_arg(self): + code, stdout, stderr = self.run('needsWrite', b'', allow_write=True) + assert code == 0 + assert stdout == b'' + assert stderr == b'' + + def test_write_no(self): + code, stdout, stderr = self.run('needsWrite', b'N\n') + assert code == 1 + # FIXME this error message should be in stderr + assert b'PermissionDenied: permission denied' in stdout + assert b'Deno requests write access' in stderr + + def test_env_yes(self): + code, stdout, stderr = self.run('needsEnv', b'y\n') + assert code == 0 + assert stdout == b'' + assert b'Deno requests access to environment' in stderr + + def test_env_arg(self): + code, stdout, stderr = self.run('needsEnv', b'', allow_env=True) + assert code == 0 + assert stdout == b'' + assert stderr == b'' + + def test_env_no(self): + code, stdout, stderr = self.run('needsEnv', b'N\n') + assert code == 1 + # FIXME this error message should be in stderr + assert b'PermissionDenied: permission denied' in stdout + assert b'Deno requests access to environment' in stderr + + def test_net_yes(self): + code, stdout, stderr = self.run('needsEnv', b'y\n') + assert code == 0 + assert stdout == b'' + assert b'Deno requests access to environment' in stderr + + def test_net_arg(self): + code, stdout, stderr = self.run('needsNet', b'', allow_net=True) + assert code == 0 + assert stdout == b'' + assert stderr == b'' + + def test_net_no(self): + code, stdout, stderr = self.run('needsNet', b'N\n') + assert code == 1 + # FIXME this error message should be in stderr + assert b'PermissionDenied: permission denied' in stdout + assert b'Deno requests network access' in stderr + + +def permission_prompt_test(deno_exe): + p = Prompt(deno_exe) + p.warm_up() + p.test_write_yes() + p.test_write_arg() + p.test_write_no() + p.test_env_yes() + p.test_env_arg() + p.test_env_no() + p.test_net_yes() + p.test_net_arg() + p.test_net_no() + + +if __name__ == "__main__": + deno_exe = os.path.join(build_path(), "deno" + executable_suffix) + permission_prompt_test(deno_exe) diff --git a/tools/permission_prompt_test.ts b/tools/permission_prompt_test.ts new file mode 100644 index 0000000000..cf8a09805c --- /dev/null +++ b/tools/permission_prompt_test.ts @@ -0,0 +1,21 @@ +import { args, listen, env, exit, makeTempDirSync } from "deno"; + +const name = args[1]; +const test = { + needsWrite: () => { + makeTempDirSync(); + }, + needsEnv: () => { + env().home; + }, + needsNet: () => { + listen("tcp", "127.0.0.1:4540"); + } +}[name]; + +if (!test) { + console.log("Unknown test:", name); + exit(1); +} + +test(); diff --git a/tools/test.py b/tools/test.py index fb05442564..e2d0772453 100755 --- a/tools/test.py +++ b/tools/test.py @@ -60,6 +60,13 @@ def main(argv): check_output_test(deno_exe) + # TODO We currently skip testing the prompt in Windows completely. + # Windows does not support the pty module used for testing the permission + # prompt. + if os.name != 'nt': + from permission_prompt_test import permission_prompt_test + permission_prompt_test(deno_exe) + rmtree(deno_dir) deno_dir_test(deno_exe, deno_dir) diff --git a/tools/third_party.py b/tools/third_party.py index d6c97f13b9..65ff437b79 100644 --- a/tools/third_party.py +++ b/tools/third_party.py @@ -125,8 +125,8 @@ def run_cargo(): # If the lockfile ends up in the git repo, it'll make cargo hang for everyone # else who tries to run sync_third_party. def delete_lockfile(): - lockfiles = find_exts( - path.join(rust_crates_path, "registry/index"), '.cargo-index-lock') + lockfiles = find_exts([path.join(rust_crates_path, "registry/index")], + ['.cargo-index-lock']) for lockfile in lockfiles: os.remove(lockfile)