1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
//! A port of the famous C# SteamAuth library, that allows users to add/remove a mobile
//! authenticator, and also confirm/deny mobile confirmations.

#![allow(dead_code)]
#![warn(missing_docs, missing_doc_code_examples)]
#![deny(
    missing_debug_implementations,
    missing_copy_implementations,
    trivial_casts,
    trivial_numeric_casts,
    unsafe_code,
    unused_import_braces,
    unused_qualifications
)]

use std::fmt;
use std::fmt::{Debug, Formatter};
use std::fs::OpenOptions;
use std::io::Read;
use std::path::PathBuf;

use const_format::concatcp;
/// re-export
pub use reqwest::{header::HeaderMap, Error as HttpError, Method, Url};
use serde::{Deserialize, Serialize};
use steam_totp::Secret;
use steamid_parser::SteamID;
pub use utils::format_captcha_url;
use uuid::Uuid;

pub use web_handler::confirmation::{ConfirmationMethod, Confirmations, EConfirmationType};
pub use web_handler::steam_guard_linker::AddAuthenticatorStep;

pub mod client;
pub mod errors;
mod page_scraper;
pub(crate) mod retry;
mod types;
pub(crate) mod utils;
mod web_handler;

/// Recommended time to allow STEAM to catch up.
const STEAM_DELAY_MS: u64 = 350;
/// Extension of the mobile authenticator files.
const MA_FILE_EXT: &str = ".maFile";

// HOST SHOULD BE USED FOR COOKIE RETRIEVAL INSIDE COOKIE JAR!!

/// Steam Community Cookie Host
pub const STEAM_COMMUNITY_HOST: &str = ".steamcommunity.com";
/// Steam Help Cookie Host
pub const STEAM_HELP_HOST: &str = ".help.steampowered.com";
/// Steam Store Cookie Host
pub const STEAM_STORE_HOST: &str = ".store.steampowered.com";

/// Should not be used for cookie retrieval. Use `STEAM_COMMUNTY_HOST` instead.
const STEAM_COMMUNITY_BASE: &str = "https://steamcommunity.com";
/// Should not be used for cookie retrieval. Use `STEAM_STORE_HOST` instead.
const STEAM_STORE_BASE: &str = "https://store.steampowered.com";
/// Should not be used for cookie retrieval. Use `STEAM_API_HOST` instead.
const STEAM_API_BASE: &str = "https://api.steampowered.com";

const MOBILE_REFERER: &str = concatcp!(
    STEAM_COMMUNITY_BASE,
    "/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client"
);

#[derive(Debug, Clone)]
/// User that is needed for the authenticator to work.
/// Ideally all fields should be populated before authenticator operations are made.
///
/// A simple implementation that has everything required to work properly:
/// ```no_run
/// use steam_mobile::User;
///
/// User::build()
///     .username("test_username")
///     .password("password")
///     .parental_code("1111") // Only needed if the is a parental code, otherwise skip
///     .ma_file_from_disk("assets/my.maFile");
/// ```
pub struct User {
    username: String,
    password: String,
    parental_code: Option<String>,
    linked_mafile: Option<MobileAuthFile>,
}

#[derive(Default, Debug, Clone)]
/// Information that we cache after the login operation to avoid querying Steam multiple times.
///
///
/// SteamID, API KEY and the login Oauth token are currently cached by `SteamAuthenticator`.
struct CachedInfo {
    steamid: Option<SteamID>,
    api_key: Option<String>,
    /// Oauth token recovered at the login.
    /// Some places call this access_token.
    oauth_token: Option<String>,
}

impl CachedInfo {
    // FIXME: This should not unwrap, probably result with steamid parse error.
    fn set_steamid(&mut self, steamid: &str) {
        let parsed_steamid = SteamID::parse(steamid).unwrap();
        self.steamid = Some(parsed_steamid);
    }

    fn set_oauth_token(&mut self, token: String) {
        self.oauth_token = Some(token);
    }

    fn set_api_key(&mut self, api_key: String) {
        self.api_key = Some(api_key);
    }

    fn api_key(&self) -> Option<&str> {
        self.api_key.as_deref()
    }

    fn steam_id(&self) -> Option<u64> {
        Some(self.steamid.as_ref()?.to_steam64())
    }

    fn oauth_token(&self) -> Option<&str> {
        self.oauth_token.as_deref()
    }
}

impl User {
    /// Constructs a new user.
    // TODO: This should be a UserBuilder, not simply this methods.
    pub fn build() -> Self {
        Self {
            username: "".to_string(),
            password: "".to_string(),
            parental_code: None,
            linked_mafile: None,
        }
    }

    fn shared_secret(&self) -> Option<Secret> {
        Some(Secret::from_b64(&self.linked_mafile.as_ref()?.shared_secret).unwrap())
    }

    fn identity_secret(&self) -> Option<Secret> {
        Some(Secret::from_b64(&self.linked_mafile.as_ref()?.identity_secret).unwrap())
    }

    fn device_id(&self) -> Option<&str> {
        Some(&self.linked_mafile.as_ref()?.device_id.as_ref()?)
    }

    /// Sets the account username, mandatory
    pub fn username<T: ToString>(mut self, username: T) -> Self {
        self.username = username.to_string();
        self
    }

    /// Sets the account password, mandatory
    pub fn password<T: ToString>(mut self, password: T) -> Self {
        self.password = password.to_string();
        self
    }

    /// Sets the parental code, if any.
    pub fn parental_code<T: ToString>(mut self, parental_code: T) -> Self {
        self.parental_code = Some(parental_code.to_string());
        self
    }

    /// Convenience function that imports the file from disk
    pub fn ma_file_from_disk<T>(mut self, path: T) -> Self
    where
        T: Into<PathBuf>,
    {
        let mut file = OpenOptions::new().read(true).open(path.into()).unwrap();
        let mut buffer = String::new();

        file.read_to_string(&mut buffer).unwrap();
        self.linked_mafile = Some(serde_json::from_str::<MobileAuthFile>(&buffer).unwrap());
        self
    }

    pub fn ma_file(mut self, ma_file: MobileAuthFile) -> Self {
        self.linked_mafile = Some(ma_file);
        self
    }
}

#[derive(Clone, Serialize, Deserialize, PartialEq)]
/// The MobileAuthFile (.maFile) is the standard file format that custom authenticators use to save auth secrets to
/// disk.
///
/// It follows strictly the JSON format.
/// Both identity_secret and shared_secret should be base64 encoded. If you don't know if they are, they probably
/// already are.
///
///
/// Example:
/// ```json
/// {
///     identity_secret: "secret"
///     shared_secret: "secret"
///     device_id: "android:xxxxxxxxxxxxxxx"
/// }
/// ```
pub struct MobileAuthFile {
    /// Identity secret is used to generate the confirmation links for our trade requests.
    /// If we are generating our own Authenticator, this is given by Steam.
    identity_secret: String,
    /// The shared secret is used to generate TOTP codes.
    shared_secret: String,
    /// Device ID is used to generate the confirmation links for our trade requests.
    /// Can be retrieved from mobile device, such as a rooted android, iOS, or generated from the account's SteamID if
    /// creating our own authenticator. Needed for confirmations to trade to work properly.
    device_id: Option<String>,
    /// Used if shared secret is lost. Please, don't lose it.
    revocation_code: Option<String>,
    /// Account name where this maFile was originated.
    pub account_name: Option<String>,
}

impl Debug for MobileAuthFile {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("MobileAuthFile")
            .field("AccountName", &self.account_name)
            .finish()
    }
}

impl MobileAuthFile {
    fn set_device_id(&mut self, device_id: String) {
        self.device_id = Some(device_id);
    }

    /// Creates a new `MobileAuthFile`
    pub fn new<T>(identity_secret: String, shared_secret: String, device_id: T) -> Self
    where
        T: Into<Option<String>>,
    {
        Self {
            identity_secret,
            shared_secret,
            device_id: device_id.into(),
            revocation_code: None,
            account_name: None,
        }
    }
}

#[derive(Serialize, Deserialize, Debug)]
/// Identifies the mobile device and needed to generate confirmation links.
///
/// It is on the format of a UUID V4.
struct DeviceId(String);

impl DeviceId {
    const PREFIX: &'static str = "android:";

    /// Generates a random device ID on the format of UUID v4
    /// Example: android:780c3700-2b4f-4b9a-a196-9af6e6010d09
    pub fn generate() -> Self {
        Self {
            0: Self::PREFIX.to_owned() + &Uuid::new_v4().to_string(),
        }
    }
    pub fn validate() {}
}