add HTTP proxy support (#264)

- allow extracting the http client from web api transport
- refactor most commands so that they can accept an external transport
- update confirmer to use transport's http client
- update remove command
- update TwoFactorClient so it doesn't need to be mutable
- update get_server_time so it requires a transport
- update remove_authenticator
- update login proceedure to use given transport
- update usages of do_login
- update setup
- update trade
- update code command
- update qr-login command
- make borrowcheck happy
- make WebApiTransport Clone and remove Default impl
- remove dead code
- fix lints

closes #177
This commit is contained in:
Carson McManus 2023-07-02 08:57:13 -04:00 committed by GitHub
parent 1d99bae469
commit fe663cf43f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 164 additions and 105 deletions

View file

@ -4,7 +4,7 @@ use clap::{clap_derive::ArgEnum, Parser};
use clap_complete::Shell; use clap_complete::Shell;
use secrecy::SecretString; use secrecy::SecretString;
use std::str::FromStr; use std::str::FromStr;
use steamguard::SteamGuardAccount; use steamguard::{transport::Transport, SteamGuardAccount};
use crate::AccountManager; use crate::AccountManager;
@ -40,23 +40,33 @@ pub(crate) trait ConstCommand {
} }
/// A command that operates the manifest as a whole /// A command that operates the manifest as a whole
pub(crate) trait ManifestCommand { pub(crate) trait ManifestCommand<T>
fn execute(&self, manager: &mut AccountManager) -> anyhow::Result<()>; where
T: Transport,
{
fn execute(&self, transport: T, manager: &mut AccountManager) -> anyhow::Result<()>;
} }
/// A command that operates on individual accounts. /// A command that operates on individual accounts.
pub(crate) trait AccountCommand { pub(crate) trait AccountCommand<T>
where
T: Transport,
{
fn execute( fn execute(
&self, &self,
transport: T,
manager: &mut AccountManager, manager: &mut AccountManager,
accounts: Vec<Arc<Mutex<SteamGuardAccount>>>, accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
) -> anyhow::Result<()>; ) -> anyhow::Result<()>;
} }
pub(crate) enum CommandType { pub(crate) enum CommandType<T>
where
T: Transport,
{
Const(Box<dyn ConstCommand>), Const(Box<dyn ConstCommand>),
Manifest(Box<dyn ManifestCommand>), Manifest(Box<dyn ManifestCommand<T>>),
Account(Box<dyn AccountCommand>), Account(Box<dyn AccountCommand<T>>),
} }
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
@ -114,6 +124,14 @@ pub(crate) struct GlobalArgs {
long_help = "Disable checking for updates. By default, steamguard-cli will check for updates every now and then. This can be disabled with this flag." long_help = "Disable checking for updates. By default, steamguard-cli will check for updates every now and then. This can be disabled with this flag."
)] )]
pub no_update_check: bool, pub no_update_check: bool,
#[clap(
long,
env = "HTTP_PROXY",
help = "Use a proxy for HTTP requests.",
long_help = "Use a proxy for HTTP requests. This is useful if you are behind a firewall and need to use a proxy to access the internet."
)]
pub http_proxy: Option<String>,
} }
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]

View file

