keyfork-prompt: add Headless
This commit is contained in:
parent
92dde3dcee
commit
f8db8702ce
|
@ -1,41 +1,26 @@
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
use std::io::{stdin, stdout};
|
|
||||||
|
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
prompt_validated_wordlist,
|
prompt_validated_wordlist,
|
||||||
validators::{mnemonic, Validator},
|
validators::{mnemonic, Validator},
|
||||||
Terminal,
|
default_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_mnemonic::English;
|
use keyfork_mnemonic::English;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
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 {
|
let transport_validator = mnemonic::MnemonicSetValidator {
|
||||||
word_lengths: [9, 24],
|
word_lengths: [24],
|
||||||
};
|
|
||||||
let combine_validator = mnemonic::MnemonicSetValidator {
|
|
||||||
word_lengths: [24, 48],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mnemonics = prompt_validated_wordlist::<English, _>(
|
let mnemonics = prompt_validated_wordlist::<English, _>(
|
||||||
&mut mgr,
|
&mut *handler,
|
||||||
"Enter a 9-word and 24-word mnemonic: ",
|
"Enter a 24-word mnemonic: ",
|
||||||
3,
|
3,
|
||||||
&*transport_validator.to_fn(),
|
&*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[0].as_bytes().len(), 32);
|
||||||
assert_eq!(mnemonics[1].as_bytes().len(), 64);
|
|
||||||
|
|
||||||
Ok(())
|
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.
|
//! Prompt display and interaction management.
|
||||||
|
|
||||||
|
use std::io::IsTerminal;
|
||||||
|
|
||||||
#[cfg(feature = "mnemonic")]
|
#[cfg(feature = "mnemonic")]
|
||||||
use keyfork_mnemonic::Wordlist;
|
use keyfork_mnemonic::Wordlist;
|
||||||
|
|
||||||
///
|
pub mod headless;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod validators;
|
pub mod validators;
|
||||||
|
pub use headless::Headless;
|
||||||
pub use terminal::{default_terminal, DefaultTerminal, Terminal};
|
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.
|
/// An error occurred while displaying a prompt.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
@ -157,3 +181,50 @@ pub fn prompt_validated_passphrase<V>(
|
||||||
})?;
|
})?;
|
||||||
Ok(opt.unwrap())
|
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