2021-08-01 14:43:18 +02:00
use log ::* ;
use reqwest ::{
2021-08-08 18:54:46 +02:00
blocking ::RequestBuilder ,
cookie ::CookieStore ,
header ::COOKIE ,
header ::{ HeaderMap , HeaderName , HeaderValue , SET_COOKIE } ,
Url ,
2021-08-01 14:43:18 +02:00
} ;
2021-08-08 18:34:06 +02:00
use serde ::{ Deserialize , Deserializer , Serialize } ;
2021-08-08 19:37:18 +02:00
use serde_json ::Value ;
2021-08-08 00:47:39 +02:00
use std ::iter ::FromIterator ;
use std ::str ::FromStr ;
2021-03-24 22:49:09 +01:00
use std ::time ::{ SystemTime , UNIX_EPOCH } ;
2021-08-08 21:25:27 +02:00
use crate ::SteamGuardAccount ;
2021-08-08 17:52:12 +02:00
lazy_static! {
2021-08-08 18:54:46 +02:00
static ref STEAM_COOKIE_URL : Url = " https://steamcommunity.com " . parse ::< Url > ( ) . unwrap ( ) ;
2021-08-08 21:25:27 +02:00
static ref STEAM_API_BASE : String = " https://api.steampowered.com " . into ( ) ;
2021-08-08 17:52:12 +02:00
}
2021-03-25 17:43:41 +01:00
#[ derive(Debug, Clone, Deserialize) ]
2021-08-08 17:52:12 +02:00
pub struct LoginResponse {
2021-08-08 18:54:46 +02:00
pub success : bool ,
#[ serde(default) ]
pub login_complete : bool ,
#[ serde(default) ]
pub captcha_needed : bool ,
#[ serde(default) ]
pub captcha_gid : String ,
2021-08-11 03:07:04 +02:00
#[ serde(default, deserialize_with = " parse_json_string_as_number " ) ]
2021-08-08 18:54:46 +02:00
pub emailsteamid : u64 ,
#[ serde(default) ]
pub emailauth_needed : bool ,
#[ serde(default) ]
pub requires_twofactor : bool ,
#[ serde(default) ]
pub message : String ,
// #[serde(rename = "oauth")]
// oauth_raw: String,
#[ serde(default, deserialize_with = " oauth_data_from_string " ) ]
oauth : Option < OAuthData > ,
transfer_urls : Option < Vec < String > > ,
transfer_parameters : Option < LoginTransferParameters > ,
2021-03-30 21:51:26 +02:00
}
2021-08-08 17:16:06 +02:00
/// 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.
fn oauth_data_from_string < ' de , D > ( deserializer : D ) -> Result < Option < OAuthData > , D ::Error >
where
2021-08-08 18:54:46 +02:00
D : Deserializer < ' de > ,
2021-08-08 17:16:06 +02:00
{
2021-08-08 18:54:46 +02:00
// for some reason, deserializing to &str doesn't work but this does.
let s : String = Deserialize ::deserialize ( deserializer ) ? ;
let data : OAuthData = serde_json ::from_str ( s . as_str ( ) ) . map_err ( serde ::de ::Error ::custom ) ? ;
Ok ( Some ( data ) )
2021-08-08 17:16:06 +02:00
}
2021-08-08 17:52:12 +02:00
impl LoginResponse {
2021-08-08 18:54:46 +02:00
pub fn needs_transfer_login ( & self ) -> bool {
self . transfer_urls . is_some ( ) | | self . transfer_parameters . is_some ( )
}
2021-08-08 17:52:12 +02:00
}
2021-03-30 21:51:26 +02:00
#[ derive(Debug, Clone, Serialize, Deserialize) ]
struct LoginTransferParameters {
2021-08-08 18:54:46 +02:00
steamid : String ,
token_secure : String ,
auth : String ,
remember_login : bool ,
webcookie : String ,
2021-03-24 22:49:09 +01:00
}
2021-03-25 17:43:41 +01:00
#[ derive(Debug, Clone, Deserialize) ]
2021-08-08 17:52:12 +02:00
pub struct RsaResponse {
2021-08-08 18:54:46 +02:00
pub success : bool ,
pub publickey_exp : String ,
pub publickey_mod : String ,
pub timestamp : String ,
pub token_gid : String ,
2021-03-25 18:52:55 +01:00
}
2021-03-26 00:47:44 +01:00
#[ derive(Debug, Clone, Deserialize) ]
2021-08-08 17:52:12 +02:00
pub struct OAuthData {
2021-08-08 18:54:46 +02:00
oauth_token : String ,
steamid : String ,
wgtoken : String ,
wgtoken_secure : String ,
2021-08-10 01:49:53 +02:00
#[ serde(default) ]
2021-08-08 18:54:46 +02:00
webcookie : String ,
2021-03-24 22:49:09 +01:00
}
2021-03-27 13:17:56 +01:00
#[ derive(Debug, Clone, Serialize, Deserialize) ]
2021-03-25 18:52:55 +01:00
pub struct Session {
2021-08-08 18:54:46 +02:00
#[ serde(rename = " SessionID " ) ]
pub session_id : String ,
#[ serde(rename = " SteamLogin " ) ]
pub steam_login : String ,
#[ serde(rename = " SteamLoginSecure " ) ]
pub steam_login_secure : String ,
2021-08-10 01:49:53 +02:00
#[ serde(default, rename = " WebCookie " ) ]
2021-08-08 18:54:46 +02:00
pub web_cookie : String ,
#[ serde(rename = " OAuthToken " ) ]
pub token : String ,
#[ serde(rename = " SteamID " ) ]
pub steam_id : u64 ,
2021-03-25 18:52:55 +01:00
}
2021-03-24 22:49:09 +01:00
pub fn get_server_time ( ) -> i64 {
2021-08-08 18:54:46 +02:00
let client = reqwest ::blocking ::Client ::new ( ) ;
let resp = client
. post ( " https://api.steampowered.com/ITwoFactorService/QueryTime/v0001 " )
. body ( " steamid=0 " )
. send ( ) ;
let value : serde_json ::Value = resp . unwrap ( ) . json ( ) . unwrap ( ) ;
return String ::from ( value [ " response " ] [ " server_time " ] . as_str ( ) . unwrap ( ) )
. parse ( )
. unwrap ( ) ;
2021-03-24 22:49:09 +01:00
}
2021-04-04 15:38:34 +02:00
2021-08-09 06:09:34 +02:00
/// Provides raw access to the Steam API. Handles cookies, some deserialization, etc. to make it easier. It covers `ITwoFactorService` from the Steam web API, and some mobile app specific api endpoints.
2021-08-08 00:47:39 +02:00
#[ derive(Debug) ]
2021-08-08 17:52:12 +02:00
pub struct SteamApiClient {
2021-08-08 18:54:46 +02:00
cookies : reqwest ::cookie ::Jar ,
client : reqwest ::blocking ::Client ,
pub session : Option < Session > ,
2021-08-08 00:47:39 +02:00
}
impl SteamApiClient {
2021-08-10 02:05:23 +02:00
pub fn new ( session : Option < Session > ) -> SteamApiClient {
2021-08-08 18:54:46 +02:00
SteamApiClient {
2021-08-08 00:47:39 +02:00
cookies : reqwest ::cookie ::Jar ::default ( ) ,
client : reqwest ::blocking ::ClientBuilder ::new ( )
. cookie_store ( true )
. 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 " )
. default_headers ( HeaderMap ::from_iter ( hashmap! {
HeaderName ::from_str ( " X-Requested-With " ) . expect ( " could not build default request headers " ) = > HeaderValue ::from_str ( " com.valvesoftware.android.steam.community " ) . expect ( " could not build default request headers " )
} . into_iter ( ) ) )
. build ( )
. unwrap ( ) ,
2021-08-10 02:05:23 +02:00
session : session ,
2021-08-08 00:47:39 +02:00
}
2021-08-08 18:54:46 +02:00
}
fn build_session ( & self , data : & OAuthData ) -> Session {
2021-08-10 02:05:23 +02:00
trace! ( " SteamApiClient::build_session " ) ;
2021-08-08 18:54:46 +02:00
return Session {
token : data . oauth_token . clone ( ) ,
steam_id : data . steamid . parse ( ) . unwrap ( ) ,
steam_login : format ! ( " {}%7C%7C{} " , data . steamid , data . wgtoken ) ,
steam_login_secure : format ! ( " {}%7C%7C{} " , data . steamid , data . wgtoken_secure ) ,
2021-08-10 23:17:41 +02:00
session_id : self
. extract_session_id ( )
. expect ( " failed to extract session id from cookies " ) ,
2021-08-08 18:54:46 +02:00
web_cookie : data . webcookie . clone ( ) ,
} ;
}
fn extract_session_id ( & self ) -> Option < String > {
let cookies = self . cookies . cookies ( & STEAM_COOKIE_URL ) . unwrap ( ) ;
let all_cookies = cookies . to_str ( ) . unwrap ( ) ;
for cookie in all_cookies
. split ( " ; " )
. map ( | s | cookie ::Cookie ::parse ( s ) . unwrap ( ) )
{
if cookie . name ( ) = = " sessionid " {
return Some ( cookie . value ( ) . into ( ) ) ;
}
}
return None ;
}
pub fn save_cookies_from_response ( & mut self , response : & reqwest ::blocking ::Response ) {
let set_cookie_iter = response . headers ( ) . get_all ( SET_COOKIE ) ;
for c in set_cookie_iter {
c . to_str ( )
. into_iter ( )
. for_each ( | cookie_str | self . cookies . add_cookie_str ( cookie_str , & STEAM_COOKIE_URL ) ) ;
}
}
2021-08-10 23:17:41 +02:00
pub fn request < U : reqwest ::IntoUrl + std ::fmt ::Display > (
& self ,
method : reqwest ::Method ,
url : U ,
) -> RequestBuilder {
2021-08-10 02:40:06 +02:00
trace! ( " making request: {} {} " , method , url ) ;
2021-08-08 18:54:46 +02:00
self . cookies
. add_cookie_str ( " mobileClientVersion=0 (2.1.3) " , & STEAM_COOKIE_URL ) ;
self . cookies
. add_cookie_str ( " mobileClient=android " , & STEAM_COOKIE_URL ) ;
self . cookies
. add_cookie_str ( " Steam_Language=english " , & STEAM_COOKIE_URL ) ;
2021-08-10 02:40:06 +02:00
if let Some ( session ) = & self . session {
2021-08-10 23:17:41 +02:00
self . cookies . add_cookie_str (
format! ( " sessionid= {} " , session . session_id ) . as_str ( ) ,
& STEAM_COOKIE_URL ,
) ;
2021-08-10 02:40:06 +02:00
}
2021-08-08 18:54:46 +02:00
self . client
. request ( method , url )
. header ( COOKIE , self . cookies . cookies ( & STEAM_COOKIE_URL ) . unwrap ( ) )
}
2021-08-10 02:40:06 +02:00
pub fn get < U : reqwest ::IntoUrl + std ::fmt ::Display > ( & self , url : U ) -> RequestBuilder {
2021-08-08 18:54:46 +02:00
self . request ( reqwest ::Method ::GET , url )
}
2021-08-10 02:40:06 +02:00
pub fn post < U : reqwest ::IntoUrl + std ::fmt ::Display > ( & self , url : U ) -> RequestBuilder {
2021-08-08 18:54:46 +02:00
self . request ( reqwest ::Method ::POST , url )
}
/// Updates the cookie jar with the session cookies by pinging steam servers.
pub fn update_session ( & mut self ) -> anyhow ::Result < ( ) > {
trace! ( " SteamApiClient::update_session " ) ;
let resp = self
2021-08-08 00:47:39 +02:00
. get ( " https://steamcommunity.com/login?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client " . parse ::< Url > ( ) . unwrap ( ) )
. send ( ) ? ;
2021-08-08 18:54:46 +02:00
self . save_cookies_from_response ( & resp ) ;
trace! ( " {:?} " , resp ) ;
trace! ( " cookies: {:?} " , self . cookies ) ;
Ok ( ( ) )
}
/// Endpoint: POST /login/dologin
pub fn login (
& mut self ,
username : String ,
encrypted_password : String ,
twofactor_code : String ,
email_code : String ,
captcha_gid : String ,
captcha_text : String ,
rsa_timestamp : String ,
) -> anyhow ::Result < LoginResponse > {
let params = hashmap! {
" donotcache " = > format! (
" {} " ,
SystemTime ::now ( )
. duration_since ( UNIX_EPOCH )
. unwrap ( )
. as_secs ( )
* 1000
) ,
" username " = > username ,
" password " = > encrypted_password ,
" twofactorcode " = > twofactor_code ,
" emailauth " = > email_code ,
" captchagid " = > captcha_gid ,
" captcha_text " = > captcha_text ,
" rsatimestamp " = > rsa_timestamp ,
" remember_login " = > " true " . into ( ) ,
" oauth_client_id " = > " DE45CD61 " . into ( ) ,
" oauth_scope " = > " read_profile write_profile read_client write_client " . into ( ) ,
} ;
let resp = self
. post ( " https://steamcommunity.com/login/dologin " )
. form ( & params )
. send ( ) ? ;
2021-08-10 02:05:23 +02:00
self . save_cookies_from_response ( & resp ) ;
2021-08-08 18:54:46 +02:00
let text = resp . text ( ) ? ;
trace! ( " raw login response: {} " , text ) ;
let login_resp : LoginResponse = serde_json ::from_str ( text . as_str ( ) ) ? ;
if let Some ( oauth ) = & login_resp . oauth {
self . session = Some ( self . build_session ( & oauth ) ) ;
}
return Ok ( login_resp ) ;
}
/// A secondary step in the login flow. Does not seem to always be needed?
/// Endpoints: provided by `login()`
pub fn transfer_login ( & mut self , login_resp : LoginResponse ) -> anyhow ::Result < OAuthData > {
match ( login_resp . transfer_urls , login_resp . transfer_parameters ) {
( Some ( urls ) , Some ( params ) ) = > {
debug! ( " received transfer parameters, relaying data... " ) ;
for url in urls {
trace! ( " posting transfer to {} " , url ) ;
let resp = self . client . post ( url ) . json ( & params ) . send ( ) ? ;
self . save_cookies_from_response ( & resp ) ;
}
let oauth = OAuthData {
oauth_token : params . auth ,
steamid : params . steamid . parse ( ) . unwrap ( ) ,
wgtoken : params . token_secure . clone ( ) , // guessing
wgtoken_secure : params . token_secure ,
webcookie : params . webcookie ,
} ;
self . session = Some ( self . build_session ( & oauth ) ) ;
return Ok ( oauth ) ;
}
( None , None ) = > {
bail! ( " did not receive transfer_urls and transfer_parameters " ) ;
}
( _ , None ) = > {
bail! ( " did not receive transfer_parameters " ) ;
}
( None , _ ) = > {
bail! ( " did not receive transfer_urls " ) ;
}
}
}
2021-08-08 19:37:18 +02:00
2021-08-10 03:41:20 +02:00
/// One of the endpoints that handles phone number things. Can check to see if phone is present on account, and maybe do some other stuff. It's not really super clear.
///
/// Host: steamcommunity.com
2021-08-08 19:37:18 +02:00
/// Endpoint: POST /steamguard/phoneajax
2021-08-10 02:40:06 +02:00
/// Requires `sessionid` cookie to be set.
2021-08-08 19:37:18 +02:00
fn phoneajax ( & self , op : & str , arg : & str ) -> anyhow ::Result < bool > {
let mut params = hashmap! {
" op " = > op ,
" arg " = > arg ,
" sessionid " = > self . session . as_ref ( ) . unwrap ( ) . session_id . as_str ( ) ,
} ;
if op = = " check_sms_code " {
params . insert ( " checkfortos " , " 0 " ) ;
params . insert ( " skipvoip " , " 1 " ) ;
}
let resp = self
. post ( " https://steamcommunity.com/steamguard/phoneajax " )
. form ( & params )
. send ( ) ? ;
2021-08-10 03:41:20 +02:00
trace! ( " phoneajax: status={} " , resp . status ( ) ) ;
2021-08-08 19:37:18 +02:00
let result : Value = resp . json ( ) ? ;
2021-08-10 02:11:15 +02:00
trace! ( " phoneajax: {:?} " , result ) ;
2021-08-08 19:37:18 +02:00
if result [ " has_phone " ] ! = Value ::Null {
2021-08-10 02:11:15 +02:00
trace! ( " op: {} - found has_phone field " , op ) ;
2021-08-08 19:37:18 +02:00
return result [ " has_phone " ]
. as_bool ( )
. ok_or ( anyhow! ( " failed to parse has_phone field into boolean " ) ) ;
} else if result [ " success " ] ! = Value ::Null {
2021-08-10 02:11:15 +02:00
trace! ( " op: {} - found success field " , op ) ;
2021-08-08 19:37:18 +02:00
return result [ " success " ]
. as_bool ( )
. ok_or ( anyhow! ( " failed to parse success field into boolean " ) ) ;
} else {
2021-08-10 02:11:15 +02:00
trace! ( " op: {} - did not find any expected field " , op ) ;
2021-08-08 19:37:18 +02:00
return Ok ( false ) ;
}
}
2021-08-11 02:28:20 +02:00
/// Works similar to phoneajax. Used in the process to add a phone number to a steam account.
/// Valid ops:
/// - get_phone_number => `input` is treated as a phone number to add to the account. Yes, this is somewhat counter intuitive.
/// - resend_sms
2021-08-11 02:54:01 +02:00
/// - get_sms_code => `input` is treated as a the SMS code that was texted to the phone number. Again, this is somewhat counter intuitive. After this succeeds, the phone number is added to the account.
/// - email_verification => If the account is protected with steam guard email, a verification link is sent. After the link in the email is clicked, send this op. After, an SMS code is sent to the phone number.
/// - retry_email_verification
2021-08-11 02:28:20 +02:00
///
/// Host: store.steampowered.com
/// Endpoint: /phone/add_ajaxop
fn phone_add_ajaxop ( & self , op : & str , input : & str ) -> anyhow ::Result < ( ) > {
trace! ( " phone_add_ajaxop: op={} input={} " , op , input ) ;
let params = hashmap! {
" op " = > op ,
" input " = > input ,
" sessionid " = > self . session . as_ref ( ) . unwrap ( ) . session_id . as_str ( ) ,
} ;
let resp = self
2021-08-11 02:54:01 +02:00
. post ( " https://store.steampowered.com/phone/add_ajaxop " )
2021-08-11 02:28:20 +02:00
. form ( & params )
. send ( ) ? ;
trace! ( " phone_add_ajaxop: http status={} " , resp . status ( ) ) ;
let text = resp . text ( ) ? ;
trace! ( " phone_add_ajaxop response: {} " , text ) ;
todo! ( ) ;
}
2021-08-08 19:37:18 +02:00
pub fn has_phone ( & self ) -> anyhow ::Result < bool > {
return self . phoneajax ( " has_phone " , " null " ) ;
}
pub fn check_sms_code ( & self , sms_code : String ) -> anyhow ::Result < bool > {
return self . phoneajax ( " check_sms_code " , sms_code . as_str ( ) ) ;
}
pub fn check_email_confirmation ( & self ) -> anyhow ::Result < bool > {
return self . phoneajax ( " email_confirmation " , " " ) ;
}
pub fn add_phone_number ( & self , phone_number : String ) -> anyhow ::Result < bool > {
2021-08-10 03:41:20 +02:00
// return self.phoneajax("add_phone_number", phone_number.as_str());
todo! ( ) ;
}
/// Provides lots of juicy information, like if the number is a VOIP number.
/// Host: store.steampowered.com
/// Endpoint: POST /phone/validate
/// Found on page: https://store.steampowered.com/phone/add
pub fn phone_validate ( & self , phone_number : String ) -> anyhow ::Result < bool > {
2021-08-10 23:17:41 +02:00
let params = hashmap! {
2021-08-10 03:41:20 +02:00
" sessionID " = > " " ,
" phoneNumber " = > " " ,
} ;
todo! ( ) ;
2021-08-08 19:37:18 +02:00
}
2021-08-08 21:25:27 +02:00
/// Starts the authenticator linking process.
/// This doesn't check any prereqisites to ensure the request will pass validation on Steam's side (eg. sms/email confirmations).
/// A valid `Session` is required for this request. Cookies are not needed for this request, but they are set anyway.
///
/// Host: api.steampowered.com
/// Endpoint: POST /ITwoFactorService/AddAuthenticator/v0001
2021-08-10 23:17:41 +02:00
pub fn add_authenticator (
& mut self ,
device_id : String ,
) -> anyhow ::Result < AddAuthenticatorResponse > {
2021-08-08 21:25:27 +02:00
ensure! ( matches! ( self . session , Some ( _ ) ) ) ;
let params = hashmap! {
" access_token " = > self . session . as_ref ( ) . unwrap ( ) . token . clone ( ) ,
" steamid " = > self . session . as_ref ( ) . unwrap ( ) . steam_id . to_string ( ) ,
" authenticator_type " = > " 1 " . into ( ) ,
" device_identifier " = > device_id ,
" sms_phone_id " = > " 1 " . into ( ) ,
} ;
2021-08-10 02:05:23 +02:00
let resp = self
2021-08-08 21:25:27 +02:00
. post ( format! (
" {}/ITwoFactorService/AddAuthenticator/v0001 " ,
STEAM_API_BASE . to_string ( )
) )
. form ( & params )
2021-08-10 02:05:23 +02:00
. send ( ) ? ;
self . save_cookies_from_response ( & resp ) ;
let text = resp . text ( ) ? ;
trace! ( " raw add authenticator response: {} " , text ) ;
2021-08-08 21:25:27 +02:00
2021-08-09 01:09:15 +02:00
let resp : SteamApiResponse < AddAuthenticatorResponse > = serde_json ::from_str ( text . as_str ( ) ) ? ;
2021-08-08 21:25:27 +02:00
2021-08-09 01:09:15 +02:00
Ok ( resp . response )
2021-08-08 21:25:27 +02:00
}
2021-08-09 00:32:50 +02:00
///
/// Host: api.steampowered.com
/// Endpoint: POST /ITwoFactorService/FinalizeAddAuthenticator/v0001
pub fn finalize_authenticator (
& self ,
sms_code : String ,
code_2fa : String ,
2021-08-09 06:09:34 +02:00
time_2fa : i64 ,
2021-08-09 00:32:50 +02:00
) -> anyhow ::Result < FinalizeAddAuthenticatorResponse > {
ensure! ( matches! ( self . session , Some ( _ ) ) ) ;
let params = hashmap! {
" steamid " = > self . session . as_ref ( ) . unwrap ( ) . steam_id . to_string ( ) ,
" access_token " = > self . session . as_ref ( ) . unwrap ( ) . token . clone ( ) ,
" activation_code " = > sms_code ,
" authenticator_code " = > code_2fa ,
" authenticator_time " = > time_2fa . to_string ( ) ,
} ;
2021-08-10 04:11:09 +02:00
let resp = self
2021-08-09 00:32:50 +02:00
. post ( format! (
" {}/ITwoFactorService/FinalizeAddAuthenticator/v0001 " ,
STEAM_API_BASE . to_string ( )
) )
. form ( & params )
2021-08-10 04:11:09 +02:00
. send ( ) ? ;
let text = resp . text ( ) ? ;
trace! ( " raw finalize authenticator response: {} " , text ) ;
2021-08-10 23:17:41 +02:00
let resp : SteamApiResponse < FinalizeAddAuthenticatorResponse > =
serde_json ::from_str ( text . as_str ( ) ) ? ;
2021-08-09 00:32:50 +02:00
2021-08-09 01:11:15 +02:00
return Ok ( resp . response ) ;
2021-08-09 00:32:50 +02:00
}
2021-08-10 05:08:51 +02:00
/// Host: api.steampowered.com
/// Endpoint: POST /ITwoFactorService/RemoveAuthenticator/v0001
2021-08-10 23:17:41 +02:00
pub fn remove_authenticator (
& self ,
revocation_code : String ,
) -> anyhow ::Result < RemoveAuthenticatorResponse > {
let params = hashmap! {
2021-08-10 05:08:51 +02:00
" steamid " = > self . session . as_ref ( ) . unwrap ( ) . steam_id . to_string ( ) ,
" steamguard_scheme " = > " 2 " . into ( ) ,
" revocation_code " = > revocation_code ,
" access_token " = > self . session . as_ref ( ) . unwrap ( ) . token . to_string ( ) ,
} ;
let resp = self
. post ( format! (
" {}/ITwoFactorService/RemoveAuthenticator/v0001 " ,
STEAM_API_BASE . to_string ( )
) )
. form ( & params )
. send ( ) ? ;
let text = resp . text ( ) ? ;
trace! ( " raw remove authenticator response: {} " , text ) ;
2021-08-10 23:17:41 +02:00
let resp : SteamApiResponse < RemoveAuthenticatorResponse > =
serde_json ::from_str ( text . as_str ( ) ) ? ;
2021-08-10 05:08:51 +02:00
return Ok ( resp . response ) ;
}
2021-08-08 00:47:39 +02:00
}
#[ test ]
fn test_oauth_data_parse ( ) {
2021-08-08 18:54:46 +02:00
// 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 ( ) ;
assert_eq! ( oauth . steamid , " 78562647129469312 " ) ;
assert_eq! ( oauth . oauth_token , " fd2fdb3d0717bcd2220d98c7ec61c7bd " ) ;
assert_eq! ( oauth . wgtoken , " 72E7013D598A4F68C7E268F6FA3767D89D763732 " ) ;
assert_eq! (
oauth . wgtoken_secure ,
" 21061EA13C36D7C29812CAED900A215171AD13A2 "
) ;
assert_eq! ( oauth . webcookie , " 6298070A226E5DAD49938D78BCF36F7A7118FDD5 " ) ;
2021-08-08 00:47:39 +02:00
}
2021-08-08 17:16:06 +02:00
#[ test ]
fn test_login_response_parse ( ) {
2021-08-08 18:54:46 +02:00
let result = serde_json ::from_str ::< LoginResponse > ( include_str! (
" fixtures/api-responses/login-response1.json "
) ) ;
assert! (
matches! ( result , Ok ( _ ) ) ,
" got error: {} " ,
result . unwrap_err ( )
) ;
let resp = result . unwrap ( ) ;
let oauth = resp . oauth . unwrap ( ) ;
assert_eq! ( oauth . steamid , " 78562647129469312 " ) ;
assert_eq! ( oauth . oauth_token , " fd2fdb3d0717bad2220d98c7ec61c7bd " ) ;
assert_eq! ( oauth . wgtoken , " 72E7013D598A4F68C7E268F6FA3767D89D763732 " ) ;
assert_eq! (
oauth . wgtoken_secure ,
" 21061EA13C36D7C29812CAED900A215171AD13A2 "
) ;
assert_eq! ( oauth . webcookie , " 6298070A226E5DAD49938D78BCF36F7A7118FDD5 " ) ;
2021-08-08 17:16:06 +02:00
}
2021-08-08 21:25:27 +02:00
2021-08-10 01:49:53 +02:00
#[ test ]
fn test_login_response_parse_missing_webcookie ( ) {
let result = serde_json ::from_str ::< LoginResponse > ( include_str! (
" fixtures/api-responses/login-response-missing-webcookie.json "
) ) ;
assert! (
matches! ( result , Ok ( _ ) ) ,
" got error: {} " ,
result . unwrap_err ( )
) ;
let resp = result . unwrap ( ) ;
let oauth = resp . oauth . unwrap ( ) ;
assert_eq! ( oauth . steamid , " 92591609556178617 " ) ;
assert_eq! ( oauth . oauth_token , " 1cc83205dab2979e558534dab29f6f3aa " ) ;
assert_eq! ( oauth . wgtoken , " 3EDA9DEF07D7B39361D95203525D8AFE82A " ) ;
2021-08-10 23:17:41 +02:00
assert_eq! ( oauth . wgtoken_secure , " F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48 " ) ;
2021-08-10 01:49:53 +02:00
assert_eq! ( oauth . webcookie , " " ) ;
}
2021-08-08 21:25:27 +02:00
#[ derive(Debug, Clone, Deserialize) ]
2021-08-09 01:09:15 +02:00
pub struct SteamApiResponse < T > {
2021-08-09 06:09:34 +02:00
pub response : T ,
2021-08-08 21:25:27 +02:00
}
#[ derive(Debug, Clone, Deserialize) ]
2021-08-09 01:09:15 +02:00
pub struct AddAuthenticatorResponse {
2021-08-08 21:25:27 +02:00
/// Shared secret between server and authenticator
2021-08-10 04:46:50 +02:00
#[ serde(default) ]
2021-08-08 21:25:27 +02:00
pub shared_secret : String ,
/// Authenticator serial number (unique per token)
2021-08-10 04:46:50 +02:00
#[ serde(default) ]
2021-08-08 21:25:27 +02:00
pub serial_number : String ,
/// code used to revoke authenticator
2021-08-10 04:46:50 +02:00
#[ serde(default) ]
2021-08-08 21:25:27 +02:00
pub revocation_code : String ,
/// URI for QR code generation
2021-08-10 04:46:50 +02:00
#[ serde(default) ]
2021-08-08 21:25:27 +02:00
pub uri : String ,
/// Current server time
2021-08-10 04:46:50 +02:00
#[ serde(default, deserialize_with = " parse_json_string_as_number " ) ]
2021-08-08 21:25:27 +02:00
pub server_time : u64 ,
/// Account name to display on token client
2021-08-10 04:46:50 +02:00
#[ serde(default) ]
2021-08-08 21:25:27 +02:00
pub account_name : String ,
/// Token GID assigned by server
2021-08-10 04:46:50 +02:00
#[ serde(default) ]
2021-08-08 21:25:27 +02:00
pub token_gid : String ,
/// Secret used for identity attestation (e.g., for eventing)
2021-08-10 04:46:50 +02:00
#[ serde(default) ]
2021-08-08 21:25:27 +02:00
pub identity_secret : String ,
/// Spare shared secret
2021-08-10 04:46:50 +02:00
#[ serde(default) ]
2021-08-08 21:25:27 +02:00
pub secret_1 : String ,
/// Result code
2021-08-09 00:32:50 +02:00
pub status : i32 ,
2021-08-08 21:25:27 +02:00
}
impl AddAuthenticatorResponse {
pub fn to_steam_guard_account ( & self ) -> SteamGuardAccount {
SteamGuardAccount {
2021-08-09 01:09:15 +02:00
shared_secret : self . shared_secret . clone ( ) ,
serial_number : self . serial_number . clone ( ) ,
revocation_code : self . revocation_code . clone ( ) ,
uri : self . uri . clone ( ) ,
server_time : self . server_time ,
account_name : self . account_name . clone ( ) ,
token_gid : self . token_gid . clone ( ) ,
identity_secret : self . identity_secret . clone ( ) ,
secret_1 : self . secret_1 . clone ( ) ,
2021-08-08 21:25:27 +02:00
fully_enrolled : false ,
device_id : " " . into ( ) ,
session : None ,
}
}
}
2021-08-09 00:32:50 +02:00
2021-08-09 01:09:15 +02:00
#[ derive(Debug, Clone, Deserialize) ]
2021-08-09 06:09:34 +02:00
pub struct FinalizeAddAuthenticatorResponse {
pub status : i32 ,
2021-08-10 04:11:09 +02:00
#[ serde(deserialize_with = " parse_json_string_as_number " ) ]
2021-08-09 06:09:34 +02:00
pub server_time : u64 ,
pub want_more : bool ,
pub success : bool ,
}
2021-08-10 04:00:49 +02:00
2021-08-10 05:08:51 +02:00
#[ derive(Debug, Clone, Deserialize) ]
pub struct RemoveAuthenticatorResponse {
2021-08-10 23:17:41 +02:00
pub success : bool ,
2021-08-10 05:08:51 +02:00
}
2021-08-10 04:00:49 +02:00
fn parse_json_string_as_number < ' de , D > ( deserializer : D ) -> Result < u64 , D ::Error >
where
D : Deserializer < ' de > ,
{
// for some reason, deserializing to &str doesn't work but this does.
let s : String = Deserialize ::deserialize ( deserializer ) ? ;
Ok ( s . parse ( ) . unwrap ( ) )
}
#[ test ]
fn test_parse_add_auth_response ( ) {
let result = serde_json ::from_str ::< SteamApiResponse < AddAuthenticatorResponse > > ( include_str! (
" fixtures/api-responses/add-authenticator-1.json "
) ) ;
assert! (
matches! ( result , Ok ( _ ) ) ,
" got error: {} " ,
result . unwrap_err ( )
) ;
let resp = result . unwrap ( ) . response ;
assert_eq! ( resp . server_time , 1628559846 ) ;
assert_eq! ( resp . shared_secret , " wGwZx=sX5MmTxi6QgA3Gi " ) ;
assert_eq! ( resp . revocation_code , " R123456 " ) ;
2021-08-10 04:46:50 +02:00
}
#[ test ]
fn test_parse_add_auth_response2 ( ) {
let result = serde_json ::from_str ::< SteamApiResponse < AddAuthenticatorResponse > > ( include_str! (
" fixtures/api-responses/add-authenticator-2.json "
) ) ;
assert! (
matches! ( result , Ok ( _ ) ) ,
" got error: {} " ,
result . unwrap_err ( )
) ;
let resp = result . unwrap ( ) . response ;
assert_eq! ( resp . status , 29 ) ;
}