1mod 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 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 pub fn set_locale<S: Into<String>>(&mut self, locale: S){
126 self.locale = locale.into();
127 }
128
129 pub fn set_timezone<S: Into<String>>(&mut self, timezone: S){
131 self.timezone = timezone.into();
132 }
133
134 pub fn set_aas_token<S: Into<String>>(&mut self, aas_token: S) {
137 self.aas_token = Some(aas_token.into());
138 }
139
140 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 pub fn get_aas_token(&self) -> Option<&str> {
184 self.aas_token.as_ref().map(|token| token.as_str())
185 }
186
187 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 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 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 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 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(¶ms)
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 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}