mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 06:26:16 +01:00
Add refreshing anisette & Rethink ownership for GrandSlam and anisette to prepare for concurrency
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -824,6 +824,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
"uuid",
|
||||
|
||||
@@ -49,7 +49,7 @@ async fn main() {
|
||||
|
||||
let mut account = account.unwrap();
|
||||
|
||||
let dev_session = DeveloperSession::from_account(&mut account)
|
||||
let mut dev_session = DeveloperSession::from_account(&mut account)
|
||||
.await
|
||||
.expect("Failed to create developer session");
|
||||
|
||||
|
||||
@@ -38,3 +38,4 @@ hmac = "0.12.1"
|
||||
cbc = { version = "0.1.2", features = ["std"] }
|
||||
aes = "0.8.4"
|
||||
aes-gcm = "0.10.3"
|
||||
tokio = "1.49.0"
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
use crate::{
|
||||
anisette::{AnisetteProvider, AnisetteProviderConfig, remote_v3::state::AnisetteState},
|
||||
auth::grandslam::GrandSlam,
|
||||
};
|
||||
use plist::Dictionary;
|
||||
use plist_macro::plist;
|
||||
use reqwest::header::HeaderMap;
|
||||
use rootcause::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, time::SystemTime};
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct AnisetteClientInfo {
|
||||
pub client_info: String,
|
||||
pub user_agent: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnisetteData {
|
||||
pub routing_info: String,
|
||||
pub machine_id: String,
|
||||
pub one_time_password: String,
|
||||
pub device_description: String,
|
||||
pub device_unique_identifier: String,
|
||||
pub local_user_id: String,
|
||||
pub generated_at: SystemTime,
|
||||
}
|
||||
|
||||
// Some headers don't seem to be required. I guess not including them is technically more efficient soooo
|
||||
impl AnisetteData {
|
||||
pub fn get_headers(&self) -> HashMap<String, String> {
|
||||
//let dt: DateTime<Utc> = Utc::now().round_subsecs(0);
|
||||
|
||||
HashMap::from_iter(
|
||||
[
|
||||
// (
|
||||
// "X-Apple-I-Client-Time".to_string(),
|
||||
// dt.format("%+").to_string().replace("+00:00", "Z"),
|
||||
// ),
|
||||
// ("X-Apple-I-SRL-NO".to_string(), serial),
|
||||
// ("X-Apple-I-TimeZone".to_string(), "UTC".to_string()),
|
||||
// ("X-Apple-Locale".to_string(), "en_US".to_string()),
|
||||
// ("X-Apple-I-MD-RINFO".to_string(), self.routing_info.clone()),
|
||||
// ("X-Apple-I-MD-LU".to_string(), self.local_user_id.clone()),
|
||||
(
|
||||
"X-Mme-Device-Id".to_string(),
|
||||
self.device_unique_identifier.clone(),
|
||||
),
|
||||
("X-Apple-I-MD".to_string(), self.one_time_password.clone()),
|
||||
("X-Apple-I-MD-M".to_string(), self.machine_id.clone()),
|
||||
// (
|
||||
// "X-Mme-Client-Info".to_string(),
|
||||
// self.device_description.clone(),
|
||||
// ),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_header_map(&self) -> HeaderMap {
|
||||
let headers_map = self.get_headers();
|
||||
let mut header_map = HeaderMap::new();
|
||||
|
||||
for (key, value) in headers_map {
|
||||
header_map.insert(
|
||||
reqwest::header::HeaderName::from_bytes(key.as_bytes()).unwrap(),
|
||||
reqwest::header::HeaderValue::from_str(&value).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
header_map
|
||||
}
|
||||
|
||||
pub fn get_client_provided_data(&self) -> Dictionary {
|
||||
let headers = self.get_headers();
|
||||
|
||||
let mut cpd = plist!(dict {
|
||||
"bootstrap": "true",
|
||||
"icscrec": "true",
|
||||
"loc": "en_US",
|
||||
"pbe": "false",
|
||||
"prkgen": "true",
|
||||
"svct": "iCloud"
|
||||
});
|
||||
|
||||
for (key, value) in headers {
|
||||
cpd.insert(key.to_string(), plist::Value::String(value));
|
||||
}
|
||||
|
||||
cpd
|
||||
}
|
||||
|
||||
pub fn needs_refresh(&self) -> bool {
|
||||
let elapsed = self.generated_at.elapsed().unwrap();
|
||||
elapsed.as_secs() > 60
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenewableAnisetteData {
|
||||
config: AnisetteProviderConfig,
|
||||
anisette_data: Option<AnisetteData>,
|
||||
client_info: Option<AnisetteClientInfo>,
|
||||
state: Option<AnisetteState>,
|
||||
}
|
||||
|
||||
impl RenewableAnisetteData {
|
||||
pub fn new(config: AnisetteProviderConfig) -> Self {
|
||||
RenewableAnisetteData {
|
||||
config,
|
||||
anisette_data: None,
|
||||
client_info: None,
|
||||
state: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_anisette_data(&mut self, gs: &mut GrandSlam) -> Result<&AnisetteData, Report> {
|
||||
if self
|
||||
.anisette_data
|
||||
.as_ref()
|
||||
.map_or(true, |data| data.needs_refresh())
|
||||
{
|
||||
if self.client_info.is_none() || self.state.is_none() {
|
||||
let mut provider = self.config.get_provider(self.client_info.clone());
|
||||
let client_info = provider.get_client_info().await?;
|
||||
self.client_info = Some(client_info);
|
||||
let data = provider.get_anisette_data(gs).await?;
|
||||
self.anisette_data = Some(data);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.anisette_data.as_ref().unwrap())
|
||||
}
|
||||
|
||||
pub async fn get_client_info(
|
||||
&mut self,
|
||||
gs: &mut GrandSlam,
|
||||
) -> Result<AnisetteClientInfo, Report> {
|
||||
self.get_anisette_data(gs).await?;
|
||||
|
||||
if let Some(client_info) = &self.client_info {
|
||||
return Ok(client_info.clone());
|
||||
} else {
|
||||
bail!("Anisette client info not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ use plist_macro::plist;
|
||||
use reqwest::header::HeaderMap;
|
||||
use rootcause::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, sync::Arc, time::SystemTime};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct AnisetteClientInfo {
|
||||
@@ -22,6 +23,7 @@ pub struct AnisetteData {
|
||||
_device_description: String,
|
||||
device_unique_identifier: String,
|
||||
_local_user_id: String,
|
||||
generated_at: SystemTime,
|
||||
}
|
||||
|
||||
// Some headers don't seem to be required. I guess not including them is technically more efficient soooo
|
||||
@@ -87,11 +89,72 @@ impl AnisetteData {
|
||||
|
||||
cpd
|
||||
}
|
||||
|
||||
pub fn needs_refresh(&self) -> bool {
|
||||
let elapsed = self.generated_at.elapsed().unwrap();
|
||||
elapsed.as_secs() > 60
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait AnisetteProvider {
|
||||
async fn get_anisette_data(&mut self, gs: &mut GrandSlam) -> Result<AnisetteData, Report>;
|
||||
async fn get_anisette_data(&self) -> Result<AnisetteData, Report>;
|
||||
|
||||
async fn get_client_info(&mut self) -> Result<AnisetteClientInfo, Report>;
|
||||
|
||||
async fn provision(&mut self, gs: Arc<GrandSlam>) -> Result<(), Report>;
|
||||
|
||||
fn needs_provisioning(&self) -> Result<bool, Report>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnisetteDataGenerator {
|
||||
provider: Arc<RwLock<dyn AnisetteProvider + Send + Sync>>,
|
||||
data: Option<Arc<AnisetteData>>,
|
||||
}
|
||||
|
||||
impl AnisetteDataGenerator {
|
||||
pub fn new(provider: Arc<RwLock<dyn AnisetteProvider + Send + Sync>>) -> Self {
|
||||
AnisetteDataGenerator {
|
||||
provider,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_anisette_data(
|
||||
&mut self,
|
||||
gs: Arc<GrandSlam>,
|
||||
) -> Result<Arc<AnisetteData>, Report> {
|
||||
if let Some(data) = &self.data {
|
||||
if !data.needs_refresh() {
|
||||
return Ok(data.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// trying to avoid locking as write unless necessary to promote concurrency
|
||||
let provider = self.provider.read().await;
|
||||
|
||||
if provider.needs_provisioning()? {
|
||||
drop(provider);
|
||||
let mut provider_write = self.provider.write().await;
|
||||
provider_write.provision(gs).await?;
|
||||
drop(provider_write);
|
||||
|
||||
let provider = self.provider.read().await;
|
||||
let data = provider.get_anisette_data().await?;
|
||||
let arc_data = Arc::new(data);
|
||||
self.data = Some(arc_data.clone());
|
||||
Ok(arc_data)
|
||||
} else {
|
||||
let data = provider.get_anisette_data().await?;
|
||||
let arc_data = Arc::new(data);
|
||||
self.data = Some(arc_data.clone());
|
||||
Ok(arc_data)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_client_info(&self) -> Result<AnisetteClientInfo, Report> {
|
||||
let mut provider = self.provider.write().await;
|
||||
provider.get_client_info().await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ mod state;
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use base64::prelude::*;
|
||||
use plist_macro::plist;
|
||||
@@ -11,6 +13,7 @@ use serde::Deserialize;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::SideloadError;
|
||||
use crate::anisette::remote_v3::state::AnisetteState;
|
||||
use crate::anisette::{AnisetteClientInfo, AnisetteData, AnisetteProvider};
|
||||
use crate::auth::grandslam::GrandSlam;
|
||||
@@ -74,13 +77,19 @@ impl Default for RemoteV3AnisetteProvider {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AnisetteProvider for RemoteV3AnisetteProvider {
|
||||
async fn get_anisette_data(&mut self, gs: &mut GrandSlam) -> Result<AnisetteData, Report> {
|
||||
let state = self.get_state(gs).await?.clone();
|
||||
async fn get_anisette_data(&self) -> Result<AnisetteData, Report> {
|
||||
let state = self
|
||||
.state
|
||||
.as_ref()
|
||||
.ok_or(SideloadError::AnisetteNotProvisioned)?;
|
||||
let adi_pb = state
|
||||
.adi_pb
|
||||
.as_ref()
|
||||
.ok_or(report!("Anisette state is not provisioned"))?;
|
||||
let client_info = self.get_client_info().await?.client_info.clone();
|
||||
.ok_or(SideloadError::AnisetteNotProvisioned)?;
|
||||
let client_info = self
|
||||
.client_info
|
||||
.as_ref()
|
||||
.ok_or(SideloadError::AnisetteNotProvisioned)?;
|
||||
|
||||
let headers = self
|
||||
.client
|
||||
@@ -109,9 +118,10 @@ impl AnisetteProvider for RemoteV3AnisetteProvider {
|
||||
machine_id,
|
||||
one_time_password,
|
||||
routing_info,
|
||||
_device_description: client_info,
|
||||
_device_description: client_info.client_info.clone(),
|
||||
device_unique_identifier: state.get_device_id(),
|
||||
_local_user_id: hex::encode(&state.get_md_lu()),
|
||||
generated_at: SystemTime::now(),
|
||||
};
|
||||
|
||||
Ok(data)
|
||||
@@ -140,10 +150,24 @@ impl AnisetteProvider for RemoteV3AnisetteProvider {
|
||||
|
||||
Ok(self.client_info.as_ref().unwrap().clone())
|
||||
}
|
||||
|
||||
fn needs_provisioning(&self) -> Result<bool, Report> {
|
||||
if let Some(state) = &self.state {
|
||||
Ok(!state.is_provisioned() || self.client_info.is_none())
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
async fn provision(&mut self, gs: Arc<GrandSlam>) -> Result<(), Report> {
|
||||
self.get_client_info().await?;
|
||||
self.get_state(gs).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteV3AnisetteProvider {
|
||||
async fn get_state(&mut self, gs: &mut GrandSlam) -> Result<&mut AnisetteState, Report> {
|
||||
async fn get_state(&mut self, gs: Arc<GrandSlam>) -> Result<&mut AnisetteState, Report> {
|
||||
let state_path = self.config_path.join("state.plist");
|
||||
fs::create_dir_all(&self.config_path)?;
|
||||
if self.state.is_none() {
|
||||
@@ -195,13 +219,13 @@ impl RemoteV3AnisetteProvider {
|
||||
}
|
||||
async fn provision(
|
||||
state: &mut AnisetteState,
|
||||
gs: &mut GrandSlam,
|
||||
gs: Arc<GrandSlam>,
|
||||
url: &str,
|
||||
) -> Result<(), Report> {
|
||||
debug!("Starting provisioning");
|
||||
|
||||
let start_provisioning = gs.get_url("midStartProvisioning").await?;
|
||||
let end_provisioning = gs.get_url("midFinishProvisioning").await?;
|
||||
let start_provisioning = gs.get_url("midStartProvisioning")?;
|
||||
let end_provisioning = gs.get_url("midFinishProvisioning")?;
|
||||
|
||||
let websocket_url = format!("{}/v3/provisioning_session", url)
|
||||
.replace("https://", "wss://")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
anisette::{AnisetteData, AnisetteProvider},
|
||||
anisette::{AnisetteData, AnisetteDataGenerator},
|
||||
auth::{
|
||||
builder::AppleAccountBuilder,
|
||||
grandslam::{GrandSlam, GrandSlamErrorChecker},
|
||||
@@ -30,9 +30,8 @@ use tracing::{debug, info, warn};
|
||||
pub struct AppleAccount {
|
||||
pub email: String,
|
||||
pub spd: Option<plist::Dictionary>,
|
||||
pub anisette_provider: Arc<Mutex<dyn AnisetteProvider + Send>>,
|
||||
pub anisette_data: AnisetteData,
|
||||
pub grandslam_client: GrandSlam,
|
||||
pub anisette_generator: AnisetteDataGenerator,
|
||||
pub grandslam_client: Arc<GrandSlam>,
|
||||
login_state: LoginState,
|
||||
debug: bool,
|
||||
}
|
||||
@@ -64,7 +63,7 @@ impl AppleAccount {
|
||||
/// - `debug`: DANGER, If true, accept invalid certificates and enable verbose connection
|
||||
pub async fn new(
|
||||
email: &str,
|
||||
mut anisette_provider: Arc<Mutex<dyn AnisetteProvider + Send>>,
|
||||
anisette_generator: AnisetteDataGenerator,
|
||||
debug: bool,
|
||||
) -> Result<Self, Report> {
|
||||
info!("Initializing apple account");
|
||||
@@ -72,26 +71,18 @@ impl AppleAccount {
|
||||
warn!("Debug mode enabled: this is a security risk!");
|
||||
}
|
||||
|
||||
let client_info = anisette_provider
|
||||
.lock()
|
||||
.unwrap()
|
||||
let client_info = anisette_generator
|
||||
.get_client_info()
|
||||
.await
|
||||
.context("Failed to get anisette client info")?;
|
||||
|
||||
let mut grandslam_client = GrandSlam::new(client_info, debug);
|
||||
|
||||
let anisette_data = anisette_provider
|
||||
.get_anisette_data(&mut grandslam_client)
|
||||
.await
|
||||
.context("Failed to get anisette data for login")?;
|
||||
let grandslam_client = GrandSlam::new(client_info, debug).await?;
|
||||
|
||||
Ok(AppleAccount {
|
||||
email: email.to_string(),
|
||||
spd: None,
|
||||
anisette_provider,
|
||||
anisette_data,
|
||||
grandslam_client,
|
||||
anisette_generator,
|
||||
grandslam_client: Arc::new(grandslam_client),
|
||||
debug,
|
||||
login_state: LoginState::NeedsLogin,
|
||||
})
|
||||
@@ -197,16 +188,22 @@ impl AppleAccount {
|
||||
two_factor_callback: impl Fn() -> Option<String>,
|
||||
) -> Result<(), Report> {
|
||||
debug!("Trusted device 2FA required");
|
||||
|
||||
let anisette_data = self
|
||||
.anisette_generator
|
||||
.get_anisette_data(self.grandslam_client.clone())
|
||||
.await
|
||||
.context("Failed to get anisette data for 2FA")?;
|
||||
|
||||
let request_code_url = self
|
||||
.grandslam_client
|
||||
.get_url("trustedDeviceSecondaryAuth")
|
||||
.await?;
|
||||
.get_url("trustedDeviceSecondaryAuth")?;
|
||||
|
||||
let submit_code_url = self.grandslam_client.get_url("validateCode").await?;
|
||||
let submit_code_url = self.grandslam_client.get_url("validateCode")?;
|
||||
|
||||
self.grandslam_client
|
||||
.get(&request_code_url)?
|
||||
.headers(self.build_2fa_headers().await?)
|
||||
.headers(self.build_2fa_headers(&anisette_data).await?)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to request trusted device 2fa")?
|
||||
@@ -221,7 +218,7 @@ impl AppleAccount {
|
||||
let res = self
|
||||
.grandslam_client
|
||||
.get(&submit_code_url)?
|
||||
.headers(self.build_2fa_headers().await?)
|
||||
.headers(self.build_2fa_headers(&anisette_data).await?)
|
||||
.header("security-code", code)
|
||||
.send()
|
||||
.await
|
||||
@@ -248,11 +245,17 @@ impl AppleAccount {
|
||||
) -> Result<(), Report> {
|
||||
debug!("SMS 2FA required");
|
||||
|
||||
let request_code_url = self.grandslam_client.get_url("secondaryAuth").await?;
|
||||
let anisette_data = self
|
||||
.anisette_generator
|
||||
.get_anisette_data(self.grandslam_client.clone())
|
||||
.await
|
||||
.context("Failed to get anisette data for 2FA")?;
|
||||
|
||||
let request_code_url = self.grandslam_client.get_url("secondaryAuth")?;
|
||||
|
||||
self.grandslam_client
|
||||
.get_sms(&request_code_url)?
|
||||
.headers(self.build_2fa_headers().await?)
|
||||
.headers(self.build_2fa_headers(&anisette_data).await?)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to request SMS 2FA")?
|
||||
@@ -274,7 +277,7 @@ impl AppleAccount {
|
||||
"mode": "sms"
|
||||
});
|
||||
|
||||
let mut headers = self.build_2fa_headers().await?;
|
||||
let mut headers = self.build_2fa_headers(&anisette_data).await?;
|
||||
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
|
||||
headers.insert(
|
||||
"Accept",
|
||||
@@ -332,8 +335,8 @@ impl AppleAccount {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn build_2fa_headers(&mut self) -> Result<HeaderMap, Report> {
|
||||
let mut headers = self.anisette_data.get_header_map();
|
||||
async fn build_2fa_headers(&self, anisette_data: &AnisetteData) -> Result<HeaderMap, Report> {
|
||||
let mut headers = anisette_data.get_header_map();
|
||||
|
||||
let spd = self
|
||||
.spd
|
||||
@@ -354,18 +357,23 @@ impl AppleAccount {
|
||||
);
|
||||
headers.insert(
|
||||
"X-Apple-I-MD-RINFO",
|
||||
reqwest::header::HeaderValue::from_str(&self.anisette_data.routing_info)?,
|
||||
reqwest::header::HeaderValue::from_str(&anisette_data.routing_info)?,
|
||||
);
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
async fn login_inner(&mut self, password: &str) -> Result<LoginState, Report> {
|
||||
let gs_service_url = self.grandslam_client.get_url("gsService").await?;
|
||||
let anisette_data = self
|
||||
.anisette_generator
|
||||
.get_anisette_data(self.grandslam_client.clone())
|
||||
.await
|
||||
.context("Failed to get anisette data for login")?;
|
||||
|
||||
let gs_service_url = self.grandslam_client.get_url("gsService")?;
|
||||
debug!("GrandSlam service URL: {}", gs_service_url);
|
||||
|
||||
let cpd = self.anisette_data.get_client_provided_data();
|
||||
let cpd = anisette_data.get_client_provided_data();
|
||||
|
||||
let srp_client = SrpClient::<Sha256>::new(&G_2048);
|
||||
let a: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
|
||||
@@ -510,6 +518,12 @@ impl AppleAccount {
|
||||
format!("com.apple.gs.{}", app)
|
||||
};
|
||||
|
||||
let anisette_data = self
|
||||
.anisette_generator
|
||||
.get_anisette_data(self.grandslam_client.clone())
|
||||
.await
|
||||
.context("Failed to get anisette data for login")?;
|
||||
|
||||
let spd = self
|
||||
.spd
|
||||
.as_ref()
|
||||
@@ -531,8 +545,8 @@ impl AppleAccount {
|
||||
.into_bytes()
|
||||
.to_vec();
|
||||
|
||||
let gs_service_url = self.grandslam_client.get_url("gsService").await?;
|
||||
let cpd = self.anisette_data.get_client_provided_data();
|
||||
let gs_service_url = self.grandslam_client.get_url("gsService")?;
|
||||
let cpd = anisette_data.get_client_provided_data();
|
||||
|
||||
let request = plist!(dict {
|
||||
"Header": {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use rootcause::prelude::*;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
anisette::{AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
|
||||
anisette::{AnisetteDataGenerator, AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
|
||||
auth::apple_account::AppleAccount,
|
||||
};
|
||||
|
||||
pub struct AppleAccountBuilder {
|
||||
email: String,
|
||||
debug: Option<bool>,
|
||||
anisette_provider: Option<Box<dyn AnisetteProvider>>,
|
||||
anisette_generator: Option<AnisetteDataGenerator>,
|
||||
}
|
||||
|
||||
impl AppleAccountBuilder {
|
||||
@@ -20,7 +23,7 @@ impl AppleAccountBuilder {
|
||||
Self {
|
||||
email: email.to_string(),
|
||||
debug: None,
|
||||
anisette_provider: None,
|
||||
anisette_generator: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +36,13 @@ impl AppleAccountBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn anisette_provider(mut self, anisette_provider: impl AnisetteProvider + 'static) -> Self {
|
||||
self.anisette_provider = Some(Box::new(anisette_provider));
|
||||
pub fn anisette_provider(
|
||||
mut self,
|
||||
anisette_provider: impl AnisetteProvider + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.anisette_generator = Some(AnisetteDataGenerator::new(Arc::new(RwLock::new(
|
||||
anisette_provider,
|
||||
))));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -44,11 +52,11 @@ impl AppleAccountBuilder {
|
||||
/// Returns an error if the reqwest client cannot be built
|
||||
pub async fn build(self) -> Result<AppleAccount, Report> {
|
||||
let debug = self.debug.unwrap_or(false);
|
||||
let anisette_provider = self
|
||||
.anisette_provider
|
||||
.unwrap_or_else(|| Box::new(RemoteV3AnisetteProvider::default()));
|
||||
let anisette_generator = self.anisette_generator.unwrap_or_else(|| {
|
||||
AnisetteDataGenerator::new(Arc::new(RwLock::new(RemoteV3AnisetteProvider::default())))
|
||||
});
|
||||
|
||||
AppleAccount::new(&self.email, anisette_provider, debug).await
|
||||
AppleAccount::new(&self.email, anisette_generator, debug).await
|
||||
}
|
||||
|
||||
/// Build the AppleAccount and log in
|
||||
|
||||
@@ -16,7 +16,7 @@ const URL_BAG: &str = "https://gsa.apple.com/grandslam/GsService2/lookup";
|
||||
pub struct GrandSlam {
|
||||
pub client: reqwest::Client,
|
||||
pub client_info: AnisetteClientInfo,
|
||||
pub url_bag: Option<Dictionary>,
|
||||
url_bag: Dictionary,
|
||||
}
|
||||
|
||||
impl GrandSlam {
|
||||
@@ -24,64 +24,75 @@ impl GrandSlam {
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `client`: The reqwest client to use for requests
|
||||
pub fn new(client_info: AnisetteClientInfo, debug: bool) -> Self {
|
||||
Self {
|
||||
client: Self::build_reqwest_client(debug).unwrap(),
|
||||
pub async fn new(client_info: AnisetteClientInfo, debug: bool) -> Result<Self, Report> {
|
||||
let client = Self::build_reqwest_client(debug).unwrap();
|
||||
let base_headers = Self::base_headers(&client_info, false)?;
|
||||
let url_bag = Self::fetch_url_bag(&client, base_headers).await?;
|
||||
Ok(Self {
|
||||
client,
|
||||
client_info,
|
||||
url_bag: None,
|
||||
}
|
||||
url_bag,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the URL bag from GrandSlam
|
||||
pub async fn get_url_bag(&mut self) -> Result<&Dictionary, Report> {
|
||||
if self.url_bag.is_none() {
|
||||
debug!("Fetching URL bag from GrandSlam");
|
||||
let resp = self
|
||||
.client
|
||||
.get(URL_BAG)
|
||||
.headers(self.base_headers(false)?)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to fetch URL Bag")?
|
||||
.text()
|
||||
.await
|
||||
.context("Failed to read URL Bag response text")?;
|
||||
/// Fetch the URL bag from GrandSlam and cache it
|
||||
pub async fn fetch_url_bag(
|
||||
client: &reqwest::Client,
|
||||
base_headers: HeaderMap,
|
||||
) -> Result<Dictionary, Report> {
|
||||
debug!("Fetching URL bag from GrandSlam");
|
||||
let resp = client
|
||||
.get(URL_BAG)
|
||||
.headers(base_headers)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to fetch URL Bag")?
|
||||
.text()
|
||||
.await
|
||||
.context("Failed to read URL Bag response text")?;
|
||||
|
||||
let dict: Dictionary =
|
||||
plist::from_bytes(resp.as_bytes()).context("Failed to parse URL Bag plist")?;
|
||||
let urls = dict
|
||||
.get("urls")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.cloned()
|
||||
.ok_or_else(|| report!("URL Bag plist missing 'urls' dictionary"))?;
|
||||
let dict: Dictionary =
|
||||
plist::from_bytes(resp.as_bytes()).context("Failed to parse URL Bag plist")?;
|
||||
let urls = dict
|
||||
.get("urls")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.cloned()
|
||||
.ok_or_else(|| report!("URL Bag plist missing 'urls' dictionary"))?;
|
||||
|
||||
self.url_bag = Some(urls);
|
||||
}
|
||||
Ok(self.url_bag.as_ref().unwrap())
|
||||
Ok(urls)
|
||||
}
|
||||
|
||||
pub async fn get_url(&mut self, key: &str) -> Result<String, Report> {
|
||||
let url_bag = self.get_url_bag().await?;
|
||||
let url = url_bag
|
||||
pub fn get_url(&self, key: &str) -> Result<String, Report> {
|
||||
let url = self
|
||||
.url_bag
|
||||
.get_string(key)
|
||||
.context("Unable to find key in URL bag")?;
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
pub fn get(&self, url: &str) -> Result<reqwest::RequestBuilder, Report> {
|
||||
let builder = self.client.get(url).headers(self.base_headers(false)?);
|
||||
let builder = self
|
||||
.client
|
||||
.get(url)
|
||||
.headers(Self::base_headers(&self.client_info, false)?);
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
|
||||
pub fn get_sms(&self, url: &str) -> Result<reqwest::RequestBuilder, Report> {
|
||||
let builder = self.client.get(url).headers(self.base_headers(true)?);
|
||||
let builder = self
|
||||
.client
|
||||
.get(url)
|
||||
.headers(Self::base_headers(&self.client_info, true)?);
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
|
||||
pub fn post(&self, url: &str) -> Result<reqwest::RequestBuilder, Report> {
|
||||
let builder = self.client.post(url).headers(self.base_headers(false)?);
|
||||
let builder = self
|
||||
.client
|
||||
.post(url)
|
||||
.headers(Self::base_headers(&self.client_info, false)?);
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
@@ -121,7 +132,10 @@ impl GrandSlam {
|
||||
Ok(response_plist)
|
||||
}
|
||||
|
||||
fn base_headers(&self, sms: bool) -> Result<reqwest::header::HeaderMap, Report> {
|
||||
fn base_headers(
|
||||
client_info: &AnisetteClientInfo,
|
||||
sms: bool,
|
||||
) -> Result<reqwest::header::HeaderMap, Report> {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
if !sms {
|
||||
headers.insert("Content-Type", HeaderValue::from_static("text/x-xml-plist"));
|
||||
@@ -129,11 +143,11 @@ impl GrandSlam {
|
||||
}
|
||||
headers.insert(
|
||||
"X-Mme-Client-Info",
|
||||
HeaderValue::from_str(&self.client_info.client_info)?,
|
||||
HeaderValue::from_str(&client_info.client_info)?,
|
||||
);
|
||||
headers.insert(
|
||||
"User-Agent",
|
||||
HeaderValue::from_str(&self.client_info.user_agent)?,
|
||||
HeaderValue::from_str(&client_info.user_agent)?,
|
||||
);
|
||||
headers.insert("X-Xcode-Version", HeaderValue::from_static("14.2 (14C18)"));
|
||||
headers.insert(
|
||||
|
||||
@@ -18,10 +18,10 @@ pub struct AppGroup {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait AppGroupsApi {
|
||||
fn developer_session(&self) -> &DeveloperSession<'_>;
|
||||
fn developer_session(&mut self) -> &mut DeveloperSession;
|
||||
|
||||
async fn list_app_groups(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
device_type: impl Into<Option<DeveloperDeviceType>> + Send,
|
||||
) -> Result<Vec<AppGroup>, Report> {
|
||||
@@ -43,7 +43,7 @@ pub trait AppGroupsApi {
|
||||
}
|
||||
|
||||
async fn add_app_group(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
name: &str,
|
||||
identifier: &str,
|
||||
@@ -69,7 +69,7 @@ pub trait AppGroupsApi {
|
||||
}
|
||||
|
||||
async fn assign_app_group(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
app_group: &AppGroup,
|
||||
app_id: &AppId,
|
||||
@@ -93,8 +93,8 @@ pub trait AppGroupsApi {
|
||||
}
|
||||
}
|
||||
|
||||
impl AppGroupsApi for DeveloperSession<'_> {
|
||||
fn developer_session(&self) -> &DeveloperSession<'_> {
|
||||
impl AppGroupsApi for DeveloperSession {
|
||||
fn developer_session(&mut self) -> &mut DeveloperSession {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +52,10 @@ pub struct Profile {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait AppIdsApi {
|
||||
fn developer_session(&self) -> &DeveloperSession<'_>;
|
||||
fn developer_session(&mut self) -> &mut DeveloperSession;
|
||||
|
||||
async fn add_app_id(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
name: &str,
|
||||
identifier: &str,
|
||||
@@ -77,7 +77,7 @@ pub trait AppIdsApi {
|
||||
}
|
||||
|
||||
async fn list_app_ids(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
device_type: impl Into<Option<DeveloperDeviceType>> + Send,
|
||||
) -> Result<ListAppIdsResponse, Report> {
|
||||
@@ -107,7 +107,7 @@ pub trait AppIdsApi {
|
||||
}
|
||||
|
||||
async fn update_app_id(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
app_id: &AppId,
|
||||
features: Dictionary,
|
||||
@@ -130,7 +130,7 @@ pub trait AppIdsApi {
|
||||
}
|
||||
|
||||
async fn delete_app_id(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
app_id: &AppId,
|
||||
device_type: impl Into<Option<DeveloperDeviceType>> + Send,
|
||||
@@ -149,7 +149,7 @@ pub trait AppIdsApi {
|
||||
}
|
||||
|
||||
async fn download_team_provisioning_profile(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
app_id: &AppId,
|
||||
device_type: impl Into<Option<DeveloperDeviceType>> + Send,
|
||||
@@ -173,8 +173,8 @@ pub trait AppIdsApi {
|
||||
}
|
||||
}
|
||||
|
||||
impl AppIdsApi for DeveloperSession<'_> {
|
||||
fn developer_session(&self) -> &DeveloperSession<'_> {
|
||||
impl AppIdsApi for DeveloperSession {
|
||||
fn developer_session(&mut self) -> &mut DeveloperSession {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,10 @@ impl std::fmt::Debug for DevelopmentCertificate {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait CertificatesApi {
|
||||
fn developer_session(&self) -> &DeveloperSession<'_>;
|
||||
fn developer_session(&mut self) -> &mut DeveloperSession;
|
||||
|
||||
async fn list_all_development_certs(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
device_type: impl Into<Option<DeveloperDeviceType>> + Send,
|
||||
) -> Result<Vec<DevelopmentCertificate>, Report> {
|
||||
@@ -72,7 +72,7 @@ pub trait CertificatesApi {
|
||||
}
|
||||
|
||||
async fn revoke_development_cert(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
serial_number: &str,
|
||||
device_type: impl Into<Option<DeveloperDeviceType>> + Send,
|
||||
@@ -94,7 +94,7 @@ pub trait CertificatesApi {
|
||||
}
|
||||
|
||||
async fn submit_development_csr(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
csr_content: String,
|
||||
machine_name: String,
|
||||
@@ -121,8 +121,8 @@ pub trait CertificatesApi {
|
||||
}
|
||||
}
|
||||
|
||||
impl CertificatesApi for DeveloperSession<'_> {
|
||||
fn developer_session(&self) -> &DeveloperSession<'_> {
|
||||
impl CertificatesApi for DeveloperSession {
|
||||
fn developer_session(&mut self) -> &mut DeveloperSession {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use plist::Dictionary;
|
||||
use plist_macro::{plist, plist_to_xml_string};
|
||||
@@ -8,7 +8,7 @@ use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
anisette::{AnisetteData, AnisetteProvider},
|
||||
anisette::AnisetteDataGenerator,
|
||||
auth::{
|
||||
apple_account::{AppToken, AppleAccount},
|
||||
grandslam::GrandSlam,
|
||||
@@ -23,29 +23,29 @@ pub use super::device_type::DeveloperDeviceType;
|
||||
pub use super::devices::*;
|
||||
pub use super::teams::*;
|
||||
|
||||
pub struct DeveloperSession<'a> {
|
||||
pub struct DeveloperSession {
|
||||
token: AppToken,
|
||||
adsid: String,
|
||||
client: &'a GrandSlam,
|
||||
anisette_provider: Arc<Mutex<dyn AnisetteProvider + Send>>,
|
||||
client: Arc<GrandSlam>,
|
||||
anisette_generator: AnisetteDataGenerator,
|
||||
}
|
||||
|
||||
impl<'a> DeveloperSession<'a> {
|
||||
impl DeveloperSession {
|
||||
pub fn new(
|
||||
token: AppToken,
|
||||
adsid: String,
|
||||
client: &'a GrandSlam,
|
||||
anisette_provider: Arc<Mutex<dyn AnisetteProvider + Send>>,
|
||||
client: Arc<GrandSlam>,
|
||||
anisette_generator: AnisetteDataGenerator,
|
||||
) -> Self {
|
||||
DeveloperSession {
|
||||
token,
|
||||
adsid,
|
||||
client,
|
||||
anisette_provider,
|
||||
anisette_generator,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_account(account: &'a mut AppleAccount) -> Result<Self, Report> {
|
||||
pub async fn from_account(account: &mut AppleAccount) -> Result<Self, Report> {
|
||||
let token = account
|
||||
.get_app_token("xcode.auth")
|
||||
.await
|
||||
@@ -59,13 +59,13 @@ impl<'a> DeveloperSession<'a> {
|
||||
Ok(DeveloperSession::new(
|
||||
token,
|
||||
spd.get_string("adsid")?,
|
||||
&account.grandslam_client,
|
||||
&account.anisette_data,
|
||||
account.grandslam_client.clone(),
|
||||
account.anisette_generator.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn send_dev_request_internal(
|
||||
&self,
|
||||
&mut self,
|
||||
url: &str,
|
||||
body: impl Into<Option<Dictionary>>,
|
||||
) -> Result<(Dictionary, Option<String>), Report> {
|
||||
@@ -86,7 +86,12 @@ impl<'a> DeveloperSession<'a> {
|
||||
.body(plist_to_xml_string(&body))
|
||||
.header("X-Apple-GS-Token", &self.token.token)
|
||||
.header("X-Apple-I-Identity-Id", &self.adsid)
|
||||
.headers(self.anisette_data.get_header_map())
|
||||
.headers(
|
||||
self.anisette_generator
|
||||
.get_anisette_data(self.client.clone())
|
||||
.await?
|
||||
.get_header_map(),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()
|
||||
@@ -133,7 +138,7 @@ impl<'a> DeveloperSession<'a> {
|
||||
}
|
||||
|
||||
pub async fn send_dev_request<T: DeserializeOwned>(
|
||||
&self,
|
||||
&mut self,
|
||||
url: &str,
|
||||
body: impl Into<Option<Dictionary>>,
|
||||
response_key: &str,
|
||||
@@ -152,7 +157,7 @@ impl<'a> DeveloperSession<'a> {
|
||||
}
|
||||
|
||||
pub async fn send_dev_request_no_response(
|
||||
&self,
|
||||
&mut self,
|
||||
url: &str,
|
||||
body: impl Into<Option<Dictionary>>,
|
||||
) -> Result<Dictionary, Report> {
|
||||
|
||||
@@ -18,10 +18,10 @@ pub struct DeveloperDevice {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait DevicesApi {
|
||||
fn developer_session(&self) -> &DeveloperSession<'_>;
|
||||
fn developer_session(&mut self) -> &mut DeveloperSession;
|
||||
|
||||
async fn list_devices(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
device_type: impl Into<Option<DeveloperDeviceType>> + Send,
|
||||
) -> Result<Vec<DeveloperDevice>, Report> {
|
||||
@@ -39,7 +39,7 @@ pub trait DevicesApi {
|
||||
}
|
||||
|
||||
async fn add_device(
|
||||
&self,
|
||||
&mut self,
|
||||
team: &DeveloperTeam,
|
||||
name: &str,
|
||||
udid: &str,
|
||||
@@ -61,8 +61,8 @@ pub trait DevicesApi {
|
||||
}
|
||||
}
|
||||
|
||||
impl DevicesApi for DeveloperSession<'_> {
|
||||
fn developer_session(&self) -> &DeveloperSession<'_> {
|
||||
impl DevicesApi for DeveloperSession {
|
||||
fn developer_session(&mut self) -> &mut DeveloperSession {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ pub struct DeveloperTeam {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait TeamsApi {
|
||||
fn developer_session(&self) -> &DeveloperSession<'_>;
|
||||
fn developer_session(&mut self) -> &mut DeveloperSession;
|
||||
|
||||
async fn list_teams(&self) -> Result<Vec<DeveloperTeam>, Report> {
|
||||
async fn list_teams(&mut self) -> Result<Vec<DeveloperTeam>, Report> {
|
||||
let response: Vec<DeveloperTeam> = self
|
||||
.developer_session()
|
||||
.send_dev_request(&dev_url("listTeams", Any), None, "teams")
|
||||
@@ -29,8 +29,8 @@ pub trait TeamsApi {
|
||||
}
|
||||
}
|
||||
|
||||
impl TeamsApi for DeveloperSession<'_> {
|
||||
fn developer_session(&self) -> &DeveloperSession<'_> {
|
||||
impl TeamsApi for DeveloperSession {
|
||||
fn developer_session(&mut self) -> &mut DeveloperSession {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ pub enum SideloadError {
|
||||
|
||||
#[error("Plist parse error: {0}")]
|
||||
PlistParseError(String),
|
||||
|
||||
#[error("Failed to get anisette data, anisette not provisioned")]
|
||||
AnisetteNotProvisioned,
|
||||
}
|
||||
|
||||
struct ReqwestErrorFormatter;
|
||||
|
||||
Reference in New Issue
Block a user