keyfork-prompt: add Headless
This commit is contained in:
parent
92dde3dcee
commit
f8db8702ce
|
@ -1,41 +1,26 @@
|
|||
#![allow(missing_docs)]
|
||||
|
||||
use std::io::{stdin, stdout};
|
||||
|
||||
use keyfork_prompt::{
|
||||
prompt_validated_wordlist,
|
||||
validators::{mnemonic, Validator},
|
||||
Terminal,
|
||||
default_handler,
|
||||
};
|
||||
|
||||
use keyfork_mnemonic::English;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut mgr = Terminal::new(stdin(), stdout())?;
|
||||
let mut handler = default_handler().unwrap();
|
||||
let transport_validator = mnemonic::MnemonicSetValidator {
|
||||
word_lengths: [9, 24],
|
||||
};
|
||||
let combine_validator = mnemonic::MnemonicSetValidator {
|
||||
word_lengths: [24, 48],
|
||||
word_lengths: [24],
|
||||
};
|
||||
|
||||
let mnemonics = prompt_validated_wordlist::<English, _>(
|
||||
&mut mgr,
|
||||
"Enter a 9-word and 24-word mnemonic: ",
|
||||
&mut *handler,
|
||||
"Enter a 24-word mnemonic: ",
|
||||
3,
|
||||
&*transport_validator.to_fn(),
|
||||
)?;
|
||||
assert_eq!(mnemonics[0].as_bytes().len(), 12);
|
||||
assert_eq!(mnemonics[1].as_bytes().len(), 32);
|
||||
|
||||
let mnemonics = prompt_validated_wordlist::<English, _>(
|
||||
&mut mgr,
|
||||
"Enter a 24 and 48-word mnemonic: ",
|
||||
3,
|
||||
&*combine_validator.to_fn(),
|
||||
)?;
|
||||
assert_eq!(mnemonics[0].as_bytes().len(), 32);
|
||||
assert_eq!(mnemonics[1].as_bytes().len(), 64);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
//! A headless prompt handler.
|
||||
//!
|
||||
//! This prompt handler uses the program's standard input and output to read inputs. It is not
|
||||
//! directly intended to be machine-readable, but can be used for scriptable automation in a
|
||||
//! fashion similar to a terminal handler.
|
||||
|
||||
use std::io::{IsTerminal, Write};
|
||||
|
||||
use crate::{BoxResult, Error, Message, PromptHandler, Result};
|
||||
|
||||
/// A headless prompt handler, usable in situations when a terminal might not be available, or for
|
||||
/// scripting purposes where manual input from a terminal is not desirable.
|
||||
pub struct Headless {
|
||||
stdin: std::io::Stdin,
|
||||
stderr: std::io::Stderr,
|
||||
}
|
||||
|
||||
impl Headless {
|
||||
/// Create a new [`Headless`] prompt handler.
|
||||
#[allow(clippy::missing_errors_doc, clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
stdin: std::io::stdin(),
|
||||
stderr: std::io::stderr(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptHandler for Headless {
|
||||
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
|
||||
self.stderr.write_all(prompt.as_bytes())?;
|
||||
self.stderr.flush()?;
|
||||
let mut line = String::new();
|
||||
self.stdin.read_line(&mut line)?;
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
fn prompt_wordlist(&mut self, prompt: &str, _wordlist: &[&str]) -> Result<String> {
|
||||
self.stderr.write_all(prompt.as_bytes())?;
|
||||
self.stderr.flush()?;
|
||||
let mut line = String::new();
|
||||
self.stdin.read_line(&mut line)?;
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
|
||||
// Temporarily perform an IOCTL to disable printed output.
|
||||
if self.stdin.is_terminal() {
|
||||
eprintln!("WARNING: Headless terminal mode may leak passwords!");
|
||||
}
|
||||
self.stderr.write_all(prompt.as_bytes())?;
|
||||
self.stderr.flush()?;
|
||||
let mut line = String::new();
|
||||
self.stdin.read_line(&mut line)?;
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
fn prompt_message(&mut self, prompt: Message) -> Result<()> {
|
||||
match prompt {
|
||||
Message::Text(s) => {
|
||||
self.stderr.write_all(s.as_bytes())?;
|
||||
self.stderr.flush()?;
|
||||
}
|
||||
Message::Data(s) => {
|
||||
self.stderr.write_all(s.as_bytes())?;
|
||||
self.stderr.flush()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt_validated_wordlist(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
retries: u8,
|
||||
_wordlist: &[&str],
|
||||
validator_fn: &mut dyn FnMut(String) -> BoxResult,
|
||||
) -> Result<()> {
|
||||
let mut line = String::new();
|
||||
let mut last_error = String::new();
|
||||
for _ in 0..retries {
|
||||
self.stderr.write_all(prompt.as_bytes())?;
|
||||
self.stderr.flush()?;
|
||||
self.stderr.flush()?;
|
||||
self.stdin.read_line(&mut line)?;
|
||||
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
|
||||
last_error = e.to_string();
|
||||
self.stderr.write_all(e.to_string().as_bytes())?;
|
||||
self.stderr.flush()?;
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(Error::Validation(retries, last_error))
|
||||
}
|
||||
|
||||
fn prompt_validated_passphrase(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
retries: u8,
|
||||
validator_fn: &mut dyn FnMut(String) -> BoxResult,
|
||||
) -> Result<()> {
|
||||
let mut line = String::new();
|
||||
let mut last_error = String::new();
|
||||
for _ in 0..retries {
|
||||
self.stderr.write_all(prompt.as_bytes())?;
|
||||
self.stderr.flush()?;
|
||||
self.stdin.read_line(&mut line)?;
|
||||
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
|
||||
last_error = e.to_string();
|
||||
self.stderr.write_all(e.to_string().as_bytes())?;
|
||||
self.stderr.write_all(b"\n")?;
|
||||
self.stderr.flush()?;
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(Error::Validation(retries, last_error))
|
||||
}
|
||||
}
|
|
@ -1,13 +1,37 @@
|
|||
//! Prompt display and interaction management.
|
||||
|
||||
use std::io::IsTerminal;
|
||||
|
||||
#[cfg(feature = "mnemonic")]
|
||||
use keyfork_mnemonic::Wordlist;
|
||||
|
||||
///
|
||||
pub mod headless;
|
||||
pub mod terminal;
|
||||
pub mod validators;
|
||||
pub use headless::Headless;
|
||||
pub use terminal::{default_terminal, DefaultTerminal, Terminal};
|
||||
|
||||
/// An error occurred in the process of loading a default handler.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum DefaultHandlerError {
|
||||
/// An invalid handler was loaded.
|
||||
#[error("An invalid handler was loaded: {handler} ({error})")]
|
||||
InvalidHandler {
|
||||
/// The handle that caused an error.
|
||||
handler: String,
|
||||
|
||||
/// The error that occurred.
|
||||
error: String,
|
||||
},
|
||||
|
||||
/// An unknown handler was requested.
|
||||
#[error("An unknown handler was requested: {handler}")]
|
||||
UnknownHandler {
|
||||
/// The requested, but unknown, handler.
|
||||
handler: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// An error occurred while displaying a prompt.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
|
@ -157,3 +181,50 @@ pub fn prompt_validated_passphrase<V>(
|
|||
})?;
|
||||
Ok(opt.unwrap())
|
||||
}
|
||||
|
||||
/// Get a Prompt Handler that is most suitable for the given environment.
|
||||
///
|
||||
/// The following handlers will be used based on the `KEYFORK_PROMPT_TYPE` variable:
|
||||
/// * `KEYFORK_PROMPT_TYPE=terminal`: [`DefaultTerminal`]
|
||||
/// * `KEYFORK_PROMPT_TYPE=headless`: [`Headless`]
|
||||
///
|
||||
/// Otherwise, the following heuristics are followed:
|
||||
/// * [`std::io::IsTerminal::is_terminal`]: [`DefaultTerminal`]
|
||||
/// * default: [`Headless`]
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The function will return an error if a specific handler was requested but could not be
|
||||
/// constructed.
|
||||
pub fn default_handler() -> Result<Box<dyn PromptHandler>, DefaultHandlerError> {
|
||||
if let Some((_, value)) = std::env::vars().find(|(k, _)| k == "KEYFORK_PROMPT_TYPE") {
|
||||
match value.as_str() {
|
||||
"terminal" => match default_terminal() {
|
||||
Ok(terminal) => return Ok(Box::new(terminal)),
|
||||
Err(e) => {
|
||||
return Err(DefaultHandlerError::InvalidHandler {
|
||||
handler: value,
|
||||
error: e.to_string(),
|
||||
})
|
||||
}
|
||||
},
|
||||
"headless" => {
|
||||
return Ok(Box::new(Headless::new()));
|
||||
}
|
||||
_ => {
|
||||
return Err(DefaultHandlerError::UnknownHandler { handler: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stdout can be not-a-terminal and we'll just override it anyways, stdin is the
|
||||
// important one.
|
||||
if std::io::stdin().is_terminal() {
|
||||
// because this is a "guessed" handler, let's take the nice route and not error, just skip.
|
||||
if let Ok(terminal) = default_terminal() {
|
||||
return Ok(Box::new(terminal));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Box::new(Headless::new()))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue