gplay_client/
lib.rs

1//! A library for interacting with the Google Play API.
2//!
3//! # Getting Started
4//!
5//! To interact with the API, first you'll have to obtain an OAuth token by visiting the Google
6//! [embedded setup page](https://accounts.google.com/EmbeddedSetup/identifier?flowName=EmbeddedSetupAndroid)
7//! and opening the browser debugging console, logging in, and looking for the `oauth_token` cookie
8//! being set on your browser.  It will be present in the last requests being made and start with
9//! "oauth2_4/".  Copy this value.  It can only be used once, in order to obtain the `aas_token`,
10//! which can be used subsequently.  To obtain this token:
11//!
12//! ```rust
13//! use gpapi::Gpapi;
14//!
15//! #[tokio::main]
16//! async fn main() {
17//!     let mut api = Gpapi::new("ad_g3_pro", &email);
18//!     api.request_aas_token(oauth_token).await.unwrap();
19//!     println!("{:?}", api.get_aas_token());
20//! }
21//! ```
22//!
23//! Now, you can begin interacting with the API by initializing it setting the `aas_token` and
24//! logging in.
25//!
26//! ```rust
27//! use gpapi::Gpapi;
28//!
29//! #[tokio::main]
30//! async fn main() {
31//!     let mut api = Gpapi::new("px_7a", &email);
32//!     api.set_aas_token(aas_token);
33//!     api.login().await.unwrap();
34//!     // do something
35//! }
36//! ```
37//!
38//! From here, you can get package details, get the info to download a package, or use the library to download it.
39//!
40//! ```rust
41//! # use gpapi::Gpapi;
42//! # use std::path::Path;
43//! # #[tokio::main]
44//! # async fn main() {
45//! # let mut api = Gpapi::new("px_7a", &email);
46//! # api.set_aas_token(aas_token);
47//! # api.login().await.unwrap();
48//! let details = api.details("com.instagram.android").await;
49//! println!("{:?}", details);
50//!
51//! let download_info = api.get_download_info("com.instagram.android", None).await;
52//! println!("{:?}", download_info);
53//!
54//! api.download("com.instagram.android", None, true, true, &Path::new("/tmp/testing"), None).await;
55//! # }
56//! ```
57
58mod consts;
59pub mod error;
60pub mod devices;
61
62use std::collections::HashMap;
63use std::error::Error;
64use std::io::Cursor;
65use std::time::{SystemTime, UNIX_EPOCH};
66
67use base64::Engine;
68use bytes::Bytes;
69use prost::Message;
70use reqwest::header::{HeaderMap, HeaderValue, HeaderName};
71use reqwest::Url;
72use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64_URL_SAFE_NO_PAD;
73
74use crate::{error::{Error as GpapiError, ErrorKind as GpapiErrorKind}, devices::Device};
75
76use gplay_protobuf::{
77    AcceptTosResponse,
78    AndroidCheckinRequest,
79    AndroidCheckinResponse,
80    BulkDetailsRequest,
81    BulkDetailsResponse,
82    DetailsResponse,
83    ResponseWrapper,
84    UploadDeviceConfigRequest,
85    UploadDeviceConfigResponse,
86};
87
88#[derive(Debug)]
89pub struct Gpapi {
90    locale: String,
91    timezone: String,
92    device: Device,
93    email: String,
94    aas_token: Option<String>,
95    auth_token: Option<String>,
96    device_config_token: Option<String>,
97    device_checkin_consistency_token: Option<String>,
98    tos_token: Option<String>,
99    dfe_cookie: Option<String>,
100    gsf_id: Option<i64>,
101    client: Box<reqwest::Client>,
102}
103
104impl Gpapi {
105    /// Returns a Gpapi struct.
106    ///
107    pub fn new<S: Into<String>>(device: Device, email: S) -> Self {
108        Gpapi {
109            locale: String::from("en_US"),
110            timezone: String::from("UTC"),
111            device,
112            email: email.into(),
113            aas_token: None,
114            auth_token: None,
115            device_config_token: None,
116            device_checkin_consistency_token: None,
117            tos_token: None,
118            dfe_cookie: None,
119            gsf_id: None,
120            client: Box::new(reqwest::Client::new()),
121        }
122    }
123
124    /// Set the locale
125    pub fn set_locale<S: Into<String>>(&mut self, locale: S){
126        self.locale = locale.into();
127    }
128
129    /// Set the time zone
130    pub fn set_timezone<S: Into<String>>(&mut self, timezone: S){
131        self.timezone = timezone.into();
132    }
133
134    /// Set the aas token. This can be requested via `request_aas_token`, and is required for most
135    /// other actions.
136    pub fn set_aas_token<S: Into<String>>(&mut self, aas_token: S) {
137        self.aas_token = Some(aas_token.into());
138    }
139
140    /// Request and set the aas token given an oauth token and the associated email.
141    ///
142    /// # Arguments
143    ///
144    /// * `oauth_token` - An oauth token you previously retrieved separately
145    pub async fn request_aas_token<S: Into<String>>(&mut self, oauth_token: S) -> Result<(), GpapiError> {
146        let oauth_token = oauth_token.into();
147        let auth_req = AuthRequest::new(&self.email, &oauth_token);
148        let mut resp = self.request_aas_token_helper(&auth_req).await?;
149        self.aas_token = Some(resp.remove("token").ok_or(GpapiError::new(GpapiErrorKind::Authentication))?);
150        Ok(())
151    }
152
153    async fn request_aas_token_helper(
154        &self,
155        auth_req: &AuthRequest,
156    ) -> Result<HashMap<String, String>, Box<dyn Error>> {
157        let form_body = form_post(&auth_req.params);
158
159        let mut headers = HashMap::new();
160        headers.insert(
161            "user-agent",
162            String::from(consts::defaults::DEFAULT_AUTH_USER_AGENT),
163        );
164        headers.insert(
165            "content-type",
166            String::from("application/x-www-form-urlencoded"),
167        );
168        headers.insert(
169            "app",
170            String::from("com.google.android.gms"),
171        );
172
173        let body_bytes = self
174            .execute_request_helper("auth", None, Some(&form_body.into_bytes()), headers, false)
175            .await?;
176
177        let reply = parse_form_reply(&std::str::from_utf8(&body_bytes.to_vec()).unwrap());
178        Ok(reply)
179    }
180
181    /// Get the aas token that has been previously set by either `request_aas_token` or
182    /// `set_aas_token`.
183    pub fn get_aas_token(&self) -> Option<&str> {
184        self.aas_token.as_ref().map(|token| token.as_str())
185    }
186
187    /// Log in to Google's Play Store API.  This is required for most other actions. The aas token
188    /// has to be set via `request_aas_token` or `set_aas_token` first.
189    pub async fn login(
190        &mut self,
191    ) -> Result<(), GpapiError> {
192        self.checkin().await?;
193        if let Some(upload_device_config_token) = self.upload_device_config().await? {
194            self.device_config_token =
195                Some(upload_device_config_token.upload_device_config_token.unwrap());
196            self.request_auth_token().await?;
197            self.toc().await?;
198            Ok(())
199        } else {
200            Err("No device config token".into())
201        }
202    }
203
204    /// Retrieve files with download URLs for a package.
205    ///
206    /// # Arguments
207    ///
208    /// * `package` - Package ID, such as `moe.low.arc`.
209    /// * `version_code` - Optional version code, if `None`, the latest version will be used.
210    ///
211    /// # Returns
212    ///
213    /// If successful, `Ok` with the files to download for the package.
214    pub async fn get_download_info<S>(&self, package: S, version_code: Option<i32>) -> Result<DownloadInfo, GpapiError>
215        where S: Into<String>
216    {
217        let package = package.into();
218        if self.auth_token.is_none() {
219            return Err(GpapiError::new(GpapiErrorKind::LoginRequired));
220        }
221        let version_code = if let Some(version_code) = version_code {
222            version_code
223        } else {
224            self.get_latest_version_for_pkg_name(&package).await?
225        };
226        let response = {
227            let version_code_string = version_code.to_string();
228            let mut params = HashMap::new();
229            params.insert("ot", String::from("1"));
230            params.insert("doc", String::from(&package));
231            params.insert("vc", version_code_string);
232
233            let mut headers = self.get_default_headers()?;
234            headers.insert("content-length", String::from("0"));
235
236            self.execute_request("purchase", Some(params), Some(&[]), headers).await?
237        };
238        let delivery_token = response.payload
239            .and_then(|payload| payload.buy_response)
240            .and_then(|buy_response| buy_response.encoded_delivery_token);
241        self.delivery(&package, version_code, delivery_token.as_ref()).await
242    }
243
244    async fn delivery<S>(&self, package: S, version_code: i32, delivery_token: Option<S>) -> Result<DownloadInfo, GpapiError>
245        where S: Into<String>
246    {
247        let pkg_name = package.into();
248        let delivery_token = delivery_token.map(|delivery_token| delivery_token.into());
249        if self.auth_token.is_none() {
250            return Err(GpapiError::new(GpapiErrorKind::LoginRequired));
251        }
252        let response = {
253            let version_code_string = version_code.to_string();
254            let mut request = HashMap::new();
255            request.insert("ot", String::from("1"));
256            request.insert("doc", pkg_name.clone());
257            request.insert("vc", version_code_string);
258            if let Some(delivery_token) = delivery_token {
259                request.insert("dtok", delivery_token);
260            }
261            self.execute_request("delivery", Some(request), None, self.get_default_headers()?).await?
262        };
263        let app_delivery_data = response.payload
264            .and_then(|payload| payload.delivery_response)
265            .and_then(|delivery_response| delivery_response.app_delivery_data);
266        let Some(app_delivery_data) = app_delivery_data else {
267            return Err(GpapiError::new(GpapiErrorKind::InvalidApp));
268        };
269        let base = BaseFileDownload {
270            url: app_delivery_data.download_url.unwrap(),
271            sha256: decode_sha256(&app_delivery_data.sha256.unwrap()),
272        };
273        let mut splits = Vec::new();
274        for app_split_delivery_data in app_delivery_data.split_delivery_data {
275            splits.push(SplitFileDownload {
276                url: app_split_delivery_data.download_url.unwrap(),
277                sha256: decode_sha256(&app_split_delivery_data.sha256.unwrap()),
278                name: app_split_delivery_data.name.unwrap(),
279            });
280        }
281        let mut expansions = Vec::new();
282        for additional_file in app_delivery_data.additional_file {
283            let kind = match additional_file.file_type.unwrap() {
284                0 => ExpansionFileKind::Main,
285                1 => ExpansionFileKind::Patch,
286                kind => panic!("Unknown expansion type {kind}"),
287            };
288            expansions.push(ExpansionFileDownload {
289                url: additional_file.download_url.unwrap(),
290                sha1: decode_sha1(&additional_file.sha1.unwrap()),
291                kind,
292                version_code: additional_file.version_code.unwrap(),
293            });
294        }
295        return Ok(DownloadInfo {
296            base,
297            splits,
298            expansions
299        });
300    }
301
302    async fn get_latest_version_for_pkg_name(&self, pkg_name: &str) -> Result<i32, GpapiError> {
303        if let Some(details) = self.details(pkg_name).await? {
304            if let Some(item) = details.item {
305                if let Some(details) = item.details {
306                    if let Some(app_details) = details.app_details {
307                        if let Some(version_code) = app_details.version_code {
308                            return Ok(version_code);
309                        }
310                    }
311                }
312            }
313        }
314        Err(GpapiError::new(GpapiErrorKind::InvalidApp))
315    }
316
317    /// Play Store package detail request (provides more detail than bulk requests).
318    ///
319    /// # Arguments
320    ///
321    /// * `pkg_name` - A string type specifying the package's app ID, e.g. `com.instagram.android`
322    pub async fn details<S: Into<String>>(
323        &self,
324        pkg_name: S,
325    ) -> Result<Option<DetailsResponse>, Box<dyn Error>> {
326        if self.auth_token.is_none() {
327            return Err(Box::new(GpapiError::new(GpapiErrorKind::LoginRequired)));
328        }
329        let mut form_params = HashMap::new();
330        form_params.insert("doc", pkg_name.into());
331
332        let headers = self.get_default_headers()?;
333
334        let resp = self
335            .execute_request("details", Some(form_params), None, headers)
336            .await?;
337
338        if let Some(payload) = resp.payload {
339            Ok(payload.details_response)
340        } else {
341            Ok(None)
342        }
343    }
344
345
346    /// Play Store bulk detail request for multiple apps.
347    ///
348    /// # Arguments
349    ///
350    /// * `pkg_names` - An array of string types specifying package app IDs
351    pub async fn bulk_details(
352        &self,
353        pkg_names: &[&str],
354    ) -> Result<Option<BulkDetailsResponse>, GpapiError> {
355        if self.auth_token.is_none() {
356            return Err(GpapiError::new(GpapiErrorKind::LoginRequired));
357        }
358        let mut req = BulkDetailsRequest::default();
359        req.doc_id = pkg_names.into_iter().cloned().map(String::from).collect();
360        req.include_child_docs = Some(false);
361        let mut bytes = Vec::new();
362        bytes.reserve(req.encoded_len());
363        req.encode(&mut bytes).unwrap();
364        let resp = self
365            .execute_request("bulkDetails", None, Some(&bytes), self.get_default_headers()?)
366            .await?;
367        if let Some(payload) = resp.payload {
368            Ok(payload.bulk_details_response)
369        } else {
370            Ok(None)
371        }
372    }
373
374    async fn checkin(&mut self) -> Result<(), Box<dyn Error>> {
375        let mut checkin = self.device.checkin();
376
377        checkin.build.as_mut().map(|b| {
378            b.timestamp = Some(
379                (SystemTime::now()
380                    .duration_since(UNIX_EPOCH)
381                    .unwrap()
382                    .as_secs()
383                    / 1000) as i64,
384            )
385        });
386
387        let mut req = AndroidCheckinRequest::default();
388        req.id = Some(0);
389        req.checkin = Some(checkin);
390        req.locale = Some(self.locale.clone());
391        req.time_zone = Some(self.timezone.clone());
392        req.version = Some(3);
393        req.device_configuration = Some(self.device.configuration());
394        req.fragment = Some(0);
395        let mut bytes = Vec::new();
396        bytes.reserve(req.encoded_len());
397        req.encode(&mut bytes).unwrap();
398
399        let build_id = self.device.build_id.clone();
400        let build_device = self.device.build_device.clone().unwrap();
401        let mut headers = HashMap::new();
402        self.append_auth_headers(&mut headers, build_device, build_id);
403
404        let resp = self.execute_checkin_request(&bytes, headers).await?;
405        self.device_checkin_consistency_token = resp.device_checkin_consistency_token;
406        self.gsf_id = resp.android_id.map(|id| id as i64);
407        Ok(())
408    }
409
410    async fn execute_checkin_request(
411        &self,
412        msg: &[u8],
413        mut auth_headers: HashMap<&str, String>,
414    ) -> Result<AndroidCheckinResponse, Box<dyn Error>> {
415        auth_headers.insert(
416            "content-type",
417            String::from("application/x-protobuf"),
418        );
419        auth_headers.insert(
420            "host",
421            String::from("android.clients.google.com"),
422        );
423        let bytes = self
424            .execute_request_helper("checkin", None, Some(msg), auth_headers, false)
425            .await?;
426        let resp = AndroidCheckinResponse::decode(&mut Cursor::new(bytes))?;
427        Ok(resp)
428    }
429
430    fn get_default_headers(
431        &self,
432    ) -> Result<HashMap<&str, String>, Box<dyn Error>> {
433        let mut headers = HashMap::new();
434        self.append_default_headers(&mut headers)?;
435        Ok(headers)
436    }
437
438    fn append_default_headers(
439        &self,
440        headers: &mut HashMap<&str, String>,
441    ) -> Result<(), Box<dyn Error>> {
442        if let Some(auth_token) = &self.auth_token {
443            headers.insert(
444                "Authorization",
445                format!("Bearer {}", auth_token.clone()),
446            );
447        }
448
449        let build_configuration = BuildConfiguration::new(
450            &self.device.vending_version_string,
451            &self.device.vending_version,
452            &self.device.build_version_sdk_int.as_ref().unwrap().to_string(),
453            self.device.build_device.as_ref().unwrap(),
454            self.device.build_hardware.as_ref().unwrap(),
455            self.device.build_product.as_ref().unwrap(),
456            &self.device.build_version_release,
457            self.device.build_model.as_ref().unwrap(),
458            &self.device.build_id,
459            &self.device.platforms.join(";"),
460        );
461
462        headers.insert(
463            "user-agent",
464            build_configuration.user_agent(),
465        );
466
467        if let Some(gsf_id) = &self.gsf_id {
468            headers.insert(
469                "X-DFE-Device-Id",
470                format!("{:x}", gsf_id),
471            );
472        }
473        headers.insert(
474           "accept-language",
475            self.locale.replace("_", "-"),
476        );
477        headers.insert(
478            "X-DFE-Encoded-Targets",
479            String::from(consts::defaults::DEFAULT_DFE_TARGETS),
480        );
481        headers.insert(
482            "X-DFE-Phenotype",
483            String::from(consts::defaults::DEFAULT_DFE_PHENOTYPE),
484        );
485        headers.insert(
486            "X-DFE-Client-Id",
487            String::from("am-android-google"),
488        );
489        headers.insert("X-DFE-Network-Type", String::from("4"));
490        headers.insert("X-DFE-Content-Filters", String::from(""));
491        headers.insert("X-Limit-Ad-Tracking-Enabled", String::from("false"));
492        headers.insert("X-Ad-Id", String::from(""));
493        headers.insert("X-DFE-UserLanguages", String::from(&self.locale));
494        headers.insert(
495            "X-DFE-Request-Params",
496            String::from("timeoutMs=4000"),
497        );
498        if let Some(device_checkin_consistency_token) = &self.device_checkin_consistency_token {
499            headers.insert(
500                "X-DFE-Device-Checkin-Consistency-Token",
501                device_checkin_consistency_token.clone(),
502            );
503        }
504        if let Some(device_config_token) = &self.device_config_token {
505            headers.insert(
506                "X-DFE-Device-Config-Token",
507                device_config_token.clone(),
508            );
509        }
510        if let Some(dfe_cookie) = &self.dfe_cookie {
511            headers.insert("X-DFE-Cookie", dfe_cookie.clone());
512        }
513        if let Some(mcc_mcn) = &self.device.sim_operator {
514            headers.insert("X-DFE-MCCMCN", mcc_mcn.to_string());
515        }
516        Ok(())
517    }
518
519    fn append_auth_headers<S: Into<String>>(
520        &self,
521        headers: &mut HashMap<&str, String>,
522        build_device: S,
523        build_id: S,
524    ) {
525        headers.insert(
526            "app",
527            String::from(consts::defaults::DEFAULT_ANDROID_VENDING),
528        );
529        headers.insert(
530            "User-Agent",
531            format!("GoogleAuth/1.4 ({} {})", build_device.into(), build_id.into()),
532        );
533        if let Some(gsf_id) = self.gsf_id {
534            headers.insert(
535                "device",
536                format!("{:x}", gsf_id),
537            );
538        }
539    }
540
541    fn append_default_auth_params(
542        &self,
543        params: &mut HashMap<&str, String>
544    ) {
545        if let Some(gsf_id) = self.gsf_id {
546            params.insert("androidId", format!("{:x}", gsf_id));
547        }
548
549        params.insert("sdk_version", self.device.build_version_sdk_int.unwrap().to_string());
550        params.insert("Email", self.email.clone());
551        params.insert("google_play_services_version", self.device.gsf_version.unwrap().to_string());
552        params.insert("device_country", String::from(consts::defaults::DEFAULT_COUNTRY_CODE).to_ascii_lowercase());
553        params.insert("lang", String::from(consts::defaults::DEFAULT_LANGUAGE).to_ascii_lowercase());
554        params.insert("callerSig", String::from(consts::defaults::DEFAULT_CALLER_SIG));
555    }
556
557    fn append_auth_params(
558        &self,
559        params: &mut HashMap<&str, String>
560    ) {
561        params.insert("app", String::from("com.android.vending"));
562        params.insert("client_sig", String::from(consts::defaults::DEFAULT_CLIENT_SIG));
563        params.insert("callerPkg", String::from(consts::defaults::DEFAULT_ANDROID_VENDING));
564        params.insert("Token", self.aas_token.as_ref().unwrap().clone());
565        params.insert("oauth2_foreground", String::from("1"));
566        params.insert("token_request_options", String::from("CAA4AVAB"));
567        params.insert("check_email", String::from("1"));
568        params.insert("system_partition", String::from("1"));
569    }
570
571    async fn upload_device_config(
572        &self,
573    ) -> Result<Option<UploadDeviceConfigResponse>, Box<dyn Error>> {
574        let mut req = UploadDeviceConfigRequest::default();
575        req.device_configuration = Some(self.device.configuration());
576        let mut bytes = Vec::new();
577        bytes.reserve(req.encoded_len());
578        req.encode(&mut bytes).unwrap();
579
580        let mut headers = self.get_default_headers()?;
581        headers.insert(
582            "content-type",
583            String::from("application/x-protobuf"),
584        );
585
586        let resp = self
587            .execute_request("uploadDeviceConfig", None, Some(&bytes), headers)
588            .await?;
589        if let Some(payload) = resp.payload {
590            Ok(payload.upload_device_config_response)
591        } else {
592            Ok(None)
593        }
594    }
595
596    async fn request_auth_token(
597        &mut self,
598    ) -> Result<(), Box<dyn Error>> {
599        let form_params = {
600            let mut params = HashMap::new();
601            self.append_default_auth_params(&mut params);
602            self.append_auth_params(&mut params);
603            params.insert("service", String::from("oauth2:https://www.googleapis.com/auth/googleplay"));
604            params
605        };
606
607        let headers = {
608            let mut headers = HashMap::new();
609            let build_id = self.device.build_id.clone();
610            let build_device = self.device.build_device.clone().unwrap();
611            self.append_auth_headers(&mut headers, build_device, build_id);
612            headers.insert(
613                "content-length",
614                String::from("0"),
615            );
616            headers
617        };
618
619        let bytes = self
620            .execute_request_helper("auth", Some(form_params), Some(&[]), headers, false)
621            .await?;
622
623        let reply = parse_form_reply(&std::str::from_utf8(&bytes.to_vec()).unwrap());
624        self.auth_token = reply.get("auth").map(|a| a.clone());
625        Ok(())
626    }
627
628    async fn toc(&mut self) -> Result<(), Box<dyn Error>>{
629        let resp = self
630            .execute_request("toc", None, None, self.get_default_headers()?)
631            .await?;
632        let toc_response = resp
633            .payload.ok_or(GpapiError::from("Invalid payload."))?
634            .toc_response.ok_or(GpapiError::from("Invalid toc response."))?;
635        if toc_response.tos_token.is_some() || toc_response.tos_content.is_some() {
636            self.tos_token = toc_response.tos_token.clone();
637            return Err(Box::new(GpapiError::new(GpapiErrorKind::TermsOfService)));
638        }
639        if let Some(cookie) = toc_response.cookie {
640            self.dfe_cookie = Some(cookie.clone());
641            Ok(())
642        } else {
643            Err("No DFE cookie found.".into())
644        }
645    }
646
647    /// Accept the play store terms of service.
648    pub async fn accept_tos(&mut self) -> Result<Option<AcceptTosResponse>, Box<dyn Error>>{
649        if let Some(tos_token) = &self.tos_token {
650            let form_body = {
651                let mut params = HashMap::new();
652                params.insert(String::from("tost"), tos_token.clone());
653                params.insert(String::from("toscme"), String::from("false"));
654                form_post(&params)
655            };
656
657            let resp = self
658                .execute_request("acceptTos", None, Some(&form_body.into_bytes()), self.get_default_headers()?)
659                .await?;
660            if let Some(payload) = resp.payload {
661                Ok(payload.accept_tos_response)
662            } else {
663                Ok(None)
664            }
665        } else {
666            Err("ToS token must be set by `toc` call first.".into())
667        }
668    }
669
670    /// Lower level Play Store request, used by APIs but exposed for specialized
671    /// requests. Returns a `ResponseWrapper` which depending on the request
672    /// populates different fields/values.
673    async fn execute_request(
674        &self,
675        endpoint: &str,
676        query: Option<HashMap<&str, String>>,
677        msg: Option<&[u8]>,
678        headers: HashMap<&str, String>,
679    ) -> Result<ResponseWrapper, Box<dyn Error>> {
680        let bytes = self
681            .execute_request_helper(endpoint, query, msg, headers, true)
682            .await?;
683        let resp = ResponseWrapper::decode(&mut Cursor::new(bytes))?;
684        Ok(resp)
685    }
686
687    async fn execute_request_helper(
688        &self,
689        endpoint: &str,
690        query: Option<HashMap<&str, String>>,
691        msg: Option<&[u8]>,
692        headers: HashMap<&str, String>,
693        fdfe: bool,
694    ) -> Result<Bytes, Box<dyn Error>> {
695        let mut url = if fdfe {
696            Url::parse(&format!(
697                "{}/fdfe/{}",
698                consts::defaults::DEFAULT_BASE_URL,
699                endpoint
700            ))?
701        } else {
702            Url::parse(&format!(
703                "{}/{}",
704                consts::defaults::DEFAULT_BASE_URL,
705                endpoint
706            ))?
707        };
708
709        if let Some(query) = query {
710            let mut queries = url.query_pairs_mut();
711            for (key, val) in query {
712                queries.append_pair(key, &val);
713            }
714        }
715
716        let mut reqwest_headers = HeaderMap::new();
717        for (key, val) in headers {
718            reqwest_headers.insert(HeaderName::from_bytes(key.as_bytes())?, HeaderValue::from_str(&val)?);
719        }
720
721        let res = {
722            if let Some(msg) = msg {
723                (*self.client)
724                    .post(url)
725                    .headers(reqwest_headers)
726                    .body(msg.to_owned())
727                    .send()
728                    .await?
729            } else {
730                (*self.client).get(url).headers(reqwest_headers).send().await?
731            }
732        };
733
734        Ok(res.bytes().await?)
735    }
736
737}
738
739fn parse_form_reply(data: &str) -> HashMap<String, String> {
740    let mut form_resp = HashMap::new();
741    let lines: Vec<&str> = data.split_terminator('\n').collect();
742    for line in lines.iter() {
743        let kv: Vec<&str> = line.split_terminator('=').collect();
744        form_resp.insert(
745            String::from(kv[0]).to_lowercase(),
746            String::from(kv[1..].join("=")),
747        );
748    }
749    form_resp
750}
751
752#[derive(Debug, Clone)]
753struct AuthRequest {
754    params: HashMap<String, String>,
755}
756
757impl AuthRequest{
758    fn new(email: &str, oauth_token: &str) -> Self {
759        let mut auth_request = Self::default();
760        auth_request
761            .params
762            .insert(String::from("Email"), String::from(email));
763        auth_request.params.insert(
764            String::from("Token"),
765            String::from(oauth_token)
766        );
767        auth_request
768    }
769}
770
771impl Default for AuthRequest {
772    fn default() -> Self {
773        let mut params = HashMap::new();
774        params.insert(
775            String::from("lang"),
776            String::from(consts::defaults::DEFAULT_LANGUAGE)
777        );
778        params.insert(
779            String::from("google_play_services_version"),
780            String::from(consts::defaults::DEFAULT_GOOGLE_PLAY_SERVICES_VERSION)
781        );
782        params.insert(
783            String::from("sdk_version"),String::from(consts::defaults::api_user_agent::DEFAULT_SDK)
784        );
785        params.insert(
786            String::from("device_country"),
787            String::from(consts::defaults::DEFAULT_COUNTRY_CODE)
788        );
789        params.insert(String::from("Email"), String::from(""));
790        params.insert(
791            String::from("service"),
792            String::from(consts::defaults::DEFAULT_SERVICE)
793        );
794        params.insert(
795            String::from("get_accountid"),
796            String::from("1")
797        );
798        params.insert(
799            String::from("ACCESS_TOKEN"),
800            String::from("1")
801        );
802        params.insert(
803            String::from("callerPkg"),
804            String::from(consts::defaults::DEFAULT_ANDROID_VENDING)
805        );
806        params.insert(
807            String::from("add_account"),
808            String::from("1")
809        );
810        params.insert(
811            String::from("Token"),
812            String::from("")
813        );
814        params.insert(
815            String::from("callerSig"),
816            String::from(consts::defaults::DEFAULT_CALLER_SIG)
817        );
818        AuthRequest {
819            params,
820        }
821    }
822}
823
824fn form_post(params: &HashMap<String, String>) -> String {
825    params
826        .iter()
827        .map(|(k, v)| format!("{}={}", k, v))
828        .collect::<Vec<String>>()
829        .join("&")
830}
831
832#[derive(Debug, Clone)]
833struct BuildConfiguration {
834    pub finsky_agent: String,
835    pub finsky_version: String,
836    pub api: String,
837    pub version_code: String,
838    pub sdk: String,
839    pub device: String,
840    pub hardware: String,
841    pub product: String,
842    pub platform_version_release: String,
843    pub model: String,
844    pub build_id: String,
845    pub is_wide_screen: String,
846    pub supported_abis: String,
847}
848
849impl BuildConfiguration {
850    pub fn user_agent(&self) -> String {
851        format!("{}/{} (api={},versionCode={},sdk={},device={},hardware={},product={},platformVersionRelease={},model={},buildId={},isWideScreen={},supportedAbis={})", 
852          self.finsky_agent, self.finsky_version, self.api, self.version_code, self.sdk,
853          self.device, self.hardware, self.product,
854          self.platform_version_release, self.model, self.build_id,
855          self.is_wide_screen, self.supported_abis
856        )
857    }
858}
859
860impl BuildConfiguration {
861    fn new(
862        finsky_version: &str,
863        version_code: &str,
864        sdk: &str,
865        device: &str,
866        hardware: &str,
867        product: &str,
868        platform_version_release: &str,
869        model: &str,
870        build_id: &str,
871        supported_abis: &str,
872    ) -> Self {
873        use consts::defaults::api_user_agent::{DEFAULT_IS_WIDE_SCREEN, DEFAULT_API};
874        use consts::defaults::DEFAULT_FINSKY_AGENT;
875
876        BuildConfiguration {
877            finsky_agent: DEFAULT_FINSKY_AGENT.to_string(),
878            finsky_version: finsky_version.to_string(),
879            api: DEFAULT_API.to_string(),
880            version_code: version_code.to_string(),
881            sdk: sdk.to_string(),
882            device: device.to_string(),
883            hardware: hardware.to_string(),
884            product: product.to_string(),
885            platform_version_release: platform_version_release.to_string(),
886            model: model.to_string(),
887            build_id: build_id.to_string(),
888            is_wide_screen: DEFAULT_IS_WIDE_SCREEN.to_string(),
889            supported_abis: supported_abis.to_string(),
890        }
891    }
892}
893
894type Sha256 = [u8; 32];
895type Sha1 = [u8; 20];
896
897pub struct DownloadInfo {
898    pub base: BaseFileDownload,
899    pub splits: Vec<SplitFileDownload>,
900    pub expansions: Vec<ExpansionFileDownload>,
901}
902
903pub struct BaseFileDownload {
904    pub url: String,
905    pub sha256: Sha256,
906}
907
908pub struct SplitFileDownload {
909    pub url: String,
910    pub sha256: Sha256,
911    pub name: String,
912}
913
914pub struct ExpansionFileDownload {
915    pub url: String,
916    pub sha1: Sha1,
917    pub kind: ExpansionFileKind,
918    pub version_code: i32,
919}
920
921#[derive(PartialEq, Eq, Clone, Copy)]
922pub enum ExpansionFileKind {
923    Main,
924    Patch,
925}
926
927fn decode_sha256(string: &str) -> Sha256 {
928    let mut hash = [0; _];
929    BASE64_URL_SAFE_NO_PAD.decode_slice(string, &mut hash).unwrap();
930    hash
931}
932
933fn decode_sha1(string: &str) -> Sha1 {
934    let mut hash = [0; _];
935    BASE64_URL_SAFE_NO_PAD.decode_slice(string, &mut hash).unwrap();
936    hash
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942
943    #[test]
944    fn parse_form() {
945        let form_reply = "FOO=BAR\nbaz=qux";
946        let mut expected_reply = HashMap::new();
947        expected_reply.insert("baz".to_string(), "qux".to_string());
948        expected_reply.insert("foo".to_string(), "BAR".to_string());
949        let parsed_form_reply = parse_form_reply(&form_reply);
950        assert_eq!(expected_reply, parsed_form_reply);
951    }
952
953    mod gpapi {
954        use std::env;
955
956        use super::*;
957        use googleplay_protobuf::BulkDetailsRequest;
958
959        #[tokio::test]
960        async fn test_request_aas_token() {
961            if let (Ok(email), Ok(oauth_token)) = (env::var("EMAIL"), env::var("OAUTH_TOKEN")) {
962                let mut api = Gpapi::new("ad_g3_pro", &email);
963                assert!(api.request_aas_token(oauth_token).await.is_ok());
964                assert!(api.aas_token.is_some());
965            }
966        }
967
968        #[tokio::test]
969        async fn test_login() {
970            if let (Ok(email), Ok(aas_token)) = (env::var("EMAIL"), env::var("AAS_TOKEN")) {
971                let mut api = Gpapi::new("px_7a", &email);
972                api.set_aas_token(aas_token);
973                assert!(api.login().await.is_ok());
974                assert!(api.device_checkin_consistency_token.is_some());
975                assert!(api.gsf_id.is_some());
976                assert!(api.device_config_token.is_some());
977                assert!(api.auth_token.is_some());
978                assert!(api.dfe_cookie.is_some() || api.tos_token.is_some());
979            }
980        }
981
982        #[tokio::test]
983        async fn test_details() {
984            if let (Ok(email), Ok(aas_token)) = (env::var("EMAIL"), env::var("AAS_TOKEN")) {
985                let mut api = Gpapi::new("px_7a", &email);
986                api.set_aas_token(aas_token);
987                if api.login().await.is_ok() {
988                    assert!(api.details("com.viber.voip").await.is_ok());
989                }
990            }
991        }
992
993        #[tokio::test]
994        async fn test_bulk_details() {
995            if let (Ok(email), Ok(aas_token)) = (env::var("EMAIL"), env::var("AAS_TOKEN")) {
996                let mut api = Gpapi::new("px_7a", &email);
997                api.set_aas_token(aas_token);
998                if api.login().await.is_ok() {
999                    let pkg_names = ["com.viber.voip", "com.instagram.android"];
1000                    assert!(api.bulk_details(&pkg_names).await.is_ok());
1001                }
1002            }
1003        }
1004
1005        #[tokio::test]
1006        async fn test_get_download_info() {
1007            if let (Ok(email), Ok(aas_token)) = (env::var("EMAIL"), env::var("AAS_TOKEN")) {
1008                let mut api = Gpapi::new("px_7a", &email);
1009                api.set_aas_token(aas_token);
1010                if api.login().await.is_ok() {
1011                    assert!(api.get_download_info("com.viber.voip", None).await.is_ok());
1012                }
1013            }
1014        }
1015
1016        #[tokio::test]
1017        async fn test_download() {
1018            if let (Ok(email), Ok(aas_token)) = (env::var("EMAIL"), env::var("AAS_TOKEN")) {
1019                let mut api = Gpapi::new("px_7a", &email);
1020                api.set_aas_token(aas_token);
1021                if api.login().await.is_ok() {
1022                    assert!(api.download("com.instagram.android", None, true, true, &Path::new("/tmp/testing"), None).await.is_ok());
1023                }
1024            }
1025        }
1026
1027        #[test]
1028        fn test_protobuf() {
1029            let mut bdr = BulkDetailsRequest::default();
1030            bdr.doc_id = vec!["test".to_string()].into();
1031            bdr.include_child_docs = Some(true);
1032        }
1033    }
1034}