Merge pull request #176 from dyc3/api-response-refactor
api response refactor
This commit is contained in:
commit
15bb6230f0
9 changed files with 313 additions and 283 deletions
|
@ -257,10 +257,12 @@ impl Manifest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return all loaded accounts. Order is not guarenteed.
|
/// Return all loaded accounts. Order is not guarenteed.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn get_all_loaded(&self) -> Vec<Arc<Mutex<SteamGuardAccount>>> {
|
pub fn get_all_loaded(&self) -> Vec<Arc<Mutex<SteamGuardAccount>>> {
|
||||||
return self.accounts.values().cloned().into_iter().collect();
|
return self.accounts.values().cloned().into_iter().collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn get_entry(
|
pub fn get_entry(
|
||||||
&self,
|
&self,
|
||||||
account_name: &String,
|
account_name: &String,
|
||||||
|
@ -271,6 +273,7 @@ impl Manifest {
|
||||||
.ok_or(ManifestAccountLoadError::MissingManifestEntry)
|
.ok_or(ManifestAccountLoadError::MissingManifestEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn get_entry_mut(
|
pub fn get_entry_mut(
|
||||||
&mut self,
|
&mut self,
|
||||||
account_name: &String,
|
account_name: &String,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
steamapi::{
|
api_responses::{AddAuthenticatorResponse, FinalizeAddAuthenticatorResponse},
|
||||||
AddAuthenticatorResponse, FinalizeAddAuthenticatorResponse, Session, SteamApiClient,
|
steamapi::{Session, SteamApiClient},
|
||||||
},
|
|
||||||
SteamGuardAccount,
|
SteamGuardAccount,
|
||||||
};
|
};
|
||||||
use log::*;
|
use log::*;
|
||||||
|
|
130
steamguard/src/api_responses/i_two_factor_service.rs
Normal file
130
steamguard/src/api_responses/i_two_factor_service.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
use crate::{token::TwoFactorSecret, SteamGuardAccount};
|
||||||
|
|
||||||
|
use super::parse_json_string_as_number;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Represents the response from `/ITwoFactorService/QueryTime/v0001`
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct QueryTimeResponse {
|
||||||
|
/// The time that the server will use to check your two factor code.
|
||||||
|
#[serde(deserialize_with = "parse_json_string_as_number")]
|
||||||
|
pub server_time: u64,
|
||||||
|
#[serde(deserialize_with = "parse_json_string_as_number")]
|
||||||
|
pub skew_tolerance_seconds: u64,
|
||||||
|
#[serde(deserialize_with = "parse_json_string_as_number")]
|
||||||
|
pub large_time_jink: u64,
|
||||||
|
pub probe_frequency_seconds: u64,
|
||||||
|
pub adjusted_time_probe_frequency_seconds: u64,
|
||||||
|
pub hint_probe_frequency_seconds: u64,
|
||||||
|
pub sync_timeout: u64,
|
||||||
|
pub try_again_seconds: u64,
|
||||||
|
pub max_attempts: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct AddAuthenticatorResponse {
|
||||||
|
/// Shared secret between server and authenticator
|
||||||
|
#[serde(default)]
|
||||||
|
pub shared_secret: String,
|
||||||
|
/// Authenticator serial number (unique per token)
|
||||||
|
#[serde(default)]
|
||||||
|
pub serial_number: String,
|
||||||
|
/// code used to revoke authenticator
|
||||||
|
#[serde(default)]
|
||||||
|
pub revocation_code: String,
|
||||||
|
/// URI for QR code generation
|
||||||
|
#[serde(default)]
|
||||||
|
pub uri: String,
|
||||||
|
/// Current server time
|
||||||
|
#[serde(default, deserialize_with = "parse_json_string_as_number")]
|
||||||
|
pub server_time: u64,
|
||||||
|
/// Account name to display on token client
|
||||||
|
#[serde(default)]
|
||||||
|
pub account_name: String,
|
||||||
|
/// Token GID assigned by server
|
||||||
|
#[serde(default)]
|
||||||
|
pub token_gid: String,
|
||||||
|
/// Secret used for identity attestation (e.g., for eventing)
|
||||||
|
#[serde(default)]
|
||||||
|
pub identity_secret: String,
|
||||||
|
/// Spare shared secret
|
||||||
|
#[serde(default)]
|
||||||
|
pub secret_1: String,
|
||||||
|
/// Result code
|
||||||
|
pub status: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub phone_number_hint: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddAuthenticatorResponse {
|
||||||
|
pub fn to_steam_guard_account(self) -> SteamGuardAccount {
|
||||||
|
SteamGuardAccount {
|
||||||
|
shared_secret: TwoFactorSecret::parse_shared_secret(self.shared_secret).unwrap(),
|
||||||
|
serial_number: self.serial_number.clone(),
|
||||||
|
revocation_code: self.revocation_code.into(),
|
||||||
|
uri: self.uri.into(),
|
||||||
|
server_time: self.server_time,
|
||||||
|
account_name: self.account_name.clone(),
|
||||||
|
token_gid: self.token_gid.clone(),
|
||||||
|
identity_secret: self.identity_secret.into(),
|
||||||
|
secret_1: self.secret_1.into(),
|
||||||
|
fully_enrolled: false,
|
||||||
|
device_id: "".into(),
|
||||||
|
session: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct FinalizeAddAuthenticatorResponse {
|
||||||
|
pub status: i32,
|
||||||
|
#[serde(deserialize_with = "parse_json_string_as_number")]
|
||||||
|
pub server_time: u64,
|
||||||
|
pub want_more: bool,
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct RemoveAuthenticatorResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::api_responses::SteamApiResponse;
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
}
|
137
steamguard/src/api_responses/login.rs
Normal file
137
steamguard/src/api_responses/login.rs
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
|
use super::parse_json_string_as_number;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LoginTransferParameters {
|
||||||
|
pub steamid: String,
|
||||||
|
pub token_secure: String,
|
||||||
|
pub auth: String,
|
||||||
|
pub remember_login: bool,
|
||||||
|
pub webcookie: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct OAuthData {
|
||||||
|
pub oauth_token: String,
|
||||||
|
pub steamid: String,
|
||||||
|
pub wgtoken: String,
|
||||||
|
pub wgtoken_secure: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub webcookie: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct RsaResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub publickey_exp: String,
|
||||||
|
pub publickey_mod: String,
|
||||||
|
pub timestamp: String,
|
||||||
|
pub token_gid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct LoginResponse {
|
||||||
|
pub success: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub login_complete: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub captcha_needed: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub captcha_gid: String,
|
||||||
|
#[serde(default, deserialize_with = "parse_json_string_as_number")]
|
||||||
|
pub emailsteamid: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub emailauth_needed: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requires_twofactor: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub message: String,
|
||||||
|
#[serde(default, deserialize_with = "oauth_data_from_string")]
|
||||||
|
pub oauth: Option<OAuthData>,
|
||||||
|
pub transfer_urls: Option<Vec<String>>,
|
||||||
|
pub transfer_parameters: Option<LoginTransferParameters>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginResponse {
|
||||||
|
pub fn needs_transfer_login(&self) -> bool {
|
||||||
|
self.transfer_urls.is_some() || self.transfer_parameters.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_oauth_data_parse() {
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_login_response_parse() {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
assert_eq!(oauth.wgtoken_secure, "F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48");
|
||||||
|
assert_eq!(oauth.webcookie, "");
|
||||||
|
}
|
||||||
|
}
|
23
steamguard/src/api_responses/mod.rs
Normal file
23
steamguard/src/api_responses/mod.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
mod i_two_factor_service;
|
||||||
|
mod login;
|
||||||
|
mod phone_ajax;
|
||||||
|
|
||||||
|
pub use i_two_factor_service::*;
|
||||||
|
pub use login::*;
|
||||||
|
pub use phone_ajax::*;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
|
pub(crate) 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct SteamApiResponse<T> {
|
||||||
|
pub response: T,
|
||||||
|
}
|
11
steamguard/src/api_responses/phone_ajax.rs
Normal file
11
steamguard/src/api_responses/phone_ajax.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct PhoneValidateResponse {
|
||||||
|
success: bool,
|
||||||
|
number: String,
|
||||||
|
is_valid: bool,
|
||||||
|
is_voip: bool,
|
||||||
|
is_fixed: bool,
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ use anyhow::Result;
|
||||||
pub use confirmation::{Confirmation, ConfirmationType};
|
pub use confirmation::{Confirmation, ConfirmationType};
|
||||||
use hmacsha1::hmac_sha1;
|
use hmacsha1::hmac_sha1;
|
||||||
use log::*;
|
use log::*;
|
||||||
use regex::Regex;
|
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
cookie::CookieStore,
|
cookie::CookieStore,
|
||||||
header::{COOKIE, USER_AGENT},
|
header::{COOKIE, USER_AGENT},
|
||||||
|
@ -24,6 +23,7 @@ extern crate anyhow;
|
||||||
extern crate maplit;
|
extern crate maplit;
|
||||||
|
|
||||||
mod accountlinker;
|
mod accountlinker;
|
||||||
|
mod api_responses;
|
||||||
mod confirmation;
|
mod confirmation;
|
||||||
mod secret_string;
|
mod secret_string;
|
||||||
pub mod steamapi;
|
pub mod steamapi;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::token::TwoFactorSecret;
|
use crate::api_responses::*;
|
||||||
use crate::SteamGuardAccount;
|
|
||||||
use log::*;
|
use log::*;
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
blocking::RequestBuilder,
|
blocking::RequestBuilder,
|
||||||
|
@ -9,7 +8,7 @@ use reqwest::{
|
||||||
Url,
|
Url,
|
||||||
};
|
};
|
||||||
use secrecy::{CloneableSecret, DebugSecret, ExposeSecret, SerializableSecret};
|
use secrecy::{CloneableSecret, DebugSecret, ExposeSecret, SerializableSecret};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -21,77 +20,6 @@ lazy_static! {
|
||||||
static ref STEAM_API_BASE: String = "https://api.steampowered.com".into();
|
static ref STEAM_API_BASE: String = "https://api.steampowered.com".into();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct LoginResponse {
|
|
||||||
pub success: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub login_complete: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub captcha_needed: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub captcha_gid: String,
|
|
||||||
#[serde(default, deserialize_with = "parse_json_string_as_number")]
|
|
||||||
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>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoginResponse {
|
|
||||||
pub fn needs_transfer_login(&self) -> bool {
|
|
||||||
self.transfer_urls.is_some() || self.transfer_parameters.is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct LoginTransferParameters {
|
|
||||||
steamid: String,
|
|
||||||
token_secure: String,
|
|
||||||
auth: String,
|
|
||||||
remember_login: bool,
|
|
||||||
webcookie: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct RsaResponse {
|
|
||||||
pub success: bool,
|
|
||||||
pub publickey_exp: String,
|
|
||||||
pub publickey_mod: String,
|
|
||||||
pub timestamp: String,
|
|
||||||
pub token_gid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct OAuthData {
|
|
||||||
oauth_token: String,
|
|
||||||
steamid: String,
|
|
||||||
wgtoken: String,
|
|
||||||
wgtoken_secure: String,
|
|
||||||
#[serde(default)]
|
|
||||||
webcookie: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Zeroize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Zeroize)]
|
||||||
#[zeroize(drop)]
|
#[zeroize(drop)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
|
@ -113,24 +41,6 @@ impl SerializableSecret for Session {}
|
||||||
impl CloneableSecret for Session {}
|
impl CloneableSecret for Session {}
|
||||||
impl DebugSecret for Session {}
|
impl DebugSecret for Session {}
|
||||||
|
|
||||||
/// Represents the response from `/ITwoFactorService/QueryTime/v0001`
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct QueryTimeResponse {
|
|
||||||
/// The time that the server will use to check your two factor code.
|
|
||||||
#[serde(deserialize_with = "parse_json_string_as_number")]
|
|
||||||
pub server_time: u64,
|
|
||||||
#[serde(deserialize_with = "parse_json_string_as_number")]
|
|
||||||
pub skew_tolerance_seconds: u64,
|
|
||||||
#[serde(deserialize_with = "parse_json_string_as_number")]
|
|
||||||
pub large_time_jink: u64,
|
|
||||||
pub probe_frequency_seconds: u64,
|
|
||||||
pub adjusted_time_probe_frequency_seconds: u64,
|
|
||||||
pub hint_probe_frequency_seconds: u64,
|
|
||||||
pub sync_timeout: u64,
|
|
||||||
pub try_again_seconds: u64,
|
|
||||||
pub max_attempts: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Queries Steam for the current time.
|
/// Queries Steam for the current time.
|
||||||
///
|
///
|
||||||
/// Endpoint: `/ITwoFactorService/QueryTime/v0001`
|
/// Endpoint: `/ITwoFactorService/QueryTime/v0001`
|
||||||
|
@ -565,189 +475,3 @@ impl SteamApiClient {
|
||||||
return Ok(resp.response);
|
return Ok(resp.response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_oauth_data_parse() {
|
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_login_response_parse() {
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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");
|
|
||||||
assert_eq!(oauth.wgtoken_secure, "F31641B9AFC2F8B0EE7B6F44D7E73EA3FA48");
|
|
||||||
assert_eq!(oauth.webcookie, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct SteamApiResponse<T> {
|
|
||||||
pub response: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct AddAuthenticatorResponse {
|
|
||||||
/// Shared secret between server and authenticator
|
|
||||||
#[serde(default)]
|
|
||||||
pub shared_secret: String,
|
|
||||||
/// Authenticator serial number (unique per token)
|
|
||||||
#[serde(default)]
|
|
||||||
pub serial_number: String,
|
|
||||||
/// code used to revoke authenticator
|
|
||||||
#[serde(default)]
|
|
||||||
pub revocation_code: String,
|
|
||||||
/// URI for QR code generation
|
|
||||||
#[serde(default)]
|
|
||||||
pub uri: String,
|
|
||||||
/// Current server time
|
|
||||||
#[serde(default, deserialize_with = "parse_json_string_as_number")]
|
|
||||||
pub server_time: u64,
|
|
||||||
/// Account name to display on token client
|
|
||||||
#[serde(default)]
|
|
||||||
pub account_name: String,
|
|
||||||
/// Token GID assigned by server
|
|
||||||
#[serde(default)]
|
|
||||||
pub token_gid: String,
|
|
||||||
/// Secret used for identity attestation (e.g., for eventing)
|
|
||||||
#[serde(default)]
|
|
||||||
pub identity_secret: String,
|
|
||||||
/// Spare shared secret
|
|
||||||
#[serde(default)]
|
|
||||||
pub secret_1: String,
|
|
||||||
/// Result code
|
|
||||||
pub status: i32,
|
|
||||||
#[serde(default)]
|
|
||||||
pub phone_number_hint: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AddAuthenticatorResponse {
|
|
||||||
pub fn to_steam_guard_account(self) -> SteamGuardAccount {
|
|
||||||
SteamGuardAccount {
|
|
||||||
shared_secret: TwoFactorSecret::parse_shared_secret(self.shared_secret).unwrap(),
|
|
||||||
serial_number: self.serial_number.clone(),
|
|
||||||
revocation_code: self.revocation_code.into(),
|
|
||||||
uri: self.uri.into(),
|
|
||||||
server_time: self.server_time,
|
|
||||||
account_name: self.account_name.clone(),
|
|
||||||
token_gid: self.token_gid.clone(),
|
|
||||||
identity_secret: self.identity_secret.into(),
|
|
||||||
secret_1: self.secret_1.into(),
|
|
||||||
fully_enrolled: false,
|
|
||||||
device_id: "".into(),
|
|
||||||
session: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct FinalizeAddAuthenticatorResponse {
|
|
||||||
pub status: i32,
|
|
||||||
#[serde(deserialize_with = "parse_json_string_as_number")]
|
|
||||||
pub server_time: u64,
|
|
||||||
pub want_more: bool,
|
|
||||||
pub success: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct PhoneValidateResponse {
|
|
||||||
success: bool,
|
|
||||||
number: String,
|
|
||||||
is_valid: bool,
|
|
||||||
is_voip: bool,
|
|
||||||
is_fixed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct RemoveAuthenticatorResponse {
|
|
||||||
pub success: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use crate::steamapi::{LoginResponse, RsaResponse, Session, SteamApiClient};
|
use crate::{
|
||||||
|
api_responses::{LoginResponse, RsaResponse},
|
||||||
|
steamapi::{Session, SteamApiClient},
|
||||||
|
};
|
||||||
use log::*;
|
use log::*;
|
||||||
use rsa::{PublicKey, RsaPublicKey};
|
use rsa::{PublicKey, RsaPublicKey};
|
||||||
use secrecy::ExposeSecret;
|
use secrecy::ExposeSecret;
|
||||||
|
|
Loading…
Reference in a new issue