diff options
-rw-r--r-- | cli/tests/integration/run_tests.rs | 1 | ||||
-rw-r--r-- | cli/tests/testdata/run/stdio_streams_are_locked_in_permission_prompt/worker.js | 1 | ||||
-rw-r--r-- | runtime/permissions/prompter.rs | 88 |
3 files changed, 56 insertions, 34 deletions
diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index 2accd5444..3fa2f9896 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -4370,6 +4370,7 @@ fn stdio_streams_are_locked_in_permission_prompt() { std::thread::sleep(Duration::from_millis(50)); // give the other thread some time to output console.write_line_raw("invalid"); console.expect("Unrecognized option."); + console.expect("Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all write permissions)"); console.write_line_raw("y"); console.expect("Granted write access to"); diff --git a/cli/tests/testdata/run/stdio_streams_are_locked_in_permission_prompt/worker.js b/cli/tests/testdata/run/stdio_streams_are_locked_in_permission_prompt/worker.js index 6d67c51a2..1e5a632d3 100644 --- a/cli/tests/testdata/run/stdio_streams_are_locked_in_permission_prompt/worker.js +++ b/cli/tests/testdata/run/stdio_streams_are_locked_in_permission_prompt/worker.js @@ -1,2 +1 @@ -console.clear(); console.log("Are you sure you want to continue?"); diff --git a/runtime/permissions/prompter.rs b/runtime/permissions/prompter.rs index 502771636..93a7c9686 100644 --- a/runtime/permissions/prompter.rs +++ b/runtime/permissions/prompter.rs @@ -5,6 +5,10 @@ use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use once_cell::sync::Lazy; use std::fmt::Write; +use std::io::BufRead; +use std::io::StderrLock; +use std::io::StdinLock; +use std::io::Write as IoWrite; /// Helper function to strip ansi codes and ASCII control characters. fn strip_ansi_codes_and_ascii_control(s: &str) -> std::borrow::Cow<str> { @@ -85,7 +89,10 @@ impl PermissionPrompter for TtyPrompter { }; #[cfg(unix)] - fn clear_stdin() -> Result<(), AnyError> { + 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) }; @@ -94,7 +101,10 @@ impl PermissionPrompter for TtyPrompter { } #[cfg(not(unix))] - fn clear_stdin() -> Result<(), AnyError> { + 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; @@ -118,11 +128,11 @@ impl PermissionPrompter for TtyPrompter { // 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()?; + 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()?; + 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 @@ -181,37 +191,36 @@ impl PermissionPrompter for TtyPrompter { Ok(events_read == 0) } - fn move_cursor_up() -> Result<(), AnyError> { - use std::io::Write; - write!(std::io::stderr(), "\x1B[1A")?; + fn move_cursor_up(stderr_lock: &mut StderrLock) -> Result<(), AnyError> { + write!(stderr_lock, "\x1B[1A")?; Ok(()) } - fn read_stdin_line() -> Result<(), AnyError> { + fn read_stdin_line(stdin_lock: &mut StdinLock) -> Result<(), AnyError> { let mut input = String::new(); - let stdin = std::io::stdin(); - stdin.read_line(&mut input)?; + 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(n: usize) { - eprint!("\x1B[{n}A\x1B[0J"); + fn clear_n_lines(stderr_lock: &mut StderrLock, n: usize) { + write!(stderr_lock, "\x1B[{n}A\x1B[0J").unwrap(); } + // 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(); + // For security reasons we must consume everything in stdin so that previously - // buffered data cannot effect the prompt. - if let Err(err) = clear_stdin() { + // buffered data cannot affect the prompt. + 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 } - // Lock stdio streams, so no other output is written while the prompt is - // displayed. - let _stdout_guard = std::io::stdout().lock(); - let _stderr_guard = std::io::stderr().lock(); - 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); @@ -238,13 +247,12 @@ impl PermissionPrompter for TtyPrompter { write!(&mut output, "└ {}", colors::bold("Allow?")).unwrap(); write!(&mut output, " {opts} > ").unwrap(); - eprint!("{}", output); + stderr_lock.write_all(output.as_bytes()).unwrap(); } let value = loop { let mut input = String::new(); - let stdin = std::io::stdin(); - let result = stdin.read_line(&mut input); + let result = stdin_lock.read_line(&mut input); if result.is_err() { break PromptResponse::Deny; }; @@ -254,34 +262,48 @@ impl PermissionPrompter for TtyPrompter { }; match ch { 'y' | 'Y' => { - clear_n_lines(if api_name.is_some() { 4 } else { 3 }); + clear_n_lines( + &mut stderr_lock, + if api_name.is_some() { 4 } else { 3 }, + ); let msg = format!("Granted {message}."); - eprintln!("✅ {}", colors::bold(&msg)); + writeln!(stderr_lock, "✅ {}", colors::bold(&msg)).unwrap(); break PromptResponse::Allow; } 'n' | 'N' => { - clear_n_lines(if api_name.is_some() { 4 } else { 3 }); + clear_n_lines( + &mut stderr_lock, + if api_name.is_some() { 4 } else { 3 }, + ); let msg = format!("Denied {message}."); - eprintln!("❌ {}", colors::bold(&msg)); + writeln!(stderr_lock, "❌ {}", colors::bold(&msg)).unwrap(); break PromptResponse::Deny; } 'A' if is_unary => { - clear_n_lines(if api_name.is_some() { 4 } else { 3 }); + clear_n_lines( + &mut stderr_lock, + if api_name.is_some() { 4 } else { 3 }, + ); let msg = format!("Granted all {name} access."); - eprintln!("✅ {}", colors::bold(&msg)); + writeln!(stderr_lock, "✅ {}", colors::bold(&msg)).unwrap(); break PromptResponse::AllowAll; } _ => { // If we don't get a recognized option try again. - clear_n_lines(1); - eprint!("└ {}", colors::bold("Unrecognized option. Allow?")); - eprint!(" {opts} > "); + clear_n_lines(&mut stderr_lock, 1); + write!( + stderr_lock, + "└ {} {opts} > ", + colors::bold("Unrecognized option. Allow?") + ) + .unwrap(); } }; }; - drop(_stdout_guard); - drop(_stderr_guard); + drop(stdout_lock); + drop(stderr_lock); + drop(stdin_lock); value } |