move input prompting code into tui module

This commit is contained in:
Carson McManus 2021-08-14 10:01:25 -04:00
parent d594da3b86
commit f38f9b6e52
3 changed files with 239 additions and 231 deletions

34
src/demos.rs Normal file
View file

@ -0,0 +1,34 @@
use crate::tui;
use log::*;
use steamguard::{Confirmation, ConfirmationType};
pub fn demo_confirmation_menu() {
info!("showing demo menu");
let (accept, deny) = tui::prompt_confirmation_menu(vec![
Confirmation {
id: 1234,
key: 12345,
conf_type: ConfirmationType::Trade,
creator: 09870987,
},
Confirmation {
id: 1234,
key: 12345,
conf_type: ConfirmationType::MarketSell,
creator: 09870987,
},
Confirmation {
id: 1234,
key: 12345,
conf_type: ConfirmationType::AccountRecovery,
creator: 09870987,
},
Confirmation {
id: 1234,
key: 12345,
conf_type: ConfirmationType::Trade,
creator: 09870987,
},
]);
println!("accept: {}, deny: {}", accept.len(), deny.len());
}

View file

@ -1,22 +1,14 @@
extern crate rpassword;
use clap::{crate_version, App, Arg};
use log::*;
use regex::Regex;
use std::collections::HashSet;
use std::{
io::{stdin, stdout, Write},
io::{stdout, Write},
path::Path,
sync::{Arc, Mutex},
};
use steamguard::{
steamapi, AccountLinkError, AccountLinker, Confirmation, ConfirmationType, FinalizeLinkError,
LoginError, SteamGuardAccount, UserLogin,
};
use termion::{
event::{Event, Key},
input::TermRead,
raw::IntoRawMode,
screen::AlternateScreen,
steamapi, AccountLinkError, AccountLinker, Confirmation, FinalizeLinkError, LoginError,
SteamGuardAccount, UserLogin,
};
#[macro_use]
@ -25,11 +17,8 @@ extern crate lazy_static;
extern crate anyhow;
extern crate dirs;
mod accountmanager;
lazy_static! {
static ref CAPTCHA_VALID_CHARS: Regex =
Regex::new("^([A-H]|[J-N]|[P-R]|[T-Z]|[2-4]|[7-9]|[@%&])+$").unwrap();
}
mod demos;
mod tui;
fn main() {
let matches = App::new("steamguard-cli")
@ -121,7 +110,7 @@ fn main() {
if let Some(demo_matches) = matches.subcommand_matches("debug") {
if demo_matches.is_present("demo-conf-menu") {
demo_confirmation_menu();
demos::demo_confirmation_menu();
}
return;
}
@ -140,7 +129,7 @@ fn main() {
"Would you like to create a manifest in {} ? [Yn] ",
mafiles_dir
);
match prompt().to_lowercase().as_str() {
match tui::prompt().to_lowercase().as_str() {
"n" => {
info!("Aborting!");
return;
@ -185,7 +174,7 @@ fn main() {
Err(AccountLinkError::MustProvidePhoneNumber) => {
println!("Enter your phone number in the following format: +1 123-456-7890");
print!("Phone number: ");
linker.phone_number = prompt().replace(&['(', ')', '-'][..], "");
linker.phone_number = tui::prompt().replace(&['(', ')', '-'][..], "");
}
Err(AccountLinkError::AuthenticatorPresent) => {
println!("An authenticator is already present on this account.");
@ -193,7 +182,7 @@ fn main() {
}
Err(AccountLinkError::MustConfirmEmail) => {
println!("Check your email and click the link.");
pause();
tui::pause();
}
Err(err) => {
error!(
@ -228,7 +217,7 @@ fn main() {
debug!("attempting link finalization");
print!("Enter SMS code: ");
let sms_code = prompt();
let sms_code = tui::prompt();
let mut tries = 0;
loop {
match linker.finalize(&mut account, sms_code.clone()) {
@ -333,7 +322,7 @@ fn main() {
}
} else {
if termion::is_tty(&stdout()) {
let (accept, deny) = prompt_confirmation_menu(confirmations);
let (accept, deny) = tui::prompt_confirmation_menu(confirmations);
for conf in &accept {
let result = account.accept_confirmation(conf);
debug!("accept confirmation result: {:?}", result);
@ -364,7 +353,7 @@ fn main() {
);
print!("Do you want to continue? [yN] ");
match prompt().as_str() {
match tui::prompt().as_str() {
"y" => {}
_ => {
println!("Aborting!");
@ -411,175 +400,12 @@ fn main() {
}
}
fn validate_captcha_text(text: &String) -> bool {
return CAPTCHA_VALID_CHARS.is_match(text);
}
#[test]
fn test_validate_captcha_text() {
assert!(validate_captcha_text(&String::from("2WWUA@")));
assert!(validate_captcha_text(&String::from("3G8HT2")));
assert!(validate_captcha_text(&String::from("3J%@X3")));
assert!(validate_captcha_text(&String::from("2GCZ4A")));
assert!(validate_captcha_text(&String::from("3G8HT2")));
assert!(!validate_captcha_text(&String::from("asd823")));
assert!(!validate_captcha_text(&String::from("!PQ4RD")));
assert!(!validate_captcha_text(&String::from("1GQ4XZ")));
assert!(!validate_captcha_text(&String::from("8GO4XZ")));
assert!(!validate_captcha_text(&String::from("IPQ4RD")));
assert!(!validate_captcha_text(&String::from("0PT4RD")));
assert!(!validate_captcha_text(&String::from("APTSRD")));
assert!(!validate_captcha_text(&String::from("AP5TRD")));
assert!(!validate_captcha_text(&String::from("AP6TRD")));
}
/// Prompt the user for text input.
fn prompt() -> String {
let mut text = String::new();
let _ = std::io::stdout().flush();
stdin()
.read_line(&mut text)
.expect("Did not enter a correct string");
return String::from(text.strip_suffix('\n').unwrap());
}
fn prompt_captcha_text(captcha_gid: &String) -> String {
println!("Captcha required. Open this link in your web browser: https://steamcommunity.com/public/captcha.php?gid={}", captcha_gid);
let mut captcha_text;
loop {
print!("Enter captcha text: ");
captcha_text = prompt();
if captcha_text.len() > 0 && validate_captcha_text(&captcha_text) {
break;
}
warn!("Invalid chars for captcha text found in user's input. Prompting again...");
}
return captcha_text;
}
/// Returns a tuple of (accepted, denied). Ignored confirmations are not included.
fn prompt_confirmation_menu(
confirmations: Vec<Confirmation>,
) -> (Vec<Confirmation>, Vec<Confirmation>) {
println!("press a key other than enter to show the menu.");
let mut to_accept_idx: HashSet<usize> = HashSet::new();
let mut to_deny_idx: HashSet<usize> = HashSet::new();
let mut screen = AlternateScreen::from(stdout().into_raw_mode().unwrap());
let stdin = stdin();
let mut selected_idx = 0;
for c in stdin.events() {
match c.expect("could not get events") {
Event::Key(Key::Char('a')) => {
to_accept_idx.insert(selected_idx);
to_deny_idx.remove(&selected_idx);
}
Event::Key(Key::Char('d')) => {
to_accept_idx.remove(&selected_idx);
to_deny_idx.insert(selected_idx);
}
Event::Key(Key::Char('i')) => {
to_accept_idx.remove(&selected_idx);
to_deny_idx.remove(&selected_idx);
}
Event::Key(Key::Char('A')) => {
(0..confirmations.len()).for_each(|i| {
to_accept_idx.insert(i);
to_deny_idx.remove(&i);
});
}
Event::Key(Key::Char('D')) => {
(0..confirmations.len()).for_each(|i| {
to_accept_idx.remove(&i);
to_deny_idx.insert(i);
});
}
Event::Key(Key::Char('I')) => {
(0..confirmations.len()).for_each(|i| {
to_accept_idx.remove(&i);
to_deny_idx.remove(&i);
});
}
Event::Key(Key::Up) if selected_idx > 0 => {
selected_idx -= 1;
}
Event::Key(Key::Down) if selected_idx < confirmations.len() - 1 => {
selected_idx += 1;
}
Event::Key(Key::Char('\n')) => {
break;
}
Event::Key(Key::Esc) | Event::Key(Key::Ctrl('c')) => {
return (vec![], vec![]);
}
_ => {}
}
write!(
screen,
"{}{}{}arrow keys to select, [a]ccept, [d]eny, [i]gnore, [enter] confirm choices\n\n",
termion::clear::All,
termion::cursor::Goto(1, 1),
termion::color::Fg(termion::color::White)
)
.unwrap();
for i in 0..confirmations.len() {
if selected_idx == i {
write!(
screen,
"\r{} >",
termion::color::Fg(termion::color::LightYellow)
)
.unwrap();
} else {
write!(screen, "\r{} ", termion::color::Fg(termion::color::White)).unwrap();
}
if to_accept_idx.contains(&i) {
write!(
screen,
"{}[a]",
termion::color::Fg(termion::color::LightGreen)
)
.unwrap();
} else if to_deny_idx.contains(&i) {
write!(
screen,
"{}[d]",
termion::color::Fg(termion::color::LightRed)
)
.unwrap();
} else {
write!(screen, "[ ]").unwrap();
}
if selected_idx == i {
write!(
screen,
"{}",
termion::color::Fg(termion::color::LightYellow)
)
.unwrap();
}
write!(screen, " {}\n", confirmations[i].description()).unwrap();
}
}
return (
to_accept_idx.iter().map(|i| confirmations[*i]).collect(),
to_deny_idx.iter().map(|i| confirmations[*i]).collect(),
);
}
fn do_login(account: &mut SteamGuardAccount) {
if account.account_name.len() > 0 {
println!("Username: {}", account.account_name);
} else {
print!("Username: ");
account.account_name = prompt();
account.account_name = tui::prompt();
}
let _ = std::io::stdout().flush();
let password = rpassword::prompt_password_stdout("Password: ").unwrap();
@ -604,12 +430,12 @@ fn do_login(account: &mut SteamGuardAccount) {
}
Err(LoginError::NeedCaptcha { captcha_gid }) => {
debug!("need captcha to log in");
login.captcha_text = prompt_captcha_text(&captcha_gid);
login.captcha_text = tui::prompt_captcha_text(&captcha_gid);
}
Err(LoginError::NeedEmail) => {
println!("You should have received an email with a code.");
print!("Enter code");
login.email_code = prompt();
login.email_code = tui::prompt();
}
r => {
error!("Fatal login result: {:?}", r);
@ -626,7 +452,7 @@ fn do_login(account: &mut SteamGuardAccount) {
fn do_login_raw() -> anyhow::Result<steamapi::Session> {
print!("Username: ");
let username = prompt();
let username = tui::prompt();
let _ = std::io::stdout().flush();
let password = rpassword::prompt_password_stdout("Password: ").unwrap();
if password.len() > 0 {
@ -644,16 +470,16 @@ fn do_login_raw() -> anyhow::Result<steamapi::Session> {
}
Err(LoginError::Need2FA) => {
print!("Enter 2fa code: ");
login.twofactor_code = prompt();
login.twofactor_code = tui::prompt();
}
Err(LoginError::NeedCaptcha { captcha_gid }) => {
debug!("need captcha to log in");
login.captcha_text = prompt_captcha_text(&captcha_gid);
login.captcha_text = tui::prompt_captcha_text(&captcha_gid);
}
Err(LoginError::NeedEmail) => {
println!("You should have received an email with a code.");
print!("Enter code: ");
login.email_code = prompt();
login.email_code = tui::prompt();
}
Err(r) => {
error!("Fatal login result: {:?}", r);
@ -668,44 +494,6 @@ fn do_login_raw() -> anyhow::Result<steamapi::Session> {
}
}
fn pause() {
println!("Press any key to continue...");
let mut stdout = stdout().into_raw_mode().unwrap();
stdout.flush().unwrap();
stdin().events().next();
}
fn demo_confirmation_menu() {
info!("showing demo menu");
let (accept, deny) = prompt_confirmation_menu(vec![
Confirmation {
id: 1234,
key: 12345,
conf_type: ConfirmationType::Trade,
creator: 09870987,
},
Confirmation {
id: 1234,
key: 12345,
conf_type: ConfirmationType::MarketSell,
creator: 09870987,
},
Confirmation {
id: 1234,
key: 12345,
conf_type: ConfirmationType::AccountRecovery,
creator: 09870987,
},
Confirmation {
id: 1234,
key: 12345,
conf_type: ConfirmationType::Trade,
creator: 09870987,
},
]);
println!("accept: {}, deny: {}", accept.len(), deny.len());
}
fn get_mafiles_dir() -> String {
let paths = vec![
Path::new(&dirs::config_dir().unwrap()).join("steamguard-cli/maFiles"),

186
src/tui.rs Normal file
View file

@ -0,0 +1,186 @@
use log::*;
use regex::Regex;
use std::collections::HashSet;
use std::io::Write;
use steamguard::Confirmation;
use termion::{
event::{Event, Key},
input::TermRead,
raw::IntoRawMode,
screen::AlternateScreen,
};
lazy_static! {
static ref CAPTCHA_VALID_CHARS: Regex =
Regex::new("^([A-H]|[J-N]|[P-R]|[T-Z]|[2-4]|[7-9]|[@%&])+$").unwrap();
}
pub fn validate_captcha_text(text: &String) -> bool {
return CAPTCHA_VALID_CHARS.is_match(text);
}
#[test]
fn test_validate_captcha_text() {
assert!(validate_captcha_text(&String::from("2WWUA@")));
assert!(validate_captcha_text(&String::from("3G8HT2")));
assert!(validate_captcha_text(&String::from("3J%@X3")));
assert!(validate_captcha_text(&String::from("2GCZ4A")));
assert!(validate_captcha_text(&String::from("3G8HT2")));
assert!(!validate_captcha_text(&String::from("asd823")));
assert!(!validate_captcha_text(&String::from("!PQ4RD")));
assert!(!validate_captcha_text(&String::from("1GQ4XZ")));
assert!(!validate_captcha_text(&String::from("8GO4XZ")));
assert!(!validate_captcha_text(&String::from("IPQ4RD")));
assert!(!validate_captcha_text(&String::from("0PT4RD")));
assert!(!validate_captcha_text(&String::from("APTSRD")));
assert!(!validate_captcha_text(&String::from("AP5TRD")));
assert!(!validate_captcha_text(&String::from("AP6TRD")));
}
/// Prompt the user for text input.
pub fn prompt() -> String {
let mut text = String::new();
let _ = std::io::stdout().flush();
std::io::stdin()
.read_line(&mut text)
.expect("Did not enter a correct string");
return String::from(text.strip_suffix('\n').unwrap());
}
pub fn prompt_captcha_text(captcha_gid: &String) -> String {
println!("Captcha required. Open this link in your web browser: https://steamcommunity.com/public/captcha.php?gid={}", captcha_gid);
let mut captcha_text;
loop {
print!("Enter captcha text: ");
captcha_text = prompt();
if captcha_text.len() > 0 && validate_captcha_text(&captcha_text) {
break;
}
warn!("Invalid chars for captcha text found in user's input. Prompting again...");
}
return captcha_text;
}
/// Returns a tuple of (accepted, denied). Ignored confirmations are not included.
pub fn prompt_confirmation_menu(
confirmations: Vec<Confirmation>,
) -> (Vec<Confirmation>, Vec<Confirmation>) {
println!("press a key other than enter to show the menu.");
let mut to_accept_idx: HashSet<usize> = HashSet::new();
let mut to_deny_idx: HashSet<usize> = HashSet::new();
let mut screen = AlternateScreen::from(std::io::stdout().into_raw_mode().unwrap());
let stdin = std::io::stdin();
let mut selected_idx = 0;
for c in stdin.events() {
match c.expect("could not get events") {
Event::Key(Key::Char('a')) => {
to_accept_idx.insert(selected_idx);
to_deny_idx.remove(&selected_idx);
}
Event::Key(Key::Char('d')) => {
to_accept_idx.remove(&selected_idx);
to_deny_idx.insert(selected_idx);
}
Event::Key(Key::Char('i')) => {
to_accept_idx.remove(&selected_idx);
to_deny_idx.remove(&selected_idx);
}
Event::Key(Key::Char('A')) => {
(0..confirmations.len()).for_each(|i| {
to_accept_idx.insert(i);
to_deny_idx.remove(&i);
});
}
Event::Key(Key::Char('D')) => {
(0..confirmations.len()).for_each(|i| {
to_accept_idx.remove(&i);
to_deny_idx.insert(i);
});
}
Event::Key(Key::Char('I')) => {
(0..confirmations.len()).for_each(|i| {
to_accept_idx.remove(&i);
to_deny_idx.remove(&i);
});
}
Event::Key(Key::Up) if selected_idx > 0 => {
selected_idx -= 1;
}
Event::Key(Key::Down) if selected_idx < confirmations.len() - 1 => {
selected_idx += 1;
}
Event::Key(Key::Char('\n')) => {
break;
}
Event::Key(Key::Esc) | Event::Key(Key::Ctrl('c')) => {
return (vec![], vec![]);
}
_ => {}
}
write!(
screen,
"{}{}{}arrow keys to select, [a]ccept, [d]eny, [i]gnore, [enter] confirm choices\n\n",
termion::clear::All,
termion::cursor::Goto(1, 1),
termion::color::Fg(termion::color::White)
)
.unwrap();
for i in 0..confirmations.len() {
if selected_idx == i {
write!(
screen,
"\r{} >",
termion::color::Fg(termion::color::LightYellow)
)
.unwrap();
} else {
write!(screen, "\r{} ", termion::color::Fg(termion::color::White)).unwrap();
}
if to_accept_idx.contains(&i) {
write!(
screen,
"{}[a]",
termion::color::Fg(termion::color::LightGreen)
)
.unwrap();
} else if to_deny_idx.contains(&i) {
write!(
screen,
"{}[d]",
termion::color::Fg(termion::color::LightRed)
)
.unwrap();
} else {
write!(screen, "[ ]").unwrap();
}
if selected_idx == i {
write!(
screen,
"{}",
termion::color::Fg(termion::color::LightYellow)
)
.unwrap();
}
write!(screen, " {}\n", confirmations[i].description()).unwrap();
}
}
return (
to_accept_idx.iter().map(|i| confirmations[*i]).collect(),
to_deny_idx.iter().map(|i| confirmations[*i]).collect(),
);
}
pub fn pause() {
println!("Press any key to continue...");
let mut stdout = std::io::stdout().into_raw_mode().unwrap();
stdout.flush().unwrap();
std::io::stdin().events().next();
}