add rustfmt.toml and run cargo fmt
This commit is contained in:
parent
52d247f102
commit
b57bc38341
8 changed files with 1123 additions and 1120 deletions
3
rustfmt.toml
Normal file
3
rustfmt.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
tab_spaces = 4
|
||||||
|
hard_tabs = true
|
||||||
|
normalize_comments = true
|
|
@ -7,81 +7,81 @@ use steamguard::{steamapi::Session, SteamGuardAccount};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AccountLinker {
|
pub struct AccountLinker {
|
||||||
device_id: String,
|
device_id: String,
|
||||||
phone_number: String,
|
phone_number: String,
|
||||||
pub account: SteamGuardAccount,
|
pub account: SteamGuardAccount,
|
||||||
client: reqwest::blocking::Client,
|
client: reqwest::blocking::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountLinker {
|
impl AccountLinker {
|
||||||
pub fn new() -> AccountLinker {
|
pub fn new() -> AccountLinker {
|
||||||
return AccountLinker {
|
return AccountLinker {
|
||||||
device_id: generate_device_id(),
|
device_id: generate_device_id(),
|
||||||
phone_number: String::from(""),
|
phone_number: String::from(""),
|
||||||
account: SteamGuardAccount::new(),
|
account: SteamGuardAccount::new(),
|
||||||
client: reqwest::blocking::ClientBuilder::new()
|
client: reqwest::blocking::ClientBuilder::new()
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn link(&self, session: &mut Session) {
|
pub fn link(&self, session: &mut Session) {
|
||||||
let mut params = HashMap::new();
|
let mut params = HashMap::new();
|
||||||
params.insert("access_token", session.token.clone());
|
params.insert("access_token", session.token.clone());
|
||||||
params.insert("steamid", session.steam_id.to_string());
|
params.insert("steamid", session.steam_id.to_string());
|
||||||
params.insert("device_identifier", self.device_id.clone());
|
params.insert("device_identifier", self.device_id.clone());
|
||||||
params.insert("authenticator_type", String::from("1"));
|
params.insert("authenticator_type", String::from("1"));
|
||||||
params.insert("sms_phone_id", String::from("1"));
|
params.insert("sms_phone_id", String::from("1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_phone(&self, session: &Session) -> bool {
|
fn has_phone(&self, session: &Session) -> bool {
|
||||||
return self._phoneajax(session, "has_phone", "null");
|
return self._phoneajax(session, "has_phone", "null");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _phoneajax(&self, session: &Session, op: &str, arg: &str) -> bool {
|
fn _phoneajax(&self, session: &Session, op: &str, arg: &str) -> bool {
|
||||||
trace!("_phoneajax: op={}", op);
|
trace!("_phoneajax: op={}", op);
|
||||||
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
||||||
let cookies = reqwest::cookie::Jar::default();
|
let cookies = reqwest::cookie::Jar::default();
|
||||||
cookies.add_cookie_str("mobileClientVersion=0 (2.1.3)", &url);
|
cookies.add_cookie_str("mobileClientVersion=0 (2.1.3)", &url);
|
||||||
cookies.add_cookie_str("mobileClient=android", &url);
|
cookies.add_cookie_str("mobileClient=android", &url);
|
||||||
cookies.add_cookie_str("Steam_Language=english", &url);
|
cookies.add_cookie_str("Steam_Language=english", &url);
|
||||||
|
|
||||||
let mut params = HashMap::new();
|
let mut params = HashMap::new();
|
||||||
params.insert("op", op);
|
params.insert("op", op);
|
||||||
params.insert("arg", arg);
|
params.insert("arg", arg);
|
||||||
params.insert("sessionid", session.session_id.as_str());
|
params.insert("sessionid", session.session_id.as_str());
|
||||||
if op == "check_sms_code" {
|
if op == "check_sms_code" {
|
||||||
params.insert("checkfortos", "0");
|
params.insert("checkfortos", "0");
|
||||||
params.insert("skipvoip", "1");
|
params.insert("skipvoip", "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = self
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.post("https://steamcommunity.com/steamguard/phoneajax")
|
.post("https://steamcommunity.com/steamguard/phoneajax")
|
||||||
.header(COOKIE, cookies.cookies(&url).unwrap())
|
.header(COOKIE, cookies.cookies(&url).unwrap())
|
||||||
.send()
|
.send()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result: Value = resp.json().unwrap();
|
let result: Value = resp.json().unwrap();
|
||||||
if result["has_phone"] != Value::Null {
|
if result["has_phone"] != Value::Null {
|
||||||
trace!("found has_phone field");
|
trace!("found has_phone field");
|
||||||
return result["has_phone"].as_bool().unwrap();
|
return result["has_phone"].as_bool().unwrap();
|
||||||
} else if result["success"] != Value::Null {
|
} else if result["success"] != Value::Null {
|
||||||
trace!("found success field");
|
trace!("found success field");
|
||||||
return result["success"].as_bool().unwrap();
|
return result["success"].as_bool().unwrap();
|
||||||
} else {
|
} else {
|
||||||
trace!("did not find any expected field");
|
trace!("did not find any expected field");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_device_id() -> String {
|
fn generate_device_id() -> String {
|
||||||
return format!("android:{}", uuid::Uuid::new_v4().to_string());
|
return format!("android:{}", uuid::Uuid::new_v4().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct AddAuthenticatorResponse {
|
pub struct AddAuthenticatorResponse {
|
||||||
pub response: SteamGuardAccount,
|
pub response: SteamGuardAccount,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,87 +8,87 @@ use steamguard::SteamGuardAccount;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Manifest {
|
pub struct Manifest {
|
||||||
pub encrypted: bool,
|
pub encrypted: bool,
|
||||||
pub entries: Vec<ManifestEntry>,
|
pub entries: Vec<ManifestEntry>,
|
||||||
pub first_run: bool,
|
pub first_run: bool,
|
||||||
pub periodic_checking: bool,
|
pub periodic_checking: bool,
|
||||||
pub periodic_checking_interval: i32,
|
pub periodic_checking_interval: i32,
|
||||||
pub periodic_checking_checkall: bool,
|
pub periodic_checking_checkall: bool,
|
||||||
pub auto_confirm_market_transactions: bool,
|
pub auto_confirm_market_transactions: bool,
|
||||||
pub auto_confirm_trades: bool,
|
pub auto_confirm_trades: bool,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
|
pub accounts: Vec<Arc<Mutex<SteamGuardAccount>>>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
folder: String, // I wanted to use a Path here, but it was too hard to make it work...
|
folder: String, // I wanted to use a Path here, but it was too hard to make it work...
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ManifestEntry {
|
pub struct ManifestEntry {
|
||||||
pub encryption_iv: Option<String>,
|
pub encryption_iv: Option<String>,
|
||||||
pub encryption_salt: Option<String>,
|
pub encryption_salt: Option<String>,
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
#[serde(rename = "steamid")]
|
#[serde(rename = "steamid")]
|
||||||
pub steam_id: u64,
|
pub steam_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Manifest {
|
impl Manifest {
|
||||||
pub fn load(path: &Path) -> anyhow::Result<Manifest> {
|
pub fn load(path: &Path) -> anyhow::Result<Manifest> {
|
||||||
debug!("loading manifest: {:?}", &path);
|
debug!("loading manifest: {:?}", &path);
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
let mut manifest: Manifest = serde_json::from_reader(reader)?;
|
let mut manifest: Manifest = serde_json::from_reader(reader)?;
|
||||||
manifest.folder = String::from(path.parent().unwrap().to_str().unwrap());
|
manifest.folder = String::from(path.parent().unwrap().to_str().unwrap());
|
||||||
return Ok(manifest);
|
return Ok(manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_accounts(&mut self) -> anyhow::Result<()> {
|
pub fn load_accounts(&mut self) -> anyhow::Result<()> {
|
||||||
for entry in &self.entries {
|
for entry in &self.entries {
|
||||||
let path = Path::new(&self.folder).join(&entry.filename);
|
let path = Path::new(&self.folder).join(&entry.filename);
|
||||||
debug!("loading account: {:?}", path);
|
debug!("loading account: {:?}", path);
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
let account: SteamGuardAccount = serde_json::from_reader(reader)?;
|
let account: SteamGuardAccount = serde_json::from_reader(reader)?;
|
||||||
self.accounts.push(Arc::new(Mutex::new(account)));
|
self.accounts.push(Arc::new(Mutex::new(account)));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_account(&mut self, account: SteamGuardAccount) {
|
pub fn add_account(&mut self, account: SteamGuardAccount) {
|
||||||
debug!("adding account to manifest: {}", account.account_name);
|
debug!("adding account to manifest: {}", account.account_name);
|
||||||
let steamid = account.session.clone().unwrap().steam_id;
|
let steamid = account.session.clone().unwrap().steam_id;
|
||||||
self.entries.push(ManifestEntry {
|
self.entries.push(ManifestEntry {
|
||||||
filename: format!("{}.maFile", &account.account_name),
|
filename: format!("{}.maFile", &account.account_name),
|
||||||
steam_id: steamid,
|
steam_id: steamid,
|
||||||
encryption_iv: None,
|
encryption_iv: None,
|
||||||
encryption_salt: None,
|
encryption_salt: None,
|
||||||
});
|
});
|
||||||
self.accounts.push(Arc::new(Mutex::new(account)));
|
self.accounts.push(Arc::new(Mutex::new(account)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> anyhow::Result<()> {
|
pub fn save(&self) -> anyhow::Result<()> {
|
||||||
ensure!(
|
ensure!(
|
||||||
self.entries.len() == self.accounts.len(),
|
self.entries.len() == self.accounts.len(),
|
||||||
"Manifest entries don't match accounts."
|
"Manifest entries don't match accounts."
|
||||||
);
|
);
|
||||||
for (entry, account) in self.entries.iter().zip(&self.accounts) {
|
for (entry, account) in self.entries.iter().zip(&self.accounts) {
|
||||||
debug!("saving {}", entry.filename);
|
debug!("saving {}", entry.filename);
|
||||||
let serialized = serde_json::to_string(account.as_ref())?;
|
let serialized = serde_json::to_string(account.as_ref())?;
|
||||||
ensure!(
|
ensure!(
|
||||||
serialized.len() > 2,
|
serialized.len() > 2,
|
||||||
"Something extra weird happened and the account was serialized into nothing."
|
"Something extra weird happened and the account was serialized into nothing."
|
||||||
);
|
);
|
||||||
let path = Path::new(&self.folder).join(&entry.filename);
|
let path = Path::new(&self.folder).join(&entry.filename);
|
||||||
let mut file = File::create(path)?;
|
let mut file = File::create(path)?;
|
||||||
file.write_all(serialized.as_bytes())?;
|
file.write_all(serialized.as_bytes())?;
|
||||||
file.sync_data()?;
|
file.sync_data()?;
|
||||||
}
|
}
|
||||||
debug!("saving manifest");
|
debug!("saving manifest");
|
||||||
let manifest_serialized = serde_json::to_string(&self)?;
|
let manifest_serialized = serde_json::to_string(&self)?;
|
||||||
let path = Path::new(&self.folder).join("manifest.json");
|
let path = Path::new(&self.folder).join("manifest.json");
|
||||||
let mut file = File::create(path)?;
|
let mut file = File::create(path)?;
|
||||||
file.write_all(manifest_serialized.as_bytes())?;
|
file.write_all(manifest_serialized.as_bytes())?;
|
||||||
file.sync_data()?;
|
file.sync_data()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
660
src/main.rs
660
src/main.rs
|
@ -4,18 +4,18 @@ use log::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::{
|
use std::{
|
||||||
io::{stdin, stdout, Write},
|
io::{stdin, stdout, Write},
|
||||||
path::Path,
|
path::Path,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
use steamguard::{
|
use steamguard::{
|
||||||
steamapi, Confirmation, ConfirmationType, LoginError, SteamGuardAccount, UserLogin,
|
steamapi, Confirmation, ConfirmationType, LoginError, SteamGuardAccount, UserLogin,
|
||||||
};
|
};
|
||||||
use termion::{
|
use termion::{
|
||||||
event::{Event, Key},
|
event::{Event, Key},
|
||||||
input::TermRead,
|
input::TermRead,
|
||||||
raw::IntoRawMode,
|
raw::IntoRawMode,
|
||||||
screen::AlternateScreen,
|
screen::AlternateScreen,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
@ -26,12 +26,12 @@ mod accountlinker;
|
||||||
mod accountmanager;
|
mod accountmanager;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CAPTCHA_VALID_CHARS: Regex =
|
static ref CAPTCHA_VALID_CHARS: Regex =
|
||||||
Regex::new("^([A-H]|[J-N]|[P-R]|[T-Z]|[2-4]|[7-9]|[@%&])+$").unwrap();
|
Regex::new("^([A-H]|[J-N]|[P-R]|[T-Z]|[2-4]|[7-9]|[@%&])+$").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let matches = App::new("steamguard-cli")
|
let matches = App::new("steamguard-cli")
|
||||||
.version(crate_version!())
|
.version(crate_version!())
|
||||||
.bin_name("steamguard")
|
.bin_name("steamguard")
|
||||||
.author("dyc3 (Carson McManus)")
|
.author("dyc3 (Carson McManus)")
|
||||||
|
@ -94,366 +94,366 @@ fn main() {
|
||||||
)
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let verbosity = matches.occurrences_of("verbosity") as usize + 2;
|
let verbosity = matches.occurrences_of("verbosity") as usize + 2;
|
||||||
stderrlog::new()
|
stderrlog::new()
|
||||||
.verbosity(verbosity)
|
.verbosity(verbosity)
|
||||||
.module(module_path!())
|
.module(module_path!())
|
||||||
.init()
|
.init()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
if let Some(demo_matches) = matches.subcommand_matches("debug") {
|
if let Some(demo_matches) = matches.subcommand_matches("debug") {
|
||||||
if demo_matches.is_present("demo-conf-menu") {
|
if demo_matches.is_present("demo-conf-menu") {
|
||||||
demo_confirmation_menu();
|
demo_confirmation_menu();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = Path::new(matches.value_of("mafiles-path").unwrap()).join("manifest.json");
|
let path = Path::new(matches.value_of("mafiles-path").unwrap()).join("manifest.json");
|
||||||
let mut manifest: accountmanager::Manifest;
|
let mut manifest: accountmanager::Manifest;
|
||||||
match accountmanager::Manifest::load(path.as_path()) {
|
match accountmanager::Manifest::load(path.as_path()) {
|
||||||
Ok(m) => {
|
Ok(m) => {
|
||||||
manifest = m;
|
manifest = m;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Could not load manifest: {}", e);
|
error!("Could not load manifest: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest.load_accounts();
|
manifest.load_accounts();
|
||||||
|
|
||||||
if matches.is_present("setup") {
|
if matches.is_present("setup") {
|
||||||
info!("setup");
|
info!("setup");
|
||||||
let mut linker = accountlinker::AccountLinker::new();
|
let mut linker = accountlinker::AccountLinker::new();
|
||||||
// do_login(&mut linker.account);
|
// do_login(&mut linker.account);
|
||||||
// linker.link(linker.account.session.expect("no login session"));
|
// linker.link(linker.account.session.expect("no login session"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>> = vec![];
|
let mut selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>> = vec![];
|
||||||
if matches.is_present("all") {
|
if matches.is_present("all") {
|
||||||
// manifest.accounts.iter().map(|a| selected_accounts.push(a.b));
|
// manifest.accounts.iter().map(|a| selected_accounts.push(a.b));
|
||||||
for account in &manifest.accounts {
|
for account in &manifest.accounts {
|
||||||
selected_accounts.push(account.clone());
|
selected_accounts.push(account.clone());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for account in &manifest.accounts {
|
for account in &manifest.accounts {
|
||||||
if !matches.is_present("username") {
|
if !matches.is_present("username") {
|
||||||
selected_accounts.push(account.clone());
|
selected_accounts.push(account.clone());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if matches.value_of("username").unwrap() == account.lock().unwrap().account_name {
|
if matches.value_of("username").unwrap() == account.lock().unwrap().account_name {
|
||||||
selected_accounts.push(account.clone());
|
selected_accounts.push(account.clone());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"selected accounts: {:?}",
|
"selected accounts: {:?}",
|
||||||
selected_accounts
|
selected_accounts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| a.lock().unwrap().account_name.clone())
|
.map(|a| a.lock().unwrap().account_name.clone())
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(trade_matches) = matches.subcommand_matches("trade") {
|
if let Some(trade_matches) = matches.subcommand_matches("trade") {
|
||||||
info!("trade");
|
info!("trade");
|
||||||
for a in selected_accounts.iter_mut() {
|
for a in selected_accounts.iter_mut() {
|
||||||
let mut account = a.lock().unwrap();
|
let mut account = a.lock().unwrap();
|
||||||
|
|
||||||
info!("Checking for trade confirmations");
|
info!("Checking for trade confirmations");
|
||||||
let confirmations: Vec<Confirmation>;
|
let confirmations: Vec<Confirmation>;
|
||||||
loop {
|
loop {
|
||||||
match account.get_trade_confirmations() {
|
match account.get_trade_confirmations() {
|
||||||
Ok(confs) => {
|
Ok(confs) => {
|
||||||
confirmations = confs;
|
confirmations = confs;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
info!("failed to get trade confirmations, asking user to log in");
|
info!("failed to get trade confirmations, asking user to log in");
|
||||||
do_login(&mut account);
|
do_login(&mut account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if trade_matches.is_present("accept-all") {
|
if trade_matches.is_present("accept-all") {
|
||||||
info!("accepting all confirmations");
|
info!("accepting all confirmations");
|
||||||
for conf in &confirmations {
|
for conf in &confirmations {
|
||||||
let result = account.accept_confirmation(conf);
|
let result = account.accept_confirmation(conf);
|
||||||
debug!("accept confirmation result: {:?}", result);
|
debug!("accept confirmation result: {:?}", result);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if termion::is_tty(&stdout()) {
|
if termion::is_tty(&stdout()) {
|
||||||
let (accept, deny) = prompt_confirmation_menu(confirmations);
|
let (accept, deny) = prompt_confirmation_menu(confirmations);
|
||||||
for conf in &accept {
|
for conf in &accept {
|
||||||
let result = account.accept_confirmation(conf);
|
let result = account.accept_confirmation(conf);
|
||||||
debug!("accept confirmation result: {:?}", result);
|
debug!("accept confirmation result: {:?}", result);
|
||||||
}
|
}
|
||||||
for conf in &deny {
|
for conf in &deny {
|
||||||
let result = account.deny_confirmation(conf);
|
let result = account.deny_confirmation(conf);
|
||||||
debug!("deny confirmation result: {:?}", result);
|
debug!("deny confirmation result: {:?}", result);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("not a tty, not showing menu");
|
warn!("not a tty, not showing menu");
|
||||||
for conf in &confirmations {
|
for conf in &confirmations {
|
||||||
println!("{}", conf.description());
|
println!("{}", conf.description());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest.save();
|
manifest.save();
|
||||||
} else {
|
} else {
|
||||||
let server_time = steamapi::get_server_time();
|
let server_time = steamapi::get_server_time();
|
||||||
for account in selected_accounts {
|
for account in selected_accounts {
|
||||||
trace!("{:?}", account);
|
trace!("{:?}", account);
|
||||||
let code = account.lock().unwrap().generate_code(server_time);
|
let code = account.lock().unwrap().generate_code(server_time);
|
||||||
println!("{}", code);
|
println!("{}", code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_captcha_text(text: &String) -> bool {
|
fn validate_captcha_text(text: &String) -> bool {
|
||||||
return CAPTCHA_VALID_CHARS.is_match(text);
|
return CAPTCHA_VALID_CHARS.is_match(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_captcha_text() {
|
fn test_validate_captcha_text() {
|
||||||
assert!(validate_captcha_text(&String::from("2WWUA@")));
|
assert!(validate_captcha_text(&String::from("2WWUA@")));
|
||||||
assert!(validate_captcha_text(&String::from("3G8HT2")));
|
assert!(validate_captcha_text(&String::from("3G8HT2")));
|
||||||
assert!(validate_captcha_text(&String::from("3J%@X3")));
|
assert!(validate_captcha_text(&String::from("3J%@X3")));
|
||||||
assert!(validate_captcha_text(&String::from("2GCZ4A")));
|
assert!(validate_captcha_text(&String::from("2GCZ4A")));
|
||||||
assert!(validate_captcha_text(&String::from("3G8HT2")));
|
assert!(validate_captcha_text(&String::from("3G8HT2")));
|
||||||
assert!(!validate_captcha_text(&String::from("asd823")));
|
assert!(!validate_captcha_text(&String::from("asd823")));
|
||||||
assert!(!validate_captcha_text(&String::from("!PQ4RD")));
|
assert!(!validate_captcha_text(&String::from("!PQ4RD")));
|
||||||
assert!(!validate_captcha_text(&String::from("1GQ4XZ")));
|
assert!(!validate_captcha_text(&String::from("1GQ4XZ")));
|
||||||
assert!(!validate_captcha_text(&String::from("8GO4XZ")));
|
assert!(!validate_captcha_text(&String::from("8GO4XZ")));
|
||||||
assert!(!validate_captcha_text(&String::from("IPQ4RD")));
|
assert!(!validate_captcha_text(&String::from("IPQ4RD")));
|
||||||
assert!(!validate_captcha_text(&String::from("0PT4RD")));
|
assert!(!validate_captcha_text(&String::from("0PT4RD")));
|
||||||
assert!(!validate_captcha_text(&String::from("APTSRD")));
|
assert!(!validate_captcha_text(&String::from("APTSRD")));
|
||||||
assert!(!validate_captcha_text(&String::from("AP5TRD")));
|
assert!(!validate_captcha_text(&String::from("AP5TRD")));
|
||||||
assert!(!validate_captcha_text(&String::from("AP6TRD")));
|
assert!(!validate_captcha_text(&String::from("AP6TRD")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prompt the user for text input.
|
/// Prompt the user for text input.
|
||||||
fn prompt() -> String {
|
fn prompt() -> String {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let _ = std::io::stdout().flush();
|
let _ = std::io::stdout().flush();
|
||||||
stdin()
|
stdin()
|
||||||
.read_line(&mut text)
|
.read_line(&mut text)
|
||||||
.expect("Did not enter a correct string");
|
.expect("Did not enter a correct string");
|
||||||
return String::from(text.strip_suffix('\n').unwrap());
|
return String::from(text.strip_suffix('\n').unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_captcha_text(captcha_gid: &String) -> String {
|
fn prompt_captcha_text(captcha_gid: &String) -> String {
|
||||||
println!("Captcha required. Open this link in your web browser: https://steamcommunity.com/public/captcha.php?gid={}", captcha_gid);
|
println!("Captcha required. Open this link in your web browser: https://steamcommunity.com/public/captcha.php?gid={}", captcha_gid);
|
||||||
let mut captcha_text;
|
let mut captcha_text;
|
||||||
loop {
|
loop {
|
||||||
print!("Enter captcha text: ");
|
print!("Enter captcha text: ");
|
||||||
captcha_text = prompt();
|
captcha_text = prompt();
|
||||||
if captcha_text.len() > 0 && validate_captcha_text(&captcha_text) {
|
if captcha_text.len() > 0 && validate_captcha_text(&captcha_text) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
warn!("Invalid chars for captcha text found in user's input. Prompting again...");
|
warn!("Invalid chars for captcha text found in user's input. Prompting again...");
|
||||||
}
|
}
|
||||||
return captcha_text;
|
return captcha_text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a tuple of (accepted, denied). Ignored confirmations are not included.
|
/// Returns a tuple of (accepted, denied). Ignored confirmations are not included.
|
||||||
fn prompt_confirmation_menu(
|
fn prompt_confirmation_menu(
|
||||||
confirmations: Vec<Confirmation>,
|
confirmations: Vec<Confirmation>,
|
||||||
) -> (Vec<Confirmation>, Vec<Confirmation>) {
|
) -> (Vec<Confirmation>, Vec<Confirmation>) {
|
||||||
println!("press a key other than enter to show the menu.");
|
println!("press a key other than enter to show the menu.");
|
||||||
let mut to_accept_idx: HashSet<usize> = HashSet::new();
|
let mut to_accept_idx: HashSet<usize> = HashSet::new();
|
||||||
let mut to_deny_idx: HashSet<usize> = HashSet::new();
|
let mut to_deny_idx: HashSet<usize> = HashSet::new();
|
||||||
|
|
||||||
let mut screen = AlternateScreen::from(stdout().into_raw_mode().unwrap());
|
let mut screen = AlternateScreen::from(stdout().into_raw_mode().unwrap());
|
||||||
let stdin = stdin();
|
let stdin = stdin();
|
||||||
|
|
||||||
let mut selected_idx = 0;
|
let mut selected_idx = 0;
|
||||||
|
|
||||||
for c in stdin.events() {
|
for c in stdin.events() {
|
||||||
match c.expect("could not get events") {
|
match c.expect("could not get events") {
|
||||||
Event::Key(Key::Char('a')) => {
|
Event::Key(Key::Char('a')) => {
|
||||||
to_accept_idx.insert(selected_idx);
|
to_accept_idx.insert(selected_idx);
|
||||||
to_deny_idx.remove(&selected_idx);
|
to_deny_idx.remove(&selected_idx);
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('d')) => {
|
Event::Key(Key::Char('d')) => {
|
||||||
to_accept_idx.remove(&selected_idx);
|
to_accept_idx.remove(&selected_idx);
|
||||||
to_deny_idx.insert(selected_idx);
|
to_deny_idx.insert(selected_idx);
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('i')) => {
|
Event::Key(Key::Char('i')) => {
|
||||||
to_accept_idx.remove(&selected_idx);
|
to_accept_idx.remove(&selected_idx);
|
||||||
to_deny_idx.remove(&selected_idx);
|
to_deny_idx.remove(&selected_idx);
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('A')) => {
|
Event::Key(Key::Char('A')) => {
|
||||||
(0..confirmations.len()).for_each(|i| {
|
(0..confirmations.len()).for_each(|i| {
|
||||||
to_accept_idx.insert(i);
|
to_accept_idx.insert(i);
|
||||||
to_deny_idx.remove(&i);
|
to_deny_idx.remove(&i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('D')) => {
|
Event::Key(Key::Char('D')) => {
|
||||||
(0..confirmations.len()).for_each(|i| {
|
(0..confirmations.len()).for_each(|i| {
|
||||||
to_accept_idx.remove(&i);
|
to_accept_idx.remove(&i);
|
||||||
to_deny_idx.insert(i);
|
to_deny_idx.insert(i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('I')) => {
|
Event::Key(Key::Char('I')) => {
|
||||||
(0..confirmations.len()).for_each(|i| {
|
(0..confirmations.len()).for_each(|i| {
|
||||||
to_accept_idx.remove(&i);
|
to_accept_idx.remove(&i);
|
||||||
to_deny_idx.remove(&i);
|
to_deny_idx.remove(&i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Event::Key(Key::Up) if selected_idx > 0 => {
|
Event::Key(Key::Up) if selected_idx > 0 => {
|
||||||
selected_idx -= 1;
|
selected_idx -= 1;
|
||||||
}
|
}
|
||||||
Event::Key(Key::Down) if selected_idx < confirmations.len() - 1 => {
|
Event::Key(Key::Down) if selected_idx < confirmations.len() - 1 => {
|
||||||
selected_idx += 1;
|
selected_idx += 1;
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('\n')) => {
|
Event::Key(Key::Char('\n')) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Event::Key(Key::Esc) | Event::Key(Key::Ctrl('c')) => {
|
Event::Key(Key::Esc) | Event::Key(Key::Ctrl('c')) => {
|
||||||
return (vec![], vec![]);
|
return (vec![], vec![]);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(
|
write!(
|
||||||
screen,
|
screen,
|
||||||
"{}{}{}arrow keys to select, [a]ccept, [d]eny, [i]gnore, [enter] confirm choices\n\n",
|
"{}{}{}arrow keys to select, [a]ccept, [d]eny, [i]gnore, [enter] confirm choices\n\n",
|
||||||
termion::clear::All,
|
termion::clear::All,
|
||||||
termion::cursor::Goto(1, 1),
|
termion::cursor::Goto(1, 1),
|
||||||
termion::color::Fg(termion::color::White)
|
termion::color::Fg(termion::color::White)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for i in 0..confirmations.len() {
|
for i in 0..confirmations.len() {
|
||||||
if selected_idx == i {
|
if selected_idx == i {
|
||||||
write!(
|
write!(
|
||||||
screen,
|
screen,
|
||||||
"\r{} >",
|
"\r{} >",
|
||||||
termion::color::Fg(termion::color::LightYellow)
|
termion::color::Fg(termion::color::LightYellow)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else {
|
} else {
|
||||||
write!(screen, "\r{} ", termion::color::Fg(termion::color::White)).unwrap();
|
write!(screen, "\r{} ", termion::color::Fg(termion::color::White)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if to_accept_idx.contains(&i) {
|
if to_accept_idx.contains(&i) {
|
||||||
write!(
|
write!(
|
||||||
screen,
|
screen,
|
||||||
"{}[a]",
|
"{}[a]",
|
||||||
termion::color::Fg(termion::color::LightGreen)
|
termion::color::Fg(termion::color::LightGreen)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else if to_deny_idx.contains(&i) {
|
} else if to_deny_idx.contains(&i) {
|
||||||
write!(
|
write!(
|
||||||
screen,
|
screen,
|
||||||
"{}[d]",
|
"{}[d]",
|
||||||
termion::color::Fg(termion::color::LightRed)
|
termion::color::Fg(termion::color::LightRed)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else {
|
} else {
|
||||||
write!(screen, "[ ]").unwrap();
|
write!(screen, "[ ]").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if selected_idx == i {
|
if selected_idx == i {
|
||||||
write!(
|
write!(
|
||||||
screen,
|
screen,
|
||||||
"{}",
|
"{}",
|
||||||
termion::color::Fg(termion::color::LightYellow)
|
termion::color::Fg(termion::color::LightYellow)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(screen, " {}\n", confirmations[i].description()).unwrap();
|
write!(screen, " {}\n", confirmations[i].description()).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
to_accept_idx.iter().map(|i| confirmations[*i]).collect(),
|
to_accept_idx.iter().map(|i| confirmations[*i]).collect(),
|
||||||
to_deny_idx.iter().map(|i| confirmations[*i]).collect(),
|
to_deny_idx.iter().map(|i| confirmations[*i]).collect(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_login(account: &mut SteamGuardAccount) {
|
fn do_login(account: &mut SteamGuardAccount) {
|
||||||
if account.account_name.len() > 0 {
|
if account.account_name.len() > 0 {
|
||||||
println!("Username: {}", account.account_name);
|
println!("Username: {}", account.account_name);
|
||||||
} else {
|
} else {
|
||||||
print!("Username: ");
|
print!("Username: ");
|
||||||
account.account_name = prompt();
|
account.account_name = prompt();
|
||||||
}
|
}
|
||||||
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.len() > 0 {
|
if password.len() > 0 {
|
||||||
debug!("password is present");
|
debug!("password is present");
|
||||||
} else {
|
} else {
|
||||||
debug!("password is empty");
|
debug!("password is empty");
|
||||||
}
|
}
|
||||||
// TODO: reprompt if password is empty
|
// TODO: reprompt if password is empty
|
||||||
let mut login = UserLogin::new(account.account_name.clone(), password);
|
let mut login = UserLogin::new(account.account_name.clone(), password);
|
||||||
let mut loops = 0;
|
let mut loops = 0;
|
||||||
loop {
|
loop {
|
||||||
match login.login() {
|
match login.login() {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
account.session = Option::Some(s);
|
account.session = Option::Some(s);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(LoginError::Need2FA) => {
|
Err(LoginError::Need2FA) => {
|
||||||
debug!("generating 2fa code and retrying");
|
debug!("generating 2fa code and retrying");
|
||||||
let server_time = steamapi::get_server_time();
|
let server_time = steamapi::get_server_time();
|
||||||
login.twofactor_code = account.generate_code(server_time);
|
login.twofactor_code = account.generate_code(server_time);
|
||||||
}
|
}
|
||||||
Err(LoginError::NeedCaptcha { captcha_gid }) => {
|
Err(LoginError::NeedCaptcha { captcha_gid }) => {
|
||||||
debug!("need captcha to log in");
|
debug!("need captcha to log in");
|
||||||
login.captcha_text = prompt_captcha_text(&captcha_gid);
|
login.captcha_text = prompt_captcha_text(&captcha_gid);
|
||||||
}
|
}
|
||||||
Err(LoginError::NeedEmail) => {
|
Err(LoginError::NeedEmail) => {
|
||||||
println!("You should have received an email with a code.");
|
println!("You should have received an email with a code.");
|
||||||
print!("Enter code");
|
print!("Enter code");
|
||||||
login.email_code = prompt();
|
login.email_code = prompt();
|
||||||
}
|
}
|
||||||
r => {
|
r => {
|
||||||
error!("Fatal login result: {:?}", r);
|
error!("Fatal login result: {:?}", r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loops += 1;
|
loops += 1;
|
||||||
if loops > 2 {
|
if loops > 2 {
|
||||||
error!("Too many loops. Aborting login process, to avoid getting rate limited.");
|
error!("Too many loops. Aborting login process, to avoid getting rate limited.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn demo_confirmation_menu() {
|
fn demo_confirmation_menu() {
|
||||||
info!("showing demo menu");
|
info!("showing demo menu");
|
||||||
let (accept, deny) = prompt_confirmation_menu(vec![
|
let (accept, deny) = prompt_confirmation_menu(vec![
|
||||||
Confirmation {
|
Confirmation {
|
||||||
id: 1234,
|
id: 1234,
|
||||||
key: 12345,
|
key: 12345,
|
||||||
conf_type: ConfirmationType::Trade,
|
conf_type: ConfirmationType::Trade,
|
||||||
creator: 09870987,
|
creator: 09870987,
|
||||||
},
|
},
|
||||||
Confirmation {
|
Confirmation {
|
||||||
id: 1234,
|
id: 1234,
|
||||||
key: 12345,
|
key: 12345,
|
||||||
conf_type: ConfirmationType::MarketSell,
|
conf_type: ConfirmationType::MarketSell,
|
||||||
creator: 09870987,
|
creator: 09870987,
|
||||||
},
|
},
|
||||||
Confirmation {
|
Confirmation {
|
||||||
id: 1234,
|
id: 1234,
|
||||||
key: 12345,
|
key: 12345,
|
||||||
conf_type: ConfirmationType::AccountRecovery,
|
conf_type: ConfirmationType::AccountRecovery,
|
||||||
creator: 09870987,
|
creator: 09870987,
|
||||||
},
|
},
|
||||||
Confirmation {
|
Confirmation {
|
||||||
id: 1234,
|
id: 1234,
|
||||||
key: 12345,
|
key: 12345,
|
||||||
conf_type: ConfirmationType::Trade,
|
conf_type: ConfirmationType::Trade,
|
||||||
creator: 09870987,
|
creator: 09870987,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
println!("accept: {}, deny: {}", accept.len(), deny.len());
|
println!("accept: {}, deny: {}", accept.len(), deny.len());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,37 @@
|
||||||
/// A mobile confirmation. There are multiple things that can be confirmed, like trade offers.
|
/// A mobile confirmation. There are multiple things that can be confirmed, like trade offers.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Confirmation {
|
pub struct Confirmation {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub key: u64,
|
pub key: u64,
|
||||||
/// Trade offer ID or market transaction ID
|
/// Trade offer ID or market transaction ID
|
||||||
pub creator: u64,
|
pub creator: u64,
|
||||||
pub conf_type: ConfirmationType,
|
pub conf_type: ConfirmationType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Confirmation {
|
impl Confirmation {
|
||||||
/// Human readable representation of this confirmation.
|
/// Human readable representation of this confirmation.
|
||||||
pub fn description(&self) -> String {
|
pub fn description(&self) -> String {
|
||||||
format!("{:?} id={} key={}", self.conf_type, self.id, self.key)
|
format!("{:?} id={} key={}", self.conf_type, self.id, self.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ConfirmationType {
|
pub enum ConfirmationType {
|
||||||
Generic = 1,
|
Generic = 1,
|
||||||
Trade = 2,
|
Trade = 2,
|
||||||
MarketSell = 3,
|
MarketSell = 3,
|
||||||
AccountRecovery = 6,
|
AccountRecovery = 6,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for ConfirmationType {
|
impl From<&str> for ConfirmationType {
|
||||||
fn from(text: &str) -> Self {
|
fn from(text: &str) -> Self {
|
||||||
match text {
|
match text {
|
||||||
"1" => ConfirmationType::Generic,
|
"1" => ConfirmationType::Generic,
|
||||||
"2" => ConfirmationType::Trade,
|
"2" => ConfirmationType::Trade,
|
||||||
"3" => ConfirmationType::MarketSell,
|
"3" => ConfirmationType::MarketSell,
|
||||||
"6" => ConfirmationType::AccountRecovery,
|
"6" => ConfirmationType::AccountRecovery,
|
||||||
_ => ConfirmationType::Unknown,
|
_ => ConfirmationType::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ pub use confirmation::{Confirmation, ConfirmationType};
|
||||||
use hmacsha1::hmac_sha1;
|
use hmacsha1::hmac_sha1;
|
||||||
use log::*;
|
use log::*;
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
cookie::CookieStore,
|
cookie::CookieStore,
|
||||||
header::{COOKIE, USER_AGENT},
|
header::{COOKIE, USER_AGENT},
|
||||||
Url,
|
Url,
|
||||||
};
|
};
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -35,128 +35,128 @@ extern crate hmacsha1;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SteamGuardAccount {
|
pub struct SteamGuardAccount {
|
||||||
pub account_name: String,
|
pub account_name: String,
|
||||||
pub serial_number: String,
|
pub serial_number: String,
|
||||||
pub revocation_code: String,
|
pub revocation_code: String,
|
||||||
pub shared_secret: String,
|
pub shared_secret: String,
|
||||||
pub token_gid: String,
|
pub token_gid: String,
|
||||||
pub identity_secret: String,
|
pub identity_secret: String,
|
||||||
pub server_time: u64,
|
pub server_time: u64,
|
||||||
pub uri: String,
|
pub uri: String,
|
||||||
pub fully_enrolled: bool,
|
pub fully_enrolled: bool,
|
||||||
pub device_id: String,
|
pub device_id: String,
|
||||||
#[serde(rename = "Session")]
|
#[serde(rename = "Session")]
|
||||||
pub session: Option<steamapi::Session>,
|
pub session: Option<steamapi::Session>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_time_bytes(time: i64) -> [u8; 8] {
|
fn build_time_bytes(time: i64) -> [u8; 8] {
|
||||||
return time.to_be_bytes();
|
return time.to_be_bytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_shared_secret(secret: String) -> anyhow::Result<[u8; 20]> {
|
pub fn parse_shared_secret(secret: String) -> anyhow::Result<[u8; 20]> {
|
||||||
ensure!(secret.len() != 0, "unable to parse empty shared secret");
|
ensure!(secret.len() != 0, "unable to parse empty shared secret");
|
||||||
let result = base64::decode(secret)?.try_into();
|
let result = base64::decode(secret)?.try_into();
|
||||||
return Ok(result.unwrap());
|
return Ok(result.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_confirmation_hash_for_time(time: i64, tag: &str, identity_secret: &String) -> String {
|
fn generate_confirmation_hash_for_time(time: i64, tag: &str, identity_secret: &String) -> String {
|
||||||
let decode: &[u8] = &base64::decode(&identity_secret).unwrap();
|
let decode: &[u8] = &base64::decode(&identity_secret).unwrap();
|
||||||
let time_bytes = build_time_bytes(time);
|
let time_bytes = build_time_bytes(time);
|
||||||
let tag_bytes = tag.as_bytes();
|
let tag_bytes = tag.as_bytes();
|
||||||
let array = [&time_bytes, tag_bytes].concat();
|
let array = [&time_bytes, tag_bytes].concat();
|
||||||
let hash = hmac_sha1(decode, &array);
|
let hash = hmac_sha1(decode, &array);
|
||||||
let encoded = base64::encode(hash);
|
let encoded = base64::encode(hash);
|
||||||
return encoded;
|
return encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SteamGuardAccount {
|
impl SteamGuardAccount {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
return SteamGuardAccount {
|
return SteamGuardAccount {
|
||||||
account_name: String::from(""),
|
account_name: String::from(""),
|
||||||
serial_number: String::from(""),
|
serial_number: String::from(""),
|
||||||
revocation_code: String::from(""),
|
revocation_code: String::from(""),
|
||||||
shared_secret: String::from(""),
|
shared_secret: String::from(""),
|
||||||
token_gid: String::from(""),
|
token_gid: String::from(""),
|
||||||
identity_secret: String::from(""),
|
identity_secret: String::from(""),
|
||||||
server_time: 0,
|
server_time: 0,
|
||||||
uri: String::from(""),
|
uri: String::from(""),
|
||||||
fully_enrolled: false,
|
fully_enrolled: false,
|
||||||
device_id: String::from(""),
|
device_id: String::from(""),
|
||||||
session: Option::None,
|
session: Option::None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_code(&self, time: i64) -> String {
|
pub fn generate_code(&self, time: i64) -> String {
|
||||||
let steam_guard_code_translations: [u8; 26] = [
|
let steam_guard_code_translations: [u8; 26] = [
|
||||||
50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84,
|
50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84,
|
||||||
86, 87, 88, 89,
|
86, 87, 88, 89,
|
||||||
];
|
];
|
||||||
|
|
||||||
// this effectively makes it so that it creates a new code every 30 seconds.
|
// this effectively makes it so that it creates a new code every 30 seconds.
|
||||||
let time_bytes: [u8; 8] = build_time_bytes(time / 30i64);
|
let time_bytes: [u8; 8] = build_time_bytes(time / 30i64);
|
||||||
let shared_secret: [u8; 20] = parse_shared_secret(self.shared_secret.clone()).unwrap();
|
let shared_secret: [u8; 20] = parse_shared_secret(self.shared_secret.clone()).unwrap();
|
||||||
let hashed_data = hmacsha1::hmac_sha1(&shared_secret, &time_bytes);
|
let hashed_data = hmacsha1::hmac_sha1(&shared_secret, &time_bytes);
|
||||||
let mut code_array: [u8; 5] = [0; 5];
|
let mut code_array: [u8; 5] = [0; 5];
|
||||||
let b = (hashed_data[19] & 0xF) as usize;
|
let b = (hashed_data[19] & 0xF) as usize;
|
||||||
let mut code_point: i32 = ((hashed_data[b] & 0x7F) as i32) << 24
|
let mut code_point: i32 = ((hashed_data[b] & 0x7F) as i32) << 24
|
||||||
| ((hashed_data[b + 1] & 0xFF) as i32) << 16
|
| ((hashed_data[b + 1] & 0xFF) as i32) << 16
|
||||||
| ((hashed_data[b + 2] & 0xFF) as i32) << 8
|
| ((hashed_data[b + 2] & 0xFF) as i32) << 8
|
||||||
| ((hashed_data[b + 3] & 0xFF) as i32);
|
| ((hashed_data[b + 3] & 0xFF) as i32);
|
||||||
|
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
code_array[i] = steam_guard_code_translations
|
code_array[i] = steam_guard_code_translations
|
||||||
[code_point as usize % steam_guard_code_translations.len()];
|
[code_point as usize % steam_guard_code_translations.len()];
|
||||||
code_point /= steam_guard_code_translations.len() as i32;
|
code_point /= steam_guard_code_translations.len() as i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
return String::from_utf8(code_array.iter().map(|c| *c).collect()).unwrap();
|
return String::from_utf8(code_array.iter().map(|c| *c).collect()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_confirmation_query_params(&self, tag: &str) -> HashMap<&str, String> {
|
fn get_confirmation_query_params(&self, tag: &str) -> HashMap<&str, String> {
|
||||||
let session = self.session.clone().unwrap();
|
let session = self.session.clone().unwrap();
|
||||||
let time = steamapi::get_server_time();
|
let time = steamapi::get_server_time();
|
||||||
let mut params = HashMap::new();
|
let mut params = HashMap::new();
|
||||||
params.insert("p", self.device_id.clone());
|
params.insert("p", self.device_id.clone());
|
||||||
params.insert("a", session.steam_id.to_string());
|
params.insert("a", session.steam_id.to_string());
|
||||||
params.insert(
|
params.insert(
|
||||||
"k",
|
"k",
|
||||||
generate_confirmation_hash_for_time(time, tag, &self.identity_secret),
|
generate_confirmation_hash_for_time(time, tag, &self.identity_secret),
|
||||||
);
|
);
|
||||||
params.insert("t", time.to_string());
|
params.insert("t", time.to_string());
|
||||||
params.insert("m", String::from("android"));
|
params.insert("m", String::from("android"));
|
||||||
params.insert("tag", String::from(tag));
|
params.insert("tag", String::from(tag));
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_cookie_jar(&self) -> reqwest::cookie::Jar {
|
fn build_cookie_jar(&self) -> reqwest::cookie::Jar {
|
||||||
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
||||||
let cookies = reqwest::cookie::Jar::default();
|
let cookies = reqwest::cookie::Jar::default();
|
||||||
let session = self.session.clone().unwrap();
|
let session = self.session.clone().unwrap();
|
||||||
let session_id = session.session_id;
|
let session_id = session.session_id;
|
||||||
cookies.add_cookie_str("mobileClientVersion=0 (2.1.3)", &url);
|
cookies.add_cookie_str("mobileClientVersion=0 (2.1.3)", &url);
|
||||||
cookies.add_cookie_str("mobileClient=android", &url);
|
cookies.add_cookie_str("mobileClient=android", &url);
|
||||||
cookies.add_cookie_str("Steam_Language=english", &url);
|
cookies.add_cookie_str("Steam_Language=english", &url);
|
||||||
cookies.add_cookie_str("dob=", &url);
|
cookies.add_cookie_str("dob=", &url);
|
||||||
cookies.add_cookie_str(format!("sessionid={}", session_id).as_str(), &url);
|
cookies.add_cookie_str(format!("sessionid={}", session_id).as_str(), &url);
|
||||||
cookies.add_cookie_str(format!("steamid={}", session.steam_id).as_str(), &url);
|
cookies.add_cookie_str(format!("steamid={}", session.steam_id).as_str(), &url);
|
||||||
cookies.add_cookie_str(format!("steamLogin={}", session.steam_login).as_str(), &url);
|
cookies.add_cookie_str(format!("steamLogin={}", session.steam_login).as_str(), &url);
|
||||||
cookies.add_cookie_str(
|
cookies.add_cookie_str(
|
||||||
format!("steamLoginSecure={}", session.steam_login_secure).as_str(),
|
format!("steamLoginSecure={}", session.steam_login_secure).as_str(),
|
||||||
&url,
|
&url,
|
||||||
);
|
);
|
||||||
return cookies;
|
return cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_trade_confirmations(&self) -> Result<Vec<Confirmation>, anyhow::Error> {
|
pub fn get_trade_confirmations(&self) -> Result<Vec<Confirmation>, anyhow::Error> {
|
||||||
// uri: "https://steamcommunity.com/mobileconf/conf"
|
// uri: "https://steamcommunity.com/mobileconf/conf"
|
||||||
// confirmation details:
|
// confirmation details:
|
||||||
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
||||||
let cookies = self.build_cookie_jar();
|
let cookies = self.build_cookie_jar();
|
||||||
let client = reqwest::blocking::ClientBuilder::new()
|
let client = reqwest::blocking::ClientBuilder::new()
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.get("https://steamcommunity.com/mobileconf/conf".parse::<Url>().unwrap())
|
.get("https://steamcommunity.com/mobileconf/conf".parse::<Url>().unwrap())
|
||||||
.header("X-Requested-With", "com.valvesoftware.android.steam.community")
|
.header("X-Requested-With", "com.valvesoftware.android.steam.community")
|
||||||
.header(USER_AGENT, "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30")
|
.header(USER_AGENT, "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30")
|
||||||
|
@ -164,37 +164,37 @@ impl SteamGuardAccount {
|
||||||
.query(&self.get_confirmation_query_params("conf"))
|
.query(&self.get_confirmation_query_params("conf"))
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
trace!("{:?}", resp);
|
trace!("{:?}", resp);
|
||||||
let text = resp.text().unwrap();
|
let text = resp.text().unwrap();
|
||||||
trace!("text: {:?}", text);
|
trace!("text: {:?}", text);
|
||||||
println!("{}", text);
|
println!("{}", text);
|
||||||
return parse_confirmations(text);
|
return parse_confirmations(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Respond to a confirmation.
|
/// Respond to a confirmation.
|
||||||
///
|
///
|
||||||
/// Host: https://steamcommunity.com
|
/// Host: https://steamcommunity.com
|
||||||
/// Steam Endpoint: `GET /mobileconf/ajaxop`
|
/// Steam Endpoint: `GET /mobileconf/ajaxop`
|
||||||
fn send_confirmation_ajax(&self, conf: &Confirmation, operation: String) -> anyhow::Result<()> {
|
fn send_confirmation_ajax(&self, conf: &Confirmation, operation: String) -> anyhow::Result<()> {
|
||||||
ensure!(operation == "allow" || operation == "cancel");
|
ensure!(operation == "allow" || operation == "cancel");
|
||||||
|
|
||||||
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
||||||
let cookies = self.build_cookie_jar();
|
let cookies = self.build_cookie_jar();
|
||||||
let client = reqwest::blocking::ClientBuilder::new()
|
let client = reqwest::blocking::ClientBuilder::new()
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let mut query_params = self.get_confirmation_query_params("conf");
|
let mut query_params = self.get_confirmation_query_params("conf");
|
||||||
query_params.insert("op", operation);
|
query_params.insert("op", operation);
|
||||||
query_params.insert("cid", conf.id.to_string());
|
query_params.insert("cid", conf.id.to_string());
|
||||||
query_params.insert("ck", conf.key.to_string());
|
query_params.insert("ck", conf.key.to_string());
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||||
struct SendConfirmationResponse {
|
struct SendConfirmationResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp: SendConfirmationResponse = client.get("https://steamcommunity.com/mobileconf/ajaxop".parse::<Url>().unwrap())
|
let resp: SendConfirmationResponse = client.get("https://steamcommunity.com/mobileconf/ajaxop".parse::<Url>().unwrap())
|
||||||
.header("X-Requested-With", "com.valvesoftware.android.steam.community")
|
.header("X-Requested-With", "com.valvesoftware.android.steam.community")
|
||||||
.header(USER_AGENT, "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30")
|
.header(USER_AGENT, "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30")
|
||||||
.header(COOKIE, cookies.cookies(&url).unwrap())
|
.header(COOKIE, cookies.cookies(&url).unwrap())
|
||||||
|
@ -202,35 +202,35 @@ impl SteamGuardAccount {
|
||||||
.send()?
|
.send()?
|
||||||
.json()?;
|
.json()?;
|
||||||
|
|
||||||
ensure!(resp.success);
|
ensure!(resp.success);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accept_confirmation(&self, conf: &Confirmation) -> anyhow::Result<()> {
|
pub fn accept_confirmation(&self, conf: &Confirmation) -> anyhow::Result<()> {
|
||||||
self.send_confirmation_ajax(conf, "allow".into())
|
self.send_confirmation_ajax(conf, "allow".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deny_confirmation(&self, conf: &Confirmation) -> anyhow::Result<()> {
|
pub fn deny_confirmation(&self, conf: &Confirmation) -> anyhow::Result<()> {
|
||||||
self.send_confirmation_ajax(conf, "cancel".into())
|
self.send_confirmation_ajax(conf, "cancel".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Steam Endpoint: `GET /mobileconf/details/:id`
|
/// Steam Endpoint: `GET /mobileconf/details/:id`
|
||||||
pub fn get_confirmation_details(&self, conf: &Confirmation) -> anyhow::Result<String> {
|
pub fn get_confirmation_details(&self, conf: &Confirmation) -> anyhow::Result<String> {
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
struct ConfirmationDetailsResponse {
|
struct ConfirmationDetailsResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
pub html: String,
|
pub html: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
let url = "https://steamcommunity.com".parse::<Url>().unwrap();
|
||||||
let cookies = self.build_cookie_jar();
|
let cookies = self.build_cookie_jar();
|
||||||
let client = reqwest::blocking::ClientBuilder::new()
|
let client = reqwest::blocking::ClientBuilder::new()
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let query_params = self.get_confirmation_query_params("details");
|
let query_params = self.get_confirmation_query_params("details");
|
||||||
|
|
||||||
let resp: ConfirmationDetailsResponse = client.get(format!("https://steamcommunity.com/mobileconf/details/{}", conf.id).parse::<Url>().unwrap())
|
let resp: ConfirmationDetailsResponse = client.get(format!("https://steamcommunity.com/mobileconf/details/{}", conf.id).parse::<Url>().unwrap())
|
||||||
.header("X-Requested-With", "com.valvesoftware.android.steam.community")
|
.header("X-Requested-With", "com.valvesoftware.android.steam.community")
|
||||||
.header(USER_AGENT, "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30")
|
.header(USER_AGENT, "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30")
|
||||||
.header(COOKIE, cookies.cookies(&url).unwrap())
|
.header(COOKIE, cookies.cookies(&url).unwrap())
|
||||||
|
@ -238,125 +238,125 @@ impl SteamGuardAccount {
|
||||||
.send()?
|
.send()?
|
||||||
.json()?;
|
.json()?;
|
||||||
|
|
||||||
ensure!(resp.success);
|
ensure!(resp.success);
|
||||||
Ok(resp.html)
|
Ok(resp.html)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_confirmations(text: String) -> anyhow::Result<Vec<Confirmation>> {
|
fn parse_confirmations(text: String) -> anyhow::Result<Vec<Confirmation>> {
|
||||||
// possible errors:
|
// possible errors:
|
||||||
//
|
//
|
||||||
// Invalid authenticator:
|
// Invalid authenticator:
|
||||||
// <div>Invalid authenticator</div>
|
// <div>Invalid authenticator</div>
|
||||||
// <div>It looks like your Steam Guard Mobile Authenticator is providing incorrect Steam Guard codes. This could be caused by an inaccurate clock or bad timezone settings on your device. If your time settings are correct, it could be that a different device has been set up to provide the Steam Guard codes for your account, which means the authenticator on this device is no longer valid.</div>
|
// <div>It looks like your Steam Guard Mobile Authenticator is providing incorrect Steam Guard codes. This could be caused by an inaccurate clock or bad timezone settings on your device. If your time settings are correct, it could be that a different device has been set up to provide the Steam Guard codes for your account, which means the authenticator on this device is no longer valid.</div>
|
||||||
//
|
//
|
||||||
// <div>Nothing to confirm</div>
|
// <div>Nothing to confirm</div>
|
||||||
|
|
||||||
let fragment = Html::parse_fragment(&text);
|
let fragment = Html::parse_fragment(&text);
|
||||||
let selector = Selector::parse(".mobileconf_list_entry").unwrap();
|
let selector = Selector::parse(".mobileconf_list_entry").unwrap();
|
||||||
let mut confirmations = vec![];
|
let mut confirmations = vec![];
|
||||||
for elem in fragment.select(&selector) {
|
for elem in fragment.select(&selector) {
|
||||||
let conf = Confirmation {
|
let conf = Confirmation {
|
||||||
id: elem.value().attr("data-confid").unwrap().parse()?,
|
id: elem.value().attr("data-confid").unwrap().parse()?,
|
||||||
key: elem.value().attr("data-key").unwrap().parse()?,
|
key: elem.value().attr("data-key").unwrap().parse()?,
|
||||||
conf_type: elem
|
conf_type: elem
|
||||||
.value()
|
.value()
|
||||||
.attr("data-type")
|
.attr("data-type")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap_or(ConfirmationType::Unknown),
|
.unwrap_or(ConfirmationType::Unknown),
|
||||||
creator: elem.value().attr("data-creator").unwrap().parse()?,
|
creator: elem.value().attr("data-creator").unwrap().parse()?,
|
||||||
};
|
};
|
||||||
confirmations.push(conf);
|
confirmations.push(conf);
|
||||||
}
|
}
|
||||||
return Ok(confirmations);
|
return Ok(confirmations);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_time_bytes() {
|
fn test_build_time_bytes() {
|
||||||
let t1 = build_time_bytes(1617591917i64);
|
let t1 = build_time_bytes(1617591917i64);
|
||||||
let t2: [u8; 8] = [0, 0, 0, 0, 96, 106, 126, 109];
|
let t2: [u8; 8] = [0, 0, 0, 0, 96, 106, 126, 109];
|
||||||
assert!(
|
assert!(
|
||||||
t1.iter().zip(t2.iter()).all(|(a, b)| a == b),
|
t1.iter().zip(t2.iter()).all(|(a, b)| a == b),
|
||||||
"Arrays are not equal, got {:?}",
|
"Arrays are not equal, got {:?}",
|
||||||
t1
|
t1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_code() {
|
fn test_generate_code() {
|
||||||
let mut account = SteamGuardAccount::new();
|
let mut account = SteamGuardAccount::new();
|
||||||
account.shared_secret = String::from("zvIayp3JPvtvX/QGHqsqKBk/44s=");
|
account.shared_secret = String::from("zvIayp3JPvtvX/QGHqsqKBk/44s=");
|
||||||
|
|
||||||
let code = account.generate_code(1616374841i64);
|
let code = account.generate_code(1616374841i64);
|
||||||
assert_eq!(code, "2F9J5")
|
assert_eq!(code, "2F9J5")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_confirmation_hash_for_time() {
|
fn test_generate_confirmation_hash_for_time() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
generate_confirmation_hash_for_time(
|
generate_confirmation_hash_for_time(
|
||||||
1617591917,
|
1617591917,
|
||||||
"conf",
|
"conf",
|
||||||
&String::from("GQP46b73Ws7gr8GmZFR0sDuau5c=")
|
&String::from("GQP46b73Ws7gr8GmZFR0sDuau5c=")
|
||||||
),
|
),
|
||||||
String::from("NaL8EIMhfy/7vBounJ0CvpKbrPk=")
|
String::from("NaL8EIMhfy/7vBounJ0CvpKbrPk=")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_multiple_confirmations() {
|
fn test_parse_multiple_confirmations() {
|
||||||
let text = include_str!("fixtures/confirmations/multiple-confirmations.html");
|
let text = include_str!("fixtures/confirmations/multiple-confirmations.html");
|
||||||
let confirmations = parse_confirmations(text.into()).unwrap();
|
let confirmations = parse_confirmations(text.into()).unwrap();
|
||||||
assert_eq!(confirmations.len(), 5);
|
assert_eq!(confirmations.len(), 5);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
confirmations[0],
|
confirmations[0],
|
||||||
Confirmation {
|
Confirmation {
|
||||||
id: 9890792058,
|
id: 9890792058,
|
||||||
key: 15509106087034649470,
|
key: 15509106087034649470,
|
||||||
conf_type: ConfirmationType::MarketSell,
|
conf_type: ConfirmationType::MarketSell,
|
||||||
creator: 3392884950693131245,
|
creator: 3392884950693131245,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
confirmations[1],
|
confirmations[1],
|
||||||
Confirmation {
|
Confirmation {
|
||||||
id: 9890791666,
|
id: 9890791666,
|
||||||
key: 2661901169510258722,
|
key: 2661901169510258722,
|
||||||
conf_type: ConfirmationType::MarketSell,
|
conf_type: ConfirmationType::MarketSell,
|
||||||
creator: 3392884950693130525,
|
creator: 3392884950693130525,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
confirmations[2],
|
confirmations[2],
|
||||||
Confirmation {
|
Confirmation {
|
||||||
id: 9890791241,
|
id: 9890791241,
|
||||||
key: 15784514761287735229,
|
key: 15784514761287735229,
|
||||||
conf_type: ConfirmationType::MarketSell,
|
conf_type: ConfirmationType::MarketSell,
|
||||||
creator: 3392884950693129565,
|
creator: 3392884950693129565,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
confirmations[3],
|
confirmations[3],
|
||||||
Confirmation {
|
Confirmation {
|
||||||
id: 9890790828,
|
id: 9890790828,
|
||||||
key: 5049250785011653560,
|
key: 5049250785011653560,
|
||||||
conf_type: ConfirmationType::MarketSell,
|
conf_type: ConfirmationType::MarketSell,
|
||||||
creator: 3392884950693128685,
|
creator: 3392884950693128685,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
confirmations[4],
|
confirmations[4],
|
||||||
Confirmation {
|
Confirmation {
|
||||||
id: 9890790159,
|
id: 9890790159,
|
||||||
key: 6133112455066694993,
|
key: 6133112455066694993,
|
||||||
conf_type: ConfirmationType::MarketSell,
|
conf_type: ConfirmationType::MarketSell,
|
||||||
creator: 3392884950693127345,
|
creator: 3392884950693127345,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use log::*;
|
use log::*;
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
blocking::RequestBuilder,
|
blocking::RequestBuilder,
|
||||||
cookie::CookieStore,
|
cookie::CookieStore,
|
||||||
header::COOKIE,
|
header::COOKIE,
|
||||||
header::{HeaderMap, HeaderName, HeaderValue, SET_COOKIE},
|
header::{HeaderMap, HeaderName, HeaderValue, SET_COOKIE},
|
||||||
Url,
|
Url,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
|
@ -12,119 +12,119 @@ use std::str::FromStr;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct LoginResponse {
|
pub struct LoginResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub login_complete: bool,
|
pub login_complete: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub captcha_needed: bool,
|
pub captcha_needed: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub captcha_gid: String,
|
pub captcha_gid: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub emailsteamid: u64,
|
pub emailsteamid: u64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub emailauth_needed: bool,
|
pub emailauth_needed: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub requires_twofactor: bool,
|
pub requires_twofactor: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub message: String,
|
pub message: String,
|
||||||
// #[serde(rename = "oauth")]
|
// #[serde(rename = "oauth")]
|
||||||
// oauth_raw: String,
|
// oauth_raw: String,
|
||||||
#[serde(default, deserialize_with = "oauth_data_from_string")]
|
#[serde(default, deserialize_with = "oauth_data_from_string")]
|
||||||
oauth: Option<OAuthData>,
|
oauth: Option<OAuthData>,
|
||||||
transfer_urls: Option<Vec<String>>,
|
transfer_urls: Option<Vec<String>>,
|
||||||
transfer_parameters: Option<LoginTransferParameters>,
|
transfer_parameters: Option<LoginTransferParameters>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For some reason, the `oauth` field in the login response is a string of JSON, not a JSON object.
|
/// For some reason, the `oauth` field in the login response is a string of JSON, not a JSON object.
|
||||||
/// Deserializes to `Option` because the `oauth` field is not always there.
|
/// Deserializes to `Option` because the `oauth` field is not always there.
|
||||||
fn oauth_data_from_string<'de, D>(deserializer: D) -> Result<Option<OAuthData>, D::Error>
|
fn oauth_data_from_string<'de, D>(deserializer: D) -> Result<Option<OAuthData>, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
// for some reason, deserializing to &str doesn't work but this does.
|
// for some reason, deserializing to &str doesn't work but this does.
|
||||||
let s: String = Deserialize::deserialize(deserializer)?;
|
let s: String = Deserialize::deserialize(deserializer)?;
|
||||||
let data: OAuthData = serde_json::from_str(s.as_str()).map_err(serde::de::Error::custom)?;
|
let data: OAuthData = serde_json::from_str(s.as_str()).map_err(serde::de::Error::custom)?;
|
||||||
Ok(Some(data))
|
Ok(Some(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoginResponse {
|
impl LoginResponse {
|
||||||
pub fn needs_transfer_login(&self) -> bool {
|
pub fn needs_transfer_login(&self) -> bool {
|
||||||
self.transfer_urls.is_some() || self.transfer_parameters.is_some()
|
self.transfer_urls.is_some() || self.transfer_parameters.is_some()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct LoginTransferParameters {
|
struct LoginTransferParameters {
|
||||||
steamid: String,
|
steamid: String,
|
||||||
token_secure: String,
|
token_secure: String,
|
||||||
auth: String,
|
auth: String,
|
||||||
remember_login: bool,
|
remember_login: bool,
|
||||||
webcookie: String,
|
webcookie: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct RsaResponse {
|
pub struct RsaResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
pub publickey_exp: String,
|
pub publickey_exp: String,
|
||||||
pub publickey_mod: String,
|
pub publickey_mod: String,
|
||||||
pub timestamp: String,
|
pub timestamp: String,
|
||||||
pub token_gid: String,
|
pub token_gid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct OAuthData {
|
pub struct OAuthData {
|
||||||
oauth_token: String,
|
oauth_token: String,
|
||||||
steamid: String,
|
steamid: String,
|
||||||
wgtoken: String,
|
wgtoken: String,
|
||||||
wgtoken_secure: String,
|
wgtoken_secure: String,
|
||||||
webcookie: String,
|
webcookie: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
#[serde(rename = "SessionID")]
|
#[serde(rename = "SessionID")]
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
#[serde(rename = "SteamLogin")]
|
#[serde(rename = "SteamLogin")]
|
||||||
pub steam_login: String,
|
pub steam_login: String,
|
||||||
#[serde(rename = "SteamLoginSecure")]
|
#[serde(rename = "SteamLoginSecure")]
|
||||||
pub steam_login_secure: String,
|
pub steam_login_secure: String,
|
||||||
#[serde(rename = "WebCookie")]
|
#[serde(rename = "WebCookie")]
|
||||||
pub web_cookie: String,
|
pub web_cookie: String,
|
||||||
#[serde(rename = "OAuthToken")]
|
#[serde(rename = "OAuthToken")]
|
||||||
pub token: String,
|
pub token: String,
|
||||||
#[serde(rename = "SteamID")]
|
#[serde(rename = "SteamID")]
|
||||||
pub steam_id: u64,
|
pub steam_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_server_time() -> i64 {
|
pub fn get_server_time() -> i64 {
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let resp = client
|
let resp = client
|
||||||
.post("https://api.steampowered.com/ITwoFactorService/QueryTime/v0001")
|
.post("https://api.steampowered.com/ITwoFactorService/QueryTime/v0001")
|
||||||
.body("steamid=0")
|
.body("steamid=0")
|
||||||
.send();
|
.send();
|
||||||
let value: serde_json::Value = resp.unwrap().json().unwrap();
|
let value: serde_json::Value = resp.unwrap().json().unwrap();
|
||||||
|
|
||||||
return String::from(value["response"]["server_time"].as_str().unwrap())
|
return String::from(value["response"]["server_time"].as_str().unwrap())
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provides raw access to the Steam API. Handles cookies, some deserialization, etc. to make it easier.
|
/// Provides raw access to the Steam API. Handles cookies, some deserialization, etc. to make it easier.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SteamApiClient {
|
pub struct SteamApiClient {
|
||||||
cookies: reqwest::cookie::Jar,
|
cookies: reqwest::cookie::Jar,
|
||||||
client: reqwest::blocking::Client,
|
client: reqwest::blocking::Client,
|
||||||
pub session: Option<Session>,
|
pub session: Option<Session>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SteamApiClient {
|
impl SteamApiClient {
|
||||||
pub fn new() -> SteamApiClient {
|
pub fn new() -> SteamApiClient {
|
||||||
SteamApiClient {
|
SteamApiClient {
|
||||||
cookies: reqwest::cookie::Jar::default(),
|
cookies: reqwest::cookie::Jar::default(),
|
||||||
client: reqwest::blocking::ClientBuilder::new()
|
client: reqwest::blocking::ClientBuilder::new()
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
|
@ -136,196 +136,196 @@ impl SteamApiClient {
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
session: None,
|
session: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_session(&self, data: &OAuthData) -> Session {
|
fn build_session(&self, data: &OAuthData) -> Session {
|
||||||
return Session {
|
return Session {
|
||||||
token: data.oauth_token.clone(),
|
token: data.oauth_token.clone(),
|
||||||
steam_id: data.steamid.parse().unwrap(),
|
steam_id: data.steamid.parse().unwrap(),
|
||||||
steam_login: format!("{}%7C%7C{}", data.steamid, data.wgtoken),
|
steam_login: format!("{}%7C%7C{}", data.steamid, data.wgtoken),
|
||||||
steam_login_secure: format!("{}%7C%7C{}", data.steamid, data.wgtoken_secure),
|
steam_login_secure: format!("{}%7C%7C{}", data.steamid, data.wgtoken_secure),
|
||||||
session_id: self.extract_session_id().unwrap(),
|
session_id: self.extract_session_id().unwrap(),
|
||||||
web_cookie: data.webcookie.clone(),
|
web_cookie: data.webcookie.clone(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_session_id(&self) -> Option<String> {
|
fn extract_session_id(&self) -> Option<String> {
|
||||||
let cookies = self.cookies.cookies(&STEAM_COOKIE_URL).unwrap();
|
let cookies = self.cookies.cookies(&STEAM_COOKIE_URL).unwrap();
|
||||||
let all_cookies = cookies.to_str().unwrap();
|
let all_cookies = cookies.to_str().unwrap();
|
||||||
for cookie in all_cookies
|
for cookie in all_cookies
|
||||||
.split(";")
|
.split(";")
|
||||||
.map(|s| cookie::Cookie::parse(s).unwrap())
|
.map(|s| cookie::Cookie::parse(s).unwrap())
|
||||||
{
|
{
|
||||||
if cookie.name() == "sessionid" {
|
if cookie.name() == "sessionid" {
|
||||||
return Some(cookie.value().into());
|
return Some(cookie.value().into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_cookies_from_response(&mut self, response: &reqwest::blocking::Response) {
|
pub fn save_cookies_from_response(&mut self, response: &reqwest::blocking::Response) {
|
||||||
let set_cookie_iter = response.headers().get_all(SET_COOKIE);
|
let set_cookie_iter = response.headers().get_all(SET_COOKIE);
|
||||||
|
|
||||||
for c in set_cookie_iter {
|
for c in set_cookie_iter {
|
||||||
c.to_str()
|
c.to_str()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.for_each(|cookie_str| self.cookies.add_cookie_str(cookie_str, &STEAM_COOKIE_URL));
|
.for_each(|cookie_str| self.cookies.add_cookie_str(cookie_str, &STEAM_COOKIE_URL));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request<U: reqwest::IntoUrl>(&self, method: reqwest::Method, url: U) -> RequestBuilder {
|
pub fn request<U: reqwest::IntoUrl>(&self, method: reqwest::Method, url: U) -> RequestBuilder {
|
||||||
self.cookies
|
self.cookies
|
||||||
.add_cookie_str("mobileClientVersion=0 (2.1.3)", &STEAM_COOKIE_URL);
|
.add_cookie_str("mobileClientVersion=0 (2.1.3)", &STEAM_COOKIE_URL);
|
||||||
self.cookies
|
self.cookies
|
||||||
.add_cookie_str("mobileClient=android", &STEAM_COOKIE_URL);
|
.add_cookie_str("mobileClient=android", &STEAM_COOKIE_URL);
|
||||||
self.cookies
|
self.cookies
|
||||||
.add_cookie_str("Steam_Language=english", &STEAM_COOKIE_URL);
|
.add_cookie_str("Steam_Language=english", &STEAM_COOKIE_URL);
|
||||||
|
|
||||||
self.client
|
self.client
|
||||||
.request(method, url)
|
.request(method, url)
|
||||||
.header(COOKIE, self.cookies.cookies(&STEAM_COOKIE_URL).unwrap())
|
.header(COOKIE, self.cookies.cookies(&STEAM_COOKIE_URL).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get<U: reqwest::IntoUrl>(&self, url: U) -> RequestBuilder {
|
pub fn get<U: reqwest::IntoUrl>(&self, url: U) -> RequestBuilder {
|
||||||
self.request(reqwest::Method::GET, url)
|
self.request(reqwest::Method::GET, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn post<U: reqwest::IntoUrl>(&self, url: U) -> RequestBuilder {
|
pub fn post<U: reqwest::IntoUrl>(&self, url: U) -> RequestBuilder {
|
||||||
self.request(reqwest::Method::POST, url)
|
self.request(reqwest::Method::POST, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the cookie jar with the session cookies by pinging steam servers.
|
/// Updates the cookie jar with the session cookies by pinging steam servers.
|
||||||
pub fn update_session(&mut self) -> anyhow::Result<()> {
|
pub fn update_session(&mut self) -> anyhow::Result<()> {
|
||||||
trace!("SteamApiClient::update_session");
|
trace!("SteamApiClient::update_session");
|
||||||
|
|
||||||
let resp = self
|
let resp = self
|
||||||
.get("https://steamcommunity.com/login?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client".parse::<Url>().unwrap())
|
.get("https://steamcommunity.com/login?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client".parse::<Url>().unwrap())
|
||||||
.send()?;
|
.send()?;
|
||||||
self.save_cookies_from_response(&resp);
|
self.save_cookies_from_response(&resp);
|
||||||
trace!("{:?}", resp);
|
trace!("{:?}", resp);
|
||||||
|
|
||||||
trace!("cookies: {:?}", self.cookies);
|
trace!("cookies: {:?}", self.cookies);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Endpoint: POST /login/dologin
|
/// Endpoint: POST /login/dologin
|
||||||
pub fn login(
|
pub fn login(
|
||||||
&mut self,
|
&mut self,
|
||||||
username: String,
|
username: String,
|
||||||
encrypted_password: String,
|
encrypted_password: String,
|
||||||
twofactor_code: String,
|
twofactor_code: String,
|
||||||
email_code: String,
|
email_code: String,
|
||||||
captcha_gid: String,
|
captcha_gid: String,
|
||||||
captcha_text: String,
|
captcha_text: String,
|
||||||
rsa_timestamp: String,
|
rsa_timestamp: String,
|
||||||
) -> anyhow::Result<LoginResponse> {
|
) -> anyhow::Result<LoginResponse> {
|
||||||
let params = hashmap! {
|
let params = hashmap! {
|
||||||
"donotcache" => format!(
|
"donotcache" => format!(
|
||||||
"{}",
|
"{}",
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs()
|
.as_secs()
|
||||||
* 1000
|
* 1000
|
||||||
),
|
),
|
||||||
"username" => username,
|
"username" => username,
|
||||||
"password" => encrypted_password,
|
"password" => encrypted_password,
|
||||||
"twofactorcode" => twofactor_code,
|
"twofactorcode" => twofactor_code,
|
||||||
"emailauth" => email_code,
|
"emailauth" => email_code,
|
||||||
"captchagid" => captcha_gid,
|
"captchagid" => captcha_gid,
|
||||||
"captcha_text" => captcha_text,
|
"captcha_text" => captcha_text,
|
||||||
"rsatimestamp" => rsa_timestamp,
|
"rsatimestamp" => rsa_timestamp,
|
||||||
"remember_login" => "true".into(),
|
"remember_login" => "true".into(),
|
||||||
"oauth_client_id" => "DE45CD61".into(),
|
"oauth_client_id" => "DE45CD61".into(),
|
||||||
"oauth_scope" => "read_profile write_profile read_client write_client".into(),
|
"oauth_scope" => "read_profile write_profile read_client write_client".into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let resp = self
|
let resp = self
|
||||||
.post("https://steamcommunity.com/login/dologin")
|
.post("https://steamcommunity.com/login/dologin")
|
||||||
.form(¶ms)
|
.form(¶ms)
|
||||||
.send()?;
|
.send()?;
|
||||||
let text = resp.text()?;
|
let text = resp.text()?;
|
||||||
trace!("raw login response: {}", text);
|
trace!("raw login response: {}", text);
|
||||||
|
|
||||||
let login_resp: LoginResponse = serde_json::from_str(text.as_str())?;
|
let login_resp: LoginResponse = serde_json::from_str(text.as_str())?;
|
||||||
|
|
||||||
if let Some(oauth) = &login_resp.oauth {
|
if let Some(oauth) = &login_resp.oauth {
|
||||||
self.session = Some(self.build_session(&oauth));
|
self.session = Some(self.build_session(&oauth));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(login_resp);
|
return Ok(login_resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A secondary step in the login flow. Does not seem to always be needed?
|
/// A secondary step in the login flow. Does not seem to always be needed?
|
||||||
/// Endpoints: provided by `login()`
|
/// Endpoints: provided by `login()`
|
||||||
pub fn transfer_login(&mut self, login_resp: LoginResponse) -> anyhow::Result<OAuthData> {
|
pub fn transfer_login(&mut self, login_resp: LoginResponse) -> anyhow::Result<OAuthData> {
|
||||||
match (login_resp.transfer_urls, login_resp.transfer_parameters) {
|
match (login_resp.transfer_urls, login_resp.transfer_parameters) {
|
||||||
(Some(urls), Some(params)) => {
|
(Some(urls), Some(params)) => {
|
||||||
debug!("received transfer parameters, relaying data...");
|
debug!("received transfer parameters, relaying data...");
|
||||||
for url in urls {
|
for url in urls {
|
||||||
trace!("posting transfer to {}", url);
|
trace!("posting transfer to {}", url);
|
||||||
let resp = self.client.post(url).json(¶ms).send()?;
|
let resp = self.client.post(url).json(¶ms).send()?;
|
||||||
self.save_cookies_from_response(&resp);
|
self.save_cookies_from_response(&resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
let oauth = OAuthData {
|
let oauth = OAuthData {
|
||||||
oauth_token: params.auth,
|
oauth_token: params.auth,
|
||||||
steamid: params.steamid.parse().unwrap(),
|
steamid: params.steamid.parse().unwrap(),
|
||||||
wgtoken: params.token_secure.clone(), // guessing
|
wgtoken: params.token_secure.clone(), // guessing
|
||||||
wgtoken_secure: params.token_secure,
|
wgtoken_secure: params.token_secure,
|
||||||
webcookie: params.webcookie,
|
webcookie: params.webcookie,
|
||||||
};
|
};
|
||||||
self.session = Some(self.build_session(&oauth));
|
self.session = Some(self.build_session(&oauth));
|
||||||
return Ok(oauth);
|
return Ok(oauth);
|
||||||
}
|
}
|
||||||
(None, None) => {
|
(None, None) => {
|
||||||
bail!("did not receive transfer_urls and transfer_parameters");
|
bail!("did not receive transfer_urls and transfer_parameters");
|
||||||
}
|
}
|
||||||
(_, None) => {
|
(_, None) => {
|
||||||
bail!("did not receive transfer_parameters");
|
bail!("did not receive transfer_parameters");
|
||||||
}
|
}
|
||||||
(None, _) => {
|
(None, _) => {
|
||||||
bail!("did not receive transfer_urls");
|
bail!("did not receive transfer_urls");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_oauth_data_parse() {
|
fn test_oauth_data_parse() {
|
||||||
// This example is from a login response that did not contain any transfer URLs.
|
// This example is from a login response that did not contain any transfer URLs.
|
||||||
let oauth: OAuthData = serde_json::from_str("{\"steamid\":\"78562647129469312\",\"account_name\":\"feuarus\",\"oauth_token\":\"fd2fdb3d0717bcd2220d98c7ec61c7bd\",\"wgtoken\":\"72E7013D598A4F68C7E268F6FA3767D89D763732\",\"wgtoken_secure\":\"21061EA13C36D7C29812CAED900A215171AD13A2\",\"webcookie\":\"6298070A226E5DAD49938D78BCF36F7A7118FDD5\"}").unwrap();
|
let oauth: OAuthData = serde_json::from_str("{\"steamid\":\"78562647129469312\",\"account_name\":\"feuarus\",\"oauth_token\":\"fd2fdb3d0717bcd2220d98c7ec61c7bd\",\"wgtoken\":\"72E7013D598A4F68C7E268F6FA3767D89D763732\",\"wgtoken_secure\":\"21061EA13C36D7C29812CAED900A215171AD13A2\",\"webcookie\":\"6298070A226E5DAD49938D78BCF36F7A7118FDD5\"}").unwrap();
|
||||||
|
|
||||||
assert_eq!(oauth.steamid, "78562647129469312");
|
assert_eq!(oauth.steamid, "78562647129469312");
|
||||||
assert_eq!(oauth.oauth_token, "fd2fdb3d0717bcd2220d98c7ec61c7bd");
|
assert_eq!(oauth.oauth_token, "fd2fdb3d0717bcd2220d98c7ec61c7bd");
|
||||||
assert_eq!(oauth.wgtoken, "72E7013D598A4F68C7E268F6FA3767D89D763732");
|
assert_eq!(oauth.wgtoken, "72E7013D598A4F68C7E268F6FA3767D89D763732");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
oauth.wgtoken_secure,
|
oauth.wgtoken_secure,
|
||||||
"21061EA13C36D7C29812CAED900A215171AD13A2"
|
"21061EA13C36D7C29812CAED900A215171AD13A2"
|
||||||
);
|
);
|
||||||
assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5");
|
assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_login_response_parse() {
|
fn test_login_response_parse() {
|
||||||
let result = serde_json::from_str::<LoginResponse>(include_str!(
|
let result = serde_json::from_str::<LoginResponse>(include_str!(
|
||||||
"fixtures/api-responses/login-response1.json"
|
"fixtures/api-responses/login-response1.json"
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Ok(_)),
|
matches!(result, Ok(_)),
|
||||||
"got error: {}",
|
"got error: {}",
|
||||||
result.unwrap_err()
|
result.unwrap_err()
|
||||||
);
|
);
|
||||||
let resp = result.unwrap();
|
let resp = result.unwrap();
|
||||||
|
|
||||||
let oauth = resp.oauth.unwrap();
|
let oauth = resp.oauth.unwrap();
|
||||||
assert_eq!(oauth.steamid, "78562647129469312");
|
assert_eq!(oauth.steamid, "78562647129469312");
|
||||||
assert_eq!(oauth.oauth_token, "fd2fdb3d0717bad2220d98c7ec61c7bd");
|
assert_eq!(oauth.oauth_token, "fd2fdb3d0717bad2220d98c7ec61c7bd");
|
||||||
assert_eq!(oauth.wgtoken, "72E7013D598A4F68C7E268F6FA3767D89D763732");
|
assert_eq!(oauth.wgtoken, "72E7013D598A4F68C7E268F6FA3767D89D763732");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
oauth.wgtoken_secure,
|
oauth.wgtoken_secure,
|
||||||
"21061EA13C36D7C29812CAED900A215171AD13A2"
|
"21061EA13C36D7C29812CAED900A215171AD13A2"
|
||||||
);
|
);
|
||||||
assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5");
|
assert_eq!(oauth.webcookie, "6298070A226E5DAD49938D78BCF36F7A7118FDD5");
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,186 +5,186 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LoginError {
|
pub enum LoginError {
|
||||||
BadRSA,
|
BadRSA,
|
||||||
BadCredentials,
|
BadCredentials,
|
||||||
NeedCaptcha { captcha_gid: String },
|
NeedCaptcha { captcha_gid: String },
|
||||||
Need2FA,
|
Need2FA,
|
||||||
NeedEmail,
|
NeedEmail,
|
||||||
TooManyAttempts,
|
TooManyAttempts,
|
||||||
NetworkFailure(reqwest::Error),
|
NetworkFailure(reqwest::Error),
|
||||||
OtherFailure(anyhow::Error),
|
OtherFailure(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for LoginError {
|
impl std::fmt::Display for LoginError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
write!(f, "{:?}", self)
|
write!(f, "{:?}", self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for LoginError {}
|
impl std::error::Error for LoginError {}
|
||||||
|
|
||||||
impl From<reqwest::Error> for LoginError {
|
impl From<reqwest::Error> for LoginError {
|
||||||
fn from(err: reqwest::Error) -> Self {
|
fn from(err: reqwest::Error) -> Self {
|
||||||
LoginError::NetworkFailure(err)
|
LoginError::NetworkFailure(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<anyhow::Error> for LoginError {
|
impl From<anyhow::Error> for LoginError {
|
||||||
fn from(err: anyhow::Error) -> Self {
|
fn from(err: anyhow::Error) -> Self {
|
||||||
LoginError::OtherFailure(err)
|
LoginError::OtherFailure(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the user login flow.
|
/// Handles the user login flow.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct UserLogin {
|
pub struct UserLogin {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub captcha_required: bool,
|
pub captcha_required: bool,
|
||||||
pub captcha_gid: String,
|
pub captcha_gid: String,
|
||||||
pub captcha_text: String,
|
pub captcha_text: String,
|
||||||
pub twofactor_code: String,
|
pub twofactor_code: String,
|
||||||
pub email_code: String,
|
pub email_code: String,
|
||||||
pub steam_id: u64,
|
pub steam_id: u64,
|
||||||
|
|
||||||
client: SteamApiClient,
|
client: SteamApiClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserLogin {
|
impl UserLogin {
|
||||||
pub fn new(username: String, password: String) -> UserLogin {
|
pub fn new(username: String, password: String) -> UserLogin {
|
||||||
return UserLogin {
|
return UserLogin {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
captcha_required: false,
|
captcha_required: false,
|
||||||
captcha_gid: String::from("-1"),
|
captcha_gid: String::from("-1"),
|
||||||
captcha_text: String::from(""),
|
captcha_text: String::from(""),
|
||||||
twofactor_code: String::from(""),
|
twofactor_code: String::from(""),
|
||||||
email_code: String::from(""),
|
email_code: String::from(""),
|
||||||
steam_id: 0,
|
steam_id: 0,
|
||||||
client: SteamApiClient::new(),
|
client: SteamApiClient::new(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn login(&mut self) -> anyhow::Result<Session, LoginError> {
|
pub fn login(&mut self) -> anyhow::Result<Session, LoginError> {
|
||||||
trace!("UserLogin::login");
|
trace!("UserLogin::login");
|
||||||
if self.captcha_required && self.captcha_text.len() == 0 {
|
if self.captcha_required && self.captcha_text.len() == 0 {
|
||||||
return Err(LoginError::NeedCaptcha {
|
return Err(LoginError::NeedCaptcha {
|
||||||
captcha_gid: self.captcha_gid.clone(),
|
captcha_gid: self.captcha_gid.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.client.session.is_none() {
|
if self.client.session.is_none() {
|
||||||
self.client.update_session()?;
|
self.client.update_session()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let params = hashmap! {
|
let params = hashmap! {
|
||||||
"donotcache" => format!(
|
"donotcache" => format!(
|
||||||
"{}",
|
"{}",
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs()
|
.as_secs()
|
||||||
* 1000
|
* 1000
|
||||||
),
|
),
|
||||||
"username" => self.username.clone(),
|
"username" => self.username.clone(),
|
||||||
};
|
};
|
||||||
let resp = self
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.post("https://steamcommunity.com/login/getrsakey")
|
.post("https://steamcommunity.com/login/getrsakey")
|
||||||
.form(¶ms)
|
.form(¶ms)
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
let encrypted_password: String;
|
let encrypted_password: String;
|
||||||
let rsa_timestamp: String;
|
let rsa_timestamp: String;
|
||||||
match resp.json::<RsaResponse>() {
|
match resp.json::<RsaResponse>() {
|
||||||
Ok(rsa_resp) => {
|
Ok(rsa_resp) => {
|
||||||
rsa_timestamp = rsa_resp.timestamp.clone();
|
rsa_timestamp = rsa_resp.timestamp.clone();
|
||||||
encrypted_password = encrypt_password(rsa_resp, &self.password);
|
encrypted_password = encrypt_password(rsa_resp, &self.password);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
error!("rsa error: {:?}", error);
|
error!("rsa error: {:?}", error);
|
||||||
return Err(LoginError::BadRSA);
|
return Err(LoginError::BadRSA);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("captchagid: {}", self.captcha_gid);
|
trace!("captchagid: {}", self.captcha_gid);
|
||||||
trace!("captcha_text: {}", self.captcha_text);
|
trace!("captcha_text: {}", self.captcha_text);
|
||||||
trace!("twofactorcode: {}", self.twofactor_code);
|
trace!("twofactorcode: {}", self.twofactor_code);
|
||||||
trace!("emailauth: {}", self.email_code);
|
trace!("emailauth: {}", self.email_code);
|
||||||
|
|
||||||
let login_resp: LoginResponse = self.client.login(
|
let login_resp: LoginResponse = self.client.login(
|
||||||
self.username.clone(),
|
self.username.clone(),
|
||||||
encrypted_password,
|
encrypted_password,
|
||||||
self.twofactor_code.clone(),
|
self.twofactor_code.clone(),
|
||||||
self.email_code.clone(),
|
self.email_code.clone(),
|
||||||
self.captcha_gid.clone(),
|
self.captcha_gid.clone(),
|
||||||
self.captcha_text.clone(),
|
self.captcha_text.clone(),
|
||||||
rsa_timestamp,
|
rsa_timestamp,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if login_resp.message.contains("too many login") {
|
if login_resp.message.contains("too many login") {
|
||||||
return Err(LoginError::TooManyAttempts);
|
return Err(LoginError::TooManyAttempts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if login_resp.message.contains("Incorrect login") {
|
if login_resp.message.contains("Incorrect login") {
|
||||||
return Err(LoginError::BadCredentials);
|
return Err(LoginError::BadCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
if login_resp.captcha_needed {
|
if login_resp.captcha_needed {
|
||||||
self.captcha_gid = login_resp.captcha_gid.clone();
|
self.captcha_gid = login_resp.captcha_gid.clone();
|
||||||
return Err(LoginError::NeedCaptcha {
|
return Err(LoginError::NeedCaptcha {
|
||||||
captcha_gid: self.captcha_gid.clone(),
|
captcha_gid: self.captcha_gid.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if login_resp.emailauth_needed {
|
if login_resp.emailauth_needed {
|
||||||
self.steam_id = login_resp.emailsteamid.clone();
|
self.steam_id = login_resp.emailsteamid.clone();
|
||||||
return Err(LoginError::NeedEmail);
|
return Err(LoginError::NeedEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
if login_resp.requires_twofactor {
|
if login_resp.requires_twofactor {
|
||||||
return Err(LoginError::Need2FA);
|
return Err(LoginError::Need2FA);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !login_resp.login_complete {
|
if !login_resp.login_complete {
|
||||||
return Err(LoginError::BadCredentials);
|
return Err(LoginError::BadCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
if login_resp.needs_transfer_login() {
|
if login_resp.needs_transfer_login() {
|
||||||
self.client.transfer_login(login_resp)?;
|
self.client.transfer_login(login_resp)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(self.client.session.clone().unwrap());
|
return Ok(self.client.session.clone().unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encrypt_password(rsa_resp: RsaResponse, password: &String) -> String {
|
fn encrypt_password(rsa_resp: RsaResponse, password: &String) -> String {
|
||||||
let rsa_exponent = rsa::BigUint::parse_bytes(rsa_resp.publickey_exp.as_bytes(), 16).unwrap();
|
let rsa_exponent = rsa::BigUint::parse_bytes(rsa_resp.publickey_exp.as_bytes(), 16).unwrap();
|
||||||
let rsa_modulus = rsa::BigUint::parse_bytes(rsa_resp.publickey_mod.as_bytes(), 16).unwrap();
|
let rsa_modulus = rsa::BigUint::parse_bytes(rsa_resp.publickey_mod.as_bytes(), 16).unwrap();
|
||||||
let public_key = RsaPublicKey::new(rsa_modulus, rsa_exponent).unwrap();
|
let public_key = RsaPublicKey::new(rsa_modulus, rsa_exponent).unwrap();
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
let mut rng = rand::rngs::mock::StepRng::new(2, 1);
|
let mut rng = rand::rngs::mock::StepRng::new(2, 1);
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
let mut rng = rand::rngs::OsRng;
|
let mut rng = rand::rngs::OsRng;
|
||||||
let padding = rsa::PaddingScheme::new_pkcs1v15_encrypt();
|
let padding = rsa::PaddingScheme::new_pkcs1v15_encrypt();
|
||||||
let encrypted_password = base64::encode(
|
let encrypted_password = base64::encode(
|
||||||
public_key
|
public_key
|
||||||
.encrypt(&mut rng, padding, password.as_bytes())
|
.encrypt(&mut rng, padding, password.as_bytes())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
return encrypted_password;
|
return encrypted_password;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypt_password() {
|
fn test_encrypt_password() {
|
||||||
let rsa_resp = RsaResponse{
|
let rsa_resp = RsaResponse{
|
||||||
success: true,
|
success: true,
|
||||||
publickey_exp: String::from("010001"),
|
publickey_exp: String::from("010001"),
|
||||||
publickey_mod: String::from("98f9088c1250b17fe19d2b2422d54a1eef0036875301731f11bd17900e215318eb6de1546727c0b7b61b86cefccdcb2f8108c813154d9a7d55631965eece810d4ab9d8a59c486bda778651b876176070598a93c2325c275cb9c17bdbcacf8edc9c18c0c5d59bc35703505ef8a09ed4c62b9f92a3fac5740ce25e490ab0e26d872140e4103d912d1e3958f844264211277ee08d2b4dd3ac58b030b25342bd5c949ae7794e46a8eab26d5a8deca683bfd381da6c305b19868b8c7cd321ce72c693310a6ebf2ecd43642518f825894602f6c239cf193cb4346ce64beac31e20ef88f934f2f776597734bb9eae1ebdf4a453973b6df9d5e90777bffe5db83dd1757b"),
|
publickey_mod: String::from("98f9088c1250b17fe19d2b2422d54a1eef0036875301731f11bd17900e215318eb6de1546727c0b7b61b86cefccdcb2f8108c813154d9a7d55631965eece810d4ab9d8a59c486bda778651b876176070598a93c2325c275cb9c17bdbcacf8edc9c18c0c5d59bc35703505ef8a09ed4c62b9f92a3fac5740ce25e490ab0e26d872140e4103d912d1e3958f844264211277ee08d2b4dd3ac58b030b25342bd5c949ae7794e46a8eab26d5a8deca683bfd381da6c305b19868b8c7cd321ce72c693310a6ebf2ecd43642518f825894602f6c239cf193cb4346ce64beac31e20ef88f934f2f776597734bb9eae1ebdf4a453973b6df9d5e90777bffe5db83dd1757b"),
|
||||||
timestamp: String::from("asdf"),
|
timestamp: String::from("asdf"),
|
||||||
token_gid: String::from("asdf"),
|
token_gid: String::from("asdf"),
|
||||||
};
|
};
|
||||||
let result = encrypt_password(rsa_resp, &String::from("kelwleofpsm3n4ofc"));
|
let result = encrypt_password(rsa_resp, &String::from("kelwleofpsm3n4ofc"));
|
||||||
assert_eq!(result.len(), 344);
|
assert_eq!(result.len(), 344);
|
||||||
assert_eq!(result, "RUo/3IfbkVcJi1q1S5QlpKn1mEn3gNJoc/Z4VwxRV9DImV6veq/YISEuSrHB3885U5MYFLn1g94Y+cWRL6HGXoV+gOaVZe43m7O92RwiVz6OZQXMfAv3UC/jcqn/xkitnj+tNtmx55gCxmGbO2KbqQ0TQqAyqCOOw565B+Cwr2OOorpMZAViv9sKA/G3Q6yzscU6rhua179c8QjC1Hk3idUoSzpWfT4sHNBW/EREXZ3Dkjwu17xzpfwIUpnBVIlR8Vj3coHgUCpTsKVRA3T814v9BYPlvLYwmw5DW3ddx+2SyTY0P5uuog36TN2PqYS7ioF5eDe16gyfRR4Nzn/7wA==");
|
assert_eq!(result, "RUo/3IfbkVcJi1q1S5QlpKn1mEn3gNJoc/Z4VwxRV9DImV6veq/YISEuSrHB3885U5MYFLn1g94Y+cWRL6HGXoV+gOaVZe43m7O92RwiVz6OZQXMfAv3UC/jcqn/xkitnj+tNtmx55gCxmGbO2KbqQ0TQqAyqCOOw565B+Cwr2OOorpMZAViv9sKA/G3Q6yzscU6rhua179c8QjC1Hk3idUoSzpWfT4sHNBW/EREXZ3Dkjwu17xzpfwIUpnBVIlR8Vj3coHgUCpTsKVRA3T814v9BYPlvLYwmw5DW3ddx+2SyTY0P5uuog36TN2PqYS7ioF5eDe16gyfRR4Nzn/7wA==");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue