use crate::{
DeviceProperties,
api::{form::Form, login::AuthToken},
consts,
error::{GenericRequestError, PropsError},
proto,
};
use http::{HeaderMap, HeaderName, HeaderValue};
use prost::Message;
use reqwest::Response;
use std::{borrow::Cow, collections::HashMap, str::FromStr, sync::LazyLock};
use url::Url;
static HTTP_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(reqwest::Client::new);
#[derive(Default)]
pub struct Requester<'a> {
props: Option<&'a DeviceProperties>,
aas_token: Option<&'a str>,
email: Option<&'a str>,
auth_token: Option<&'a AuthToken>,
checkin_token: Option<&'a str>,
config_token: Option<&'a str>,
dfe_cookie: Option<&'a str>,
gsf: Option<i64>,
headers: HashMap<&'a str, Cow<'a, str>>,
params: &'a [(&'a str, &'a str)],
user_headers: &'a [(&'a str, &'a str)],
}
impl<'a> Requester<'a> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn props(mut self, props: &'a DeviceProperties) -> Self {
self.props = Some(props);
self
}
#[must_use]
pub const fn auth_token(mut self, token: &'a AuthToken) -> Self {
self.auth_token = Some(token);
self
}
#[must_use]
pub const fn email(mut self, email: &'a str) -> Self {
self.email = Some(email);
self
}
#[must_use]
pub const fn aas_token(mut self, token: &'a str) -> Self {
self.aas_token = Some(token);
self
}
#[must_use]
pub const fn checkin_token(mut self, token: &'a str) -> Self {
self.checkin_token = Some(token);
self
}
#[must_use]
pub const fn config_token(mut self, token: &'a str) -> Self {
self.config_token = Some(token);
self
}
#[must_use]
pub const fn dfe_cookie(mut self, cookie: &'a str) -> Self {
self.dfe_cookie = Some(cookie);
self
}
#[must_use]
pub const fn gsf(mut self, id: i64) -> Self {
self.gsf = Some(id);
self
}
#[must_use]
pub const fn headers(mut self, headers: &'a [(&'a str, &'a str)]) -> Self {
self.user_headers = headers;
self
}
#[must_use]
pub const fn params(mut self, params: &'a [(&'a str, &'a str)]) -> Self {
self.params = params;
self
}
pub fn google_auth_headers(mut self) -> Result<Self, PropsError> {
let props = self.props.ok_or(PropsError::NoProps())?;
let build = props
.android_checkin
.build
.as_ref()
.ok_or(PropsError::NoBuild())?;
let build_device = build.device.as_ref().ok_or(PropsError::NoBuildDevice())?;
let build_id = props
.extra_info
.get("Build.ID")
.ok_or(PropsError::NoBuildId())?;
self.headers
.insert("app", Cow::from(consts::ANDROID_VENDING));
self.headers.insert(
"User-Agent",
Cow::from(format!("GoogleAuth/1.4 ({build_device} {build_id})")),
);
if let Some(id) = self.gsf {
self.headers.insert("device", Cow::from(format!("{id:x}")));
}
Ok(self)
}
pub async fn default_headers(mut self) -> Result<Self, PropsError> {
if let Some(auth_token) = &self.auth_token {
let maybe_existing = auth_token.get().map(|a| format!("Bearer {a}"));
let maybe_new = if maybe_existing.is_none()
&& let Some(props) = self.props
&& let Some(aas) = self.aas_token
&& let Some(email) = self.email
&& let Some(gsf) = self.gsf
{
let token = super::login::fetch_auth(props, aas, email, gsf)
.await
.map_err(|a| PropsError::UpdateAuth(Box::new(a)))?;
auth_token.update(&token);
auth_token.get().map(|a| format!("Bearer {a}"))
} else {
None
};
if let Some(value) = maybe_existing.or(maybe_new) {
self.headers.insert("Authorization", Cow::from(value));
}
}
let (user_agent, sim_operator) = self.get_useragent()?;
let mut new = HashMap::from([
("user-agent", Cow::from(user_agent)),
(
"accept-language",
Cow::from(consts::LOCALE.replace('_', "-")),
),
("X-DFE-Encoded-Targets", Cow::from(consts::DFE_TARGETS)),
("X-DFE-Request-Params", Cow::from("timeoutMs=4000")),
("X-DFE-Phenotype", Cow::from(consts::DFE_PHENOTYPE)),
("X-Limit-Ad-Tracking-Enabled", Cow::from("false")),
("X-DFE-UserLanguages", Cow::from(consts::LOCALE)),
("X-DFE-Client-Id", Cow::from(consts::CLIENT_ID)),
("X-DFE-MCCMCN", Cow::from(sim_operator)),
("X-DFE-Content-Filters", Cow::from("")),
("X-DFE-Network-Type", Cow::from("4")),
("X-Ad-Id", Cow::from("")),
]);
if let Some(id) = self.gsf {
self.headers
.insert("X-DFE-Device-Id", Cow::from(format!("{id:x}")));
}
if let Some(token) = self.checkin_token {
new.insert("X-DFE-Device-Checkin-Consistency-Token", Cow::from(token));
}
if let Some(cookie) = self.dfe_cookie {
new.insert("X-DFE-Cookie", Cow::from(cookie));
}
if let Some(token) = self.config_token {
new.insert("X-DFE-Device-Config-Token", Cow::from(token));
}
self.headers.extend(new);
Ok(self)
}
async fn inner_request(
&self,
mut url: Url,
body: Option<Vec<u8>>,
) -> Result<Response, GenericRequestError> {
if !self.params.is_empty() {
let mut queries = url.query_pairs_mut();
for (key, val) in self.params {
queries.append_pair(key.as_ref(), val.as_ref());
}
}
let mut req_headers = HeaderMap::new();
for (name, value) in &self.headers {
req_headers.insert(HeaderName::from_str(name)?, HeaderValue::from_str(value)?);
}
for (name, value) in self.user_headers {
req_headers.insert(HeaderName::from_str(name)?, HeaderValue::from_str(value)?);
}
let response = if let Some(body) = body {
HTTP_CLIENT.post(url).body(body).headers(req_headers)
} else {
HTTP_CLIENT.get(url).headers(req_headers)
}
.send()
.await?;
Ok(response)
}
pub async fn fdfe_request(
&self,
endpoint: &str,
body: Option<Vec<u8>>,
) -> Result<Box<proto::ResponseWrapper>, GenericRequestError> {
let url = Url::parse(&format!("{}/fdfe/{}", consts::BASE_URL, endpoint))?;
let response = self.inner_request(url, body).await?;
let bytes = response.bytes().await?;
Ok(Box::new(proto::ResponseWrapper::decode(bytes)?))
}
pub async fn generic_request(
&self,
endpoint: &str,
body: Option<Vec<u8>>,
) -> Result<Response, GenericRequestError> {
let url = Url::from_str(&format!("{}/{}", consts::BASE_URL, endpoint))?;
self.inner_request(url, body).await
}
pub async fn form_request(
&self,
endpoint: &str,
form: Form<'a>,
) -> Result<Form<'a>, GenericRequestError> {
let url = Url::from_str(&format!("{}/{}", consts::BASE_URL, endpoint))?;
let body = form.format().into_bytes();
let response = self.inner_request(url, Some(body)).await?;
let text = response.text().await?;
let form = Form::parse(&text);
Ok(form)
}
fn get_useragent(&self) -> Result<(String, String), PropsError> {
let props = self.props.ok_or(PropsError::NoProps())?;
let build = props
.android_checkin
.build
.as_ref()
.ok_or(PropsError::NoBuild())?;
let version_string = props
.extra_info
.get("Vending.versionString")
.ok_or(PropsError::NoVersionString())?;
let vending_version = props
.extra_info
.get("Vending.version")
.ok_or(PropsError::NoVersion())?;
let release_version = props
.extra_info
.get("Build.VERSION.RELEASE")
.ok_or(PropsError::NoVersionRelease())?;
let build_id = props
.extra_info
.get("Build.ID")
.ok_or(PropsError::NoBuildId())?;
let sim_operator = props
.extra_info
.get("SimOperator")
.ok_or(PropsError::NoSimOperator())?;
let sdk_version = build
.sdk_version
.ok_or(PropsError::NoBuildSDK())?
.to_string();
let build_model = build.model.as_ref().ok_or(PropsError::NoBuildModel())?;
let build_device = build.device.as_ref().ok_or(PropsError::NoBuildDevice())?;
let build_product = build.product.as_ref().ok_or(PropsError::NoBuildProduct())?;
let build_build_product = build
.build_product
.as_ref()
.ok_or(PropsError::NoBuildProduct())?;
let abis = props.device_config.native_platform.join(";");
Ok((
format!(
"{}/{} (api={},versionCode={},sdk={},device={},hardware={},product={},platformVersionRelease={},model={},buildId={},isWideScreen={},supportedAbis={})",
consts::FINSKY_AGENT,
version_string,
consts::API,
vending_version,
sdk_version,
build_device,
build_product,
build_build_product,
release_version,
build_model,
build_id,
consts::IS_WIDE_SCREEN,
abis
),
sim_operator.clone(),
))
}
}