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
2024-02-18 23:51:06 -05:00
/// Helper function to make control characters visible so users can see the underlying filename.
fn escape_control_characters ( s : & str ) -> std ::borrow ::Cow < str > {
if ! s . contains ( | c : char | c . is_ascii_control ( ) | | c . is_control ( ) ) {
return std ::borrow ::Cow ::Borrowed ( s ) ;
}
let mut output = String ::with_capacity ( s . len ( ) * 2 ) ;
for c in s . chars ( ) {
match c {
c if c . is_ascii_control ( ) = > output . push_str (
& colors ::white_bold_on_red ( c . escape_debug ( ) . to_string ( ) ) . to_string ( ) ,
) ,
c if c . is_control ( ) = > output . push_str (
& colors ::white_bold_on_red ( c . escape_debug ( ) . to_string ( ) ) . to_string ( ) ,
) ,
c = > output . push ( c ) ,
}
}
output . into ( )
2023-03-09 19:09:14 -05:00
}
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 > {
2024-04-02 17:55:06 -04:00
use deno_core ::anyhow ::bail ;
use std ::mem ::MaybeUninit ;
const STDIN_FD : i32 = 0 ;
// SAFETY: use libc to flush stdin
unsafe {
// Create fd_set for select
let mut raw_fd_set = MaybeUninit ::< libc ::fd_set > ::uninit ( ) ;
libc ::FD_ZERO ( raw_fd_set . as_mut_ptr ( ) ) ;
libc ::FD_SET ( STDIN_FD , raw_fd_set . as_mut_ptr ( ) ) ;
loop {
let r = libc ::tcflush ( STDIN_FD , libc ::TCIFLUSH ) ;
if r ! = 0 {
bail! ( " clear_stdin failed (tcflush) " ) ;
}
// Initialize timeout for select to be 100ms
let mut timeout = libc ::timeval {
tv_sec : 0 ,
tv_usec : 100_000 ,
} ;
// Call select with the stdin file descriptor set
let r = libc ::select (
STDIN_FD + 1 , // nfds should be set to the highest-numbered file descriptor in any of the three sets, plus 1.
raw_fd_set . as_mut_ptr ( ) ,
std ::ptr ::null_mut ( ) ,
std ::ptr ::null_mut ( ) ,
& mut timeout ,
) ;
// Check if select returned an error
if r < 0 {
bail! ( " clear_stdin failed (select) " ) ;
}
// Check if select returned due to timeout (stdin is quiescent)
if r = = 0 {
break ; // Break out of the loop as stdin is quiescent
}
// If select returned due to data available on stdin, clear it by looping around to flush
}
}
2024-01-03 18:31:39 -05:00
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-05-08 22:45:06 -04:00
#[ allow(clippy::print_stderr) ]
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.
2024-05-08 22:45:06 -04:00
#[ allow(clippy::print_stderr) ]
2023-06-09 13:24:39 -04:00
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
}
2024-02-18 23:51:06 -05:00
let message = escape_control_characters ( message ) ;
let name = escape_control_characters ( name ) ;
let api_name = api_name . map ( escape_control_characters ) ;
2023-03-09 19:09:14 -05:00
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 {
2024-04-02 17:55:06 -04:00
// Clear stdin each time we loop around in case the user accidentally pasted
// multiple lines or otherwise did something silly to generate a torrent of
2024-04-04 11:44:43 -04:00
// input. This doesn't work on Windows because `clear_stdin` has other side-effects.
2024-05-08 22:45:06 -04:00
#[ allow(clippy::print_stderr) ]
2024-04-04 11:44:43 -04:00
#[ cfg(unix) ]
2024-04-02 17:55:06 -04:00
if let Err ( err ) = clear_stdin ( & mut stdin_lock , & mut stderr_lock ) {
eprintln! ( " Error clearing stdin for permission prompt. {err:#} " ) ;
return PromptResponse ::Deny ; // don't grant permission if this fails
}
2022-12-17 19:12:28 -05:00
let mut input = String ::new ( ) ;
2023-06-09 13:24:39 -04:00
let result = stdin_lock . read_line ( & mut input ) ;
2024-04-04 11:44:43 -04:00
let input = input . trim_end_matches ( | c | c = = '\r' | | c = = '\n' ) ;
if result . is_err ( ) | | input . len ( ) ! = 1 {
2022-12-17 19:12:28 -05:00
break PromptResponse ::Deny ;
} ;
2024-04-04 11:44:43 -04:00
match input . as_bytes ( ) [ 0 ] as char {
2023-02-22 17:02:10 -05:00
'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 ;
}
2024-04-02 17:55:06 -04:00
'n' | 'N' | '\x1b' = > {
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 ;
}
}