mirror of
https://github.com/denoland/deno.git
synced 2024-12-26 09:10:40 -05:00
refactor(permissions): factor out PermissionPrompter trait, add callbacks (#16975)
This commit refactors several things in "runtime/permissions" module: - splits it into "mod.rs" and "prompter.rs" - adds "PermissionPrompter" trait with two implementations: * "TtyPrompter" * "TestPrompter" - adds "before" and "after" prompt callback which can be used to hide progress bar in the CLI (to be done in a follow up) - "permissions_prompt" API returns "PromptResponse" enum, instead of a boolean; this allows to add "allow all"/"deny all" functionality for the prompt
This commit is contained in:
parent
7095cc6b50
commit
3eb366093e
2 changed files with 419 additions and 297 deletions
|
@ -6,8 +6,6 @@ use deno_core::error::custom_error;
|
||||||
use deno_core::error::type_error;
|
use deno_core::error::type_error;
|
||||||
use deno_core::error::uri_error;
|
use deno_core::error::uri_error;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
#[cfg(test)]
|
|
||||||
use deno_core::parking_lot::Mutex;
|
|
||||||
use deno_core::serde::de;
|
use deno_core::serde::de;
|
||||||
use deno_core::serde::Deserialize;
|
use deno_core::serde::Deserialize;
|
||||||
use deno_core::serde::Deserializer;
|
use deno_core::serde::Deserializer;
|
||||||
|
@ -24,12 +22,14 @@ use std::hash::Hash;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
#[cfg(test)]
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
#[cfg(test)]
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
|
|
||||||
const PERMISSION_EMOJI: &str = "⚠️";
|
mod prompter;
|
||||||
|
use prompter::permission_prompt;
|
||||||
|
use prompter::PromptResponse;
|
||||||
|
use prompter::PERMISSION_EMOJI;
|
||||||
|
|
||||||
|
pub use prompter::set_prompt_callbacks;
|
||||||
|
pub use prompter::PromptCallback;
|
||||||
|
|
||||||
static DEBUG_LOG_ENABLED: Lazy<bool> =
|
static DEBUG_LOG_ENABLED: Lazy<bool> =
|
||||||
Lazy::new(|| log::log_enabled!(log::Level::Debug));
|
Lazy::new(|| log::log_enabled!(log::Level::Debug));
|
||||||
|
@ -110,7 +110,7 @@ impl PermissionState {
|
||||||
name,
|
name,
|
||||||
info().map_or(String::new(), |info| { format!(" to {}", info) }),
|
info().map_or(String::new(), |info| { format!(" to {}", info) }),
|
||||||
);
|
);
|
||||||
if permission_prompt(&msg, name, api_name) {
|
if PromptResponse::Allow == permission_prompt(&msg, name, api_name) {
|
||||||
Self::log_perm_access(name, info);
|
Self::log_perm_access(name, info);
|
||||||
(Ok(()), true)
|
(Ok(()), true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -153,11 +153,13 @@ impl UnitPermission {
|
||||||
|
|
||||||
pub fn request(&mut self) -> PermissionState {
|
pub fn request(&mut self) -> PermissionState {
|
||||||
if self.state == PermissionState::Prompt {
|
if self.state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
&format!("access to {}", self.description),
|
&format!("access to {}", self.description),
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.state = PermissionState::Granted;
|
self.state = PermissionState::Granted;
|
||||||
} else {
|
} else {
|
||||||
self.state = PermissionState::Denied;
|
self.state = PermissionState::Denied;
|
||||||
|
@ -352,11 +354,13 @@ impl UnaryPermission<ReadDescriptor> {
|
||||||
let (resolved_path, display_path) = resolved_and_display_path(path);
|
let (resolved_path, display_path) = resolved_and_display_path(path);
|
||||||
let state = self.query(Some(&resolved_path));
|
let state = self.query(Some(&resolved_path));
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
&format!("read access to \"{}\"", display_path.display()),
|
&format!("read access to \"{}\"", display_path.display()),
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.insert(ReadDescriptor(resolved_path));
|
self.granted_list.insert(ReadDescriptor(resolved_path));
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
} else {
|
} else {
|
||||||
|
@ -373,11 +377,13 @@ impl UnaryPermission<ReadDescriptor> {
|
||||||
} else {
|
} else {
|
||||||
let state = self.query(None);
|
let state = self.query(None);
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
"read access",
|
"read access",
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.clear();
|
self.granted_list.clear();
|
||||||
self.global_state = PermissionState::Granted;
|
self.global_state = PermissionState::Granted;
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
|
@ -521,11 +527,13 @@ impl UnaryPermission<WriteDescriptor> {
|
||||||
let (resolved_path, display_path) = resolved_and_display_path(path);
|
let (resolved_path, display_path) = resolved_and_display_path(path);
|
||||||
let state = self.query(Some(&resolved_path));
|
let state = self.query(Some(&resolved_path));
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
&format!("write access to \"{}\"", display_path.display()),
|
&format!("write access to \"{}\"", display_path.display()),
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.insert(WriteDescriptor(resolved_path));
|
self.granted_list.insert(WriteDescriptor(resolved_path));
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
} else {
|
} else {
|
||||||
|
@ -542,11 +550,13 @@ impl UnaryPermission<WriteDescriptor> {
|
||||||
} else {
|
} else {
|
||||||
let state = self.query(None);
|
let state = self.query(None);
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
"write access",
|
"write access",
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.clear();
|
self.granted_list.clear();
|
||||||
self.global_state = PermissionState::Granted;
|
self.global_state = PermissionState::Granted;
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
|
@ -672,11 +682,13 @@ impl UnaryPermission<NetDescriptor> {
|
||||||
let state = self.query(Some(host));
|
let state = self.query(Some(host));
|
||||||
let host = NetDescriptor::new(&host);
|
let host = NetDescriptor::new(&host);
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
&format!("network access to \"{}\"", host),
|
&format!("network access to \"{}\"", host),
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.insert(host);
|
self.granted_list.insert(host);
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
} else {
|
} else {
|
||||||
|
@ -693,11 +705,13 @@ impl UnaryPermission<NetDescriptor> {
|
||||||
} else {
|
} else {
|
||||||
let state = self.query::<&str>(None);
|
let state = self.query::<&str>(None);
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
"network access",
|
"network access",
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.clear();
|
self.granted_list.clear();
|
||||||
self.global_state = PermissionState::Granted;
|
self.global_state = PermissionState::Granted;
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
|
@ -842,11 +856,13 @@ impl UnaryPermission<EnvDescriptor> {
|
||||||
if let Some(env) = env {
|
if let Some(env) = env {
|
||||||
let state = self.query(Some(env));
|
let state = self.query(Some(env));
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
&format!("env access to \"{}\"", env),
|
&format!("env access to \"{}\"", env),
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.insert(EnvDescriptor::new(env));
|
self.granted_list.insert(EnvDescriptor::new(env));
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
} else {
|
} else {
|
||||||
|
@ -863,11 +879,13 @@ impl UnaryPermission<EnvDescriptor> {
|
||||||
} else {
|
} else {
|
||||||
let state = self.query(None);
|
let state = self.query(None);
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
"env access",
|
"env access",
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.clear();
|
self.granted_list.clear();
|
||||||
self.global_state = PermissionState::Granted;
|
self.global_state = PermissionState::Granted;
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
|
@ -972,11 +990,13 @@ impl UnaryPermission<SysDescriptor> {
|
||||||
}
|
}
|
||||||
if let Some(kind) = kind {
|
if let Some(kind) = kind {
|
||||||
let desc = SysDescriptor(kind.to_string());
|
let desc = SysDescriptor(kind.to_string());
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
&format!("sys access to \"{}\"", kind),
|
&format!("sys access to \"{}\"", kind),
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.insert(desc);
|
self.granted_list.insert(desc);
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
} else {
|
} else {
|
||||||
|
@ -985,11 +1005,13 @@ impl UnaryPermission<SysDescriptor> {
|
||||||
PermissionState::Denied
|
PermissionState::Denied
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
"sys access",
|
"sys access",
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.global_state = PermissionState::Granted;
|
self.global_state = PermissionState::Granted;
|
||||||
} else {
|
} else {
|
||||||
self.granted_list.clear();
|
self.granted_list.clear();
|
||||||
|
@ -1091,11 +1113,13 @@ impl UnaryPermission<RunDescriptor> {
|
||||||
if let Some(cmd) = cmd {
|
if let Some(cmd) = cmd {
|
||||||
let state = self.query(Some(cmd));
|
let state = self.query(Some(cmd));
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
&format!("run access to \"{}\"", cmd),
|
&format!("run access to \"{}\"", cmd),
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self
|
self
|
||||||
.granted_list
|
.granted_list
|
||||||
.insert(RunDescriptor::from_str(cmd).unwrap());
|
.insert(RunDescriptor::from_str(cmd).unwrap());
|
||||||
|
@ -1118,11 +1142,13 @@ impl UnaryPermission<RunDescriptor> {
|
||||||
} else {
|
} else {
|
||||||
let state = self.query(None);
|
let state = self.query(None);
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
"run access",
|
"run access",
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.clear();
|
self.granted_list.clear();
|
||||||
self.global_state = PermissionState::Granted;
|
self.global_state = PermissionState::Granted;
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
|
@ -1238,11 +1264,13 @@ impl UnaryPermission<FfiDescriptor> {
|
||||||
let (resolved_path, display_path) = resolved_and_display_path(path);
|
let (resolved_path, display_path) = resolved_and_display_path(path);
|
||||||
let state = self.query(Some(&resolved_path));
|
let state = self.query(Some(&resolved_path));
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
&format!("ffi access to \"{}\"", display_path.display()),
|
&format!("ffi access to \"{}\"", display_path.display()),
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.insert(FfiDescriptor(resolved_path));
|
self.granted_list.insert(FfiDescriptor(resolved_path));
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
} else {
|
} else {
|
||||||
|
@ -1259,11 +1287,13 @@ impl UnaryPermission<FfiDescriptor> {
|
||||||
} else {
|
} else {
|
||||||
let state = self.query(None);
|
let state = self.query(None);
|
||||||
if state == PermissionState::Prompt {
|
if state == PermissionState::Prompt {
|
||||||
if permission_prompt(
|
if PromptResponse::Allow
|
||||||
|
== permission_prompt(
|
||||||
"ffi access",
|
"ffi access",
|
||||||
self.name,
|
self.name,
|
||||||
Some("Deno.permissions.query()"),
|
Some("Deno.permissions.query()"),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.granted_list.clear();
|
self.granted_list.clear();
|
||||||
self.global_state = PermissionState::Granted;
|
self.global_state = PermissionState::Granted;
|
||||||
PermissionState::Granted
|
PermissionState::Granted
|
||||||
|
@ -2262,225 +2292,12 @@ pub fn create_child_permissions(
|
||||||
Ok(worker_perms)
|
Ok(worker_perms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the permission prompt and returns the answer according to the user input.
|
|
||||||
/// This loops until the user gives the proper input.
|
|
||||||
#[cfg(not(test))]
|
|
||||||
fn permission_prompt(
|
|
||||||
message: &str,
|
|
||||||
name: &str,
|
|
||||||
api_name: Option<&str>,
|
|
||||||
) -> bool {
|
|
||||||
if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn clear_stdin() -> Result<(), AnyError> {
|
|
||||||
// TODO(bartlomieju):
|
|
||||||
#[allow(clippy::undocumented_unsafe_blocks)]
|
|
||||||
let r = unsafe { libc::tcflush(0, libc::TCIFLUSH) };
|
|
||||||
assert_eq!(r, 0);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
fn clear_stdin() -> Result<(), AnyError> {
|
|
||||||
use deno_core::anyhow::bail;
|
|
||||||
use winapi::shared::minwindef::TRUE;
|
|
||||||
use winapi::shared::minwindef::UINT;
|
|
||||||
use winapi::shared::minwindef::WORD;
|
|
||||||
use winapi::shared::ntdef::WCHAR;
|
|
||||||
use winapi::um::processenv::GetStdHandle;
|
|
||||||
use winapi::um::winbase::STD_INPUT_HANDLE;
|
|
||||||
use winapi::um::wincon::FlushConsoleInputBuffer;
|
|
||||||
use winapi::um::wincon::PeekConsoleInputW;
|
|
||||||
use winapi::um::wincon::WriteConsoleInputW;
|
|
||||||
use winapi::um::wincontypes::INPUT_RECORD;
|
|
||||||
use winapi::um::wincontypes::KEY_EVENT;
|
|
||||||
use winapi::um::winnt::HANDLE;
|
|
||||||
use winapi::um::winuser::MapVirtualKeyW;
|
|
||||||
use winapi::um::winuser::MAPVK_VK_TO_VSC;
|
|
||||||
use winapi::um::winuser::VK_RETURN;
|
|
||||||
|
|
||||||
// SAFETY: winapi calls
|
|
||||||
unsafe {
|
|
||||||
let stdin = GetStdHandle(STD_INPUT_HANDLE);
|
|
||||||
// emulate an enter key press to clear any line buffered console characters
|
|
||||||
emulate_enter_key_press(stdin)?;
|
|
||||||
// read the buffered line or enter key press
|
|
||||||
read_stdin_line()?;
|
|
||||||
// check if our emulated key press was executed
|
|
||||||
if is_input_buffer_empty(stdin)? {
|
|
||||||
// if so, move the cursor up to prevent a blank line
|
|
||||||
move_cursor_up()?;
|
|
||||||
} else {
|
|
||||||
// the emulated key press is still pending, so a buffered line was read
|
|
||||||
// and we can flush the emulated key press
|
|
||||||
flush_input_buffer(stdin)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
|
|
||||||
unsafe fn flush_input_buffer(stdin: HANDLE) -> Result<(), AnyError> {
|
|
||||||
let success = FlushConsoleInputBuffer(stdin);
|
|
||||||
if success != TRUE {
|
|
||||||
bail!(
|
|
||||||
"Could not flush the console input buffer: {}",
|
|
||||||
std::io::Error::last_os_error()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn emulate_enter_key_press(stdin: HANDLE) -> Result<(), AnyError> {
|
|
||||||
// https://github.com/libuv/libuv/blob/a39009a5a9252a566ca0704d02df8dabc4ce328f/src/win/tty.c#L1121-L1131
|
|
||||||
let mut input_record: INPUT_RECORD = std::mem::zeroed();
|
|
||||||
input_record.EventType = KEY_EVENT;
|
|
||||||
input_record.Event.KeyEvent_mut().bKeyDown = TRUE;
|
|
||||||
input_record.Event.KeyEvent_mut().wRepeatCount = 1;
|
|
||||||
input_record.Event.KeyEvent_mut().wVirtualKeyCode = VK_RETURN as WORD;
|
|
||||||
input_record.Event.KeyEvent_mut().wVirtualScanCode =
|
|
||||||
MapVirtualKeyW(VK_RETURN as UINT, MAPVK_VK_TO_VSC) as WORD;
|
|
||||||
*input_record.Event.KeyEvent_mut().uChar.UnicodeChar_mut() =
|
|
||||||
'\r' as WCHAR;
|
|
||||||
|
|
||||||
let mut record_written = 0;
|
|
||||||
let success =
|
|
||||||
WriteConsoleInputW(stdin, &input_record, 1, &mut record_written);
|
|
||||||
if success != TRUE {
|
|
||||||
bail!(
|
|
||||||
"Could not emulate enter key press: {}",
|
|
||||||
std::io::Error::last_os_error()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn is_input_buffer_empty(stdin: HANDLE) -> Result<bool, AnyError> {
|
|
||||||
let mut buffer = Vec::with_capacity(1);
|
|
||||||
let mut events_read = 0;
|
|
||||||
let success =
|
|
||||||
PeekConsoleInputW(stdin, buffer.as_mut_ptr(), 1, &mut events_read);
|
|
||||||
if success != TRUE {
|
|
||||||
bail!(
|
|
||||||
"Could not peek the console input buffer: {}",
|
|
||||||
std::io::Error::last_os_error()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Ok(events_read == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_cursor_up() -> Result<(), AnyError> {
|
|
||||||
use std::io::Write;
|
|
||||||
write!(std::io::stderr(), "\x1B[1A")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_stdin_line() -> Result<(), AnyError> {
|
|
||||||
let mut input = String::new();
|
|
||||||
let stdin = std::io::stdin();
|
|
||||||
stdin.read_line(&mut input)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear n-lines in terminal and move cursor to the beginning of the line.
|
|
||||||
fn clear_n_lines(n: usize) {
|
|
||||||
eprint!("\x1B[{}A\x1B[0J", n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For security reasons we must consume everything in stdin so that previously
|
|
||||||
// buffered data cannot effect the prompt.
|
|
||||||
if let Err(err) = clear_stdin() {
|
|
||||||
eprintln!("Error clearing stdin for permission prompt. {:#}", err);
|
|
||||||
return false; // don't grant permission if this fails
|
|
||||||
}
|
|
||||||
|
|
||||||
// print to stderr so that if stdout is piped this is still displayed.
|
|
||||||
const OPTS: &str = "[y/n] (y = yes, allow; n = no, deny)";
|
|
||||||
eprint!("{} ┌ ", PERMISSION_EMOJI);
|
|
||||||
eprint!("{}", colors::bold("Deno requests "));
|
|
||||||
eprint!("{}", colors::bold(message));
|
|
||||||
eprintln!("{}", colors::bold("."));
|
|
||||||
if let Some(api_name) = api_name {
|
|
||||||
eprintln!(" ├ Requested by `{}` API", api_name);
|
|
||||||
}
|
|
||||||
let msg = format!(
|
|
||||||
" ├ Run again with --allow-{} to bypass this prompt.",
|
|
||||||
name
|
|
||||||
);
|
|
||||||
eprintln!("{}", colors::italic(&msg));
|
|
||||||
eprint!(" └ {}", colors::bold("Allow?"));
|
|
||||||
eprint!(" {} > ", OPTS);
|
|
||||||
loop {
|
|
||||||
let mut input = String::new();
|
|
||||||
let stdin = std::io::stdin();
|
|
||||||
let result = stdin.read_line(&mut input);
|
|
||||||
if result.is_err() {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let ch = match input.chars().next() {
|
|
||||||
None => return false,
|
|
||||||
Some(v) => v,
|
|
||||||
};
|
|
||||||
match ch.to_ascii_lowercase() {
|
|
||||||
'y' => {
|
|
||||||
clear_n_lines(if api_name.is_some() { 4 } else { 3 });
|
|
||||||
let msg = format!("Granted {}.", message);
|
|
||||||
eprintln!("✅ {}", colors::bold(&msg));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
'n' => {
|
|
||||||
clear_n_lines(if api_name.is_some() { 4 } else { 3 });
|
|
||||||
let msg = format!("Denied {}.", message);
|
|
||||||
eprintln!("❌ {}", colors::bold(&msg));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// If we don't get a recognized option try again.
|
|
||||||
clear_n_lines(1);
|
|
||||||
eprint!(" └ {}", colors::bold("Unrecognized option. Allow?"));
|
|
||||||
eprint!(" {} > ", OPTS);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When testing, permission prompt returns the value of STUB_PROMPT_VALUE
|
|
||||||
// which we set from the test functions.
|
|
||||||
#[cfg(test)]
|
|
||||||
fn permission_prompt(
|
|
||||||
_message: &str,
|
|
||||||
_flag: &str,
|
|
||||||
_api_name: Option<&str>,
|
|
||||||
) -> bool {
|
|
||||||
STUB_PROMPT_VALUE.load(Ordering::SeqCst)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
static STUB_PROMPT_VALUE: AtomicBool = AtomicBool::new(true);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
static PERMISSION_PROMPT_STUB_VALUE_SETTER: Lazy<
|
|
||||||
Mutex<PermissionPromptStubValueSetter>,
|
|
||||||
> = Lazy::new(|| Mutex::new(PermissionPromptStubValueSetter));
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
struct PermissionPromptStubValueSetter;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
impl PermissionPromptStubValueSetter {
|
|
||||||
pub fn set(&self, value: bool) {
|
|
||||||
STUB_PROMPT_VALUE.store(value, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use deno_core::resolve_url_or_path;
|
use deno_core::resolve_url_or_path;
|
||||||
use deno_core::serde_json::json;
|
use deno_core::serde_json::json;
|
||||||
|
use prompter::tests::*;
|
||||||
|
|
||||||
// Creates vector of strings, Vec<String>
|
// Creates vector of strings, Vec<String>
|
||||||
macro_rules! svec {
|
macro_rules! svec {
|
||||||
|
@ -2489,6 +2306,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_paths() {
|
fn check_paths() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let allowlist = vec![
|
let allowlist = vec![
|
||||||
PathBuf::from("/a/specific/dir/name"),
|
PathBuf::from("/a/specific/dir/name"),
|
||||||
PathBuf::from("/a/specific"),
|
PathBuf::from("/a/specific"),
|
||||||
|
@ -2590,6 +2408,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_net_with_values() {
|
fn test_check_net_with_values() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let mut perms = Permissions::from_options(&PermissionsOptions {
|
let mut perms = Permissions::from_options(&PermissionsOptions {
|
||||||
allow_net: Some(svec![
|
allow_net: Some(svec![
|
||||||
"localhost",
|
"localhost",
|
||||||
|
@ -2633,6 +2452,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_net_only_flag() {
|
fn test_check_net_only_flag() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let mut perms = Permissions::from_options(&PermissionsOptions {
|
let mut perms = Permissions::from_options(&PermissionsOptions {
|
||||||
allow_net: Some(svec![]), // this means `--allow-net` is present without values following `=` sign
|
allow_net: Some(svec![]), // this means `--allow-net` is present without values following `=` sign
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -2668,6 +2488,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_net_no_flag() {
|
fn test_check_net_no_flag() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let mut perms = Permissions::from_options(&PermissionsOptions {
|
let mut perms = Permissions::from_options(&PermissionsOptions {
|
||||||
allow_net: None,
|
allow_net: None,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -2763,6 +2584,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_specifiers() {
|
fn check_specifiers() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let read_allowlist = if cfg!(target_os = "windows") {
|
let read_allowlist = if cfg!(target_os = "windows") {
|
||||||
vec![PathBuf::from("C:\\a")]
|
vec![PathBuf::from("C:\\a")]
|
||||||
} else {
|
} else {
|
||||||
|
@ -2807,6 +2629,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_invalid_specifiers() {
|
fn check_invalid_specifiers() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let mut perms = Permissions::allow_all();
|
let mut perms = Permissions::allow_all();
|
||||||
|
|
||||||
let mut test_cases = vec![];
|
let mut test_cases = vec![];
|
||||||
|
@ -2827,6 +2650,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_query() {
|
fn test_query() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let perms1 = Permissions::allow_all();
|
let perms1 = Permissions::allow_all();
|
||||||
let perms2 = Permissions {
|
let perms2 = Permissions {
|
||||||
read: UnaryPermission {
|
read: UnaryPermission {
|
||||||
|
@ -2906,6 +2730,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_request() {
|
fn test_request() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let mut perms: Permissions = Default::default();
|
let mut perms: Permissions = Default::default();
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
{
|
{
|
||||||
|
@ -2953,6 +2778,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_revoke() {
|
fn test_revoke() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let mut perms = Permissions {
|
let mut perms = Permissions {
|
||||||
read: UnaryPermission {
|
read: UnaryPermission {
|
||||||
global_state: PermissionState::Prompt,
|
global_state: PermissionState::Prompt,
|
||||||
|
@ -3026,6 +2852,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check() {
|
fn test_check() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let mut perms = Permissions {
|
let mut perms = Permissions {
|
||||||
read: Permissions::new_read(&None, true).unwrap(),
|
read: Permissions::new_read(&None, true).unwrap(),
|
||||||
write: Permissions::new_write(&None, true).unwrap(),
|
write: Permissions::new_write(&None, true).unwrap(),
|
||||||
|
@ -3089,6 +2916,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_fail() {
|
fn test_check_fail() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let mut perms = Permissions {
|
let mut perms = Permissions {
|
||||||
read: Permissions::new_read(&None, true).unwrap(),
|
read: Permissions::new_read(&None, true).unwrap(),
|
||||||
write: Permissions::new_write(&None, true).unwrap(),
|
write: Permissions::new_write(&None, true).unwrap(),
|
||||||
|
@ -3169,6 +2997,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn test_env_windows() {
|
fn test_env_windows() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
|
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
|
||||||
let mut perms = Permissions::allow_all();
|
let mut perms = Permissions::allow_all();
|
||||||
perms.env = UnaryPermission {
|
perms.env = UnaryPermission {
|
||||||
|
@ -3187,6 +3016,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deserialize_child_permissions_arg() {
|
fn test_deserialize_child_permissions_arg() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ChildPermissionsArg::inherit(),
|
ChildPermissionsArg::inherit(),
|
||||||
ChildPermissionsArg {
|
ChildPermissionsArg {
|
||||||
|
@ -3341,6 +3171,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_child_permissions() {
|
fn test_create_child_permissions() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let mut main_perms = Permissions {
|
let mut main_perms = Permissions {
|
||||||
env: Permissions::new_env(&Some(vec![]), false).unwrap(),
|
env: Permissions::new_env(&Some(vec![]), false).unwrap(),
|
||||||
hrtime: Permissions::new_hrtime(true),
|
hrtime: Permissions::new_hrtime(true),
|
||||||
|
@ -3393,6 +3224,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_child_permissions_with_prompt() {
|
fn test_create_child_permissions_with_prompt() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
|
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
|
||||||
let mut main_perms = Permissions::from_options(&PermissionsOptions {
|
let mut main_perms = Permissions::from_options(&PermissionsOptions {
|
||||||
prompt: true,
|
prompt: true,
|
||||||
|
@ -3414,6 +3246,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_child_permissions_with_inherited_denied_list() {
|
fn test_create_child_permissions_with_inherited_denied_list() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
|
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
|
||||||
let mut main_perms = Permissions::from_options(&PermissionsOptions {
|
let mut main_perms = Permissions::from_options(&PermissionsOptions {
|
||||||
prompt: true,
|
prompt: true,
|
||||||
|
@ -3432,6 +3265,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_handle_empty_value() {
|
fn test_handle_empty_value() {
|
||||||
|
set_prompter(Box::new(TestPrompter));
|
||||||
assert!(Permissions::new_read(&Some(vec![PathBuf::new()]), false).is_err());
|
assert!(Permissions::new_read(&Some(vec![PathBuf::new()]), false).is_err());
|
||||||
assert!(Permissions::new_env(&Some(vec![String::new()]), false).is_err());
|
assert!(Permissions::new_env(&Some(vec![String::new()]), false).is_err());
|
||||||
assert!(Permissions::new_sys(&Some(vec![String::new()]), false).is_err());
|
assert!(Permissions::new_sys(&Some(vec![String::new()]), false).is_err());
|
288
runtime/permissions/prompter.rs
Normal file
288
runtime/permissions/prompter.rs
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use crate::colors;
|
||||||
|
use deno_core::error::AnyError;
|
||||||
|
use deno_core::parking_lot::Mutex;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
pub const PERMISSION_EMOJI: &str = "⚠️";
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum PromptResponse {
|
||||||
|
Allow,
|
||||||
|
Deny,
|
||||||
|
}
|
||||||
|
|
||||||
|
static PERMISSION_PROMPTER: Lazy<Mutex<Box<dyn PermissionPrompter>>> =
|
||||||
|
Lazy::new(|| Mutex::new(Box::new(TtyPrompter)));
|
||||||
|
|
||||||
|
static MAYBE_BEFORE_PROMPT_CALLBACK: Lazy<Mutex<Option<PromptCallback>>> =
|
||||||
|
Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
static MAYBE_AFTER_PROMPT_CALLBACK: Lazy<Mutex<Option<PromptCallback>>> =
|
||||||
|
Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
pub fn permission_prompt(
|
||||||
|
message: &str,
|
||||||
|
flag: &str,
|
||||||
|
api_name: Option<&str>,
|
||||||
|
) -> PromptResponse {
|
||||||
|
if let Some(before_callback) = MAYBE_BEFORE_PROMPT_CALLBACK.lock().as_mut() {
|
||||||
|
before_callback();
|
||||||
|
}
|
||||||
|
let r = PERMISSION_PROMPTER.lock().prompt(message, flag, api_name);
|
||||||
|
if let Some(after_callback) = MAYBE_AFTER_PROMPT_CALLBACK.lock().as_mut() {
|
||||||
|
after_callback();
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_prompt_callbacks(
|
||||||
|
before_callback: Option<PromptCallback>,
|
||||||
|
after_callback: Option<PromptCallback>,
|
||||||
|
) {
|
||||||
|
*MAYBE_BEFORE_PROMPT_CALLBACK.lock() = before_callback;
|
||||||
|
*MAYBE_AFTER_PROMPT_CALLBACK.lock() = after_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PromptCallback = Box<dyn FnMut() + Send + Sync>;
|
||||||
|
|
||||||
|
pub trait PermissionPrompter: Send + Sync {
|
||||||
|
fn prompt(
|
||||||
|
&mut self,
|
||||||
|
message: &str,
|
||||||
|
name: &str,
|
||||||
|
api_name: Option<&str>,
|
||||||
|
) -> PromptResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TtyPrompter;
|
||||||
|
|
||||||
|
impl PermissionPrompter for TtyPrompter {
|
||||||
|
fn prompt(
|
||||||
|
&mut self,
|
||||||
|
message: &str,
|
||||||
|
name: &str,
|
||||||
|
api_name: Option<&str>,
|
||||||
|
) -> PromptResponse {
|
||||||
|
if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) {
|
||||||
|
return PromptResponse::Deny;
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn clear_stdin() -> Result<(), AnyError> {
|
||||||
|
// TODO(bartlomieju):
|
||||||
|
#[allow(clippy::undocumented_unsafe_blocks)]
|
||||||
|
let r = unsafe { libc::tcflush(0, libc::TCIFLUSH) };
|
||||||
|
assert_eq!(r, 0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn clear_stdin() -> Result<(), AnyError> {
|
||||||
|
use deno_core::anyhow::bail;
|
||||||
|
use winapi::shared::minwindef::TRUE;
|
||||||
|
use winapi::shared::minwindef::UINT;
|
||||||
|
use winapi::shared::minwindef::WORD;
|
||||||
|
use winapi::shared::ntdef::WCHAR;
|
||||||
|
use winapi::um::processenv::GetStdHandle;
|
||||||
|
use winapi::um::winbase::STD_INPUT_HANDLE;
|
||||||
|
use winapi::um::wincon::FlushConsoleInputBuffer;
|
||||||
|
use winapi::um::wincon::PeekConsoleInputW;
|
||||||
|
use winapi::um::wincon::WriteConsoleInputW;
|
||||||
|
use winapi::um::wincontypes::INPUT_RECORD;
|
||||||
|
use winapi::um::wincontypes::KEY_EVENT;
|
||||||
|
use winapi::um::winnt::HANDLE;
|
||||||
|
use winapi::um::winuser::MapVirtualKeyW;
|
||||||
|
use winapi::um::winuser::MAPVK_VK_TO_VSC;
|
||||||
|
use winapi::um::winuser::VK_RETURN;
|
||||||
|
|
||||||
|
// SAFETY: winapi calls
|
||||||
|
unsafe {
|
||||||
|
let stdin = GetStdHandle(STD_INPUT_HANDLE);
|
||||||
|
// emulate an enter key press to clear any line buffered console characters
|
||||||
|
emulate_enter_key_press(stdin)?;
|
||||||
|
// read the buffered line or enter key press
|
||||||
|
read_stdin_line()?;
|
||||||
|
// check if our emulated key press was executed
|
||||||
|
if is_input_buffer_empty(stdin)? {
|
||||||
|
// if so, move the cursor up to prevent a blank line
|
||||||
|
move_cursor_up()?;
|
||||||
|
} else {
|
||||||
|
// the emulated key press is still pending, so a buffered line was read
|
||||||
|
// and we can flush the emulated key press
|
||||||
|
flush_input_buffer(stdin)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
|
||||||
|
unsafe fn flush_input_buffer(stdin: HANDLE) -> Result<(), AnyError> {
|
||||||
|
let success = FlushConsoleInputBuffer(stdin);
|
||||||
|
if success != TRUE {
|
||||||
|
bail!(
|
||||||
|
"Could not flush the console input buffer: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn emulate_enter_key_press(stdin: HANDLE) -> Result<(), AnyError> {
|
||||||
|
// https://github.com/libuv/libuv/blob/a39009a5a9252a566ca0704d02df8dabc4ce328f/src/win/tty.c#L1121-L1131
|
||||||
|
let mut input_record: INPUT_RECORD = std::mem::zeroed();
|
||||||
|
input_record.EventType = KEY_EVENT;
|
||||||
|
input_record.Event.KeyEvent_mut().bKeyDown = TRUE;
|
||||||
|
input_record.Event.KeyEvent_mut().wRepeatCount = 1;
|
||||||
|
input_record.Event.KeyEvent_mut().wVirtualKeyCode = VK_RETURN as WORD;
|
||||||
|
input_record.Event.KeyEvent_mut().wVirtualScanCode =
|
||||||
|
MapVirtualKeyW(VK_RETURN as UINT, MAPVK_VK_TO_VSC) as WORD;
|
||||||
|
*input_record.Event.KeyEvent_mut().uChar.UnicodeChar_mut() =
|
||||||
|
'\r' as WCHAR;
|
||||||
|
|
||||||
|
let mut record_written = 0;
|
||||||
|
let success =
|
||||||
|
WriteConsoleInputW(stdin, &input_record, 1, &mut record_written);
|
||||||
|
if success != TRUE {
|
||||||
|
bail!(
|
||||||
|
"Could not emulate enter key press: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn is_input_buffer_empty(stdin: HANDLE) -> Result<bool, AnyError> {
|
||||||
|
let mut buffer = Vec::with_capacity(1);
|
||||||
|
let mut events_read = 0;
|
||||||
|
let success =
|
||||||
|
PeekConsoleInputW(stdin, buffer.as_mut_ptr(), 1, &mut events_read);
|
||||||
|
if success != TRUE {
|
||||||
|
bail!(
|
||||||
|
"Could not peek the console input buffer: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(events_read == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_up() -> Result<(), AnyError> {
|
||||||
|
use std::io::Write;
|
||||||
|
write!(std::io::stderr(), "\x1B[1A")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_stdin_line() -> Result<(), AnyError> {
|
||||||
|
let mut input = String::new();
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
stdin.read_line(&mut input)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear n-lines in terminal and move cursor to the beginning of the line.
|
||||||
|
fn clear_n_lines(n: usize) {
|
||||||
|
eprint!("\x1B[{}A\x1B[0J", n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For security reasons we must consume everything in stdin so that previously
|
||||||
|
// buffered data cannot effect the prompt.
|
||||||
|
if let Err(err) = clear_stdin() {
|
||||||
|
eprintln!("Error clearing stdin for permission prompt. {:#}", err);
|
||||||
|
return PromptResponse::Deny; // don't grant permission if this fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// print to stderr so that if stdout is piped this is still displayed.
|
||||||
|
const OPTS: &str = "[y/n] (y = yes, allow; n = no, deny)";
|
||||||
|
eprint!("{} ┌ ", PERMISSION_EMOJI);
|
||||||
|
eprint!("{}", colors::bold("Deno requests "));
|
||||||
|
eprint!("{}", colors::bold(message));
|
||||||
|
eprintln!("{}", colors::bold("."));
|
||||||
|
if let Some(api_name) = api_name {
|
||||||
|
eprintln!(" ├ Requested by `{}` API", api_name);
|
||||||
|
}
|
||||||
|
let msg = format!(
|
||||||
|
" ├ Run again with --allow-{} to bypass this prompt.",
|
||||||
|
name
|
||||||
|
);
|
||||||
|
eprintln!("{}", colors::italic(&msg));
|
||||||
|
eprint!(" └ {}", colors::bold("Allow?"));
|
||||||
|
eprint!(" {} > ", OPTS);
|
||||||
|
let value = loop {
|
||||||
|
let mut input = String::new();
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
let result = stdin.read_line(&mut input);
|
||||||
|
if result.is_err() {
|
||||||
|
break PromptResponse::Deny;
|
||||||
|
};
|
||||||
|
let ch = match input.chars().next() {
|
||||||
|
None => break PromptResponse::Deny,
|
||||||
|
Some(v) => v,
|
||||||
|
};
|
||||||
|
match ch.to_ascii_lowercase() {
|
||||||
|
'y' => {
|
||||||
|
clear_n_lines(if api_name.is_some() { 4 } else { 3 });
|
||||||
|
let msg = format!("Granted {}.", message);
|
||||||
|
eprintln!("✅ {}", colors::bold(&msg));
|
||||||
|
break PromptResponse::Allow;
|
||||||
|
}
|
||||||
|
'n' => {
|
||||||
|
clear_n_lines(if api_name.is_some() { 4 } else { 3 });
|
||||||
|
let msg = format!("Denied {}.", message);
|
||||||
|
eprintln!("❌ {}", colors::bold(&msg));
|
||||||
|
break PromptResponse::Deny;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// If we don't get a recognized option try again.
|
||||||
|
clear_n_lines(1);
|
||||||
|
eprint!(" └ {}", colors::bold("Unrecognized option. Allow?"));
|
||||||
|
eprint!(" {} > ", OPTS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
pub struct TestPrompter;
|
||||||
|
|
||||||
|
impl PermissionPrompter for TestPrompter {
|
||||||
|
fn prompt(
|
||||||
|
&mut self,
|
||||||
|
_message: &str,
|
||||||
|
_name: &str,
|
||||||
|
_api_name: Option<&str>,
|
||||||
|
) -> PromptResponse {
|
||||||
|
if STUB_PROMPT_VALUE.load(Ordering::SeqCst) {
|
||||||
|
PromptResponse::Allow
|
||||||
|
} else {
|
||||||
|
PromptResponse::Deny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static STUB_PROMPT_VALUE: AtomicBool = AtomicBool::new(true);
|
||||||
|
|
||||||
|
pub static PERMISSION_PROMPT_STUB_VALUE_SETTER: Lazy<
|
||||||
|
Mutex<PermissionPromptStubValueSetter>,
|
||||||
|
> = Lazy::new(|| Mutex::new(PermissionPromptStubValueSetter));
|
||||||
|
|
||||||
|
pub struct PermissionPromptStubValueSetter;
|
||||||
|
|
||||||
|
impl PermissionPromptStubValueSetter {
|
||||||
|
pub fn set(&self, value: bool) {
|
||||||
|
STUB_PROMPT_VALUE.store(value, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_prompter(prompter: Box<dyn PermissionPrompter>) {
|
||||||
|
*PERMISSION_PROMPTER.lock() = prompter;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue