From abe808b7bf1e8a55ac193b0aa8ade2c291f938f3 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 12 Jun 2022 12:17:26 -0400 Subject: [PATCH 01/27] upgrade to clap v3, convert global level arguments --- Cargo.lock | 96 +++++++++++++++++++++++++++++++++-------------- Cargo.toml | 4 +- src/main.rs | 105 +++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 151 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9e34bf..e9d684f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,15 +29,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.57" @@ -192,17 +183,41 @@ dependencies = [ [[package]] name = "clap" -version = "2.34.0" +version = "3.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" dependencies = [ - "ansi_term", "atty", "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static 1.4.0", "strsim", + "termcolor", "textwrap", - "unicode-width", - "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", ] [[package]] @@ -578,6 +593,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -938,6 +959,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "os_str_bytes" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1097,6 +1124,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1860,9 +1911,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" @@ -1957,12 +2008,9 @@ checksum = "442f2674e6bd8489052b958e0eaebd89c26eefa3be9dc359d1e2ecccdc510f45" [[package]] name = "textwrap" -version = "0.11.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thin-slice" @@ -2232,12 +2280,6 @@ dependencies = [ "getrandom 0.2.6", ] -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index cdd29e2..791a2fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ [package] name = "steamguard-cli" version = "0.4.5" -authors = ["Carson McManus "] +authors = ["dyc3 (Carson McManus) "] edition = "2018" description = "A command line utility to generate Steam 2FA codes and respond to confirmations." keywords = ["steam", "2fa", "steamguard", "authentication", "cli"] @@ -33,7 +33,7 @@ serde_json = "1.0" rsa = "0.5.0" rand = "0.8.4" standback = "0.2.17" # required to fix a compilation error on a transient dependency -clap = "2.33.3" +clap = { version = "3.1.18", features = ["derive", "cargo"] } log = "0.4.14" stderrlog = "0.4" cookie = "0.14" diff --git a/src/main.rs b/src/main.rs index 92fe153..aa56f2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ extern crate rpassword; -use clap::{crate_version, App, Arg, ArgMatches, Shell}; +use clap::{crate_version, App, Arg, ArgMatches, Parser}; use log::*; use std::str::FromStr; use std::{ @@ -29,7 +29,59 @@ mod encryption; mod errors; mod tui; -fn cli() -> App<'static, 'static> { +#[derive(Debug, Clone, Parser)] +#[clap(author, version, about, long_about = None)] +struct Args { + #[clap(short, long, help = "Steam username, case-sensitive.", long_help = "Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected.")] + username: Option, + #[clap(short, long, help = "Select all accounts in the manifest.")] + all: bool, + /// The path to the maFiles directory. + #[clap(short, long, default_value = "~/.config/steamguard-cli/maFiles", help = "Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json.")] + mafiles_path: String, + #[clap(short, long, help = "Specify your encryption passkey.")] + passkey: Option, + #[clap(short, long, default_value_t=Verbosity::Info, help = "Set the log level.")] + verbosity: Verbosity, +} + +#[derive(Debug, Clone, Copy)] +enum Verbosity { + Error = 0, + Warn = 1, + Info = 2, + Debug = 3, + Trace = 4, +} + +impl std::fmt::Display for Verbosity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", match self { + Verbosity::Error => "error", + Verbosity::Warn => "warn", + Verbosity::Info => "info", + Verbosity::Debug => "debug", + Verbosity::Trace => "trace", + })) + } +} + +impl FromStr for Verbosity { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "error" => Ok(Verbosity::Error), + "warn" => Ok(Verbosity::Warn), + "info" => Ok(Verbosity::Info), + "debug" => Ok(Verbosity::Debug), + "trace" => Ok(Verbosity::Trace), + _ => Err(anyhow!("Invalid verbosity level: {}", s)), + } + } +} + +fn cli() -> App<'static> { App::new("steamguard-cli") .version(crate_version!()) .bin_name("steamguard") @@ -38,7 +90,7 @@ fn cli() -> App<'static, 'static> { .arg( Arg::with_name("username") .long("username") - .short("u") + .short('u') .takes_value(true) .help("Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected.") .conflicts_with("all") @@ -46,7 +98,7 @@ fn cli() -> App<'static, 'static> { .arg( Arg::with_name("all") .long("all") - .short("a") + .short('a') .takes_value(false) .help("Select all accounts in the manifest.") .conflicts_with("username") @@ -54,40 +106,40 @@ fn cli() -> App<'static, 'static> { .arg( Arg::with_name("mafiles-path") .long("mafiles-path") - .short("m") + .short('m') .default_value("~/maFiles") .help("Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json.") ) .arg( Arg::with_name("passkey") .long("passkey") - .short("p") + .short('p') .help("Specify your encryption passkey.") .takes_value(true) ) .arg( Arg::with_name("verbosity") - .short("v") + .short('v') .help("Log what is going on verbosely.") .takes_value(false) .multiple(true) ) - .subcommand( - App::new("completion") - .about("Generate shell completions") - .arg( - Arg::with_name("shell") - .long("shell") - .takes_value(true) - .possible_values(&Shell::variants()) - ) - ) + // .subcommand( + // App::new("completion") + // .about("Generate shell completions") + // .arg( + // Arg::with_name("shell") + // .long("shell") + // .takes_value(true) + // .possible_values(&Shell::variants()) + // ) + // ) .subcommand( App::new("trade") .about("Interactive interface for trade confirmations") .arg( Arg::with_name("accept-all") - .short("a") + .short('a') .long("accept-all") .takes_value(false) .help("Accept all open trade confirmations. Does not open interactive interface.") @@ -144,11 +196,14 @@ fn main() { } fn run() -> anyhow::Result<()> { + let new_args = Args::parse(); + println!("{:?}", new_args); + return Ok(()); + let matches = cli().get_matches(); - let verbosity = matches.occurrences_of("verbosity") as usize + 2; stderrlog::new() - .verbosity(verbosity) + .verbosity(new_args.verbosity as usize) .module(module_path!()) .module("steamguard") .init() @@ -161,11 +216,11 @@ fn run() -> anyhow::Result<()> { return Ok(()); } if let Some(completion_matches) = matches.subcommand_matches("completion") { - cli().gen_completions_to( - "steamguard", - Shell::from_str(completion_matches.value_of("shell").unwrap()).unwrap(), - &mut std::io::stdout(), - ); + // cli().gen_completions_to( + // "steamguard", + // Shell::from_str(completion_matches.value_of("shell").unwrap()).unwrap(), + // &mut std::io::stdout(), + // ); return Ok(()); } From 6ef038efc9462559d53a9d9587d48dc3d9de78ad Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 12 Jun 2022 12:38:54 -0400 Subject: [PATCH 02/27] convert most of the remaining arg definitions --- src/main.rs | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index aa56f2b..6cb445d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,7 @@ mod errors; mod tui; #[derive(Debug, Clone, Parser)] -#[clap(author, version, about, long_about = None)] +#[clap(author, version, about = "Generate Steam 2FA codes and confirm Steam trades from the command line.", long_about = None)] struct Args { #[clap(short, long, help = "Steam username, case-sensitive.", long_help = "Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected.")] username: Option, @@ -43,6 +43,46 @@ struct Args { passkey: Option, #[clap(short, long, default_value_t=Verbosity::Info, help = "Set the log level.")] verbosity: Verbosity, + + #[clap(subcommand)] + sub: Subcommands, +} + +#[derive(Debug, Clone, Parser)] +enum Subcommands { + Debug { + #[clap(long)] + demo_conf_menu: bool + }, + // Completions { + // TODO: Add completions + // }, + #[clap(about = "Interactive interface for trade confirmations")] + Trade { + #[clap(short, long, help = "Accept all open trade confirmations. Does not open interactive interface.")] + accept_all: bool, + #[clap(short, long, help = "If submitting a confirmation response fails, exit immediately.")] + fail_fast: bool, + }, + #[clap(about = "Set up a new account with steamguard-cli")] + Setup { + #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] + username: String, + }, + #[clap(about = "Import an account with steamguard already set up")] + Import { + #[clap(long, help = "Paths to one or more maFiles, eg. \"./gaben.maFile\"")] + files: Vec, + }, + #[clap(about = "Remove the authenticator from an account.")] + Remove { + #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] + username: String, + }, + #[clap(about = "Encrypt all maFiles")] + Encrypt, + #[clap(about = "Decrypt all maFiles")] + Decrypt, } #[derive(Debug, Clone, Copy)] From e6847975ff5e880521dc945e930f406cee5c845a Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 12 Jun 2022 21:46:39 -0400 Subject: [PATCH 03/27] converted some subcommands --- src/main.rs | 86 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6cb445d..3f00b23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ extern crate rpassword; -use clap::{crate_version, App, Arg, ArgMatches, Parser}; +use clap::{crate_version, App, Arg, ArgMatches, Parser, Subcommand}; use log::*; use std::str::FromStr; use std::{ @@ -45,7 +45,7 @@ struct Args { verbosity: Verbosity, #[clap(subcommand)] - sub: Subcommands, + sub: Option, } #[derive(Debug, Clone, Parser)] @@ -67,7 +67,7 @@ enum Subcommands { #[clap(about = "Set up a new account with steamguard-cli")] Setup { #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] - username: String, + username: Option, }, #[clap(about = "Import an account with steamguard already set up")] Import { @@ -238,7 +238,6 @@ fn main() { fn run() -> anyhow::Result<()> { let new_args = Args::parse(); println!("{:?}", new_args); - return Ok(()); let matches = cli().get_matches(); @@ -249,19 +248,24 @@ fn run() -> anyhow::Result<()> { .init() .unwrap(); - if let Some(demo_matches) = matches.subcommand_matches("debug") { - if demo_matches.is_present("demo-conf-menu") { - demos::demo_confirmation_menu(); - } - return Ok(()); - } - if let Some(completion_matches) = matches.subcommand_matches("completion") { - // cli().gen_completions_to( - // "steamguard", - // Shell::from_str(completion_matches.value_of("shell").unwrap()).unwrap(), - // &mut std::io::stdout(), - // ); - return Ok(()); + if let Some(subcmd) = new_args.sub { + match subcmd { + Subcommand::Debug{demo_conf_menu} => { + if demo_conf_menu { + demos::demo_confirmation_menu(); + } + return Ok(()); + }, + // Subcommand::Completions{shell} => { + // // cli().gen_completions_to( + // // "steamguard", + // // Shell::from_str(completion_matches.value_of("shell").unwrap()).unwrap(), + // // &mut std::io::stdout(), + // // ); + // return Ok(()); + // }, + _ => {}, + }; } let mafiles_dir = if matches.occurrences_of("mafiles-path") > 0 { @@ -323,6 +327,15 @@ fn run() -> anyhow::Result<()> { } } + if let Some(subcmd) = new_args.sub { + match subcmd { + Subcommands::Setup{ username } => {}, + s => { + error!("Unknown subcommand: {:?}", s); + }, + } + } + if matches.is_present("setup") { println!("Log in to the account that you want to link to steamguard-cli"); print!("Username: "); @@ -505,6 +518,29 @@ fn run() -> anyhow::Result<()> { .collect::>() ); + if let Some(subcmd) = new_args.sub { + match subcmd { + Subcommands::Trade{ accept_all, fail_fast } => { + todo!() + }, + s => { + error!("Unknown subcommand: {:?}", s); + }, + } + } else { + let server_time = steamapi::get_server_time(); + debug!("Time used to generate codes: {}", server_time); + for account in selected_accounts { + info!( + "Generating code for {}", + account.lock().unwrap().account_name + ); + trace!("{:?}", account); + let code = account.lock().unwrap().generate_code(server_time); + println!("{}", code); + } + } + if let Some(trade_matches) = matches.subcommand_matches("trade") { info!("trade"); for a in selected_accounts.iter_mut() { @@ -640,18 +676,6 @@ fn run() -> anyhow::Result<()> { } manifest.save()?; - } else { - let server_time = steamapi::get_server_time(); - debug!("Time used to generate codes: {}", server_time); - for account in selected_accounts { - info!( - "Generating code for {}", - account.lock().unwrap().account_name - ); - trace!("{:?}", account); - let code = account.lock().unwrap().generate_code(server_time); - println!("{}", code); - } } Ok(()) } @@ -776,3 +800,7 @@ fn get_mafiles_dir() -> String { return paths[0].to_str().unwrap().into(); } + +fn do_subcmd_setup(args: Subcommands) -> anyhow::Result<()> { + +} From b55df7ea2657a527079ad8a47f26e2e2a837da31 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 10:38:59 -0400 Subject: [PATCH 04/27] add subcommand arg types --- src/main.rs | 534 ++++++++++++++++++++++++++++------------------------ 1 file changed, 288 insertions(+), 246 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3f00b23..7fc25ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -121,6 +121,46 @@ impl FromStr for Verbosity { } } +struct ArgsSetup { + username: Option, +} + +impl From for ArgsSetup { + fn from(sub: Subcommands) -> Self { + match sub { + Subcommands::Setup { username } => Self { username }, + _ => panic!("ArgsSetup::from() called with non-Setup subcommand"), + } + } +} + +struct ArgsImport { + files: Vec, +} + +impl From for ArgsImport { + fn from(sub: Subcommands) -> Self { + match sub { + Subcommands::Import { files } => Self { files }, + _ => panic!("ArgsImport::from() called with non-Import subcommand"), + } + } +} + +struct ArgsTrade { + accept_all: bool, + fail_fast: bool, +} + +impl From for ArgsTrade { + fn from(sub: Subcommands) -> Self { + match sub { + Subcommands::Trade { accept_all, fail_fast } => Self { accept_all, fail_fast }, + _ => panic!("ArgsTrade::from() called with non-Trade subcommand"), + } + } +} + fn cli() -> App<'static> { App::new("steamguard-cli") .version(crate_version!()) @@ -329,120 +369,18 @@ fn run() -> anyhow::Result<()> { if let Some(subcmd) = new_args.sub { match subcmd { - Subcommands::Setup{ username } => {}, - s => { - error!("Unknown subcommand: {:?}", s); + Subcommands::Setup{ username } => { + do_subcmd_setup(new_args.sub.unwrap().into(), &mut manifest)?; }, + Subcommands::Import { files } => {todo!()}, + Subcommands::Encrypt {} => {todo!()}, + Subcommands::Decrypt {} => {todo!()}, + _ => {}, } } if matches.is_present("setup") { - println!("Log in to the account that you want to link to steamguard-cli"); - print!("Username: "); - let username = tui::prompt(); - let account_name = username.clone(); - if manifest.account_exists(&username) { - bail!( - "Account {} already exists in manifest, remove it first", - username - ); - } - let session = - do_login_raw(username).expect("Failed to log in. Account has not been linked."); - let mut linker = AccountLinker::new(session); - let account: SteamGuardAccount; - loop { - match linker.link() { - Ok(a) => { - account = a; - break; - } - Err(AccountLinkError::MustRemovePhoneNumber) => { - println!("There is already a phone number on this account, please remove it and try again."); - bail!("There is already a phone number on this account, please remove it and try again."); - } - Err(AccountLinkError::MustProvidePhoneNumber) => { - println!("Enter your phone number in the following format: +1 123-456-7890"); - print!("Phone number: "); - linker.phone_number = tui::prompt().replace(&['(', ')', '-'][..], ""); - } - Err(AccountLinkError::AuthenticatorPresent) => { - println!("An authenticator is already present on this account."); - bail!("An authenticator is already present on this account."); - } - Err(AccountLinkError::MustConfirmEmail) => { - println!("Check your email and click the link."); - tui::pause(); - } - Err(err) => { - error!( - "Failed to link authenticator. Account has not been linked. {}", - err - ); - return Err(err.into()); - } - } - } - manifest.add_account(account); - match manifest.save() { - Ok(_) => {} - Err(err) => { - error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err); - println!( - "Just in case, here is the account info. Save it somewhere just in case!\n{:?}", - manifest.get_account(&account_name).unwrap().lock().unwrap() - ); - return Err(err.into()); - } - } - - let account_arc = manifest.get_account(&account_name).unwrap(); - let mut account = account_arc.lock().unwrap(); - - println!("Authenticator has not yet been linked. Before continuing with finalization, please take the time to write down your revocation code: {}", account.revocation_code); - tui::pause(); - - debug!("attempting link finalization"); - print!("Enter SMS code: "); - let sms_code = tui::prompt(); - let mut tries = 0; - loop { - match linker.finalize(&mut account, sms_code.clone()) { - Ok(_) => break, - Err(FinalizeLinkError::WantMore) => { - debug!("steam wants more 2fa codes (tries: {})", tries); - tries += 1; - if tries >= 30 { - error!("Failed to finalize: unable to generate valid 2fa codes"); - bail!("Failed to finalize: unable to generate valid 2fa codes"); - } - } - Err(err) => { - error!("Failed to finalize: {}", err); - return Err(err.into()); - } - } - } - - println!("Authenticator finalized."); - match manifest.save() { - Ok(_) => {} - Err(err) => { - println!( - "Failed to save manifest, but we were able to save it before. {}", - err - ); - return Err(err); - } - } - - println!( - "Authenticator has been finalized. Please actually write down your revocation code: {}", - account.revocation_code - ); - - return Ok(()); } else if let Some(import_matches) = matches.subcommand_matches("import") { for file_path in import_matches.values_of("files").unwrap() { match manifest.import_account(file_path.into()) { @@ -521,7 +459,139 @@ fn run() -> anyhow::Result<()> { if let Some(subcmd) = new_args.sub { match subcmd { Subcommands::Trade{ accept_all, fail_fast } => { - todo!() + for a in selected_accounts.iter_mut() { + let mut account = a.lock().unwrap(); + + info!("Checking for trade confirmations"); + let confirmations: Vec; + loop { + match account.get_trade_confirmations() { + Ok(confs) => { + confirmations = confs; + break; + } + Err(_) => { + info!("failed to get trade confirmations, asking user to log in"); + do_login(&mut account)?; + } + } + } + + let mut any_failed = false; + if accept_all { + info!("accepting all confirmations"); + for conf in &confirmations { + let result = account.accept_confirmation(conf); + if result.is_err() { + warn!("accept confirmation result: {:?}", result); + any_failed = true; + if fail_fast { + return result; + } + } else { + debug!("accept confirmation result: {:?}", result); + } + } + } else { + if termion::is_tty(&stdout()) { + let (accept, deny) = tui::prompt_confirmation_menu(confirmations); + for conf in &accept { + let result = account.accept_confirmation(conf); + if result.is_err() { + warn!("accept confirmation result: {:?}", result); + any_failed = true; + if fail_fast { + return result; + } + } else { + debug!("accept confirmation result: {:?}", result); + } + } + for conf in &deny { + let result = account.deny_confirmation(conf); + debug!("deny confirmation result: {:?}", result); + if result.is_err() { + warn!("deny confirmation result: {:?}", result); + any_failed = true; + if fail_fast { + return result; + } + } else { + debug!("deny confirmation result: {:?}", result); + } + } + } else { + warn!("not a tty, not showing menu"); + for conf in &confirmations { + println!("{}", conf.description()); + } + } + } + + if any_failed { + error!("Failed to respond to some confirmations."); + } + } + + manifest.save()?; + }, + Subcommands::Remove { username } => { + println!( + "This will remove the mobile authenticator from {} accounts: {}", + selected_accounts.len(), + selected_accounts + .iter() + .map(|a| a.lock().unwrap().account_name.clone()) + .collect::>() + .join(", ") + ); + + match tui::prompt_char("Do you want to continue?", "yN") { + 'y' => {} + _ => { + info!("Aborting!"); + return Err(errors::UserError::Aborted.into()); + } + } + + let mut successful = vec![]; + for a in selected_accounts { + let account = a.lock().unwrap(); + match account.remove_authenticator(None) { + Ok(success) => { + if success { + println!("Removed authenticator from {}", account.account_name); + successful.push(account.account_name.clone()); + } else { + println!( + "Failed to remove authenticator from {}", + account.account_name + ); + match tui::prompt_char( + "Would you like to remove it from the manifest anyway?", + "yN", + ) { + 'y' => { + successful.push(account.account_name.clone()); + } + _ => {} + } + } + } + Err(err) => { + error!( + "Unexpected error when removing authenticator from {}: {}", + account.account_name, err + ); + } + } + } + + for account_name in successful { + manifest.remove_account(account_name); + } + + manifest.save()?; }, s => { error!("Unknown subcommand: {:?}", s); @@ -541,142 +611,6 @@ fn run() -> anyhow::Result<()> { } } - if let Some(trade_matches) = matches.subcommand_matches("trade") { - info!("trade"); - for a in selected_accounts.iter_mut() { - let mut account = a.lock().unwrap(); - - info!("Checking for trade confirmations"); - let confirmations: Vec; - loop { - match account.get_trade_confirmations() { - Ok(confs) => { - confirmations = confs; - break; - } - Err(_) => { - info!("failed to get trade confirmations, asking user to log in"); - do_login(&mut account)?; - } - } - } - - let mut any_failed = false; - let fail_fast = trade_matches.is_present("fail-fast"); - if trade_matches.is_present("accept-all") { - info!("accepting all confirmations"); - for conf in &confirmations { - let result = account.accept_confirmation(conf); - if result.is_err() { - warn!("accept confirmation result: {:?}", result); - any_failed = true; - if fail_fast { - return result; - } - } else { - debug!("accept confirmation result: {:?}", result); - } - } - } else { - if termion::is_tty(&stdout()) { - let (accept, deny) = tui::prompt_confirmation_menu(confirmations); - for conf in &accept { - let result = account.accept_confirmation(conf); - if result.is_err() { - warn!("accept confirmation result: {:?}", result); - any_failed = true; - if fail_fast { - return result; - } - } else { - debug!("accept confirmation result: {:?}", result); - } - } - for conf in &deny { - let result = account.deny_confirmation(conf); - debug!("deny confirmation result: {:?}", result); - if result.is_err() { - warn!("deny confirmation result: {:?}", result); - any_failed = true; - if fail_fast { - return result; - } - } else { - debug!("deny confirmation result: {:?}", result); - } - } - } else { - warn!("not a tty, not showing menu"); - for conf in &confirmations { - println!("{}", conf.description()); - } - } - } - - if any_failed { - error!("Failed to respond to some confirmations."); - } - } - - manifest.save()?; - } else if let Some(_) = matches.subcommand_matches("remove") { - println!( - "This will remove the mobile authenticator from {} accounts: {}", - selected_accounts.len(), - selected_accounts - .iter() - .map(|a| a.lock().unwrap().account_name.clone()) - .collect::>() - .join(", ") - ); - - match tui::prompt_char("Do you want to continue?", "yN") { - 'y' => {} - _ => { - info!("Aborting!"); - return Err(errors::UserError::Aborted.into()); - } - } - - let mut successful = vec![]; - for a in selected_accounts { - let account = a.lock().unwrap(); - match account.remove_authenticator(None) { - Ok(success) => { - if success { - println!("Removed authenticator from {}", account.account_name); - successful.push(account.account_name.clone()); - } else { - println!( - "Failed to remove authenticator from {}", - account.account_name - ); - match tui::prompt_char( - "Would you like to remove it from the manifest anyway?", - "yN", - ) { - 'y' => { - successful.push(account.account_name.clone()); - } - _ => {} - } - } - } - Err(err) => { - error!( - "Unexpected error when removing authenticator from {}: {}", - account.account_name, err - ); - } - } - } - - for account_name in successful { - manifest.remove_account(account_name); - } - - manifest.save()?; - } Ok(()) } @@ -801,6 +735,114 @@ fn get_mafiles_dir() -> String { return paths[0].to_str().unwrap().into(); } -fn do_subcmd_setup(args: Subcommands) -> anyhow::Result<()> { +fn do_subcmd_setup(args: ArgsSetup, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { + println!("Log in to the account that you want to link to steamguard-cli"); + print!("Username: "); + let username = args.username.unwrap_or(tui::prompt()); + if args.username.is_some() { + println!("{}", username); + } + let account_name = username.clone(); + if manifest.account_exists(&username) { + bail!( + "Account {} already exists in manifest, remove it first", + username + ); + } + let session = + do_login_raw(username).expect("Failed to log in. Account has not been linked."); + let mut linker = AccountLinker::new(session); + let account: SteamGuardAccount; + loop { + match linker.link() { + Ok(a) => { + account = a; + break; + } + Err(AccountLinkError::MustRemovePhoneNumber) => { + println!("There is already a phone number on this account, please remove it and try again."); + bail!("There is already a phone number on this account, please remove it and try again."); + } + Err(AccountLinkError::MustProvidePhoneNumber) => { + println!("Enter your phone number in the following format: +1 123-456-7890"); + print!("Phone number: "); + linker.phone_number = tui::prompt().replace(&['(', ')', '-'][..], ""); + } + Err(AccountLinkError::AuthenticatorPresent) => { + println!("An authenticator is already present on this account."); + bail!("An authenticator is already present on this account."); + } + Err(AccountLinkError::MustConfirmEmail) => { + println!("Check your email and click the link."); + tui::pause(); + } + Err(err) => { + error!( + "Failed to link authenticator. Account has not been linked. {}", + err + ); + return Err(err.into()); + } + } + } + manifest.add_account(account); + match manifest.save() { + Ok(_) => {} + Err(err) => { + error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err); + println!( + "Just in case, here is the account info. Save it somewhere just in case!\n{:?}", + manifest.get_account(&account_name).unwrap().lock().unwrap() + ); + return Err(err.into()); + } + } + + let account_arc = manifest.get_account(&account_name).unwrap(); + let mut account = account_arc.lock().unwrap(); + + println!("Authenticator has not yet been linked. Before continuing with finalization, please take the time to write down your revocation code: {}", account.revocation_code); + tui::pause(); + + debug!("attempting link finalization"); + print!("Enter SMS code: "); + let sms_code = tui::prompt(); + let mut tries = 0; + loop { + match linker.finalize(&mut account, sms_code.clone()) { + Ok(_) => break, + Err(FinalizeLinkError::WantMore) => { + debug!("steam wants more 2fa codes (tries: {})", tries); + tries += 1; + if tries >= 30 { + error!("Failed to finalize: unable to generate valid 2fa codes"); + bail!("Failed to finalize: unable to generate valid 2fa codes"); + } + } + Err(err) => { + error!("Failed to finalize: {}", err); + return Err(err.into()); + } + } + } + + println!("Authenticator finalized."); + match manifest.save() { + Ok(_) => {} + Err(err) => { + println!( + "Failed to save manifest, but we were able to save it before. {}", + err + ); + return Err(err); + } + } + + println!( + "Authenticator has been finalized. Please actually write down your revocation code: {}", + account.revocation_code + ); + + return Ok(()); } From 14d49d33b198bcd63e9dd3fe2d31f5325b20f3a3 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 10:48:18 -0400 Subject: [PATCH 05/27] move args definitions to new file --- src/cli.rs | 134 ++++++++++++++++++++++++++++++++++ src/main.rs | 207 ++++++++++------------------------------------------ 2 files changed, 171 insertions(+), 170 deletions(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..be06ae2 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,134 @@ +use std::str::FromStr; +use clap::Parser; + +#[derive(Debug, Clone, Parser)] +#[clap(author, version, about = "Generate Steam 2FA codes and confirm Steam trades from the command line.", long_about = None)] +pub(crate) struct Args { + #[clap(short, long, help = "Steam username, case-sensitive.", long_help = "Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected.")] + pub username: Option, + #[clap(short, long, help = "Select all accounts in the manifest.")] + pub all: bool, + /// The path to the maFiles directory. + #[clap(short, long, default_value = "~/.config/steamguard-cli/maFiles", help = "Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json.")] + pub mafiles_path: String, + #[clap(short, long, help = "Specify your encryption passkey.")] + pub passkey: Option, + #[clap(short, long, default_value_t=Verbosity::Info, help = "Set the log level.")] + pub verbosity: Verbosity, + + #[clap(subcommand)] + pub sub: Option, +} + +#[derive(Debug, Clone, Parser)] +pub(crate) enum Subcommands { + Debug { + #[clap(long)] + demo_conf_menu: bool + }, + // Completions { + // TODO: Add completions + // }, + #[clap(about = "Interactive interface for trade confirmations")] + Trade { + #[clap(short, long, help = "Accept all open trade confirmations. Does not open interactive interface.")] + accept_all: bool, + #[clap(short, long, help = "If submitting a confirmation response fails, exit immediately.")] + fail_fast: bool, + }, + #[clap(about = "Set up a new account with steamguard-cli")] + Setup { + #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] + username: Option, + }, + #[clap(about = "Import an account with steamguard already set up")] + Import { + #[clap(long, help = "Paths to one or more maFiles, eg. \"./gaben.maFile\"")] + files: Vec, + }, + #[clap(about = "Remove the authenticator from an account.")] + Remove { + #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] + username: String, + }, + #[clap(about = "Encrypt all maFiles")] + Encrypt, + #[clap(about = "Decrypt all maFiles")] + Decrypt, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum Verbosity { + Error = 0, + Warn = 1, + Info = 2, + Debug = 3, + Trace = 4, +} + +impl std::fmt::Display for Verbosity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", match self { + Verbosity::Error => "error", + Verbosity::Warn => "warn", + Verbosity::Info => "info", + Verbosity::Debug => "debug", + Verbosity::Trace => "trace", + })) + } +} + +impl FromStr for Verbosity { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "error" => Ok(Verbosity::Error), + "warn" => Ok(Verbosity::Warn), + "info" => Ok(Verbosity::Info), + "debug" => Ok(Verbosity::Debug), + "trace" => Ok(Verbosity::Trace), + _ => Err(anyhow!("Invalid verbosity level: {}", s)), + } + } +} + +pub(crate) struct ArgsSetup { + pub username: Option, +} + +impl From for ArgsSetup { + fn from(sub: Subcommands) -> Self { + match sub { + Subcommands::Setup { username } => Self { username }, + _ => panic!("ArgsSetup::from() called with non-Setup subcommand"), + } + } +} + +pub(crate) struct ArgsImport { + pub files: Vec, +} + +impl From for ArgsImport { + fn from(sub: Subcommands) -> Self { + match sub { + Subcommands::Import { files } => Self { files }, + _ => panic!("ArgsImport::from() called with non-Import subcommand"), + } + } +} + +pub(crate) struct ArgsTrade { + pub accept_all: bool, + pub fail_fast: bool, +} + +impl From for ArgsTrade { + fn from(sub: Subcommands) -> Self { + match sub { + Subcommands::Trade { accept_all, fail_fast } => Self { accept_all, fail_fast }, + _ => panic!("ArgsTrade::from() called with non-Trade subcommand"), + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 7fc25ea..696b506 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ extern crate rpassword; use clap::{crate_version, App, Arg, ArgMatches, Parser, Subcommand}; use log::*; -use std::str::FromStr; use std::{ io::{stdout, Write}, path::Path, @@ -24,143 +23,12 @@ extern crate dirs; extern crate proptest; extern crate ring; mod accountmanager; +mod cli; mod demos; mod encryption; mod errors; mod tui; -#[derive(Debug, Clone, Parser)] -#[clap(author, version, about = "Generate Steam 2FA codes and confirm Steam trades from the command line.", long_about = None)] -struct Args { - #[clap(short, long, help = "Steam username, case-sensitive.", long_help = "Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected.")] - username: Option, - #[clap(short, long, help = "Select all accounts in the manifest.")] - all: bool, - /// The path to the maFiles directory. - #[clap(short, long, default_value = "~/.config/steamguard-cli/maFiles", help = "Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json.")] - mafiles_path: String, - #[clap(short, long, help = "Specify your encryption passkey.")] - passkey: Option, - #[clap(short, long, default_value_t=Verbosity::Info, help = "Set the log level.")] - verbosity: Verbosity, - - #[clap(subcommand)] - sub: Option, -} - -#[derive(Debug, Clone, Parser)] -enum Subcommands { - Debug { - #[clap(long)] - demo_conf_menu: bool - }, - // Completions { - // TODO: Add completions - // }, - #[clap(about = "Interactive interface for trade confirmations")] - Trade { - #[clap(short, long, help = "Accept all open trade confirmations. Does not open interactive interface.")] - accept_all: bool, - #[clap(short, long, help = "If submitting a confirmation response fails, exit immediately.")] - fail_fast: bool, - }, - #[clap(about = "Set up a new account with steamguard-cli")] - Setup { - #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] - username: Option, - }, - #[clap(about = "Import an account with steamguard already set up")] - Import { - #[clap(long, help = "Paths to one or more maFiles, eg. \"./gaben.maFile\"")] - files: Vec, - }, - #[clap(about = "Remove the authenticator from an account.")] - Remove { - #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] - username: String, - }, - #[clap(about = "Encrypt all maFiles")] - Encrypt, - #[clap(about = "Decrypt all maFiles")] - Decrypt, -} - -#[derive(Debug, Clone, Copy)] -enum Verbosity { - Error = 0, - Warn = 1, - Info = 2, - Debug = 3, - Trace = 4, -} - -impl std::fmt::Display for Verbosity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", match self { - Verbosity::Error => "error", - Verbosity::Warn => "warn", - Verbosity::Info => "info", - Verbosity::Debug => "debug", - Verbosity::Trace => "trace", - })) - } -} - -impl FromStr for Verbosity { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "error" => Ok(Verbosity::Error), - "warn" => Ok(Verbosity::Warn), - "info" => Ok(Verbosity::Info), - "debug" => Ok(Verbosity::Debug), - "trace" => Ok(Verbosity::Trace), - _ => Err(anyhow!("Invalid verbosity level: {}", s)), - } - } -} - -struct ArgsSetup { - username: Option, -} - -impl From for ArgsSetup { - fn from(sub: Subcommands) -> Self { - match sub { - Subcommands::Setup { username } => Self { username }, - _ => panic!("ArgsSetup::from() called with non-Setup subcommand"), - } - } -} - -struct ArgsImport { - files: Vec, -} - -impl From for ArgsImport { - fn from(sub: Subcommands) -> Self { - match sub { - Subcommands::Import { files } => Self { files }, - _ => panic!("ArgsImport::from() called with non-Import subcommand"), - } - } -} - -struct ArgsTrade { - accept_all: bool, - fail_fast: bool, -} - -impl From for ArgsTrade { - fn from(sub: Subcommands) -> Self { - match sub { - Subcommands::Trade { accept_all, fail_fast } => Self { accept_all, fail_fast }, - _ => panic!("ArgsTrade::from() called with non-Trade subcommand"), - } - } -} - fn cli() -> App<'static> { App::new("steamguard-cli") .version(crate_version!()) @@ -276,7 +144,7 @@ fn main() { } fn run() -> anyhow::Result<()> { - let new_args = Args::parse(); + let new_args = cli::Args::parse(); println!("{:?}", new_args); let matches = cli().get_matches(); @@ -288,25 +156,23 @@ fn run() -> anyhow::Result<()> { .init() .unwrap(); - if let Some(subcmd) = new_args.sub { - match subcmd { - Subcommand::Debug{demo_conf_menu} => { - if demo_conf_menu { - demos::demo_confirmation_menu(); - } - return Ok(()); - }, - // Subcommand::Completions{shell} => { - // // cli().gen_completions_to( - // // "steamguard", - // // Shell::from_str(completion_matches.value_of("shell").unwrap()).unwrap(), - // // &mut std::io::stdout(), - // // ); - // return Ok(()); - // }, - _ => {}, - }; - } + match new_args.sub { + Some(cli::Subcommands::Debug{demo_conf_menu}) => { + if demo_conf_menu { + demos::demo_confirmation_menu(); + } + return Ok(()); + }, + // Subcommand::Completions{shell} => { + // // cli().gen_completions_to( + // // "steamguard", + // // Shell::from_str(completion_matches.value_of("shell").unwrap()).unwrap(), + // // &mut std::io::stdout(), + // // ); + // return Ok(()); + // }, + _ => {}, + }; let mafiles_dir = if matches.occurrences_of("mafiles-path") > 0 { matches.value_of("mafiles-path").unwrap().into() @@ -367,16 +233,14 @@ fn run() -> anyhow::Result<()> { } } - if let Some(subcmd) = new_args.sub { - match subcmd { - Subcommands::Setup{ username } => { - do_subcmd_setup(new_args.sub.unwrap().into(), &mut manifest)?; - }, - Subcommands::Import { files } => {todo!()}, - Subcommands::Encrypt {} => {todo!()}, - Subcommands::Decrypt {} => {todo!()}, - _ => {}, - } + match new_args.sub { + Some(cli::Subcommands::Setup{ username }) => { + do_subcmd_setup(new_args.sub.unwrap().into(), &mut manifest)?; + }, + Some(cli::Subcommands::Import { files }) => {todo!()}, + Some(cli::Subcommands::Encrypt {}) => {todo!()}, + Some(cli::Subcommands::Decrypt {}) => {todo!()}, + _ => {}, } if matches.is_present("setup") { @@ -458,7 +322,7 @@ fn run() -> anyhow::Result<()> { if let Some(subcmd) = new_args.sub { match subcmd { - Subcommands::Trade{ accept_all, fail_fast } => { + cli::Subcommands::Trade{ accept_all, fail_fast } => { for a in selected_accounts.iter_mut() { let mut account = a.lock().unwrap(); @@ -535,7 +399,7 @@ fn run() -> anyhow::Result<()> { manifest.save()?; }, - Subcommands::Remove { username } => { + cli::Subcommands::Remove { username } => { println!( "This will remove the mobile authenticator from {} accounts: {}", selected_accounts.len(), @@ -735,13 +599,16 @@ fn get_mafiles_dir() -> String { return paths[0].to_str().unwrap().into(); } -fn do_subcmd_setup(args: ArgsSetup, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { +fn do_subcmd_setup(args: cli::ArgsSetup, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { println!("Log in to the account that you want to link to steamguard-cli"); print!("Username: "); - let username = args.username.unwrap_or(tui::prompt()); - if args.username.is_some() { - println!("{}", username); - } + let username = if args.username.is_some() { + let u = args.username.unwrap(); + println!("{}", u); + u + } else { + tui::prompt() + }; let account_name = username.clone(); if manifest.account_exists(&username) { bail!( From 2f4d1e3cfa467ad7bc8ecdfd37f262dc8d3c81a0 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 10:56:52 -0400 Subject: [PATCH 06/27] make it compile again --- src/main.rs | 259 ++++++++++++++++++++++++++-------------------------- 1 file changed, 130 insertions(+), 129 deletions(-) diff --git a/src/main.rs b/src/main.rs index 696b506..6225ee3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -233,9 +233,9 @@ fn run() -> anyhow::Result<()> { } } - match new_args.sub { + match &new_args.sub { Some(cli::Subcommands::Setup{ username }) => { - do_subcmd_setup(new_args.sub.unwrap().into(), &mut manifest)?; + return do_subcmd_setup(new_args.sub.unwrap().into(), &mut manifest); }, Some(cli::Subcommands::Import { files }) => {todo!()}, Some(cli::Subcommands::Encrypt {}) => {todo!()}, @@ -320,158 +320,159 @@ fn run() -> anyhow::Result<()> { .collect::>() ); - if let Some(subcmd) = new_args.sub { - match subcmd { - cli::Subcommands::Trade{ accept_all, fail_fast } => { - for a in selected_accounts.iter_mut() { - let mut account = a.lock().unwrap(); + match new_args.sub.as_ref() { + Some(cli::Subcommands::Trade{ accept_all, fail_fast }) => { + for a in selected_accounts.iter_mut() { + let mut account = a.lock().unwrap(); - info!("Checking for trade confirmations"); - let confirmations: Vec; - loop { - match account.get_trade_confirmations() { - Ok(confs) => { - confirmations = confs; - break; - } - Err(_) => { - info!("failed to get trade confirmations, asking user to log in"); - do_login(&mut account)?; - } + info!("Checking for trade confirmations"); + let confirmations: Vec; + loop { + match account.get_trade_confirmations() { + Ok(confs) => { + confirmations = confs; + break; + } + Err(_) => { + info!("failed to get trade confirmations, asking user to log in"); + do_login(&mut account)?; } } + } - let mut any_failed = false; - if accept_all { - info!("accepting all confirmations"); - for conf in &confirmations { + let mut any_failed = false; + if *accept_all { + info!("accepting all confirmations"); + for conf in &confirmations { + let result = account.accept_confirmation(conf); + if result.is_err() { + warn!("accept confirmation result: {:?}", result); + any_failed = true; + if *fail_fast { + return result; + } + } else { + debug!("accept confirmation result: {:?}", result); + } + } + } else { + if termion::is_tty(&stdout()) { + let (accept, deny) = tui::prompt_confirmation_menu(confirmations); + for conf in &accept { let result = account.accept_confirmation(conf); if result.is_err() { warn!("accept confirmation result: {:?}", result); any_failed = true; - if fail_fast { + if *fail_fast { return result; } } else { debug!("accept confirmation result: {:?}", result); } } - } else { - if termion::is_tty(&stdout()) { - let (accept, deny) = tui::prompt_confirmation_menu(confirmations); - for conf in &accept { - let result = account.accept_confirmation(conf); - if result.is_err() { - warn!("accept confirmation result: {:?}", result); - any_failed = true; - if fail_fast { - return result; - } - } else { - debug!("accept confirmation result: {:?}", result); + for conf in &deny { + let result = account.deny_confirmation(conf); + debug!("deny confirmation result: {:?}", result); + if result.is_err() { + warn!("deny confirmation result: {:?}", result); + any_failed = true; + if *fail_fast { + return result; } - } - for conf in &deny { - let result = account.deny_confirmation(conf); - debug!("deny confirmation result: {:?}", result); - if result.is_err() { - warn!("deny confirmation result: {:?}", result); - any_failed = true; - if fail_fast { - return result; - } - } else { - debug!("deny confirmation result: {:?}", result); - } - } - } else { - warn!("not a tty, not showing menu"); - for conf in &confirmations { - println!("{}", conf.description()); - } - } - } - - if any_failed { - error!("Failed to respond to some confirmations."); - } - } - - manifest.save()?; - }, - cli::Subcommands::Remove { username } => { - println!( - "This will remove the mobile authenticator from {} accounts: {}", - selected_accounts.len(), - selected_accounts - .iter() - .map(|a| a.lock().unwrap().account_name.clone()) - .collect::>() - .join(", ") - ); - - match tui::prompt_char("Do you want to continue?", "yN") { - 'y' => {} - _ => { - info!("Aborting!"); - return Err(errors::UserError::Aborted.into()); - } - } - - let mut successful = vec![]; - for a in selected_accounts { - let account = a.lock().unwrap(); - match account.remove_authenticator(None) { - Ok(success) => { - if success { - println!("Removed authenticator from {}", account.account_name); - successful.push(account.account_name.clone()); } else { - println!( - "Failed to remove authenticator from {}", - account.account_name - ); - match tui::prompt_char( - "Would you like to remove it from the manifest anyway?", - "yN", - ) { - 'y' => { - successful.push(account.account_name.clone()); - } - _ => {} - } + debug!("deny confirmation result: {:?}", result); } } - Err(err) => { - error!( - "Unexpected error when removing authenticator from {}: {}", - account.account_name, err - ); + } else { + warn!("not a tty, not showing menu"); + for conf in &confirmations { + println!("{}", conf.description()); } } } - for account_name in successful { - manifest.remove_account(account_name); + if any_failed { + error!("Failed to respond to some confirmations."); } + } - manifest.save()?; - }, - s => { - error!("Unknown subcommand: {:?}", s); - }, - } - } else { - let server_time = steamapi::get_server_time(); - debug!("Time used to generate codes: {}", server_time); - for account in selected_accounts { - info!( - "Generating code for {}", - account.lock().unwrap().account_name + manifest.save()?; + }, + Some(cli::Subcommands::Remove { username }) => { + println!( + "This will remove the mobile authenticator from {} accounts: {}", + selected_accounts.len(), + selected_accounts + .iter() + .map(|a| a.lock().unwrap().account_name.clone()) + .collect::>() + .join(", ") ); - trace!("{:?}", account); - let code = account.lock().unwrap().generate_code(server_time); - println!("{}", code); + + match tui::prompt_char("Do you want to continue?", "yN") { + 'y' => {} + _ => { + info!("Aborting!"); + return Err(errors::UserError::Aborted.into()); + } + } + + let mut successful = vec![]; + for a in selected_accounts { + let account = a.lock().unwrap(); + match account.remove_authenticator(None) { + Ok(success) => { + if success { + println!("Removed authenticator from {}", account.account_name); + successful.push(account.account_name.clone()); + } else { + println!( + "Failed to remove authenticator from {}", + account.account_name + ); + match tui::prompt_char( + "Would you like to remove it from the manifest anyway?", + "yN", + ) { + 'y' => { + successful.push(account.account_name.clone()); + } + _ => {} + } + } + } + Err(err) => { + error!( + "Unexpected error when removing authenticator from {}: {}", + account.account_name, err + ); + } + } + } + + for account_name in successful { + manifest.remove_account(account_name); + } + + manifest.save()?; + }, + Some(s) => { + error!("Unknown subcommand: {:?}", s); + }, + _ => { + debug!("No subcommand given, assuming user wants a 2fa code"); + + let server_time = steamapi::get_server_time(); + debug!("Time used to generate codes: {}", server_time); + for account in selected_accounts { + info!( + "Generating code for {}", + account.lock().unwrap().account_name + ); + trace!("{:?}", account); + let code = account.lock().unwrap().generate_code(server_time); + println!("{}", code); + } } } From 07b9439c27dfbeed0c635869c3b5fb21ef4e4b90 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 11:14:35 -0400 Subject: [PATCH 07/27] switch to a better setup for subcommands --- src/cli.rs | 95 ++++++++++++++++++++++------------------------------- src/main.rs | 30 ++++++++--------- 2 files changed, 54 insertions(+), 71 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index be06ae2..ad6fecc 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -22,39 +22,16 @@ pub(crate) struct Args { #[derive(Debug, Clone, Parser)] pub(crate) enum Subcommands { - Debug { - #[clap(long)] - demo_conf_menu: bool - }, + Debug(ArgsDebug), // Completions { // TODO: Add completions // }, - #[clap(about = "Interactive interface for trade confirmations")] - Trade { - #[clap(short, long, help = "Accept all open trade confirmations. Does not open interactive interface.")] - accept_all: bool, - #[clap(short, long, help = "If submitting a confirmation response fails, exit immediately.")] - fail_fast: bool, - }, - #[clap(about = "Set up a new account with steamguard-cli")] - Setup { - #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] - username: Option, - }, - #[clap(about = "Import an account with steamguard already set up")] - Import { - #[clap(long, help = "Paths to one or more maFiles, eg. \"./gaben.maFile\"")] - files: Vec, - }, - #[clap(about = "Remove the authenticator from an account.")] - Remove { - #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] - username: String, - }, - #[clap(about = "Encrypt all maFiles")] - Encrypt, - #[clap(about = "Decrypt all maFiles")] - Decrypt, + Setup(ArgsSetup), + Import(ArgsImport), + Trade(ArgsTrade), + Remove(ArgsRemove), + Encrypt(ArgsEncrypt), + Decrypt(ArgsDecrypt), } #[derive(Debug, Clone, Copy)] @@ -93,42 +70,48 @@ impl FromStr for Verbosity { } } + +#[derive(Debug, Clone, Parser)] +#[clap(about="Debug stuff, not useful for most users.")] +pub(crate) struct ArgsDebug { + #[clap(long)] + pub demo_conf_menu: bool +} + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Set up a new account with steamguard-cli")] pub(crate) struct ArgsSetup { + #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] pub username: Option, } -impl From for ArgsSetup { - fn from(sub: Subcommands) -> Self { - match sub { - Subcommands::Setup { username } => Self { username }, - _ => panic!("ArgsSetup::from() called with non-Setup subcommand"), - } - } -} - +#[derive(Debug, Clone, Parser)] +#[clap(about = "Import an account with steamguard already set up")] pub(crate) struct ArgsImport { + #[clap(long, help = "Paths to one or more maFiles, eg. \"./gaben.maFile\"")] pub files: Vec, } -impl From for ArgsImport { - fn from(sub: Subcommands) -> Self { - match sub { - Subcommands::Import { files } => Self { files }, - _ => panic!("ArgsImport::from() called with non-Import subcommand"), - } - } -} - +#[derive(Debug, Clone, Parser)] +#[clap(about = "Interactive interface for trade confirmations")] pub(crate) struct ArgsTrade { + #[clap(short, long, help = "Accept all open trade confirmations. Does not open interactive interface.")] pub accept_all: bool, + #[clap(short, long, help = "If submitting a confirmation response fails, exit immediately.")] pub fail_fast: bool, } -impl From for ArgsTrade { - fn from(sub: Subcommands) -> Self { - match sub { - Subcommands::Trade { accept_all, fail_fast } => Self { accept_all, fail_fast }, - _ => panic!("ArgsTrade::from() called with non-Trade subcommand"), - } - } -} \ No newline at end of file +#[derive(Debug, Clone, Parser)] +#[clap(about = "Remove the authenticator from an account.")] +pub(crate) struct ArgsRemove { + #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] + username: String, +} + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Encrypt all maFiles")] +pub(crate) struct ArgsEncrypt; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Decrypt all maFiles")] +pub(crate) struct ArgsDecrypt; diff --git a/src/main.rs b/src/main.rs index 6225ee3..88ee22b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use steamguard::{ SteamGuardAccount, UserLogin, }; -use crate::accountmanager::ManifestAccountLoadError; +use crate::{accountmanager::ManifestAccountLoadError, cli::Subcommands}; #[macro_use] extern crate lazy_static; @@ -157,8 +157,8 @@ fn run() -> anyhow::Result<()> { .unwrap(); match new_args.sub { - Some(cli::Subcommands::Debug{demo_conf_menu}) => { - if demo_conf_menu { + Some(cli::Subcommands::Debug(args)) => { + if args.demo_conf_menu { demos::demo_confirmation_menu(); } return Ok(()); @@ -233,13 +233,13 @@ fn run() -> anyhow::Result<()> { } } - match &new_args.sub { - Some(cli::Subcommands::Setup{ username }) => { - return do_subcmd_setup(new_args.sub.unwrap().into(), &mut manifest); + match new_args.sub { + Some(cli::Subcommands::Setup(args)) => { + return do_subcmd_setup(args, &mut manifest); }, - Some(cli::Subcommands::Import { files }) => {todo!()}, - Some(cli::Subcommands::Encrypt {}) => {todo!()}, - Some(cli::Subcommands::Decrypt {}) => {todo!()}, + Some(cli::Subcommands::Import(args)) => {todo!()}, + Some(cli::Subcommands::Encrypt(args)) => {todo!()}, + Some(cli::Subcommands::Decrypt(args)) => {todo!()}, _ => {}, } @@ -321,7 +321,7 @@ fn run() -> anyhow::Result<()> { ); match new_args.sub.as_ref() { - Some(cli::Subcommands::Trade{ accept_all, fail_fast }) => { + Some(cli::Subcommands::Trade(args)) => { for a in selected_accounts.iter_mut() { let mut account = a.lock().unwrap(); @@ -341,14 +341,14 @@ fn run() -> anyhow::Result<()> { } let mut any_failed = false; - if *accept_all { + if args.accept_all { info!("accepting all confirmations"); for conf in &confirmations { let result = account.accept_confirmation(conf); if result.is_err() { warn!("accept confirmation result: {:?}", result); any_failed = true; - if *fail_fast { + if args.fail_fast { return result; } } else { @@ -363,7 +363,7 @@ fn run() -> anyhow::Result<()> { if result.is_err() { warn!("accept confirmation result: {:?}", result); any_failed = true; - if *fail_fast { + if args.fail_fast { return result; } } else { @@ -376,7 +376,7 @@ fn run() -> anyhow::Result<()> { if result.is_err() { warn!("deny confirmation result: {:?}", result); any_failed = true; - if *fail_fast { + if args.fail_fast { return result; } } else { @@ -398,7 +398,7 @@ fn run() -> anyhow::Result<()> { manifest.save()?; }, - Some(cli::Subcommands::Remove { username }) => { + Some(cli::Subcommands::Remove(args)) => { println!( "This will remove the mobile authenticator from {} accounts: {}", selected_accounts.len(), From b04008803889734613339685ae6edc640275a874 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 11:26:18 -0400 Subject: [PATCH 08/27] swtich mafiles path to new args --- src/cli.rs | 4 ++-- src/main.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index ad6fecc..ceb46f0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,8 +9,8 @@ pub(crate) struct Args { #[clap(short, long, help = "Select all accounts in the manifest.")] pub all: bool, /// The path to the maFiles directory. - #[clap(short, long, default_value = "~/.config/steamguard-cli/maFiles", help = "Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json.")] - pub mafiles_path: String, + #[clap(short, long, help = "Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json. Default: ~/.config/steamguard-cli/maFiles")] + pub mafiles_path: Option, #[clap(short, long, help = "Specify your encryption passkey.")] pub passkey: Option, #[clap(short, long, default_value_t=Verbosity::Info, help = "Set the log level.")] diff --git a/src/main.rs b/src/main.rs index 88ee22b..a86e1f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -174,8 +174,8 @@ fn run() -> anyhow::Result<()> { _ => {}, }; - let mafiles_dir = if matches.occurrences_of("mafiles-path") > 0 { - matches.value_of("mafiles-path").unwrap().into() + let mafiles_dir = if let Some(mafiles_path) = new_args.mafiles_path { + mafiles_path } else { get_mafiles_dir() }; @@ -202,7 +202,7 @@ fn run() -> anyhow::Result<()> { manifest = accountmanager::Manifest::load(path.as_path())?; } - let mut passkey: Option = matches.value_of("passkey").map(|s| s.into()); + let mut passkey: Option = new_args.passkey; manifest.submit_passkey(passkey); loop { From 9f7c8fef6844e5ae4b084766a6ea13d8fa6d5e7e Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 11:31:56 -0400 Subject: [PATCH 09/27] remove old verbosity arg --- src/main.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index a86e1f4..09ec4c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,13 +65,6 @@ fn cli() -> App<'static> { .help("Specify your encryption passkey.") .takes_value(true) ) - .arg( - Arg::with_name("verbosity") - .short('v') - .help("Log what is going on verbosely.") - .takes_value(false) - .multiple(true) - ) // .subcommand( // App::new("completion") // .about("Generate shell completions") From 138f36f562068239d1f672a3f8d15cfeece2de5f Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 11:32:04 -0400 Subject: [PATCH 10/27] move help text --- src/cli.rs | 2 +- src/main.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index ceb46f0..4bcf2a2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -74,7 +74,7 @@ impl FromStr for Verbosity { #[derive(Debug, Clone, Parser)] #[clap(about="Debug stuff, not useful for most users.")] pub(crate) struct ArgsDebug { - #[clap(long)] + #[clap(long, help = "Show an example confirmation menu using dummy data.")] pub demo_conf_menu: bool } diff --git a/src/main.rs b/src/main.rs index 09ec4c7..48f136a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,7 +120,6 @@ fn cli() -> App<'static> { App::new("debug") .arg( Arg::with_name("demo-conf-menu") - .help("Show an example confirmation menu using dummy data.") .takes_value(false) ) ) From 37539f7679fffcc022a692bab6f5ddaf3c03fa7e Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 11:40:20 -0400 Subject: [PATCH 11/27] move encrypt and decrypt subcommand impls to functions --- src/main.rs | 71 +++++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/src/main.rs b/src/main.rs index 48f136a..78e96a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -230,14 +230,16 @@ fn run() -> anyhow::Result<()> { return do_subcmd_setup(args, &mut manifest); }, Some(cli::Subcommands::Import(args)) => {todo!()}, - Some(cli::Subcommands::Encrypt(args)) => {todo!()}, - Some(cli::Subcommands::Decrypt(args)) => {todo!()}, + Some(cli::Subcommands::Encrypt(args)) => { + return do_subcmd_encrypt(args, &mut manifest); + }, + Some(cli::Subcommands::Decrypt(args)) => { + return do_subcmd_decrypt(args, &mut manifest); + }, _ => {}, } - if matches.is_present("setup") { - - } else if let Some(import_matches) = matches.subcommand_matches("import") { + if let Some(import_matches) = matches.subcommand_matches("import") { for file_path in import_matches.values_of("files").unwrap() { match manifest.import_account(file_path.into()) { Ok(_) => { @@ -249,33 +251,6 @@ fn run() -> anyhow::Result<()> { } } - manifest.save()?; - return Ok(()); - } else if matches.is_present("encrypt") { - if !manifest.has_passkey() { - loop { - passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok(); - let passkey_confirm = - rpassword::prompt_password_stdout("Confirm encryption passkey: ").ok(); - if passkey == passkey_confirm { - break; - } - error!("Passkeys do not match, try again."); - } - manifest.submit_passkey(passkey); - } - manifest.load_accounts()?; - for entry in &mut manifest.entries { - entry.encryption = Some(accountmanager::EntryEncryptionParams::generate()); - } - manifest.save()?; - return Ok(()); - } else if matches.is_present("decrypt") { - manifest.load_accounts()?; - for entry in &mut manifest.entries { - entry.encryption = None; - } - manifest.submit_passkey(None); manifest.save()?; return Ok(()); } @@ -706,3 +681,35 @@ fn do_subcmd_setup(args: cli::ArgsSetup, manifest: &mut accountmanager::Manifest return Ok(()); } + +fn do_subcmd_encrypt(args: cli::ArgsEncrypt, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { + if !manifest.has_passkey() { + let mut passkey; + loop { + passkey = rpassword::prompt_password_stdout("Enter encryption passkey: ").ok(); + let passkey_confirm = + rpassword::prompt_password_stdout("Confirm encryption passkey: ").ok(); + if passkey == passkey_confirm { + break; + } + error!("Passkeys do not match, try again."); + } + manifest.submit_passkey(passkey); + } + manifest.load_accounts()?; + for entry in &mut manifest.entries { + entry.encryption = Some(accountmanager::EntryEncryptionParams::generate()); + } + manifest.save()?; + return Ok(()); +} + +fn do_subcmd_decrypt(args: cli::ArgsDecrypt, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { + manifest.load_accounts()?; + for entry in &mut manifest.entries { + entry.encryption = None; + } + manifest.submit_passkey(None); + manifest.save()?; + return Ok(()); +} From 73281ef069505687dd670e591c5b5a79ed987324 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 11:43:37 -0400 Subject: [PATCH 12/27] move import subcommand impl --- src/accountmanager.rs | 6 +++--- src/main.rs | 36 +++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/accountmanager.rs b/src/accountmanager.rs index 2f82556..73dfb60 100644 --- a/src/accountmanager.rs +++ b/src/accountmanager.rs @@ -180,8 +180,8 @@ impl Manifest { .insert(account.account_name.clone(), Arc::new(Mutex::new(account))); } - pub fn import_account(&mut self, import_path: String) -> anyhow::Result<()> { - let path = Path::new(&import_path); + pub fn import_account(&mut self, import_path: &String) -> anyhow::Result<()> { + let path = Path::new(import_path); ensure!(path.exists(), "{} does not exist.", import_path); ensure!(path.is_file(), "{} is not a file.", import_path); @@ -576,7 +576,7 @@ mod tests { let mut loaded_manifest = Manifest::new(manifest_path.as_path()); assert!(matches!( loaded_manifest.import_account( - tmp_dir + &tmp_dir .path() .join("asdf1234.maFile") .into_os_string() diff --git a/src/main.rs b/src/main.rs index 78e96a1..d5a632f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -229,7 +229,9 @@ fn run() -> anyhow::Result<()> { Some(cli::Subcommands::Setup(args)) => { return do_subcmd_setup(args, &mut manifest); }, - Some(cli::Subcommands::Import(args)) => {todo!()}, + Some(cli::Subcommands::Import(args)) => { + return do_subcmd_import(args, &mut manifest); + }, Some(cli::Subcommands::Encrypt(args)) => { return do_subcmd_encrypt(args, &mut manifest); }, @@ -239,22 +241,6 @@ fn run() -> anyhow::Result<()> { _ => {}, } - if let Some(import_matches) = matches.subcommand_matches("import") { - for file_path in import_matches.values_of("files").unwrap() { - match manifest.import_account(file_path.into()) { - Ok(_) => { - info!("Imported account: {}", file_path); - } - Err(err) => { - bail!("Failed to import account: {} {}", file_path, err); - } - } - } - - manifest.save()?; - return Ok(()); - } - let mut selected_accounts: Vec>>; loop { match get_selected_accounts(&matches, &mut manifest) { @@ -682,6 +668,22 @@ fn do_subcmd_setup(args: cli::ArgsSetup, manifest: &mut accountmanager::Manifest return Ok(()); } +fn do_subcmd_import(args: cli::ArgsImport, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { + for file_path in args.files { + match manifest.import_account(&file_path) { + Ok(_) => { + info!("Imported account: {}", &file_path); + } + Err(err) => { + bail!("Failed to import account: {} {}", &file_path, err); + } + } + } + + manifest.save()?; + return Ok(()); +} + fn do_subcmd_encrypt(args: cli::ArgsEncrypt, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { if !manifest.has_passkey() { let mut passkey; From 31c08ac206ead3931119fb153c6fbe6223c62531 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 11:54:16 -0400 Subject: [PATCH 13/27] move trade and remove subcommand impls --- src/cli.rs | 5 +- src/main.rs | 274 +++++++++++++++++++++++++++------------------------- 2 files changed, 143 insertions(+), 136 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 4bcf2a2..51f7590 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -103,10 +103,7 @@ pub(crate) struct ArgsTrade { #[derive(Debug, Clone, Parser)] #[clap(about = "Remove the authenticator from an account.")] -pub(crate) struct ArgsRemove { - #[clap(short, long, from_global, help = "Steam username, case-sensitive.")] - username: String, -} +pub(crate) struct ArgsRemove; #[derive(Debug, Clone, Parser)] #[clap(about = "Encrypt all maFiles")] diff --git a/src/main.rs b/src/main.rs index d5a632f..b262468 100644 --- a/src/main.rs +++ b/src/main.rs @@ -273,141 +273,12 @@ fn run() -> anyhow::Result<()> { .collect::>() ); - match new_args.sub.as_ref() { + match new_args.sub { Some(cli::Subcommands::Trade(args)) => { - for a in selected_accounts.iter_mut() { - let mut account = a.lock().unwrap(); - - info!("Checking for trade confirmations"); - let confirmations: Vec; - loop { - match account.get_trade_confirmations() { - Ok(confs) => { - confirmations = confs; - break; - } - Err(_) => { - info!("failed to get trade confirmations, asking user to log in"); - do_login(&mut account)?; - } - } - } - - let mut any_failed = false; - if args.accept_all { - info!("accepting all confirmations"); - for conf in &confirmations { - let result = account.accept_confirmation(conf); - if result.is_err() { - warn!("accept confirmation result: {:?}", result); - any_failed = true; - if args.fail_fast { - return result; - } - } else { - debug!("accept confirmation result: {:?}", result); - } - } - } else { - if termion::is_tty(&stdout()) { - let (accept, deny) = tui::prompt_confirmation_menu(confirmations); - for conf in &accept { - let result = account.accept_confirmation(conf); - if result.is_err() { - warn!("accept confirmation result: {:?}", result); - any_failed = true; - if args.fail_fast { - return result; - } - } else { - debug!("accept confirmation result: {:?}", result); - } - } - for conf in &deny { - let result = account.deny_confirmation(conf); - debug!("deny confirmation result: {:?}", result); - if result.is_err() { - warn!("deny confirmation result: {:?}", result); - any_failed = true; - if args.fail_fast { - return result; - } - } else { - debug!("deny confirmation result: {:?}", result); - } - } - } else { - warn!("not a tty, not showing menu"); - for conf in &confirmations { - println!("{}", conf.description()); - } - } - } - - if any_failed { - error!("Failed to respond to some confirmations."); - } - } - - manifest.save()?; + return do_subcmd_trade(args, &mut manifest, selected_accounts); }, Some(cli::Subcommands::Remove(args)) => { - println!( - "This will remove the mobile authenticator from {} accounts: {}", - selected_accounts.len(), - selected_accounts - .iter() - .map(|a| a.lock().unwrap().account_name.clone()) - .collect::>() - .join(", ") - ); - - match tui::prompt_char("Do you want to continue?", "yN") { - 'y' => {} - _ => { - info!("Aborting!"); - return Err(errors::UserError::Aborted.into()); - } - } - - let mut successful = vec![]; - for a in selected_accounts { - let account = a.lock().unwrap(); - match account.remove_authenticator(None) { - Ok(success) => { - if success { - println!("Removed authenticator from {}", account.account_name); - successful.push(account.account_name.clone()); - } else { - println!( - "Failed to remove authenticator from {}", - account.account_name - ); - match tui::prompt_char( - "Would you like to remove it from the manifest anyway?", - "yN", - ) { - 'y' => { - successful.push(account.account_name.clone()); - } - _ => {} - } - } - } - Err(err) => { - error!( - "Unexpected error when removing authenticator from {}: {}", - account.account_name, err - ); - } - } - } - - for account_name in successful { - manifest.remove_account(account_name); - } - - manifest.save()?; + return do_subcmd_remove(args, &mut manifest, selected_accounts); }, Some(s) => { error!("Unknown subcommand: {:?}", s); @@ -684,6 +555,145 @@ fn do_subcmd_import(args: cli::ArgsImport, manifest: &mut accountmanager::Manife return Ok(()); } +fn do_subcmd_trade(args: cli::ArgsTrade, manifest: &mut accountmanager::Manifest, mut selected_accounts: Vec>>) -> anyhow::Result<()> { + for a in selected_accounts.iter_mut() { + let mut account = a.lock().unwrap(); + + info!("Checking for trade confirmations"); + let confirmations: Vec; + loop { + match account.get_trade_confirmations() { + Ok(confs) => { + confirmations = confs; + break; + } + Err(_) => { + info!("failed to get trade confirmations, asking user to log in"); + do_login(&mut account)?; + } + } + } + + let mut any_failed = false; + if args.accept_all { + info!("accepting all confirmations"); + for conf in &confirmations { + let result = account.accept_confirmation(conf); + if result.is_err() { + warn!("accept confirmation result: {:?}", result); + any_failed = true; + if args.fail_fast { + return result; + } + } else { + debug!("accept confirmation result: {:?}", result); + } + } + } else { + if termion::is_tty(&stdout()) { + let (accept, deny) = tui::prompt_confirmation_menu(confirmations); + for conf in &accept { + let result = account.accept_confirmation(conf); + if result.is_err() { + warn!("accept confirmation result: {:?}", result); + any_failed = true; + if args.fail_fast { + return result; + } + } else { + debug!("accept confirmation result: {:?}", result); + } + } + for conf in &deny { + let result = account.deny_confirmation(conf); + debug!("deny confirmation result: {:?}", result); + if result.is_err() { + warn!("deny confirmation result: {:?}", result); + any_failed = true; + if args.fail_fast { + return result; + } + } else { + debug!("deny confirmation result: {:?}", result); + } + } + } else { + warn!("not a tty, not showing menu"); + for conf in &confirmations { + println!("{}", conf.description()); + } + } + } + + if any_failed { + error!("Failed to respond to some confirmations."); + } + } + + manifest.save()?; + return Ok(()); +} + +fn do_subcmd_remove(args: cli::ArgsRemove, manifest: &mut accountmanager::Manifest, selected_accounts: Vec>>) -> anyhow::Result<()> { + println!( + "This will remove the mobile authenticator from {} accounts: {}", + selected_accounts.len(), + selected_accounts + .iter() + .map(|a| a.lock().unwrap().account_name.clone()) + .collect::>() + .join(", ") + ); + + match tui::prompt_char("Do you want to continue?", "yN") { + 'y' => {} + _ => { + info!("Aborting!"); + return Err(errors::UserError::Aborted.into()); + } + } + + let mut successful = vec![]; + for a in selected_accounts { + let account = a.lock().unwrap(); + match account.remove_authenticator(None) { + Ok(success) => { + if success { + println!("Removed authenticator from {}", account.account_name); + successful.push(account.account_name.clone()); + } else { + println!( + "Failed to remove authenticator from {}", + account.account_name + ); + match tui::prompt_char( + "Would you like to remove it from the manifest anyway?", + "yN", + ) { + 'y' => { + successful.push(account.account_name.clone()); + } + _ => {} + } + } + } + Err(err) => { + error!( + "Unexpected error when removing authenticator from {}: {}", + account.account_name, err + ); + } + } + } + + for account_name in successful { + manifest.remove_account(account_name); + } + + manifest.save()?; + return Ok(()); +} + fn do_subcmd_encrypt(args: cli::ArgsEncrypt, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { if !manifest.has_passkey() { let mut passkey; From aaded51d38d75f4d60361274a666e7917a357867 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 11:54:42 -0400 Subject: [PATCH 14/27] move generate code subcommand impl --- src/main.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index b262468..a6c64d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -285,18 +285,7 @@ fn run() -> anyhow::Result<()> { }, _ => { debug!("No subcommand given, assuming user wants a 2fa code"); - - let server_time = steamapi::get_server_time(); - debug!("Time used to generate codes: {}", server_time); - for account in selected_accounts { - info!( - "Generating code for {}", - account.lock().unwrap().account_name - ); - trace!("{:?}", account); - let code = account.lock().unwrap().generate_code(server_time); - println!("{}", code); - } + return do_subcmd_code(selected_accounts); } } @@ -725,3 +714,18 @@ fn do_subcmd_decrypt(args: cli::ArgsDecrypt, manifest: &mut accountmanager::Mani manifest.save()?; return Ok(()); } + +fn do_subcmd_code(selected_accounts: Vec>>) -> anyhow::Result<()> { + let server_time = steamapi::get_server_time(); + debug!("Time used to generate codes: {}", server_time); + for account in selected_accounts { + info!( + "Generating code for {}", + account.lock().unwrap().account_name + ); + trace!("{:?}", account); + let code = account.lock().unwrap().generate_code(server_time); + println!("{}", code); + } + return Ok(()); +} From 8dd10c9b29fc0e3d204c39a75cf02d394186b4ca Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 12:16:20 -0400 Subject: [PATCH 15/27] move debug subcommand impl --- src/main.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index a6c64d5..8de98f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -150,10 +150,7 @@ fn run() -> anyhow::Result<()> { match new_args.sub { Some(cli::Subcommands::Debug(args)) => { - if args.demo_conf_menu { - demos::demo_confirmation_menu(); - } - return Ok(()); + return do_subcmd_debug(args); }, // Subcommand::Completions{shell} => { // // cli().gen_completions_to( @@ -413,6 +410,13 @@ fn get_mafiles_dir() -> String { return paths[0].to_str().unwrap().into(); } +fn do_subcmd_debug(args: cli::ArgsDebug) -> anyhow::Result<()> { + if args.demo_conf_menu { + demos::demo_confirmation_menu(); + } + return Ok(()); +} + fn do_subcmd_setup(args: cli::ArgsSetup, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { println!("Log in to the account that you want to link to steamguard-cli"); print!("Username: "); From 4a8f7a74d20d3e66d61119b4624cd97d28924f49 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 12:37:40 -0400 Subject: [PATCH 16/27] reimplement completion subcommand --- Cargo.lock | 26 ++++++++++++++++++-------- Cargo.toml | 1 + src/cli.rs | 12 +++++++++--- src/main.rs | 40 ++++++++++++++++++++-------------------- 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9d684f..2ff717f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,26 +183,35 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.18" +version = "3.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static 1.4.0", + "once_cell", "strsim", "termcolor", "textwrap", ] [[package]] -name = "clap_derive" -version = "3.1.18" +name = "clap_complete" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "0f6ebaab5f25e4f0312dfa07cb30a755204b96e6531457c2cfdecfdf5f2adf40" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "3.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9" dependencies = [ "heck", "proc-macro-error", @@ -213,9 +222,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" dependencies = [ "os_str_bytes", ] @@ -1859,6 +1868,7 @@ dependencies = [ "base64", "block-modes", "clap", + "clap_complete", "cookie 0.14.4", "dirs", "hmac-sha1", diff --git a/Cargo.toml b/Cargo.toml index 791a2fb..caf1ed4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ rsa = "0.5.0" rand = "0.8.4" standback = "0.2.17" # required to fix a compilation error on a transient dependency clap = { version = "3.1.18", features = ["derive", "cargo"] } +clap_complete = "3.2.1" log = "0.4.14" stderrlog = "0.4" cookie = "0.14" diff --git a/src/cli.rs b/src/cli.rs index 51f7590..d085681 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,6 @@ use std::str::FromStr; use clap::Parser; +use clap_complete::Shell; #[derive(Debug, Clone, Parser)] #[clap(author, version, about = "Generate Steam 2FA codes and confirm Steam trades from the command line.", long_about = None)] @@ -23,9 +24,7 @@ pub(crate) struct Args { #[derive(Debug, Clone, Parser)] pub(crate) enum Subcommands { Debug(ArgsDebug), - // Completions { - // TODO: Add completions - // }, + Completion(ArgsCompletions), Setup(ArgsSetup), Import(ArgsImport), Trade(ArgsTrade), @@ -78,6 +77,13 @@ pub(crate) struct ArgsDebug { pub demo_conf_menu: bool } +#[derive(Debug, Clone, Parser)] +#[clap(about="Generate shell completions")] +pub(crate) struct ArgsCompletions { + #[clap(arg_enum, help = "The shell to generate completions for.")] + pub shell: Shell, +} + #[derive(Debug, Clone, Parser)] #[clap(about = "Set up a new account with steamguard-cli")] pub(crate) struct ArgsSetup { diff --git a/src/main.rs b/src/main.rs index 8de98f1..f3c60d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ extern crate rpassword; -use clap::{crate_version, App, Arg, ArgMatches, Parser, Subcommand}; +use clap::{crate_version, App, Arg, ArgMatches, Parser, IntoApp}; use log::*; use std::{ io::{stdout, Write}, @@ -11,7 +11,7 @@ use steamguard::{ SteamGuardAccount, UserLogin, }; -use crate::{accountmanager::ManifestAccountLoadError, cli::Subcommands}; +use crate::{accountmanager::ManifestAccountLoadError}; #[macro_use] extern crate lazy_static; @@ -65,16 +65,15 @@ fn cli() -> App<'static> { .help("Specify your encryption passkey.") .takes_value(true) ) - // .subcommand( - // App::new("completion") - // .about("Generate shell completions") - // .arg( - // Arg::with_name("shell") - // .long("shell") - // .takes_value(true) - // .possible_values(&Shell::variants()) - // ) - // ) + .subcommand( + App::new("completion") + .about("Generate shell completions") + .arg( + Arg::with_name("shell") + .long("shell") + .takes_value(true) + ) + ) .subcommand( App::new("trade") .about("Interactive interface for trade confirmations") @@ -152,14 +151,9 @@ fn run() -> anyhow::Result<()> { Some(cli::Subcommands::Debug(args)) => { return do_subcmd_debug(args); }, - // Subcommand::Completions{shell} => { - // // cli().gen_completions_to( - // // "steamguard", - // // Shell::from_str(completion_matches.value_of("shell").unwrap()).unwrap(), - // // &mut std::io::stdout(), - // // ); - // return Ok(()); - // }, + Some(cli::Subcommands::Completion(args)) => { + return do_subcmd_completion(args); + }, _ => {}, }; @@ -417,6 +411,12 @@ fn do_subcmd_debug(args: cli::ArgsDebug) -> anyhow::Result<()> { return Ok(()); } +fn do_subcmd_completion(args: cli::ArgsCompletions) -> Result<(), anyhow::Error> { + let mut app = cli::Args::command_for_update(); + clap_complete::generate(args.shell, &mut app, "steamguard", &mut std::io::stdout()); + return Ok(()); +} + fn do_subcmd_setup(args: cli::ArgsSetup, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { println!("Log in to the account that you want to link to steamguard-cli"); print!("Username: "); From 221e64ab8c9924caa033f15609b249017f1608ea Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 12:38:47 -0400 Subject: [PATCH 17/27] change debug line --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index f3c60d1..bbdddbb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -136,7 +136,7 @@ fn main() { fn run() -> anyhow::Result<()> { let new_args = cli::Args::parse(); - println!("{:?}", new_args); + info!("{:?}", new_args); let matches = cli().get_matches(); From 1b7c089fa1a73ae8e30ed96f5906d0f6adaa51fa Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 12:39:33 -0400 Subject: [PATCH 18/27] fix arg parsing for shell arg --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index d085681..23620e0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -80,7 +80,7 @@ pub(crate) struct ArgsDebug { #[derive(Debug, Clone, Parser)] #[clap(about="Generate shell completions")] pub(crate) struct ArgsCompletions { - #[clap(arg_enum, help = "The shell to generate completions for.")] + #[clap(short, long, arg_enum, help = "The shell to generate completions for.")] pub shell: Shell, } From e653ac684f3435107e9541d9c3916663346db470 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 12:42:36 -0400 Subject: [PATCH 19/27] set name and bin_name metadata --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 23620e0..6be5343 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,7 +3,7 @@ use clap::Parser; use clap_complete::Shell; #[derive(Debug, Clone, Parser)] -#[clap(author, version, about = "Generate Steam 2FA codes and confirm Steam trades from the command line.", long_about = None)] +#[clap(name="steamguard-cli", bin_name="steamguard", author, version, about = "Generate Steam 2FA codes and confirm Steam trades from the command line.", long_about = None)] pub(crate) struct Args { #[clap(short, long, help = "Steam username, case-sensitive.", long_help = "Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected.")] pub username: Option, From ed9cf91cfbabfb609de40524f505434b1f658309 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 12:45:33 -0400 Subject: [PATCH 20/27] return better error for unimplemented subcommand --- src/errors.rs | 2 ++ src/main.rs | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 0def6f0..b61bdb0 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -4,4 +4,6 @@ use thiserror::Error; pub(crate) enum UserError { #[error("User aborted the operation.")] Aborted, + #[error("Unknown subcommand. It may need to be implemented.")] + UnknownSubcommand, } diff --git a/src/main.rs b/src/main.rs index bbdddbb..693e156 100644 --- a/src/main.rs +++ b/src/main.rs @@ -273,14 +273,13 @@ fn run() -> anyhow::Result<()> { }, Some(s) => { error!("Unknown subcommand: {:?}", s); + return Err(errors::UserError::UnknownSubcommand.into()); }, _ => { debug!("No subcommand given, assuming user wants a 2fa code"); return do_subcmd_code(selected_accounts); } } - - Ok(()) } fn get_selected_accounts( From 5e3b2507ab7158cc720c700db56833031ae4ded8 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 12:47:07 -0400 Subject: [PATCH 21/27] add arg_enum marker for verbosity arg --- src/cli.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 6be5343..f4d52a2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,5 @@ use std::str::FromStr; -use clap::Parser; +use clap::{Parser, clap_derive::ArgEnum}; use clap_complete::Shell; #[derive(Debug, Clone, Parser)] @@ -14,7 +14,7 @@ pub(crate) struct Args { pub mafiles_path: Option, #[clap(short, long, help = "Specify your encryption passkey.")] pub passkey: Option, - #[clap(short, long, default_value_t=Verbosity::Info, help = "Set the log level.")] + #[clap(short, long, arg_enum, default_value_t=Verbosity::Info, help = "Set the log level.")] pub verbosity: Verbosity, #[clap(subcommand)] @@ -33,7 +33,7 @@ pub(crate) enum Subcommands { Decrypt(ArgsDecrypt), } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, ArgEnum)] pub(crate) enum Verbosity { Error = 0, Warn = 1, From 174456226b13ba9cb95aae6d042a44f9e00da243 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 12:48:07 -0400 Subject: [PATCH 22/27] improve help test for verbosity --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index f4d52a2..984b259 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -14,7 +14,7 @@ pub(crate) struct Args { pub mafiles_path: Option, #[clap(short, long, help = "Specify your encryption passkey.")] pub passkey: Option, - #[clap(short, long, arg_enum, default_value_t=Verbosity::Info, help = "Set the log level.")] + #[clap(short, long, arg_enum, default_value_t=Verbosity::Info, help = "Set the log level. Be warned, trace is capable of printing sensitive data.")] pub verbosity: Verbosity, #[clap(subcommand)] From 2fc13d40e21926c988b25f753f44bd10385056e5 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 12:57:54 -0400 Subject: [PATCH 23/27] switch get_selected_accounts to new args --- src/main.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 693e156..72a039f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -157,8 +157,8 @@ fn run() -> anyhow::Result<()> { _ => {}, }; - let mafiles_dir = if let Some(mafiles_path) = new_args.mafiles_path { - mafiles_path + let mafiles_dir = if let Some(mafiles_path) = &new_args.mafiles_path { + mafiles_path.clone() } else { get_mafiles_dir() }; @@ -185,7 +185,7 @@ fn run() -> anyhow::Result<()> { manifest = accountmanager::Manifest::load(path.as_path())?; } - let mut passkey: Option = new_args.passkey; + let mut passkey: Option = new_args.passkey.clone(); manifest.submit_passkey(passkey); loop { @@ -234,7 +234,7 @@ fn run() -> anyhow::Result<()> { let mut selected_accounts: Vec>>; loop { - match get_selected_accounts(&matches, &mut manifest) { + match get_selected_accounts(&new_args, &mut manifest) { Ok(accounts) => { selected_accounts = accounts; break; @@ -283,19 +283,19 @@ fn run() -> anyhow::Result<()> { } fn get_selected_accounts( - matches: &ArgMatches, + args: &cli::Args, manifest: &mut accountmanager::Manifest, ) -> anyhow::Result>>, ManifestAccountLoadError> { let mut selected_accounts: Vec>> = vec![]; - if matches.is_present("all") { + if args.all { manifest.load_accounts()?; for entry in &manifest.entries { selected_accounts.push(manifest.get_account(&entry.account_name).unwrap().clone()); } } else { - let entry = if matches.is_present("username") { - manifest.get_entry(&matches.value_of("username").unwrap().into()) + let entry = if let Some(username) = &args.username { + manifest.get_entry(&username) } else { manifest .entries From d13ca2079ed425fc982ab533ebaba10452ce5451 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 12:59:20 -0400 Subject: [PATCH 24/27] add conflicts_with arg metadata --- src/cli.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 984b259..8bd251b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,9 +5,9 @@ use clap_complete::Shell; #[derive(Debug, Clone, Parser)] #[clap(name="steamguard-cli", bin_name="steamguard", author, version, about = "Generate Steam 2FA codes and confirm Steam trades from the command line.", long_about = None)] pub(crate) struct Args { - #[clap(short, long, help = "Steam username, case-sensitive.", long_help = "Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected.")] + #[clap(short, long, conflicts_with="all", help = "Steam username, case-sensitive.", long_help = "Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected.")] pub username: Option, - #[clap(short, long, help = "Select all accounts in the manifest.")] + #[clap(short, long, conflicts_with="username", help = "Select all accounts in the manifest.")] pub all: bool, /// The path to the maFiles directory. #[clap(short, long, help = "Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json. Default: ~/.config/steamguard-cli/maFiles")] From 0b84a103e5273bc301e5ba9290669d45a74cdf29 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 13:00:36 -0400 Subject: [PATCH 25/27] remove old arg parsing completely --- src/main.rs | 117 +++++----------------------------------------------- 1 file changed, 10 insertions(+), 107 deletions(-) diff --git a/src/main.rs b/src/main.rs index 72a039f..42ab209 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ extern crate rpassword; -use clap::{crate_version, App, Arg, ArgMatches, Parser, IntoApp}; +use clap::{Parser, IntoApp}; use log::*; use std::{ io::{stdout, Write}, @@ -29,101 +29,6 @@ mod encryption; mod errors; mod tui; -fn cli() -> App<'static> { - App::new("steamguard-cli") - .version(crate_version!()) - .bin_name("steamguard") - .author("dyc3 (Carson McManus)") - .about("Generate Steam 2FA codes and confirm Steam trades from the command line.") - .arg( - Arg::with_name("username") - .long("username") - .short('u') - .takes_value(true) - .help("Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected.") - .conflicts_with("all") - ) - .arg( - Arg::with_name("all") - .long("all") - .short('a') - .takes_value(false) - .help("Select all accounts in the manifest.") - .conflicts_with("username") - ) - .arg( - Arg::with_name("mafiles-path") - .long("mafiles-path") - .short('m') - .default_value("~/maFiles") - .help("Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json.") - ) - .arg( - Arg::with_name("passkey") - .long("passkey") - .short('p') - .help("Specify your encryption passkey.") - .takes_value(true) - ) - .subcommand( - App::new("completion") - .about("Generate shell completions") - .arg( - Arg::with_name("shell") - .long("shell") - .takes_value(true) - ) - ) - .subcommand( - App::new("trade") - .about("Interactive interface for trade confirmations") - .arg( - Arg::with_name("accept-all") - .short('a') - .long("accept-all") - .takes_value(false) - .help("Accept all open trade confirmations. Does not open interactive interface.") - ) - .arg( - Arg::with_name("fail-fast") - .takes_value(false) - .help("If submitting a confirmation response fails, exit immediately.") - ) - ) - .subcommand( - App::new("setup") - .about("Set up a new account with steamguard-cli") - ) - .subcommand( - App::new("import") - .about("Import an account with steamguard already set up") - .arg( - Arg::with_name("files") - .required(true) - .multiple(true) - ) - ) - .subcommand( - App::new("remove") - .about("Remove the authenticator from an account.") - ) - .subcommand( - App::new("encrypt") - .about("Encrypt maFiles.") - ) - .subcommand( - App::new("decrypt") - .about("Decrypt maFiles.") - ) - .subcommand( - App::new("debug") - .arg( - Arg::with_name("demo-conf-menu") - .takes_value(false) - ) - ) -} - fn main() { std::process::exit(match run() { Ok(_) => 0, @@ -135,19 +40,17 @@ fn main() { } fn run() -> anyhow::Result<()> { - let new_args = cli::Args::parse(); - info!("{:?}", new_args); - - let matches = cli().get_matches(); + let args = cli::Args::parse(); + info!("{:?}", args); stderrlog::new() - .verbosity(new_args.verbosity as usize) + .verbosity(args.verbosity as usize) .module(module_path!()) .module("steamguard") .init() .unwrap(); - match new_args.sub { + match args.sub { Some(cli::Subcommands::Debug(args)) => { return do_subcmd_debug(args); }, @@ -157,7 +60,7 @@ fn run() -> anyhow::Result<()> { _ => {}, }; - let mafiles_dir = if let Some(mafiles_path) = &new_args.mafiles_path { + let mafiles_dir = if let Some(mafiles_path) = &args.mafiles_path { mafiles_path.clone() } else { get_mafiles_dir() @@ -185,7 +88,7 @@ fn run() -> anyhow::Result<()> { manifest = accountmanager::Manifest::load(path.as_path())?; } - let mut passkey: Option = new_args.passkey.clone(); + let mut passkey: Option = args.passkey.clone(); manifest.submit_passkey(passkey); loop { @@ -216,7 +119,7 @@ fn run() -> anyhow::Result<()> { } } - match new_args.sub { + match args.sub { Some(cli::Subcommands::Setup(args)) => { return do_subcmd_setup(args, &mut manifest); }, @@ -234,7 +137,7 @@ fn run() -> anyhow::Result<()> { let mut selected_accounts: Vec>>; loop { - match get_selected_accounts(&new_args, &mut manifest) { + match get_selected_accounts(&args, &mut manifest) { Ok(accounts) => { selected_accounts = accounts; break; @@ -264,7 +167,7 @@ fn run() -> anyhow::Result<()> { .collect::>() ); - match new_args.sub { + match args.sub { Some(cli::Subcommands::Trade(args)) => { return do_subcmd_trade(args, &mut manifest, selected_accounts); }, From c86e31648add618a2be6c47b25b515adc9fbc27e Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 13:01:25 -0400 Subject: [PATCH 26/27] fix some warnings --- src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 42ab209..07b5ff5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use steamguard::{ SteamGuardAccount, UserLogin, }; -use crate::{accountmanager::ManifestAccountLoadError}; +use crate::accountmanager::ManifestAccountLoadError; #[macro_use] extern crate lazy_static; @@ -135,7 +135,7 @@ fn run() -> anyhow::Result<()> { _ => {}, } - let mut selected_accounts: Vec>>; + let selected_accounts: Vec>>; loop { match get_selected_accounts(&args, &mut manifest) { Ok(accounts) => { @@ -529,7 +529,7 @@ fn do_subcmd_trade(args: cli::ArgsTrade, manifest: &mut accountmanager::Manifest return Ok(()); } -fn do_subcmd_remove(args: cli::ArgsRemove, manifest: &mut accountmanager::Manifest, selected_accounts: Vec>>) -> anyhow::Result<()> { +fn do_subcmd_remove(_args: cli::ArgsRemove, manifest: &mut accountmanager::Manifest, selected_accounts: Vec>>) -> anyhow::Result<()> { println!( "This will remove the mobile authenticator from {} accounts: {}", selected_accounts.len(), @@ -589,7 +589,7 @@ fn do_subcmd_remove(args: cli::ArgsRemove, manifest: &mut accountmanager::Manife return Ok(()); } -fn do_subcmd_encrypt(args: cli::ArgsEncrypt, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { +fn do_subcmd_encrypt(_args: cli::ArgsEncrypt, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { if !manifest.has_passkey() { let mut passkey; loop { @@ -611,7 +611,7 @@ fn do_subcmd_encrypt(args: cli::ArgsEncrypt, manifest: &mut accountmanager::Mani return Ok(()); } -fn do_subcmd_decrypt(args: cli::ArgsDecrypt, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { +fn do_subcmd_decrypt(_args: cli::ArgsDecrypt, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { manifest.load_accounts()?; for entry in &mut manifest.entries { entry.encryption = None; From 8839a185435794c46b1d2db88688f5c451f82ce3 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sun, 19 Jun 2022 14:44:47 -0400 Subject: [PATCH 27/27] cargo fmt --- src/cli.rs | 61 +++++++++++++++++++++++++++++++++++++---------------- src/main.rs | 59 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 82 insertions(+), 38 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 8bd251b..687d904 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,16 +1,31 @@ -use std::str::FromStr; -use clap::{Parser, clap_derive::ArgEnum}; +use clap::{clap_derive::ArgEnum, Parser}; use clap_complete::Shell; +use std::str::FromStr; #[derive(Debug, Clone, Parser)] #[clap(name="steamguard-cli", bin_name="steamguard", author, version, about = "Generate Steam 2FA codes and confirm Steam trades from the command line.", long_about = None)] pub(crate) struct Args { - #[clap(short, long, conflicts_with="all", help = "Steam username, case-sensitive.", long_help = "Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected.")] + #[clap( + short, + long, + conflicts_with = "all", + help = "Steam username, case-sensitive.", + long_help = "Select the account you want by steam username. Case-sensitive. By default, the first account in the manifest is selected." + )] pub username: Option, - #[clap(short, long, conflicts_with="username", help = "Select all accounts in the manifest.")] + #[clap( + short, + long, + conflicts_with = "username", + help = "Select all accounts in the manifest." + )] pub all: bool, /// The path to the maFiles directory. - #[clap(short, long, help = "Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json. Default: ~/.config/steamguard-cli/maFiles")] + #[clap( + short, + long, + help = "Specify which folder your maFiles are in. This should be a path to a folder that contains manifest.json. Default: ~/.config/steamguard-cli/maFiles" + )] pub mafiles_path: Option, #[clap(short, long, help = "Specify your encryption passkey.")] pub passkey: Option, @@ -44,13 +59,16 @@ pub(crate) enum Verbosity { impl std::fmt::Display for Verbosity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", match self { - Verbosity::Error => "error", - Verbosity::Warn => "warn", - Verbosity::Info => "info", - Verbosity::Debug => "debug", - Verbosity::Trace => "trace", - })) + f.write_fmt(format_args!( + "{}", + match self { + Verbosity::Error => "error", + Verbosity::Warn => "warn", + Verbosity::Info => "info", + Verbosity::Debug => "debug", + Verbosity::Trace => "trace", + } + )) } } @@ -69,16 +87,15 @@ impl FromStr for Verbosity { } } - #[derive(Debug, Clone, Parser)] -#[clap(about="Debug stuff, not useful for most users.")] +#[clap(about = "Debug stuff, not useful for most users.")] pub(crate) struct ArgsDebug { #[clap(long, help = "Show an example confirmation menu using dummy data.")] - pub demo_conf_menu: bool + pub demo_conf_menu: bool, } #[derive(Debug, Clone, Parser)] -#[clap(about="Generate shell completions")] +#[clap(about = "Generate shell completions")] pub(crate) struct ArgsCompletions { #[clap(short, long, arg_enum, help = "The shell to generate completions for.")] pub shell: Shell, @@ -101,9 +118,17 @@ pub(crate) struct ArgsImport { #[derive(Debug, Clone, Parser)] #[clap(about = "Interactive interface for trade confirmations")] pub(crate) struct ArgsTrade { - #[clap(short, long, help = "Accept all open trade confirmations. Does not open interactive interface.")] + #[clap( + short, + long, + help = "Accept all open trade confirmations. Does not open interactive interface." + )] pub accept_all: bool, - #[clap(short, long, help = "If submitting a confirmation response fails, exit immediately.")] + #[clap( + short, + long, + help = "If submitting a confirmation response fails, exit immediately." + )] pub fail_fast: bool, } diff --git a/src/main.rs b/src/main.rs index 07b5ff5..d65f3d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ extern crate rpassword; -use clap::{Parser, IntoApp}; +use clap::{IntoApp, Parser}; use log::*; use std::{ io::{stdout, Write}, @@ -53,11 +53,11 @@ fn run() -> anyhow::Result<()> { match args.sub { Some(cli::Subcommands::Debug(args)) => { return do_subcmd_debug(args); - }, + } Some(cli::Subcommands::Completion(args)) => { return do_subcmd_completion(args); - }, - _ => {}, + } + _ => {} }; let mafiles_dir = if let Some(mafiles_path) = &args.mafiles_path { @@ -122,17 +122,17 @@ fn run() -> anyhow::Result<()> { match args.sub { Some(cli::Subcommands::Setup(args)) => { return do_subcmd_setup(args, &mut manifest); - }, + } Some(cli::Subcommands::Import(args)) => { return do_subcmd_import(args, &mut manifest); - }, + } Some(cli::Subcommands::Encrypt(args)) => { return do_subcmd_encrypt(args, &mut manifest); - }, + } Some(cli::Subcommands::Decrypt(args)) => { return do_subcmd_decrypt(args, &mut manifest); - }, - _ => {}, + } + _ => {} } let selected_accounts: Vec>>; @@ -170,14 +170,14 @@ fn run() -> anyhow::Result<()> { match args.sub { Some(cli::Subcommands::Trade(args)) => { return do_subcmd_trade(args, &mut manifest, selected_accounts); - }, + } Some(cli::Subcommands::Remove(args)) => { return do_subcmd_remove(args, &mut manifest, selected_accounts); - }, + } Some(s) => { error!("Unknown subcommand: {:?}", s); return Err(errors::UserError::UnknownSubcommand.into()); - }, + } _ => { debug!("No subcommand given, assuming user wants a 2fa code"); return do_subcmd_code(selected_accounts); @@ -319,7 +319,10 @@ fn do_subcmd_completion(args: cli::ArgsCompletions) -> Result<(), anyhow::Error> return Ok(()); } -fn do_subcmd_setup(args: cli::ArgsSetup, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { +fn do_subcmd_setup( + args: cli::ArgsSetup, + manifest: &mut accountmanager::Manifest, +) -> anyhow::Result<()> { println!("Log in to the account that you want to link to steamguard-cli"); print!("Username: "); let username = if args.username.is_some() { @@ -336,8 +339,7 @@ fn do_subcmd_setup(args: cli::ArgsSetup, manifest: &mut accountmanager::Manifest username ); } - let session = - do_login_raw(username).expect("Failed to log in. Account has not been linked."); + let session = do_login_raw(username).expect("Failed to log in. Account has not been linked."); let mut linker = AccountLinker::new(session); let account: SteamGuardAccount; @@ -434,7 +436,10 @@ fn do_subcmd_setup(args: cli::ArgsSetup, manifest: &mut accountmanager::Manifest return Ok(()); } -fn do_subcmd_import(args: cli::ArgsImport, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { +fn do_subcmd_import( + args: cli::ArgsImport, + manifest: &mut accountmanager::Manifest, +) -> anyhow::Result<()> { for file_path in args.files { match manifest.import_account(&file_path) { Ok(_) => { @@ -450,7 +455,11 @@ fn do_subcmd_import(args: cli::ArgsImport, manifest: &mut accountmanager::Manife return Ok(()); } -fn do_subcmd_trade(args: cli::ArgsTrade, manifest: &mut accountmanager::Manifest, mut selected_accounts: Vec>>) -> anyhow::Result<()> { +fn do_subcmd_trade( + args: cli::ArgsTrade, + manifest: &mut accountmanager::Manifest, + mut selected_accounts: Vec>>, +) -> anyhow::Result<()> { for a in selected_accounts.iter_mut() { let mut account = a.lock().unwrap(); @@ -529,7 +538,11 @@ fn do_subcmd_trade(args: cli::ArgsTrade, manifest: &mut accountmanager::Manifest return Ok(()); } -fn do_subcmd_remove(_args: cli::ArgsRemove, manifest: &mut accountmanager::Manifest, selected_accounts: Vec>>) -> anyhow::Result<()> { +fn do_subcmd_remove( + _args: cli::ArgsRemove, + manifest: &mut accountmanager::Manifest, + selected_accounts: Vec>>, +) -> anyhow::Result<()> { println!( "This will remove the mobile authenticator from {} accounts: {}", selected_accounts.len(), @@ -589,7 +602,10 @@ fn do_subcmd_remove(_args: cli::ArgsRemove, manifest: &mut accountmanager::Manif return Ok(()); } -fn do_subcmd_encrypt(_args: cli::ArgsEncrypt, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { +fn do_subcmd_encrypt( + _args: cli::ArgsEncrypt, + manifest: &mut accountmanager::Manifest, +) -> anyhow::Result<()> { if !manifest.has_passkey() { let mut passkey; loop { @@ -611,7 +627,10 @@ fn do_subcmd_encrypt(_args: cli::ArgsEncrypt, manifest: &mut accountmanager::Man return Ok(()); } -fn do_subcmd_decrypt(_args: cli::ArgsDecrypt, manifest: &mut accountmanager::Manifest) -> anyhow::Result<()> { +fn do_subcmd_decrypt( + _args: cli::ArgsDecrypt, + manifest: &mut accountmanager::Manifest, +) -> anyhow::Result<()> { manifest.load_accounts()?; for entry in &mut manifest.entries { entry.encryption = None;