2024-01-01 14:58:21 -05:00
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2022-12-17 19:12:28 -05:00
use deno_core ::error ::AnyError ;
use deno_core ::parking_lot ::Mutex ;
2024-02-07 11:25:14 -05:00
use deno_terminal ::colors ;
2022-12-17 19:12:28 -05:00
use once_cell ::sync ::Lazy ;
2023-03-28 17:49:00 -04:00
use std ::fmt ::Write ;
2023-06-09 13:24:39 -04:00
use std ::io ::BufRead ;
2023-08-25 09:43:07 -04:00
use std ::io ::IsTerminal ;
2023-06-09 13:24:39 -04:00
use std ::io ::StderrLock ;
use std ::io ::StdinLock ;
use std ::io ::Write as IoWrite ;
2022-12-17 19:12:28 -05:00
2023-03-09 19:09:14 -05:00
/// Helper function to strip ansi codes and ASCII control characters.
fn strip_ansi_codes_and_ascii_control ( s : & str ) -> std ::borrow ::Cow < str > {
2023-03-28 17:49:00 -04:00
console_static_text ::ansi ::strip_ansi_codes ( s )
2023-03-09 19:09:14 -05:00
. chars ( )
. filter ( | c | ! c . is_ascii_control ( ) )
. collect ( )
}
2022-12-17 19:12:28 -05:00
pub const PERMISSION_EMOJI : & str = " ⚠️ " ;
2024-01-03 18:31:39 -05:00
// 10kB of permission prompting should be enough for anyone
const MAX_PERMISSION_PROMPT_LENGTH : usize = 10 * 1024 ;
2022-12-17 19:12:28 -05:00
#[ derive(Debug, Eq, PartialEq) ]
pub enum PromptResponse {
Allow ,
Deny ,
2023-02-22 17:02:10 -05:00
AllowAll ,
2022-12-17 19:12:28 -05:00
}
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 > ,
2023-02-22 17:02:10 -05:00
is_unary : bool ,
2022-12-17 19:12:28 -05:00
) -> PromptResponse {
if let Some ( before_callback ) = MAYBE_BEFORE_PROMPT_CALLBACK . lock ( ) . as_mut ( ) {
before_callback ( ) ;
}
2023-02-22 17:02:10 -05:00
let r = PERMISSION_PROMPTER
. lock ( )
. prompt ( message , flag , api_name , is_unary ) ;
2022-12-17 19:12:28 -05:00
if let Some ( after_callback ) = MAYBE_AFTER_PROMPT_CALLBACK . lock ( ) . as_mut ( ) {
after_callback ( ) ;
}
r
}
pub fn set_prompt_callbacks (
2022-12-19 14:31:19 -05:00
before_callback : PromptCallback ,
after_callback : PromptCallback ,
2022-12-17 19:12:28 -05:00
) {
2022-12-19 14:31:19 -05:00
* MAYBE_BEFORE_PROMPT_CALLBACK . lock ( ) = Some ( before_callback ) ;
* MAYBE_AFTER_PROMPT_CALLBACK . lock ( ) = Some ( after_callback ) ;
2022-12-17 19:12:28 -05:00
}
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 > ,
2023-02-22 17:02:10 -05:00
is_unary : bool ,
2022-12-17 19:12:28 -05:00
) -> PromptResponse ;
}
pub struct TtyPrompter ;
2024-01-03 18:31:39 -05:00
#[ cfg(unix) ]
fn clear_stdin (
_stdin_lock : & mut StdinLock ,
_stderr_lock : & mut StderrLock ,
) -> 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 (
stdin_lock : & mut StdinLock ,
stderr_lock : & mut StderrLock ,
) -> 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 ( stdin_lock ) ? ;
// 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 ( stderr_lock ) ? ;
} 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 ( stderr_lock : & mut StderrLock ) -> Result < ( ) , AnyError > {
write! ( stderr_lock , " \x1B [1A " ) ? ;
Ok ( ( ) )
}
fn read_stdin_line ( stdin_lock : & mut StdinLock ) -> Result < ( ) , AnyError > {
let mut input = String ::new ( ) ;
stdin_lock . read_line ( & mut input ) ? ;
Ok ( ( ) )
}
}
// Clear n-lines in terminal and move cursor to the beginning of the line.
fn clear_n_lines ( stderr_lock : & mut StderrLock , n : usize ) {
write! ( stderr_lock , " \x1B [{n}A \x1B [0J " ) . unwrap ( ) ;
}
#[ cfg(unix) ]
fn get_stdin_metadata ( ) -> std ::io ::Result < std ::fs ::Metadata > {
use std ::os ::fd ::FromRawFd ;
use std ::os ::fd ::IntoRawFd ;
// SAFETY: we don't know if fd 0 is valid but metadata() will return an error in this case (bad file descriptor)
// and we can panic.
unsafe {
let stdin = std ::fs ::File ::from_raw_fd ( 0 ) ;
let metadata = stdin . metadata ( ) . unwrap ( ) ;
stdin . into_raw_fd ( ) ;
Ok ( metadata )
}
}
2022-12-17 19:12:28 -05:00
impl PermissionPrompter for TtyPrompter {
fn prompt (
& mut self ,
message : & str ,
name : & str ,
api_name : Option < & str > ,
2023-02-22 17:02:10 -05:00
is_unary : bool ,
2022-12-17 19:12:28 -05:00
) -> PromptResponse {
2023-08-25 09:43:07 -04:00
if ! std ::io ::stdin ( ) . is_terminal ( ) | | ! std ::io ::stderr ( ) . is_terminal ( ) {
2022-12-17 19:12:28 -05:00
return PromptResponse ::Deny ;
} ;
2024-01-03 18:31:39 -05:00
if message . len ( ) > MAX_PERMISSION_PROMPT_LENGTH {
eprintln! ( " ❌ Permission prompt length ( {} bytes) was larger than the configured maximum length ( {} bytes): denying request. " , message . len ( ) , MAX_PERMISSION_PROMPT_LENGTH ) ;
eprintln! ( " ❌ WARNING: This may indicate that code is trying to bypass or hide permission check requests. " ) ;
eprintln! ( " ❌ Run again with --allow- {name} to bypass this check if this is really what you want to do. " ) ;
return PromptResponse ::Deny ;
2022-12-17 19:12:28 -05:00
}
2024-01-03 18:31:39 -05:00
#[ cfg(unix) ]
let metadata_before = get_stdin_metadata ( ) . unwrap ( ) ;
2022-12-17 19:12:28 -05:00
2023-06-09 13:24:39 -04:00
// Lock stdio streams, so no other output is written while the prompt is
// displayed.
let stdout_lock = std ::io ::stdout ( ) . lock ( ) ;
let mut stderr_lock = std ::io ::stderr ( ) . lock ( ) ;
let mut stdin_lock = std ::io ::stdin ( ) . lock ( ) ;
2022-12-17 19:12:28 -05:00
// For security reasons we must consume everything in stdin so that previously
2023-06-09 13:24:39 -04:00
// buffered data cannot affect the prompt.
if let Err ( err ) = clear_stdin ( & mut stdin_lock , & mut stderr_lock ) {
2023-01-27 10:43:16 -05:00
eprintln! ( " Error clearing stdin for permission prompt. {err:#} " ) ;
2022-12-17 19:12:28 -05:00
return PromptResponse ::Deny ; // don't grant permission if this fails
}
2023-03-09 19:09:14 -05:00
let message = strip_ansi_codes_and_ascii_control ( message ) ;
let name = strip_ansi_codes_and_ascii_control ( name ) ;
let api_name = api_name . map ( strip_ansi_codes_and_ascii_control ) ;
2022-12-17 19:12:28 -05:00
// print to stderr so that if stdout is piped this is still displayed.
2023-02-22 17:02:10 -05:00
let opts : String = if is_unary {
format! ( " [y/n/A] (y = yes, allow; n = no, deny; A = allow all {name} permissions) " )
} else {
" [y/n] (y = yes, allow; n = no, deny) " . to_string ( )
} ;
2023-03-28 17:49:00 -04:00
// output everything in one shot to make the tests more reliable
{
let mut output = String ::new ( ) ;
write! ( & mut output , " ┌ {PERMISSION_EMOJI} " ) . unwrap ( ) ;
write! ( & mut output , " {} " , colors ::bold ( " Deno requests " ) ) . unwrap ( ) ;
write! ( & mut output , " {} " , colors ::bold ( message . clone ( ) ) ) . unwrap ( ) ;
writeln! ( & mut output , " {} " , colors ::bold ( " . " ) ) . unwrap ( ) ;
if let Some ( api_name ) = api_name . clone ( ) {
writeln! ( & mut output , " ├ Requested by `{api_name}` API. " ) . unwrap ( ) ;
}
let msg = format! ( " Run again with --allow- {name} to bypass this prompt. " ) ;
writeln! ( & mut output , " ├ {} " , colors ::italic ( & msg ) ) . unwrap ( ) ;
write! ( & mut output , " └ {} " , colors ::bold ( " Allow? " ) ) . unwrap ( ) ;
write! ( & mut output , " {opts} > " ) . unwrap ( ) ;
2023-06-09 13:24:39 -04:00
stderr_lock . write_all ( output . as_bytes ( ) ) . unwrap ( ) ;
2022-12-17 19:12:28 -05:00
}
2023-03-28 17:49:00 -04:00
2022-12-17 19:12:28 -05:00
let value = loop {
let mut input = String ::new ( ) ;
2023-06-09 13:24:39 -04:00
let result = stdin_lock . read_line ( & mut input ) ;
2022-12-17 19:12:28 -05:00
if result . is_err ( ) {
break PromptResponse ::Deny ;
} ;
let ch = match input . chars ( ) . next ( ) {
None = > break PromptResponse ::Deny ,
Some ( v ) = > v ,
} ;
2023-02-22 17:02:10 -05:00
match ch {
'y' | 'Y' = > {
2023-06-09 13:24:39 -04:00
clear_n_lines (
& mut stderr_lock ,
if api_name . is_some ( ) { 4 } else { 3 } ,
) ;
2023-01-27 10:43:16 -05:00
let msg = format! ( " Granted {message} . " ) ;
2023-06-09 13:24:39 -04:00
writeln! ( stderr_lock , " ✅ {} " , colors ::bold ( & msg ) ) . unwrap ( ) ;
2022-12-17 19:12:28 -05:00
break PromptResponse ::Allow ;
}
2023-02-22 17:02:10 -05:00
'n' | 'N' = > {
2023-06-09 13:24:39 -04:00
clear_n_lines (
& mut stderr_lock ,
if api_name . is_some ( ) { 4 } else { 3 } ,
) ;
2023-01-27 10:43:16 -05:00
let msg = format! ( " Denied {message} . " ) ;
2023-06-09 13:24:39 -04:00
writeln! ( stderr_lock , " ❌ {} " , colors ::bold ( & msg ) ) . unwrap ( ) ;
2022-12-17 19:12:28 -05:00
break PromptResponse ::Deny ;
}
2023-02-22 17:02:10 -05:00
'A' if is_unary = > {
2023-06-09 13:24:39 -04:00
clear_n_lines (
& mut stderr_lock ,
if api_name . is_some ( ) { 4 } else { 3 } ,
) ;
2023-02-22 17:02:10 -05:00
let msg = format! ( " Granted all {name} access. " ) ;
2023-06-09 13:24:39 -04:00
writeln! ( stderr_lock , " ✅ {} " , colors ::bold ( & msg ) ) . unwrap ( ) ;
2023-02-22 17:02:10 -05:00
break PromptResponse ::AllowAll ;
}
2022-12-17 19:12:28 -05:00
_ = > {
// If we don't get a recognized option try again.
2023-06-09 13:24:39 -04:00
clear_n_lines ( & mut stderr_lock , 1 ) ;
write! (
stderr_lock ,
" └ {} {opts} > " ,
colors ::bold ( " Unrecognized option. Allow? " )
)
. unwrap ( ) ;
2022-12-17 19:12:28 -05:00
}
} ;
} ;
2023-06-09 13:24:39 -04:00
drop ( stdout_lock ) ;
drop ( stderr_lock ) ;
drop ( stdin_lock ) ;
2023-01-13 10:05:07 -05:00
2024-01-03 18:31:39 -05:00
// Ensure that stdin has not changed from the beginning to the end of the prompt. We consider
// it sufficient to check a subset of stat calls. We do not consider the likelihood of a stdin
// swap attack on Windows to be high enough to add this check for that platform. These checks will
// terminate the runtime as they indicate something nefarious is going on.
#[ cfg(unix) ]
{
use std ::os ::unix ::fs ::MetadataExt ;
let metadata_after = get_stdin_metadata ( ) . unwrap ( ) ;
assert_eq! ( metadata_before . dev ( ) , metadata_after . dev ( ) ) ;
assert_eq! ( metadata_before . ino ( ) , metadata_after . ino ( ) ) ;
assert_eq! ( metadata_before . rdev ( ) , metadata_after . rdev ( ) ) ;
assert_eq! ( metadata_before . uid ( ) , metadata_after . uid ( ) ) ;
assert_eq! ( metadata_before . gid ( ) , metadata_after . gid ( ) ) ;
assert_eq! ( metadata_before . mode ( ) , metadata_after . mode ( ) ) ;
}
// Ensure that stdin and stderr are still terminals before we yield the response.
assert! ( std ::io ::stdin ( ) . is_terminal ( ) & & std ::io ::stderr ( ) . is_terminal ( ) ) ;
2022-12-17 19:12:28 -05:00
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 > ,
2023-02-22 17:02:10 -05:00
_is_unary : bool ,
2022-12-17 19:12:28 -05:00
) -> 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 ;
}
}