Merge pull request #151 from dyc3/refactor-clap-derive
Upgrade to clap v3, switch argument parsing to derive api
This commit is contained in:
commit
d8db1ae9b3
6 changed files with 624 additions and 451 deletions
106
Cargo.lock
generated
106
Cargo.lock
generated
|
@ -29,15 +29,6 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ansi_term"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.57"
|
version = "1.0.57"
|
||||||
|
@ -192,17 +183,50 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "2.34.0"
|
version = "3.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
|
checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term",
|
|
||||||
"atty",
|
"atty",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"clap_derive",
|
||||||
|
"clap_lex",
|
||||||
|
"indexmap",
|
||||||
|
"once_cell",
|
||||||
"strsim",
|
"strsim",
|
||||||
|
"termcolor",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"unicode-width",
|
]
|
||||||
"vec_map",
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_complete"
|
||||||
|
version = "3.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
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",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613"
|
||||||
|
dependencies = [
|
||||||
|
"os_str_bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -578,6 +602,12 @@ version = "0.11.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.1.19"
|
version = "0.1.19"
|
||||||
|
@ -938,6 +968,12 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_str_bytes"
|
||||||
|
version = "6.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
@ -1097,6 +1133,30 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
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]]
|
[[package]]
|
||||||
name = "proc-macro-hack"
|
name = "proc-macro-hack"
|
||||||
version = "0.5.19"
|
version = "0.5.19"
|
||||||
|
@ -1808,6 +1868,7 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"block-modes",
|
"block-modes",
|
||||||
"clap",
|
"clap",
|
||||||
|
"clap_complete",
|
||||||
"cookie 0.14.4",
|
"cookie 0.14.4",
|
||||||
"dirs",
|
"dirs",
|
||||||
"hmac-sha1",
|
"hmac-sha1",
|
||||||
|
@ -1860,9 +1921,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.8.0"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
|
@ -1957,12 +2018,9 @@ checksum = "442f2674e6bd8489052b958e0eaebd89c26eefa3be9dc359d1e2ecccdc510f45"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "textwrap"
|
name = "textwrap"
|
||||||
version = "0.11.0"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||||
dependencies = [
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thin-slice"
|
name = "thin-slice"
|
||||||
|
@ -2232,12 +2290,6 @@ dependencies = [
|
||||||
"getrandom 0.2.6",
|
"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]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
|
@ -7,7 +7,7 @@ members = [
|
||||||
[package]
|
[package]
|
||||||
name = "steamguard-cli"
|
name = "steamguard-cli"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
authors = ["Carson McManus <carson.mcmanus1@gmail.com>"]
|
authors = ["dyc3 (Carson McManus) <carson.mcmanus1@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "A command line utility to generate Steam 2FA codes and respond to confirmations."
|
description = "A command line utility to generate Steam 2FA codes and respond to confirmations."
|
||||||
keywords = ["steam", "2fa", "steamguard", "authentication", "cli"]
|
keywords = ["steam", "2fa", "steamguard", "authentication", "cli"]
|
||||||
|
@ -33,7 +33,8 @@ serde_json = "1.0"
|
||||||
rsa = "0.5.0"
|
rsa = "0.5.0"
|
||||||
rand = "0.8.4"
|
rand = "0.8.4"
|
||||||
standback = "0.2.17" # required to fix a compilation error on a transient dependency
|
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"] }
|
||||||
|
clap_complete = "3.2.1"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
stderrlog = "0.4"
|
stderrlog = "0.4"
|
||||||
cookie = "0.14"
|
cookie = "0.14"
|
||||||
|
|
|
@ -180,8 +180,8 @@ impl Manifest {
|
||||||
.insert(account.account_name.clone(), Arc::new(Mutex::new(account)));
|
.insert(account.account_name.clone(), Arc::new(Mutex::new(account)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_account(&mut self, import_path: String) -> anyhow::Result<()> {
|
pub fn import_account(&mut self, import_path: &String) -> anyhow::Result<()> {
|
||||||
let path = Path::new(&import_path);
|
let path = Path::new(import_path);
|
||||||
ensure!(path.exists(), "{} does not exist.", import_path);
|
ensure!(path.exists(), "{} does not exist.", import_path);
|
||||||
ensure!(path.is_file(), "{} is not a file.", 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());
|
let mut loaded_manifest = Manifest::new(manifest_path.as_path());
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
loaded_manifest.import_account(
|
loaded_manifest.import_account(
|
||||||
tmp_dir
|
&tmp_dir
|
||||||
.path()
|
.path()
|
||||||
.join("asdf1234.maFile")
|
.join("asdf1234.maFile")
|
||||||
.into_os_string()
|
.into_os_string()
|
||||||
|
|
145
src/cli.rs
Normal file
145
src/cli.rs
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
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."
|
||||||
|
)]
|
||||||
|
pub username: Option<String>,
|
||||||
|
#[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"
|
||||||
|
)]
|
||||||
|
pub mafiles_path: Option<String>,
|
||||||
|
#[clap(short, long, help = "Specify your encryption passkey.")]
|
||||||
|
pub passkey: Option<String>,
|
||||||
|
#[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)]
|
||||||
|
pub sub: Option<Subcommands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
pub(crate) enum Subcommands {
|
||||||
|
Debug(ArgsDebug),
|
||||||
|
Completion(ArgsCompletions),
|
||||||
|
Setup(ArgsSetup),
|
||||||
|
Import(ArgsImport),
|
||||||
|
Trade(ArgsTrade),
|
||||||
|
Remove(ArgsRemove),
|
||||||
|
Encrypt(ArgsEncrypt),
|
||||||
|
Decrypt(ArgsDecrypt),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, ArgEnum)]
|
||||||
|
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<Self, Self::Err> {
|
||||||
|
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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
#[clap(about = "Generate shell completions")]
|
||||||
|
pub(crate) struct ArgsCompletions {
|
||||||
|
#[clap(short, long, 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 {
|
||||||
|
#[clap(short, long, from_global, help = "Steam username, case-sensitive.")]
|
||||||
|
pub username: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
#[clap(about = "Remove the authenticator from an account.")]
|
||||||
|
pub(crate) struct ArgsRemove;
|
||||||
|
|
||||||
|
#[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;
|
|
@ -4,4 +4,6 @@ use thiserror::Error;
|
||||||
pub(crate) enum UserError {
|
pub(crate) enum UserError {
|
||||||
#[error("User aborted the operation.")]
|
#[error("User aborted the operation.")]
|
||||||
Aborted,
|
Aborted,
|
||||||
|
#[error("Unknown subcommand. It may need to be implemented.")]
|
||||||
|
UnknownSubcommand,
|
||||||
}
|
}
|
||||||
|
|
815
src/main.rs
815
src/main.rs
|
@ -1,7 +1,6 @@
|
||||||
extern crate rpassword;
|
extern crate rpassword;
|
||||||
use clap::{crate_version, App, Arg, ArgMatches, Shell};
|
use clap::{IntoApp, Parser};
|
||||||
use log::*;
|
use log::*;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{stdout, Write},
|
io::{stdout, Write},
|
||||||
path::Path,
|
path::Path,
|
||||||
|
@ -24,115 +23,12 @@ extern crate dirs;
|
||||||
extern crate proptest;
|
extern crate proptest;
|
||||||
extern crate ring;
|
extern crate ring;
|
||||||
mod accountmanager;
|
mod accountmanager;
|
||||||
|
mod cli;
|
||||||
mod demos;
|
mod demos;
|
||||||
mod encryption;
|
mod encryption;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod tui;
|
mod tui;
|
||||||
|
|
||||||
fn cli() -> App<'static, '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)
|
|
||||||
)
|
|
||||||
.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")
|
|
||||||
.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")
|
|
||||||
.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")
|
|
||||||
.help("Show an example confirmation menu using dummy data.")
|
|
||||||
.takes_value(false)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
std::process::exit(match run() {
|
std::process::exit(match run() {
|
||||||
Ok(_) => 0,
|
Ok(_) => 0,
|
||||||
|
@ -144,33 +40,28 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> anyhow::Result<()> {
|
fn run() -> anyhow::Result<()> {
|
||||||
let matches = cli().get_matches();
|
let args = cli::Args::parse();
|
||||||
|
info!("{:?}", args);
|
||||||
|
|
||||||
let verbosity = matches.occurrences_of("verbosity") as usize + 2;
|
|
||||||
stderrlog::new()
|
stderrlog::new()
|
||||||
.verbosity(verbosity)
|
.verbosity(args.verbosity as usize)
|
||||||
.module(module_path!())
|
.module(module_path!())
|
||||||
.module("steamguard")
|
.module("steamguard")
|
||||||
.init()
|
.init()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
if let Some(demo_matches) = matches.subcommand_matches("debug") {
|
match args.sub {
|
||||||
if demo_matches.is_present("demo-conf-menu") {
|
Some(cli::Subcommands::Debug(args)) => {
|
||||||
demos::demo_confirmation_menu();
|
return do_subcmd_debug(args);
|
||||||
}
|
}
|
||||||
return Ok(());
|
Some(cli::Subcommands::Completion(args)) => {
|
||||||
}
|
return do_subcmd_completion(args);
|
||||||
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(());
|
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
let mafiles_dir = if matches.occurrences_of("mafiles-path") > 0 {
|
let mafiles_dir = if let Some(mafiles_path) = &args.mafiles_path {
|
||||||
matches.value_of("mafiles-path").unwrap().into()
|
mafiles_path.clone()
|
||||||
} else {
|
} else {
|
||||||
get_mafiles_dir()
|
get_mafiles_dir()
|
||||||
};
|
};
|
||||||
|
@ -197,7 +88,7 @@ fn run() -> anyhow::Result<()> {
|
||||||
manifest = accountmanager::Manifest::load(path.as_path())?;
|
manifest = accountmanager::Manifest::load(path.as_path())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut passkey: Option<String> = matches.value_of("passkey").map(|s| s.into());
|
let mut passkey: Option<String> = args.passkey.clone();
|
||||||
manifest.submit_passkey(passkey);
|
manifest.submit_passkey(passkey);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
@ -228,159 +119,25 @@ fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches.is_present("setup") {
|
match args.sub {
|
||||||
println!("Log in to the account that you want to link to steamguard-cli");
|
Some(cli::Subcommands::Setup(args)) => {
|
||||||
print!("Username: ");
|
return do_subcmd_setup(args, &mut manifest);
|
||||||
let username = tui::prompt();
|
}
|
||||||
let account_name = username.clone();
|
Some(cli::Subcommands::Import(args)) => {
|
||||||
if manifest.account_exists(&username) {
|
return do_subcmd_import(args, &mut manifest);
|
||||||
bail!(
|
}
|
||||||
"Account {} already exists in manifest, remove it first",
|
Some(cli::Subcommands::Encrypt(args)) => {
|
||||||
username
|
return do_subcmd_encrypt(args, &mut manifest);
|
||||||
);
|
}
|
||||||
|
Some(cli::Subcommands::Decrypt(args)) => {
|
||||||
|
return do_subcmd_decrypt(args, &mut manifest);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
let session =
|
|
||||||
do_login_raw(username).expect("Failed to log in. Account has not been linked.");
|
|
||||||
|
|
||||||
let mut linker = AccountLinker::new(session);
|
let selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>>;
|
||||||
let account: SteamGuardAccount;
|
|
||||||
loop {
|
loop {
|
||||||
match linker.link() {
|
match get_selected_accounts(&args, &mut manifest) {
|
||||||
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()) {
|
|
||||||
Ok(_) => {
|
|
||||||
info!("Imported account: {}", file_path);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
bail!("Failed to import account: {} {}", file_path, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>>;
|
|
||||||
loop {
|
|
||||||
match get_selected_accounts(&matches, &mut manifest) {
|
|
||||||
Ok(accounts) => {
|
Ok(accounts) => {
|
||||||
selected_accounts = accounts;
|
selected_accounts = accounts;
|
||||||
break;
|
break;
|
||||||
|
@ -410,171 +167,38 @@ fn run() -> anyhow::Result<()> {
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(trade_matches) = matches.subcommand_matches("trade") {
|
match args.sub {
|
||||||
info!("trade");
|
Some(cli::Subcommands::Trade(args)) => {
|
||||||
for a in selected_accounts.iter_mut() {
|
return do_subcmd_trade(args, &mut manifest, selected_accounts);
|
||||||
let mut account = a.lock().unwrap();
|
|
||||||
|
|
||||||
info!("Checking for trade confirmations");
|
|
||||||
let confirmations: Vec<Confirmation>;
|
|
||||||
loop {
|
|
||||||
match account.get_trade_confirmations() {
|
|
||||||
Ok(confs) => {
|
|
||||||
confirmations = confs;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Some(cli::Subcommands::Remove(args)) => {
|
||||||
info!("failed to get trade confirmations, asking user to log in");
|
return do_subcmd_remove(args, &mut manifest, selected_accounts);
|
||||||
do_login(&mut account)?;
|
|
||||||
}
|
}
|
||||||
|
Some(s) => {
|
||||||
|
error!("Unknown subcommand: {:?}", s);
|
||||||
|
return Err(errors::UserError::UnknownSubcommand.into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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::<Vec<String>>()
|
|
||||||
.join(", ")
|
|
||||||
);
|
|
||||||
|
|
||||||
match tui::prompt_char("Do you want to continue?", "yN") {
|
|
||||||
'y' => {}
|
|
||||||
_ => {
|
_ => {
|
||||||
info!("Aborting!");
|
debug!("No subcommand given, assuming user wants a 2fa code");
|
||||||
return Err(errors::UserError::Aborted.into());
|
return do_subcmd_code(selected_accounts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()?;
|
|
||||||
} 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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_selected_accounts(
|
fn get_selected_accounts(
|
||||||
matches: &ArgMatches,
|
args: &cli::Args,
|
||||||
manifest: &mut accountmanager::Manifest,
|
manifest: &mut accountmanager::Manifest,
|
||||||
) -> anyhow::Result<Vec<Arc<Mutex<SteamGuardAccount>>>, ManifestAccountLoadError> {
|
) -> anyhow::Result<Vec<Arc<Mutex<SteamGuardAccount>>>, ManifestAccountLoadError> {
|
||||||
let mut selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>> = vec![];
|
let mut selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>> = vec![];
|
||||||
|
|
||||||
if matches.is_present("all") {
|
if args.all {
|
||||||
manifest.load_accounts()?;
|
manifest.load_accounts()?;
|
||||||
for entry in &manifest.entries {
|
for entry in &manifest.entries {
|
||||||
selected_accounts.push(manifest.get_account(&entry.account_name).unwrap().clone());
|
selected_accounts.push(manifest.get_account(&entry.account_name).unwrap().clone());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let entry = if matches.is_present("username") {
|
let entry = if let Some(username) = &args.username {
|
||||||
manifest.get_entry(&matches.value_of("username").unwrap().into())
|
manifest.get_entry(&username)
|
||||||
} else {
|
} else {
|
||||||
manifest
|
manifest
|
||||||
.entries
|
.entries
|
||||||
|
@ -681,3 +305,352 @@ fn get_mafiles_dir() -> String {
|
||||||
|
|
||||||
return paths[0].to_str().unwrap().into();
|
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_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: ");
|
||||||
|
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!(
|
||||||
|
"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(());
|
||||||
|
}
|
||||||
|
|
||||||
|
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_trade(
|
||||||
|
args: cli::ArgsTrade,
|
||||||
|
manifest: &mut accountmanager::Manifest,
|
||||||
|
mut selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
for a in selected_accounts.iter_mut() {
|
||||||
|
let mut account = a.lock().unwrap();
|
||||||
|
|
||||||
|
info!("Checking for trade confirmations");
|
||||||
|
let confirmations: Vec<Confirmation>;
|
||||||
|
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<Arc<Mutex<SteamGuardAccount>>>,
|
||||||
|
) -> 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::<Vec<String>>()
|
||||||
|
.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;
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_subcmd_code(selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>>) -> 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(());
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue