steam_mobile/
client.rs

1use std::fmt::Debug;
2use std::marker::PhantomData;
3use std::ops::Deref;
4use std::sync::Arc;
5use std::time::Duration;
6
7use backoff::future::retry;
8use base64::Engine;
9use cookie::Cookie;
10use cookie::CookieJar;
11use futures::TryFutureExt;
12use futures_timer::Delay;
13use parking_lot::RwLock;
14use reqwest::header::HeaderMap;
15use reqwest::header::HeaderValue;
16use reqwest::header::CONTENT_TYPE;
17use reqwest::redirect::Policy;
18use reqwest::Client;
19use reqwest::IntoUrl;
20use reqwest::Method;
21use reqwest::Response;
22use reqwest::Url;
23use scraper::Html;
24use serde::de::DeserializeOwned;
25use serde::Serialize;
26use steam_protobuf::ProtobufDeserialize;
27use steam_protobuf::ProtobufSerialize;
28use tracing::debug;
29use tracing::error;
30use tracing::info;
31use tracing::trace;
32use tracing::warn;
33
34use crate::adapter::SteamCookie;
35use crate::errors::AuthError;
36use crate::errors::InternalError;
37use crate::errors::LinkerError;
38use crate::retry::login_retry_strategy;
39use crate::user::IsUser;
40use crate::user::PresentMaFile;
41use crate::user::SteamUser;
42use crate::utils::dump_cookies_by_domain;
43use crate::utils::dump_cookies_by_domain_and_name;
44use crate::utils::retrieve_header_location;
45use crate::web_handler::cache_api_key;
46use crate::web_handler::confirmation::Confirmation;
47use crate::web_handler::confirmation::Confirmations;
48use crate::web_handler::get_confirmations;
49use crate::web_handler::login::login_and_store_cookies;
50use crate::web_handler::send_confirmations;
51use crate::web_handler::steam_guard_linker::account_has_phone;
52use crate::web_handler::steam_guard_linker::add_authenticator_to_account;
53use crate::web_handler::steam_guard_linker::add_phone_to_account;
54use crate::web_handler::steam_guard_linker::check_email_confirmation;
55use crate::web_handler::steam_guard_linker::check_sms;
56use crate::web_handler::steam_guard_linker::finalize;
57use crate::web_handler::steam_guard_linker::remove_authenticator;
58use crate::web_handler::steam_guard_linker::twofactor_status;
59use crate::web_handler::steam_guard_linker::validate_phone_number;
60use crate::web_handler::steam_guard_linker::AddAuthenticatorStep;
61use crate::web_handler::steam_guard_linker::QueryStatusResponse;
62use crate::web_handler::steam_guard_linker::RemoveAuthenticatorScheme;
63use crate::web_handler::steam_guard_linker::STEAM_ADD_PHONE_CATCHUP_SECS;
64use crate::CacheGuard;
65use crate::ConfirmationAction;
66use crate::MobileAuthFile;
67use crate::STEAM_COMMUNITY_HOST;
68
69/// Main authenticator. We use it to spawn and act as our "mobile" client.
70/// Responsible for accepting/denying trades, and some other operations that may or not be related
71/// to mobile operations.
72///
73/// # Example: Fetch mobile notifications
74///
75/// ```rust
76/// use steam_mobile::SteamAuthenticator;
77/// use steam_mobile::User;
78/// ```
79#[derive(Debug)]
80pub struct SteamAuthenticator<AuthState, MaFileState> {
81    inner: InnerAuthenticator<MaFileState>,
82    auth_level: PhantomData<AuthState>,
83}
84
85#[derive(Debug)]
86struct InnerAuthenticator<MaFileState> {
87    pub(crate) client: MobileClient,
88    pub(crate) user: SteamUser<MaFileState>,
89    pub(crate) cache: Option<CacheGuard>,
90}
91
92/// A successfully logged-in state. Many assumptions are made on this state.
93#[derive(Clone, Copy, Debug)]
94pub struct Authenticated;
95
96/// A pending authorization state.
97#[derive(Clone, Copy, Debug)]
98pub struct Unauthenticated;
99
100impl<AuthState, M> SteamAuthenticator<AuthState, M> {
101    const fn client(&self) -> &MobileClient {
102        &self.inner.client
103    }
104    const fn user(&self) -> &SteamUser<M> {
105        &self.inner.user
106    }
107}
108
109impl<MaFileState> SteamAuthenticator<Unauthenticated, MaFileState>
110where
111    MaFileState: 'static + Send + Sync + Clone,
112{
113    /// Returns current user API Key.
114    ///
115    /// Will return `None` if you are not logged in.
116    #[must_use]
117    pub fn new(user: SteamUser<MaFileState>) -> Self {
118        Self {
119            inner: InnerAuthenticator {
120                client: MobileClient::default(),
121                user,
122                cache: None,
123            },
124            auth_level: PhantomData::<Unauthenticated>,
125        }
126    }
127    /// Log on into Steam website and populates the inner client with cookies for the Steam Store,
128    /// Steam community and Steam help domains.
129    ///
130    /// Automatically unlocks parental control if user uses it, but it need to be included inside
131    /// the [SteamUser] builder.
132    ///
133    /// The mobile client also has a very simple exponential retry strategy for errors that are *probably*
134    /// caused by fast requests, so we retry it. For errors such as bad credentials, or inserting captcha
135    /// the proper errors are raised by `AuthError`.
136    ///
137    /// Also caches the API Key, if the user wants to use it for any operation later.
138    ///
139    /// The cookies are inside the [MobileClient] inner cookie storage.
140    pub async fn login(self) -> Result<SteamAuthenticator<Authenticated, MaFileState>, AuthError> {
141        let user = self.inner.user;
142        let client = self.inner.client;
143        let user_arc: Arc<dyn IsUser> = Arc::new(user.clone());
144
145        // FIXME: Add more permanent errors, such as bad credentials
146        let mut cache = retry(login_retry_strategy(), || async {
147            login_and_store_cookies(&client, user_arc.clone())
148                .await
149                .map_err(|error| match error {
150                    e => {
151                        warn!("Permanent error happened.");
152                        warn!("{e}");
153                        backoff::Error::permanent(e)
154                    }
155                })
156        })
157        .await?;
158        info!("Login to Steam successfully.");
159
160        // FIXME: This should work the same as login, because it can sometimes fail for no reason
161        // if user.parental_code.is_some() {
162        //     parental_unlock(client, user).await?;
163        //     info!("Parental unlock successfully.");
164        // }
165
166        let api_key = cache_api_key(&client, user_arc.clone(), cache.steamid.to_steam64()).await;
167        if let Some(api_key) = api_key {
168            cache.set_api_key(Some(api_key));
169            info!("Cached API Key successfully.");
170        }
171
172        Ok(SteamAuthenticator {
173            inner: InnerAuthenticator {
174                client,
175                user,
176                cache: Some(Arc::new(RwLock::new(cache))),
177            },
178            auth_level: PhantomData,
179        })
180    }
181}
182
183impl<M> SteamAuthenticator<Authenticated, M>
184where
185    M: Send + Sync,
186{
187    fn cache(&self) -> CacheGuard {
188        self.inner.cache.as_ref().expect("Safe to unwrap.").clone()
189    }
190
191    /// Returns account's API Key, if authenticator managed to cache it.
192    pub fn api_key(&self) -> Option<String> {
193        self.inner
194            .cache
195            .as_ref()
196            .expect("Safe to unwrap")
197            .read()
198            .api_key()
199            .map(ToString::to_string)
200    }
201
202    /// Returns this account SteamGuard information.
203    pub async fn steam_guard_status(&self) -> Result<QueryStatusResponse, AuthError> {
204        twofactor_status(self.client(), self.cache()).await.map_err(Into::into)
205    }
206
207    /// Add an authenticator to the account.
208    /// Note that this makes various assumptions about the account.
209    ///
210    /// The first argument is an enum of  `AddAuthenticatorStep` to help you automate the process of adding an
211    /// authenticator to the account.
212    ///
213    /// First call this method with `AddAuthenticatorStep::InitialStep`. This requires the account to be
214    /// already connected with a verified email address. After this step is finished, you will receive an email
215    /// about the phone confirmation.
216    ///
217    /// Once you confirm it, you will call this method with `AddAuthenticatorStep::EmailConfirmation`.
218    ///
219    /// This will return a `AddAuthenticatorStep::MobileAuthenticatorFile` now, with your maFile inside the variant.
220    /// For more complete example, you can check the CLI Tool, that performs the inclusion of an authenticator
221    /// interactively.
222    pub async fn add_authenticator(
223        &self,
224        current_step: AddAuthenticatorStep,
225        phone_number: &str,
226    ) -> Result<AddAuthenticatorStep, AuthError> {
227        let user_has_phone_registered = account_has_phone(self.client()).await?;
228        debug!("Has phone registered? {:?}", user_has_phone_registered);
229
230        if !user_has_phone_registered && current_step == AddAuthenticatorStep::InitialStep {
231            let phone_registration_result = self.add_phone_number(phone_number).await?;
232            debug!("User add phone result: {:?}", phone_registration_result);
233
234            return Ok(AddAuthenticatorStep::EmailConfirmation);
235        }
236
237        // Signal steam that user confirmed email
238        // If user already has a phone, calling email confirmation will result in a error finalizing the auth process.
239        if !user_has_phone_registered {
240            check_email_confirmation(self.client()).await?;
241            debug!("Email confirmation signal sent.");
242        }
243
244        add_authenticator_to_account(self.client(), self.cache().read())
245            .await
246            .map(AddAuthenticatorStep::MobileAuth)
247            .map_err(Into::into)
248    }
249
250    /// Finalize the authenticator process, enabling `SteamGuard` for the account.
251    /// This method wraps up the whole process, finishing the registration of the phone number into the account.
252    ///
253    /// * EXTREMELY IMPORTANT *
254    ///
255    /// Call this method **ONLY** after saving your maFile, because otherwise you WILL lose access to your
256    /// account.
257    pub async fn finalize_authenticator(&self, mafile: &MobileAuthFile, sms_code: &str) -> Result<(), AuthError> {
258        // The delay is that Steam need some seconds to catch up with the new phone number associated.
259        let account_has_phone_now: bool = check_sms(self.client(), sms_code)
260            .map_ok(|_| Delay::new(Duration::from_secs(STEAM_ADD_PHONE_CATCHUP_SECS)))
261            .and_then(|_| account_has_phone(self.client()))
262            .await?;
263
264        if !account_has_phone_now {
265            return Err(LinkerError::GeneralFailure("This should not happen.".to_string()).into());
266        }
267
268        info!("Successfully confirmed SMS code.");
269
270        finalize(self.client(), self.cache().read(), mafile, sms_code)
271            .await
272            .map_err(Into::into)
273    }
274
275    /// Remove an authenticator from a Steam Account.
276    ///
277    /// Sets account to use `SteamGuard` email confirmation codes or even remove it completely.
278    pub async fn remove_authenticator(
279        &self,
280        revocation_code: &str,
281        remove_authenticator_scheme: RemoveAuthenticatorScheme,
282    ) -> Result<(), AuthError> {
283        remove_authenticator(
284            self.client(),
285            self.cache().read(),
286            revocation_code,
287            remove_authenticator_scheme,
288        )
289        .await
290    }
291
292    /// Add a phone number into the account, and then checks it to make sure it has been added.
293    /// Returns true if number was successfully added.
294    async fn add_phone_number(&self, phone_number: &str) -> Result<bool, AuthError> {
295        if !validate_phone_number(phone_number) {
296            return Err(LinkerError::GeneralFailure(
297                "Invalid phone number. Should be in format of: +(CountryCode)(AreaCode)(PhoneNumber). E.g \
298                 +5511976914922"
299                    .to_string(),
300            )
301            .into());
302        }
303
304        // Add the phone number to user account
305        // The delay is that Steam need some seconds to catch up.
306        let response = add_phone_to_account(self.client(), phone_number).await?;
307        Delay::new(Duration::from_secs(STEAM_ADD_PHONE_CATCHUP_SECS)).await;
308
309        Ok(response)
310    }
311
312    /// You can request custom operations for any Steam operation that requires logging in.
313    ///
314    /// The authenticator will take care sending session cookies and keeping the session
315    /// operational.
316    pub async fn request_custom_endpoint<T>(
317        &self,
318        url: String,
319        method: Method,
320        custom_headers: Option<HeaderMap>,
321        data: Option<T>,
322    ) -> Result<Response, InternalError>
323    where
324        T: Serialize + Send + Sync,
325    {
326        self.client()
327            .request_with_session_guard(url, method, custom_headers, data, None::<&str>)
328            .await
329    }
330
331    #[allow(missing_docs)]
332    pub fn dump_cookie(&self, steam_domain_host: &str, steam_cookie_name: &str) -> Option<String> {
333        dump_cookies_by_domain_and_name(&self.client().cookie_store.read(), steam_domain_host, steam_cookie_name)
334    }
335}
336
337impl SteamAuthenticator<Authenticated, PresentMaFile> {
338    /// Fetch all confirmations available with the authenticator.
339    pub async fn fetch_confirmations(&self) -> Result<Confirmations, AuthError> {
340        let steamid = self.cache().read().steam_id();
341        let secret = (&self.inner.user).identity_secret();
342        let device_id = (&self.inner.user).device_id();
343
344        get_confirmations(self.client(), secret, device_id, steamid)
345            .err_into()
346            .await
347    }
348
349    /// Fetches confirmations and process them.
350    ///
351    /// `f` is a function which you can use it to filter confirmations at the moment of the query.
352    pub async fn handle_confirmations<'a, 'b, F>(&self, operation: ConfirmationAction, f: F) -> Result<(), AuthError>
353    where
354        F: Fn(Confirmations) -> Box<dyn Iterator<Item = Confirmation> + Send> + Send,
355    {
356        let confirmations = self.fetch_confirmations().await?;
357        if !confirmations.is_empty() {
358            self.process_confirmations(operation, f(confirmations)).await
359        } else {
360            Ok(())
361        }
362    }
363
364    /// Accept or deny confirmations.
365    ///
366    /// # Panics
367    /// Will panic if not logged in with [`SteamAuthenticator`] first.
368    pub async fn process_confirmations<I>(
369        &self,
370        operation: ConfirmationAction,
371        confirmations: I,
372    ) -> Result<(), AuthError>
373    where
374        I: IntoIterator<Item = Confirmation> + Send,
375    {
376        let steamid = self.cache().read().steam_id();
377
378        send_confirmations(
379            self.client(),
380            self.user().identity_secret(),
381            self.user().device_id(),
382            steamid,
383            operation,
384            confirmations,
385        )
386        .await
387        .map_err(Into::into)
388    }
389}
390
391#[derive(Debug)]
392pub struct MobileClient {
393    /// Standard HTTP Client to make requests.
394    pub inner_http_client: Client,
395    /// Cookie jar that manually handle cookies, because reqwest doesn't let us handle its cookies.
396    pub cookie_store: Arc<RwLock<CookieJar>>,
397}
398
399impl MobileClient {
400    pub(crate) fn get_cookie_value(&self, domain: &str, name: &str) -> Option<String> {
401        dump_cookies_by_domain_and_name(&self.cookie_store.read(), domain, name)
402    }
403    pub(crate) fn set_cookie_value(&self, cookie: Cookie<'static>) {
404        self.cookie_store.write().add_original(cookie);
405    }
406
407    pub(crate) async fn request_proto<INPUT, OUTPUT>(
408        &self,
409        url: impl IntoUrl + Send,
410        method: Method,
411        proto_message: INPUT,
412        _token: Option<&str>,
413    ) -> Result<OUTPUT, InternalError>
414    where
415        INPUT: ProtobufSerialize,
416        OUTPUT: ProtobufDeserialize<Output = OUTPUT> + Debug,
417    {
418        let url = url.into_url().unwrap();
419        debug!("Request url: {}", url);
420        let request_builder = self.inner_http_client.request(method.clone(), url);
421
422        let req = if method == Method::GET {
423            let encoded = base64::engine::general_purpose::URL_SAFE.encode(proto_message.to_bytes().unwrap());
424            let parameters = &[("input_protobuf_encoded", encoded)];
425            request_builder.query(parameters)
426        } else if method == Method::POST {
427            let encoded = base64::engine::general_purpose::STANDARD.encode(proto_message.to_bytes().unwrap());
428            debug!("Request proto body: {:?}", encoded);
429            let form = reqwest::multipart::Form::new().text("input_protobuf_encoded", encoded);
430            request_builder.multipart(form)
431        } else {
432            return Err(InternalError::GeneralFailure("Unsupported Method".to_string()));
433        };
434
435        let response = req.send().await?;
436        debug!("Response {:?}", response);
437
438        let res_bytes = response.bytes().await?;
439        OUTPUT::from_bytes(res_bytes).map_or_else(
440            |_| {
441                error!("Failed deserializing {}", std::any::type_name::<OUTPUT>());
442                Err(InternalError::GeneralFailure("asdfd".to_string()))
443            },
444            |res| {
445                debug!("Response body {:?}", res);
446                Ok(res)
447            },
448        )
449    }
450
451    /// Wrapper to make requests while preemptively checking if the session is still valid.
452    pub(crate) async fn request_with_session_guard<T, QP, U>(
453        &self,
454        url: U,
455        method: Method,
456        custom_headers: Option<HeaderMap>,
457        data: Option<T>,
458        query_params: Option<QP>,
459    ) -> Result<Response, InternalError>
460    where
461        T: Serialize + Send,
462        QP: Serialize + Send,
463        U: IntoUrl + Send,
464    {
465        // We check preemptively if the session is still working.
466        if self.session_is_expired().await? {
467            warn!("Session was lost. Trying to reconnect.");
468            unimplemented!()
469        };
470
471        self.request(url, method, custom_headers, data, query_params)
472            .err_into()
473            .await
474    }
475    pub(crate) async fn request_with_session_guard_and_decode<T, QP, OUTPUT>(
476        &self,
477        url: String,
478        method: Method,
479        custom_headers: Option<HeaderMap>,
480        data: Option<T>,
481        query_params: Option<QP>,
482    ) -> Result<OUTPUT, InternalError>
483    where
484        T: Serialize + Send + Sync,
485        QP: Serialize + Send + Sync,
486        OUTPUT: DeserializeOwned,
487    {
488        let req = self
489            .request_with_session_guard(url, method, custom_headers, data.as_ref(), query_params)
490            .await?;
491
492        let response_body = req
493            .text()
494            .inspect_ok(|s| {
495                debug!("{} text: {}", std::any::type_name::<OUTPUT>(), s);
496            })
497            .await?;
498
499        serde_json::from_str::<OUTPUT>(&response_body).map_err(InternalError::DeserializationError)
500    }
501
502    /// Simple wrapper to allow generic requests to be made.
503    pub(crate) async fn request<T, QS, U>(
504        &self,
505        url: U,
506        method: Method,
507        headers: Option<HeaderMap>,
508        form_data: Option<T>,
509        query_params: QS,
510    ) -> Result<Response, InternalError>
511    where
512        QS: Serialize + Send,
513        T: Serialize + Send,
514        U: IntoUrl + Send,
515    {
516        let parsed_url = url
517            .into_url()
518            .map_err(|_| InternalError::GeneralFailure("Couldn't parse passed URL. Insert a valid one.".to_string()))?;
519        let mut header_map = headers.unwrap_or_default();
520
521        let domain_cookies = dump_cookies_by_domain(&self.cookie_store.read(), parsed_url.host_str().unwrap());
522        header_map.insert(
523            reqwest::header::COOKIE,
524            domain_cookies.unwrap_or_default().parse().unwrap(),
525        );
526
527        let req_builder = self
528            .inner_http_client
529            .request(method, parsed_url)
530            .headers(header_map)
531            .query(&query_params);
532
533        let request = match form_data {
534            None => req_builder.build().unwrap(),
535            Some(data) => match serde_urlencoded::to_string(data) {
536                Ok(body) => {
537                    debug!("Request body: {}", &body);
538                    req_builder
539                        .header(
540                            CONTENT_TYPE,
541                            HeaderValue::from_static("application/x-www-form-urlencoded; charset=UTF-8"),
542                        )
543                        .body(body)
544                        .build()
545                        .expect("Safe to unwrap.")
546                }
547                Err(err) => {
548                    return Err(InternalError::GeneralFailure(format!(
549                        "Failed to serialize body: {err}"
550                    )))
551                }
552            },
553        };
554        debug!("{:?}", &request);
555
556        let res = self.inner_http_client.execute(request).err_into().await;
557        if let Ok(ref response) = res {
558            debug!("Response status: {:?}", response.status());
559            debug!("Response headers: {:?}", response.headers());
560
561            let mut cookie_jar = self.cookie_store.write();
562            for cookie in response.cookies() {
563                let mut our_cookie = SteamCookie::from(cookie);
564                let host = response.url().host().expect("Safe.").to_string();
565                our_cookie.set_domain(host);
566
567                trace!(
568                    "New cookie from: {:?}, name: {}, value: {} ",
569                    our_cookie.domain(),
570                    our_cookie.name(),
571                    our_cookie.value()
572                );
573                cookie_jar.add_original(our_cookie.deref().clone());
574            }
575        }
576        res
577    }
578
579    pub(crate) async fn request_and_decode<T, OUTPUT, QS, U>(
580        &self,
581        url: U,
582        method: Method,
583        headers: Option<HeaderMap>,
584        form_data: Option<T>,
585        query_params: QS,
586    ) -> Result<OUTPUT, InternalError>
587    where
588        OUTPUT: DeserializeOwned,
589        QS: Serialize + Send + Sync,
590        T: Serialize + Send + Sync,
591        U: IntoUrl + Send,
592    {
593        let resp = self.request(url, method, headers, form_data, query_params).await?;
594        let response_body = resp
595            .text()
596            .inspect_ok(|s| {
597                debug!("{} text: {}", std::any::type_name::<OUTPUT>(), s);
598            })
599            .await?;
600
601        serde_json::from_str::<OUTPUT>(&response_body).map_err(InternalError::DeserializationError)
602    }
603
604    /// Checks if session is expired by parsing the the redirect URL for "steamobile:://lostauth"
605    /// or a path that starts with "/login".
606    ///
607    /// This is the most reliable way to find out, since we check the session by requesting our
608    /// account page at Steam Store, which is not going to be deprecated anytime soon.
609    async fn session_is_expired(&self) -> Result<bool, InternalError> {
610        let account_url = format!("{}/account", crate::STEAM_STORE_BASE);
611
612        // FIXME: Not sure if we should request from client directly
613        let response = self
614            .request(account_url, Method::HEAD, None, None::<u8>, None::<u8>)
615            .await?;
616
617        if let Some(location) = retrieve_header_location(&response) {
618            return Ok(Url::parse(location).map(Self::url_expired_check).unwrap());
619        }
620        Ok(false)
621    }
622
623    /// If url is redirecting to '/login' or lostauth, returns true
624    fn url_expired_check(redirect_url: Url) -> bool {
625        redirect_url.host_str().unwrap() == "lostauth" || redirect_url.path().starts_with("/login")
626    }
627
628    /// Convenience function to retrieve HTML w/ session
629    pub(crate) async fn get_html<T, QS>(
630        &self,
631        url: T,
632        headers: Option<HeaderMap>,
633        query: Option<QS>,
634    ) -> Result<Html, InternalError>
635    where
636        T: IntoUrl + Send,
637        QS: Serialize + Send,
638    {
639        self.request_with_session_guard(url, Method::GET, headers, None::<&str>, query)
640            .and_then(|r| r.text().err_into())
641            .await
642            .map(|s| Html::parse_document(&s))
643    }
644
645    /// Replace current cookie jar with a new one.
646    fn reset_jar(&mut self) {
647        self.cookie_store = Arc::new(RwLock::new(CookieJar::new()));
648    }
649
650    /// Mobile cookies that makes us look like the mobile app
651    fn standard_mobile_cookies() -> Vec<Cookie<'static>> {
652        vec![
653            Cookie::build("Steam_Language", "english")
654                .domain(STEAM_COMMUNITY_HOST)
655                .finish(),
656            Cookie::build("mobileClient", "android")
657                .domain(STEAM_COMMUNITY_HOST)
658                .finish(),
659            Cookie::build("mobileClientVersion", "0 (2.1.3)")
660                .domain(STEAM_COMMUNITY_HOST)
661                .finish(),
662        ]
663    }
664
665    /// Initialize cookie jar, and populates it with mobile cookies.
666    fn init_cookie_jar() -> CookieJar {
667        let mut mobile_cookies = CookieJar::new();
668        Self::standard_mobile_cookies()
669            .into_iter()
670            .for_each(|cookie| mobile_cookies.add(cookie));
671        mobile_cookies
672    }
673
674    /// Initiate mobile client with default headers
675    fn init_mobile_client() -> Client {
676        let user_agent = "Dalvik/2.1.0 (Linux; U; Android 9; Valve Steam App Version/3)";
677        let mut default_headers = HeaderMap::new();
678        default_headers.insert(
679            reqwest::header::ACCEPT,
680            "text/javascript, text/html, application/xml, text/xml, */*"
681                .parse()
682                .unwrap(),
683        );
684        default_headers.insert(reqwest::header::REFERER, crate::MOBILE_REFERER.parse().unwrap());
685        default_headers.insert(
686            "X-Requested-With",
687            "com.valvesoftware.android.steam.community".parse().unwrap(),
688        );
689
690        reqwest::Client::builder()
691            .user_agent(user_agent)
692            .cookie_store(true)
693            .redirect(Policy::limited(5))
694            .default_headers(default_headers)
695            .referer(false)
696            .build()
697            .unwrap()
698    }
699}
700
701impl Default for MobileClient {
702    fn default() -> Self {
703        Self {
704            inner_http_client: Self::init_mobile_client(),
705            cookie_store: Arc::new(RwLock::new(Self::init_cookie_jar())),
706        }
707    }
708}