@ -20,16 +20,20 @@ pub struct CodeCommand {
pub offline: bool, pub offline: bool,
} }
impl AccountCommand for CodeCommand { impl<T> AccountCommand<T> for CodeCommand
where
T: Transport,
{
fn execute( fn execute(
&self, &self,
transport: T,
_manager: &mut AccountManager, _manager: &mut AccountManager,
accounts: Vec<Arc<Mutex<SteamGuardAccount>>>, accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let server_time = if self.offline { let server_time = if self.offline {
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()
} else { } else {
steamapi::get_server_time()?.server_time() steamapi::get_server_time(transport)?.server_time()
}; };
debug!("Time used to generate codes: {}", server_time); debug!("Time used to generate codes: {}", server_time);

View file

@ -8,8 +8,11 @@ use super::*;
#[clap(about = "Decrypt all maFiles")] #[clap(about = "Decrypt all maFiles")]
pub struct DecryptCommand; pub struct DecryptCommand;
impl ManifestCommand for DecryptCommand { impl<T> ManifestCommand<T> for DecryptCommand
fn execute(&self, manager: &mut AccountManager) -> anyhow::Result<()> { where
T: Transport,
{
fn execute(&self, _transport: T, manager: &mut AccountManager) -> anyhow::Result<()> {
load_accounts_with_prompts(manager)?; load_accounts_with_prompts(manager)?;
for mut entry in manager.iter_mut() { for mut entry in manager.iter_mut() {
entry.encryption = None; entry.encryption = None;

View file

@ -8,8 +8,11 @@ use super::*;
#[clap(about = "Encrypt all maFiles")] #[clap(about = "Encrypt all maFiles")]
pub struct EncryptCommand; pub struct EncryptCommand;
impl ManifestCommand for EncryptCommand { impl<T> ManifestCommand<T> for EncryptCommand
fn execute(&self, manager: &mut AccountManager) -> anyhow::Result<()> { where
T: Transport,
{
fn execute(&self, _transport: T, manager: &mut AccountManager) -> anyhow::Result<()> {
if !manager.has_passkey() { if !manager.has_passkey() {
let mut passkey; let mut passkey;
loop { loop {

View file

@ -18,8 +18,11 @@ pub struct ImportCommand {
pub files: Vec<String>, pub files: Vec<String>,
} }
impl ManifestCommand for ImportCommand { impl<T> ManifestCommand<T> for ImportCommand
fn execute(&self, manager: &mut AccountManager) -> anyhow::Result<()> { where
T: Transport,
{
fn execute(&self, _transport: T, manager: &mut AccountManager) -> anyhow::Result<()> {
for file_path in self.files.iter() { for file_path in self.files.iter() {
if self.sda { if self.sda {
let path = Path::new(&file_path); let path = Path::new(&file_path);

View file

@ -18,9 +18,13 @@ pub struct QrCommand {
pub ascii: bool, pub ascii: bool,
} }
impl AccountCommand for QrCommand { impl<T> AccountCommand<T> for QrCommand
where
T: Transport,
{
fn execute( fn execute(
&self, &self,
_transport: T,
_manager: &mut AccountManager, _manager: &mut AccountManager,
accounts: Vec<Arc<Mutex<SteamGuardAccount>>>, accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {

View file

@ -1,7 +1,7 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use log::*; use log::*;
use steamguard::{transport::WebApiTransport, QrApprover, QrApproverError}; use steamguard::{QrApprover, QrApproverError};
use crate::AccountManager; use crate::AccountManager;
@ -17,9 +17,13 @@ pub struct QrLoginCommand {
pub url: String, pub url: String,
} }
impl AccountCommand for QrLoginCommand { impl<T> AccountCommand<T> for QrLoginCommand
where
T: Transport + Clone,
{
fn execute( fn execute(
&self, &self,
transport: T,
_manager: &mut AccountManager, _manager: &mut AccountManager,
accounts: Vec<Arc<Mutex<SteamGuardAccount>>>, accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -33,7 +37,7 @@ impl AccountCommand for QrLoginCommand {
info!("Approving login to {}", account.account_name); info!("Approving login to {}", account.account_name);
if account.tokens.is_none() { if account.tokens.is_none() {
crate::do_login(&mut account)?; crate::do_login(transport.clone(), &mut account)?;
} }
loop { loop {
@ -42,7 +46,7 @@ impl AccountCommand for QrLoginCommand {
return Err(anyhow!("No tokens found for {}", account.account_name)); return Err(anyhow!("No tokens found for {}", account.account_name));
}; };
let mut approver = QrApprover::new(WebApiTransport::default(), tokens); let mut approver = QrApprover::new(transport.clone(), tokens);
match approver.approve(&account, &self.url) { match approver.approve(&account, &self.url) {
Ok(_) => { Ok(_) => {
info!("Login approved."); info!("Login approved.");
@ -50,7 +54,7 @@ impl AccountCommand for QrLoginCommand {
} }
Err(QrApproverError::Unauthorized) => { Err(QrApproverError::Unauthorized) => {
warn!("tokens are invalid. Attempting to log in again."); warn!("tokens are invalid. Attempting to log in again.");
crate::do_login(&mut account)?; crate::do_login(transport.clone(), &mut account)?;
} }
Err(e) => { Err(e) => {
error!("Failed to approve login: {}", e); error!("Failed to approve login: {}", e);

View file

@ -1,7 +1,7 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use log::*; use log::*;
use steamguard::{transport::TransportError, RemoveAuthenticatorError}; use steamguard::{steamapi::TwoFactorClient, transport::TransportError, RemoveAuthenticatorError};
use crate::{errors::UserError, tui, AccountManager}; use crate::{errors::UserError, tui, AccountManager};
@ -11,9 +11,13 @@ use super::*;
#[clap(about = "Remove the authenticator from an account.")] #[clap(about = "Remove the authenticator from an account.")]
pub struct RemoveCommand; pub struct RemoveCommand;
impl AccountCommand for RemoveCommand { impl<T> AccountCommand<T> for RemoveCommand
where
T: Transport + Clone,
{
fn execute( fn execute(
&self, &self,
transport: T,
manager: &mut AccountManager, manager: &mut AccountManager,
accounts: Vec<Arc<Mutex<SteamGuardAccount>>>, accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -38,10 +42,11 @@ impl AccountCommand for RemoveCommand {
let mut successful = vec![]; let mut successful = vec![];
for a in accounts { for a in accounts {
let mut account = a.lock().unwrap(); let mut account = a.lock().unwrap();
let client = TwoFactorClient::new(transport.clone());
let mut revocation: Option<String> = None; let mut revocation: Option<String> = None;
loop { loop {
match account.remove_authenticator(revocation.as_ref()) { match account.remove_authenticator(&client, revocation.as_ref()) {
Ok(_) => { Ok(_) => {
info!("Removed authenticator from {}", account.account_name); info!("Removed authenticator from {}", account.account_name);
successful.push(account.account_name.clone()); successful.push(account.account_name.clone());
@ -49,7 +54,7 @@ impl AccountCommand for RemoveCommand {
} }
Err(RemoveAuthenticatorError::TransportError(TransportError::Unauthorized)) => { Err(RemoveAuthenticatorError::TransportError(TransportError::Unauthorized)) => {
error!("Account {} is not logged in", account.account_name); error!("Account {} is not logged in", account.account_name);
crate::do_login(&mut account)?; crate::do_login(transport.clone(), &mut account)?;
continue; continue;
} }
Err(RemoveAuthenticatorError::IncorrectRevocationCode { Err(RemoveAuthenticatorError::IncorrectRevocationCode {

View file

@ -3,7 +3,7 @@ use phonenumber::PhoneNumber;
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
use steamguard::{ use steamguard::{
accountlinker::AccountLinkSuccess, phonelinker::PhoneLinker, steamapi::PhoneClient, accountlinker::AccountLinkSuccess, phonelinker::PhoneLinker, steamapi::PhoneClient,
token::Tokens, transport::WebApiTransport, AccountLinkError, AccountLinker, FinalizeLinkError, token::Tokens, AccountLinkError, AccountLinker, FinalizeLinkError,
}; };
use crate::{tui, AccountManager}; use crate::{tui, AccountManager};
@ -14,8 +14,11 @@ use super::*;
#[clap(about = "Set up a new account with steamguard-cli")] #[clap(about = "Set up a new account with steamguard-cli")]
pub struct SetupCommand; pub struct SetupCommand;
impl ManifestCommand for SetupCommand { impl<T> ManifestCommand<T> for SetupCommand
fn execute(&self, manager: &mut AccountManager) -> anyhow::Result<()> { where
T: Transport + Clone,
{
fn execute(&self, transport: T, manager: &mut AccountManager) -> anyhow::Result<()> {
eprintln!("Log in to the account that you want to link to steamguard-cli"); eprintln!("Log in to the account that you want to link to steamguard-cli");
eprint!("Username: "); eprint!("Username: ");
let username = tui::prompt().to_lowercase(); let username = tui::prompt().to_lowercase();
@ -27,11 +30,11 @@ impl ManifestCommand for SetupCommand {
); );
} }
info!("Logging in to {}", username); info!("Logging in to {}", username);
let tokens = let tokens = crate::do_login_raw(transport.clone(), username)
crate::do_login_raw(username).expect("Failed to log in. Account has not been linked."); .expect("Failed to log in. Account has not been linked.");
info!("Adding authenticator..."); info!("Adding authenticator...");
let mut linker = AccountLinker::new(WebApiTransport::default(), tokens); let mut linker = AccountLinker::new(transport.clone(), tokens);
let link: AccountLinkSuccess; let link: AccountLinkSuccess;
loop { loop {
match linker.link() { match linker.link() {
@ -41,7 +44,7 @@ impl ManifestCommand for SetupCommand {
} }
Err(AccountLinkError::MustProvidePhoneNumber) => { Err(AccountLinkError::MustProvidePhoneNumber) => {
eprintln!("Looks like you don't have a phone number on this account."); eprintln!("Looks like you don't have a phone number on this account.");
do_add_phone_number(linker.tokens())?; do_add_phone_number(transport.clone(), linker.tokens())?;
} }
Err(AccountLinkError::MustConfirmEmail) => { Err(AccountLinkError::MustConfirmEmail) => {
println!("Check your email and click the link."); println!("Check your email and click the link.");
@ -129,8 +132,8 @@ impl ManifestCommand for SetupCommand {
} }
} }
pub fn do_add_phone_number(tokens: &Tokens) -> anyhow::Result<()> { pub fn do_add_phone_number<T: Transport>(transport: T, tokens: &Tokens) -> anyhow::Result<()> {
let client = PhoneClient::new(WebApiTransport::default()); let client = PhoneClient::new(transport);
let linker = PhoneLinker::new(client, tokens.clone()); let linker = PhoneLinker::new(client, tokens.clone());

View file

@ -25,9 +25,13 @@ pub struct TradeCommand {
pub fail_fast: bool, pub fail_fast: bool,
} }
impl AccountCommand for TradeCommand { impl<T> AccountCommand<T> for TradeCommand
where
T: Transport + Clone,
{
fn execute( fn execute(
&self, &self,
transport: T,
manager: &mut AccountManager, manager: &mut AccountManager,
accounts: Vec<Arc<Mutex<SteamGuardAccount>>>, accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -36,13 +40,13 @@ impl AccountCommand for TradeCommand {
if !account.is_logged_in() { if !account.is_logged_in() {
info!("Account does not have tokens, logging in"); info!("Account does not have tokens, logging in");
crate::do_login(&mut account)?; crate::do_login(transport.clone(), &mut account)?;
} }
info!("{}: Checking for trade confirmations", account.account_name); info!("{}: Checking for trade confirmations", account.account_name);
let confirmations: Vec<Confirmation>; let confirmations: Vec<Confirmation>;
loop { loop {
let confirmer = Confirmer::new(&account); let confirmer = Confirmer::new(transport.clone(), &account);
match confirmer.get_trade_confirmations() { match confirmer.get_trade_confirmations() {
Ok(confs) => { Ok(confs) => {
@ -51,7 +55,7 @@ impl AccountCommand for TradeCommand {
} }
Err(ConfirmerError::InvalidTokens) => { Err(ConfirmerError::InvalidTokens) => {
info!("obtaining new tokens"); info!("obtaining new tokens");
crate::do_login(&mut account)?; crate::do_login(transport.clone(), &mut account)?;
} }
Err(err) => { Err(err) => {
error!("Failed to get trade confirmations: {}", err); error!("Failed to get trade confirmations: {}", err);
@ -65,7 +69,7 @@ impl AccountCommand for TradeCommand {
continue; continue;
} }
let confirmer = Confirmer::new(&account); let confirmer = Confirmer::new(transport.clone(), &account);
let mut any_failed = false; let mut any_failed = false;
if self.accept_all { if self.accept_all {
info!("accepting all confirmations"); info!("accepting all confirmations");

View file

@ -10,7 +10,7 @@ use std::{
use steamguard::{ use steamguard::{
protobufs::steammessages_auth_steamclient::{EAuthSessionGuardType, EAuthTokenPlatformType}, protobufs::steammessages_auth_steamclient::{EAuthSessionGuardType, EAuthTokenPlatformType},
refresher::TokenRefresher, refresher::TokenRefresher,
transport::WebApiTransport, transport::{Transport, WebApiTransport},
}; };
use steamguard::{steamapi, DeviceDetails, LoginError, SteamGuardAccount, UserLogin}; use steamguard::{steamapi, DeviceDetails, LoginError, SteamGuardAccount, UserLogin};
use steamguard::{steamapi::AuthenticationClient, token::Tokens}; use steamguard::{steamapi::AuthenticationClient, token::Tokens};
@ -83,7 +83,7 @@ fn main() {
fn run(args: commands::Args) -> anyhow::Result<()> { fn run(args: commands::Args) -> anyhow::Result<()> {
let globalargs = args.global; let globalargs = args.global;
let cmd: CommandType = match args.sub.unwrap_or(Subcommands::Code(args.code)) { let cmd: CommandType<WebApiTransport> = match args.sub.unwrap_or(Subcommands::Code(args.code)) {
Subcommands::Debug(args) => CommandType::Const(Box::new(args)), Subcommands::Debug(args) => CommandType::Const(Box::new(args)),
Subcommands::Completion(args) => CommandType::Const(Box::new(args)), Subcommands::Completion(args) => CommandType::Const(Box::new(args)),
Subcommands::Setup(args) => CommandType::Manifest(Box::new(args)), Subcommands::Setup(args) => CommandType::Manifest(Box::new(args)),
@ -198,8 +198,16 @@ fn run(args: commands::Args) -> anyhow::Result<()> {
} }
} }
let mut http_client = reqwest::blocking::Client::builder();
if let Some(proxy) = &globalargs.http_proxy {
let proxy = reqwest::Proxy::all(proxy)?;
http_client = http_client.proxy(proxy);
}
let http_client = http_client.build()?;
let transport = WebApiTransport::new(http_client);
if let CommandType::Manifest(cmd) = cmd { if let CommandType::Manifest(cmd) = cmd {
cmd.execute(&mut manager)?; cmd.execute(transport, &mut manager)?;
return Ok(()); return Ok(());
} }
@ -237,7 +245,7 @@ fn run(args: commands::Args) -> anyhow::Result<()> {
); );
if let CommandType::Account(cmd) = cmd { if let CommandType::Account(cmd) = cmd {
return cmd.execute(&mut manager, selected_accounts); return cmd.execute(transport, &mut manager, selected_accounts);
} }
Ok(()) Ok(())
@ -271,10 +279,13 @@ fn get_selected_accounts(
Ok(selected_accounts) Ok(selected_accounts)
} }
fn do_login(account: &mut SteamGuardAccount) -> anyhow::Result<()> { fn do_login<T: Transport + Clone>(
transport: T,
account: &mut SteamGuardAccount,
) -> anyhow::Result<()> {
if let Some(tokens) = account.tokens.as_mut() { if let Some(tokens) = account.tokens.as_mut() {
info!("Refreshing access token..."); info!("Refreshing access token...");
let client = AuthenticationClient::new(WebApiTransport::default()); let client = AuthenticationClient::new(transport.clone());
let mut refresher = TokenRefresher::new(client); let mut refresher = TokenRefresher::new(client);
match refresher.refresh(account.steam_id, tokens) { match refresher.refresh(account.steam_id, tokens) {
Ok(token) => { Ok(token) => {
@ -304,14 +315,19 @@ fn do_login(account: &mut SteamGuardAccount) -> anyhow::Result<()> {
} else { } else {
debug!("password is empty"); debug!("password is empty");
} }
let tokens = do_login_impl(account.account_name.clone(), password, Some(account))?; let tokens = do_login_impl(
transport,
account.account_name.clone(),
password,
Some(account),
)?;
let steam_id = tokens.access_token().decode()?.steam_id(); let steam_id = tokens.access_token().decode()?.steam_id();
account.set_tokens(tokens); account.set_tokens(tokens);
account.steam_id = steam_id; account.steam_id = steam_id;
Ok(()) Ok(())
} }
fn do_login_raw(username: String) -> anyhow::Result<Tokens> { fn do_login_raw<T: Transport + Clone>(transport: T, username: String) -> anyhow::Result<Tokens> {
let _ = std::io::stdout().flush(); let _ = std::io::stdout().flush();
let password = rpassword::prompt_password_stdout("Password: ").unwrap(); let password = rpassword::prompt_password_stdout("Password: ").unwrap();
if !password.is_empty() { if !password.is_empty() {
@ -319,15 +335,16 @@ fn do_login_raw(username: String) -> anyhow::Result<Tokens> {
} else { } else {
debug!("password is empty"); debug!("password is empty");
} }
do_login_impl(username, password, None) do_login_impl(transport, username, password, None)
} }
fn do_login_impl( fn do_login_impl<T: Transport + Clone>(
transport: T,
username: String, username: String,
password: String, password: String,
account: Option<&SteamGuardAccount>, account: Option<&SteamGuardAccount>,
) -> anyhow::Result<Tokens> { ) -> anyhow::Result<Tokens> {
let mut login = UserLogin::new(WebApiTransport::default(), build_device_details()); let mut login = UserLogin::new(transport.clone(), build_device_details());
let mut password = password; let mut password = password;
let confirmation_methods; let confirmation_methods;
@ -375,7 +392,7 @@ fn do_login_impl(
EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode => { EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode => {
let code = if let Some(account) = account { let code = if let Some(account) = account {
debug!("Generating 2fa code..."); debug!("Generating 2fa code...");
let time = steamapi::get_server_time()?.server_time(); let time = steamapi::get_server_time(transport)?.server_time();
account.generate_code(time) account.generate_code(time)
} else { } else {
eprint!("Enter the 2fa code from your device: "); eprint!("Enter the 2fa code from your device: ");

View file

@ -10,20 +10,30 @@ use reqwest::{
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
use serde::Deserialize; use serde::Deserialize;
use crate::{steamapi, SteamGuardAccount}; use crate::{
steamapi::{self},
transport::Transport,
SteamGuardAccount,
};
lazy_static! { lazy_static! {
static ref STEAM_COOKIE_URL: Url = "https://steamcommunity.com".parse::<Url>().unwrap(); static ref STEAM_COOKIE_URL: Url = "https://steamcommunity.com".parse::<Url>().unwrap();
} }
/// Provides an interface that wraps the Steam mobile confirmation API. /// Provides an interface that wraps the Steam mobile confirmation API.
pub struct Confirmer<'a> { ///
/// Only compatible with WebApiTransport.
pub struct Confirmer<'a, T> {
account: &'a SteamGuardAccount, account: &'a SteamGuardAccount,
transport: T,
} }
impl<'a> Confirmer<'a> { impl<'a, T> Confirmer<'a, T>
pub fn new(account: &'a SteamGuardAccount) -> Self { where
Self { account } T: Transport + Clone,
{
pub fn new(transport: T, account: &'a SteamGuardAccount) -> Self {
Self { account, transport }
} }
fn get_confirmation_query_params<'q>( fn get_confirmation_query_params<'q>(
@ -72,11 +82,9 @@ impl<'a> Confirmer<'a> {
pub fn get_trade_confirmations(&self) -> Result<Vec<Confirmation>, ConfirmerError> { pub fn get_trade_confirmations(&self) -> Result<Vec<Confirmation>, ConfirmerError> {
let cookies = self.build_cookie_jar(); let cookies = self.build_cookie_jar();
let client = reqwest::blocking::ClientBuilder::new() let client = self.transport.innner_http_client()?;
.cookie_store(true)
.build()?;
let time = steamapi::get_server_time()?.server_time(); let time = steamapi::get_server_time(self.transport.clone())?.server_time();
let resp = client let resp = client
.get( .get(
"https://steamcommunity.com/mobileconf/getlist" "https://steamcommunity.com/mobileconf/getlist"
@ -117,11 +125,9 @@ impl<'a> Confirmer<'a> {
let operation = action.to_operation(); let operation = action.to_operation();
let cookies = self.build_cookie_jar(); let cookies = self.build_cookie_jar();
let client = reqwest::blocking::ClientBuilder::new() let client = self.transport.innner_http_client()?;
.cookie_store(true)
.build()?;
let time = steamapi::get_server_time()?.server_time(); let time = steamapi::get_server_time(self.transport.clone())?.server_time();
let mut query_params = self.get_confirmation_query_params("conf", time); let mut query_params = self.get_confirmation_query_params("conf", time);
query_params.push(("op", operation.into())); query_params.push(("op", operation.into()));
query_params.push(("cid", Cow::Borrowed(&conf.id))); query_params.push(("cid", Cow::Borrowed(&conf.id)));
@ -185,11 +191,9 @@ impl<'a> Confirmer<'a> {
let operation = action.to_operation(); let operation = action.to_operation();
let cookies = self.build_cookie_jar(); let cookies = self.build_cookie_jar();
let client = reqwest::blocking::ClientBuilder::new() let client = self.transport.innner_http_client()?;
.cookie_store(true)
.build()?;
let time = steamapi::get_server_time()?.server_time(); let time = steamapi::get_server_time(self.transport.clone())?.server_time();
let mut query_params = self.get_confirmation_query_params("conf", time); let mut query_params = self.get_confirmation_query_params("conf", time);
query_params.push(("op", operation.into())); query_params.push(("op", operation.into()));
for conf in confs.iter() { for conf in confs.iter() {
@ -262,11 +266,9 @@ impl<'a> Confirmer<'a> {
} }
let cookies = self.build_cookie_jar(); let cookies = self.build_cookie_jar();
let client = reqwest::blocking::ClientBuilder::new() let client = self.transport.innner_http_client()?;
.cookie_store(true)
.build()?;
let time = steamapi::get_server_time()?.server_time(); let time = steamapi::get_server_time(self.transport.clone())?.server_time();
let query_params = self.get_confirmation_query_params("details", time); let query_params = self.get_confirmation_query_params("details", time);
let resp = client let resp = client

View file

@ -1,8 +1,6 @@
use crate::protobufs::service_twofactor::CTwoFactor_RemoveAuthenticator_Request; use crate::protobufs::service_twofactor::CTwoFactor_RemoveAuthenticator_Request;
use crate::steamapi::EResult; use crate::steamapi::EResult;
use crate::{ use crate::{steamapi::twofactor::TwoFactorClient, token::TwoFactorSecret};
steamapi::twofactor::TwoFactorClient, token::TwoFactorSecret, transport::WebApiTransport,
};
pub use accountlinker::{AccountLinkError, AccountLinker, FinalizeLinkError}; pub use accountlinker::{AccountLinkError, AccountLinker, FinalizeLinkError};
pub use confirmation::*; pub use confirmation::*;
pub use qrapprover::{QrApprover, QrApproverError}; pub use qrapprover::{QrApprover, QrApproverError};
@ -10,7 +8,7 @@ pub use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io::Read; use std::io::Read;
use token::Tokens; use token::Tokens;
use transport::TransportError; use transport::{Transport, TransportError};
pub use userlogin::{DeviceDetails, LoginError, UserLogin}; pub use userlogin::{DeviceDetails, LoginError, UserLogin};
#[macro_use] #[macro_use]
@ -99,8 +97,9 @@ impl SteamGuardAccount {
/// Removes the mobile authenticator from the steam account. If this operation succeeds, this object can no longer be considered valid. /// Removes the mobile authenticator from the steam account. If this operation succeeds, this object can no longer be considered valid.
/// Returns whether or not the operation was successful. /// Returns whether or not the operation was successful.
pub fn remove_authenticator( pub fn remove_authenticator<T: Transport>(
&self, &self,
client: &TwoFactorClient<T>,
revocation_code: Option<&String>, revocation_code: Option<&String>,
) -> Result<(), RemoveAuthenticatorError> { ) -> Result<(), RemoveAuthenticatorError> {
if !matches!(revocation_code, Some(_)) && self.revocation_code.expose_secret().is_empty() { if !matches!(revocation_code, Some(_)) && self.revocation_code.expose_secret().is_empty() {
@ -109,7 +108,6 @@ impl SteamGuardAccount {
let Some(tokens) = &self.tokens else { let Some(tokens) = &self.tokens else {
return Err(RemoveAuthenticatorError::TransportError(TransportError::Unauthorized)); return Err(RemoveAuthenticatorError::TransportError(TransportError::Unauthorized));
}; };
let mut client = TwoFactorClient::new(WebApiTransport::default());
let mut req = CTwoFactor_RemoveAuthenticator_Request::new(); let mut req = CTwoFactor_RemoveAuthenticator_Request::new();
req.set_revocation_code( req.set_revocation_code(
revocation_code revocation_code

View file

@ -2,9 +2,8 @@ pub mod authentication;
pub mod phone; pub mod phone;
pub mod twofactor; pub mod twofactor;
use crate::{ use crate::transport::Transport;
protobufs::service_twofactor::CTwoFactor_Time_Response, token::Jwt, transport::WebApiTransport, use crate::{protobufs::service_twofactor::CTwoFactor_Time_Response, token::Jwt};
};
use reqwest::Url; use reqwest::Url;
use serde::Deserialize; use serde::Deserialize;
@ -20,8 +19,8 @@ lazy_static! {
/// Queries Steam for the current time. A convenience function around TwoFactorClient. /// Queries Steam for the current time. A convenience function around TwoFactorClient.
/// ///
/// Endpoint: `/ITwoFactorService/QueryTime/v0001` /// Endpoint: `/ITwoFactorService/QueryTime/v0001`
pub fn get_server_time() -> anyhow::Result<CTwoFactor_Time_Response> { pub fn get_server_time<T: Transport>(client: T) -> anyhow::Result<CTwoFactor_Time_Response> {
let mut client = TwoFactorClient::new(WebApiTransport::default()); let client = TwoFactorClient::new(client);
let resp = client.query_time()?; let resp = client.query_time()?;
if resp.result != EResult::OK { if resp.result != EResult::OK {
return Err(anyhow::anyhow!("QueryTime failed: {:?}", resp)); return Err(anyhow::anyhow!("QueryTime failed: {:?}", resp));

View file

@ -26,7 +26,7 @@ where
} }
pub fn add_authenticator( pub fn add_authenticator(
&mut self, &self,
req: CTwoFactor_AddAuthenticator_Request, req: CTwoFactor_AddAuthenticator_Request,
access_token: &Jwt, access_token: &Jwt,
) -> anyhow::Result<ApiResponse<CTwoFactor_AddAuthenticator_Response>> { ) -> anyhow::Result<ApiResponse<CTwoFactor_AddAuthenticator_Response>> {
@ -41,7 +41,7 @@ where
} }
pub fn finalize_authenticator( pub fn finalize_authenticator(
&mut self, &self,
req: CTwoFactor_FinalizeAddAuthenticator_Request, req: CTwoFactor_FinalizeAddAuthenticator_Request,
access_token: &Jwt, access_token: &Jwt,
) -> anyhow::Result<ApiResponse<CTwoFactor_FinalizeAddAuthenticator_Response>> { ) -> anyhow::Result<ApiResponse<CTwoFactor_FinalizeAddAuthenticator_Response>> {
@ -56,7 +56,7 @@ where
} }
pub fn remove_authenticator( pub fn remove_authenticator(
&mut self, &self,
req: CTwoFactor_RemoveAuthenticator_Request, req: CTwoFactor_RemoveAuthenticator_Request,
access_token: &Jwt, access_token: &Jwt,
) -> Result<ApiResponse<CTwoFactor_RemoveAuthenticator_Response>, TransportError> { ) -> Result<ApiResponse<CTwoFactor_RemoveAuthenticator_Response>, TransportError> {
@ -71,7 +71,7 @@ where
} }
pub fn query_status( pub fn query_status(
&mut self, &self,
req: CTwoFactor_Status_Request, req: CTwoFactor_Status_Request,
access_token: &Jwt, access_token: &Jwt,
) -> anyhow::Result<ApiResponse<CTwoFactor_Status_Response>> { ) -> anyhow::Result<ApiResponse<CTwoFactor_Status_Response>> {
@ -83,7 +83,7 @@ where
Ok(resp) Ok(resp)
} }
pub fn query_time(&mut self) -> anyhow::Result<ApiResponse<CTwoFactor_Time_Response>> { pub fn query_time(&self) -> anyhow::Result<ApiResponse<CTwoFactor_Time_Response>> {
let req = ApiRequest::new(SERVICE_NAME, "QueryTime", 1, CTwoFactor_Time_Request::new()); let req = ApiRequest::new(SERVICE_NAME, "QueryTime", 1, CTwoFactor_Time_Request::new());
let resp = self let resp = self
.transport .transport

View file

@ -12,6 +12,10 @@ pub trait Transport {
) -> Result<ApiResponse<Res>, TransportError>; ) -> Result<ApiResponse<Res>, TransportError>;
fn close(&mut self); fn close(&mut self);
fn innner_http_client(&self) -> anyhow::Result<reqwest::blocking::Client> {
bail!("Transport does not support extracting HTTP client")
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -10,31 +10,15 @@ lazy_static! {
static ref STEAM_API_BASE: String = "https://api.steampowered.com".into(); static ref STEAM_API_BASE: String = "https://api.steampowered.com".into();
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct WebApiTransport { pub struct WebApiTransport {
client: reqwest::blocking::Client, client: reqwest::blocking::Client,
} }
impl Default for WebApiTransport {
fn default() -> Self {
Self::new(reqwest::blocking::Client::new())
}
}
impl WebApiTransport { impl WebApiTransport {
pub fn new(client: reqwest::blocking::Client) -> Self { pub fn new(client: reqwest::blocking::Client) -> Self {
Self { client } Self { client }
} }
// pub fn new_with_proxy(proxy: &str) -> Self {
// Self {
// client: reqwest::blocking::Client::builder()
// // .danger_accept_invalid_certs(true)
// .proxy(reqwest::Proxy::all(proxy).unwrap())
// .build()
// .unwrap(),
// }
// }
} }
impl Transport for WebApiTransport { impl Transport for WebApiTransport {
@ -129,6 +113,10 @@ impl Transport for WebApiTransport {
} }
fn close(&mut self) {} fn close(&mut self) {}
fn innner_http_client(&self) -> anyhow::Result<reqwest::blocking::Client> {
Ok(self.client.clone())
}
} }
fn encode_msg<T: MessageFull>(msg: &T, config: base64::Config) -> anyhow::Result<String> { fn encode_msg<T: MessageFull>(msg: &T, config: base64::Config) -> anyhow::Result<String> {