Add refreshing anisette & Rethink ownership for GrandSlam and anisette to prepare for concurrency

This commit is contained in:
nab138
2026-02-01 01:01:54 -05:00
parent 0073ab2568
commit 41e29caea8
16 changed files with 271 additions and 285 deletions

1
Cargo.lock generated
View File

@@ -824,6 +824,7 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio",
"tokio-tungstenite", "tokio-tungstenite",
"tracing", "tracing",
"uuid", "uuid",

View File

@@ -49,7 +49,7 @@ async fn main() {
let mut account = account.unwrap(); let mut account = account.unwrap();
let dev_session = DeveloperSession::from_account(&mut account) let mut dev_session = DeveloperSession::from_account(&mut account)
.await .await
.expect("Failed to create developer session"); .expect("Failed to create developer session");

View File

@@ -38,3 +38,4 @@ hmac = "0.12.1"
cbc = { version = "0.1.2", features = ["std"] } cbc = { version = "0.1.2", features = ["std"] }
aes = "0.8.4" aes = "0.8.4"
aes-gcm = "0.10.3" aes-gcm = "0.10.3"
tokio = "1.49.0"

View File

@@ -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");
}
}
}

View File

@@ -6,7 +6,8 @@ use plist_macro::plist;
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
use rootcause::prelude::*; use rootcause::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::{collections::HashMap, sync::Arc, time::SystemTime};
use tokio::sync::RwLock;
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct AnisetteClientInfo { pub struct AnisetteClientInfo {
@@ -22,6 +23,7 @@ pub struct AnisetteData {
_device_description: String, _device_description: String,
device_unique_identifier: String, device_unique_identifier: String,
_local_user_id: 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 // 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 cpd
} }
pub fn needs_refresh(&self) -> bool {
let elapsed = self.generated_at.elapsed().unwrap();
elapsed.as_secs() > 60
}
} }
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait AnisetteProvider { 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 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
}
} }

View File

@@ -2,6 +2,8 @@ mod state;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use std::time::SystemTime;
use base64::prelude::*; use base64::prelude::*;
use plist_macro::plist; use plist_macro::plist;
@@ -11,6 +13,7 @@ use serde::Deserialize;
use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::tungstenite::Message;
use tracing::{debug, info}; use tracing::{debug, info};
use crate::SideloadError;
use crate::anisette::remote_v3::state::AnisetteState; use crate::anisette::remote_v3::state::AnisetteState;
use crate::anisette::{AnisetteClientInfo, AnisetteData, AnisetteProvider}; use crate::anisette::{AnisetteClientInfo, AnisetteData, AnisetteProvider};
use crate::auth::grandslam::GrandSlam; use crate::auth::grandslam::GrandSlam;
@@ -74,13 +77,19 @@ impl Default for RemoteV3AnisetteProvider {
#[async_trait::async_trait] #[async_trait::async_trait]
impl AnisetteProvider for RemoteV3AnisetteProvider { impl AnisetteProvider for RemoteV3AnisetteProvider {
async fn get_anisette_data(&mut self, gs: &mut GrandSlam) -> Result<AnisetteData, Report> { async fn get_anisette_data(&self) -> Result<AnisetteData, Report> {
let state = self.get_state(gs).await?.clone(); let state = self
.state
.as_ref()
.ok_or(SideloadError::AnisetteNotProvisioned)?;
let adi_pb = state let adi_pb = state
.adi_pb .adi_pb
.as_ref() .as_ref()
.ok_or(report!("Anisette state is not provisioned"))?; .ok_or(SideloadError::AnisetteNotProvisioned)?;
let client_info = self.get_client_info().await?.client_info.clone(); let client_info = self
.client_info
.as_ref()
.ok_or(SideloadError::AnisetteNotProvisioned)?;
let headers = self let headers = self
.client .client
@@ -109,9 +118,10 @@ impl AnisetteProvider for RemoteV3AnisetteProvider {
machine_id, machine_id,
one_time_password, one_time_password,
routing_info, routing_info,
_device_description: client_info, _device_description: client_info.client_info.clone(),
device_unique_identifier: state.get_device_id(), device_unique_identifier: state.get_device_id(),
_local_user_id: hex::encode(&state.get_md_lu()), _local_user_id: hex::encode(&state.get_md_lu()),
generated_at: SystemTime::now(),
}; };
Ok(data) Ok(data)
@@ -140,10 +150,24 @@ impl AnisetteProvider for RemoteV3AnisetteProvider {
Ok(self.client_info.as_ref().unwrap().clone()) 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 { 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"); let state_path = self.config_path.join("state.plist");
fs::create_dir_all(&self.config_path)?; fs::create_dir_all(&self.config_path)?;
if self.state.is_none() { if self.state.is_none() {
@@ -195,13 +219,13 @@ impl RemoteV3AnisetteProvider {
} }
async fn provision( async fn provision(
state: &mut AnisetteState, state: &mut AnisetteState,
gs: &mut GrandSlam, gs: Arc<GrandSlam>,
url: &str, url: &str,
) -> Result<(), Report> { ) -> Result<(), Report> {
debug!("Starting provisioning"); debug!("Starting provisioning");
let start_provisioning = gs.get_url("midStartProvisioning").await?; let start_provisioning = gs.get_url("midStartProvisioning")?;
let end_provisioning = gs.get_url("midFinishProvisioning").await?; let end_provisioning = gs.get_url("midFinishProvisioning")?;
let websocket_url = format!("{}/v3/provisioning_session", url) let websocket_url = format!("{}/v3/provisioning_session", url)
.replace("https://", "wss://") .replace("https://", "wss://")

View File

@@ -1,7 +1,7 @@
use std::sync::{Arc, Mutex}; use std::sync::Arc;
use crate::{ use crate::{
anisette::{AnisetteData, AnisetteProvider}, anisette::{AnisetteData, AnisetteDataGenerator},
auth::{ auth::{
builder::AppleAccountBuilder, builder::AppleAccountBuilder,
grandslam::{GrandSlam, GrandSlamErrorChecker}, grandslam::{GrandSlam, GrandSlamErrorChecker},
@@ -30,9 +30,8 @@ use tracing::{debug, info, warn};
pub struct AppleAccount { pub struct AppleAccount {
pub email: String, pub email: String,
pub spd: Option<plist::Dictionary>, pub spd: Option<plist::Dictionary>,
pub anisette_provider: Arc<Mutex<dyn AnisetteProvider + Send>>, pub anisette_generator: AnisetteDataGenerator,
pub anisette_data: AnisetteData, pub grandslam_client: Arc<GrandSlam>,
pub grandslam_client: GrandSlam,
login_state: LoginState, login_state: LoginState,
debug: bool, debug: bool,
} }
@@ -64,7 +63,7 @@ impl AppleAccount {
/// - `debug`: DANGER, If true, accept invalid certificates and enable verbose connection /// - `debug`: DANGER, If true, accept invalid certificates and enable verbose connection
pub async fn new( pub async fn new(
email: &str, email: &str,
mut anisette_provider: Arc<Mutex<dyn AnisetteProvider + Send>>, anisette_generator: AnisetteDataGenerator,
debug: bool, debug: bool,
) -> Result<Self, Report> { ) -> Result<Self, Report> {
info!("Initializing apple account"); info!("Initializing apple account");
@@ -72,26 +71,18 @@ impl AppleAccount {
warn!("Debug mode enabled: this is a security risk!"); warn!("Debug mode enabled: this is a security risk!");
} }
let client_info = anisette_provider let client_info = anisette_generator
.lock()
.unwrap()
.get_client_info() .get_client_info()
.await .await
.context("Failed to get anisette client info")?; .context("Failed to get anisette client info")?;
let mut grandslam_client = GrandSlam::new(client_info, debug); let grandslam_client = GrandSlam::new(client_info, debug).await?;
let anisette_data = anisette_provider
.get_anisette_data(&mut grandslam_client)
.await
.context("Failed to get anisette data for login")?;
Ok(AppleAccount { Ok(AppleAccount {
email: email.to_string(), email: email.to_string(),
spd: None, spd: None,
anisette_provider, anisette_generator,
anisette_data, grandslam_client: Arc::new(grandslam_client),
grandslam_client,
debug, debug,
login_state: LoginState::NeedsLogin, login_state: LoginState::NeedsLogin,
}) })
@@ -197,16 +188,22 @@ impl AppleAccount {
two_factor_callback: impl Fn() -> Option<String>, two_factor_callback: impl Fn() -> Option<String>,
) -> Result<(), Report> { ) -> Result<(), Report> {
debug!("Trusted device 2FA required"); 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 let request_code_url = self
.grandslam_client .grandslam_client
.get_url("trustedDeviceSecondaryAuth") .get_url("trustedDeviceSecondaryAuth")?;
.await?;
let submit_code_url = self.grandslam_client.get_url("validateCode").await?; let submit_code_url = self.grandslam_client.get_url("validateCode")?;
self.grandslam_client self.grandslam_client
.get(&request_code_url)? .get(&request_code_url)?
.headers(self.build_2fa_headers().await?) .headers(self.build_2fa_headers(&anisette_data).await?)
.send() .send()
.await .await
.context("Failed to request trusted device 2fa")? .context("Failed to request trusted device 2fa")?
@@ -221,7 +218,7 @@ impl AppleAccount {
let res = self let res = self
.grandslam_client .grandslam_client
.get(&submit_code_url)? .get(&submit_code_url)?
.headers(self.build_2fa_headers().await?) .headers(self.build_2fa_headers(&anisette_data).await?)
.header("security-code", code) .header("security-code", code)
.send() .send()
.await .await
@@ -248,11 +245,17 @@ impl AppleAccount {
) -> Result<(), Report> { ) -> Result<(), Report> {
debug!("SMS 2FA required"); 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 self.grandslam_client
.get_sms(&request_code_url)? .get_sms(&request_code_url)?
.headers(self.build_2fa_headers().await?) .headers(self.build_2fa_headers(&anisette_data).await?)
.send() .send()
.await .await
.context("Failed to request SMS 2FA")? .context("Failed to request SMS 2FA")?
@@ -274,7 +277,7 @@ impl AppleAccount {
"mode": "sms" "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("Content-Type", HeaderValue::from_static("application/json"));
headers.insert( headers.insert(
"Accept", "Accept",
@@ -332,8 +335,8 @@ impl AppleAccount {
Ok(()) Ok(())
} }
async fn build_2fa_headers(&mut self) -> Result<HeaderMap, Report> { async fn build_2fa_headers(&self, anisette_data: &AnisetteData) -> Result<HeaderMap, Report> {
let mut headers = self.anisette_data.get_header_map(); let mut headers = anisette_data.get_header_map();
let spd = self let spd = self
.spd .spd
@@ -354,18 +357,23 @@ impl AppleAccount {
); );
headers.insert( headers.insert(
"X-Apple-I-MD-RINFO", "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) Ok(headers)
} }
async fn login_inner(&mut self, password: &str) -> Result<LoginState, Report> { 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); 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 srp_client = SrpClient::<Sha256>::new(&G_2048);
let a: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect(); let a: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
@@ -510,6 +518,12 @@ impl AppleAccount {
format!("com.apple.gs.{}", app) 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 let spd = self
.spd .spd
.as_ref() .as_ref()
@@ -531,8 +545,8 @@ impl AppleAccount {
.into_bytes() .into_bytes()
.to_vec(); .to_vec();
let gs_service_url = self.grandslam_client.get_url("gsService").await?; let gs_service_url = self.grandslam_client.get_url("gsService")?;
let cpd = self.anisette_data.get_client_provided_data(); let cpd = anisette_data.get_client_provided_data();
let request = plist!(dict { let request = plist!(dict {
"Header": { "Header": {

View File

@@ -1,14 +1,17 @@
use std::sync::Arc;
use rootcause::prelude::*; use rootcause::prelude::*;
use tokio::sync::RwLock;
use crate::{ use crate::{
anisette::{AnisetteProvider, remote_v3::RemoteV3AnisetteProvider}, anisette::{AnisetteDataGenerator, AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
auth::apple_account::AppleAccount, auth::apple_account::AppleAccount,
}; };
pub struct AppleAccountBuilder { pub struct AppleAccountBuilder {
email: String, email: String,
debug: Option<bool>, debug: Option<bool>,
anisette_provider: Option<Box<dyn AnisetteProvider>>, anisette_generator: Option<AnisetteDataGenerator>,
} }
impl AppleAccountBuilder { impl AppleAccountBuilder {
@@ -20,7 +23,7 @@ impl AppleAccountBuilder {
Self { Self {
email: email.to_string(), email: email.to_string(),
debug: None, debug: None,
anisette_provider: None, anisette_generator: None,
} }
} }
@@ -33,8 +36,13 @@ impl AppleAccountBuilder {
self self
} }
pub fn anisette_provider(mut self, anisette_provider: impl AnisetteProvider + 'static) -> Self { pub fn anisette_provider(
self.anisette_provider = Some(Box::new(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 self
} }
@@ -44,11 +52,11 @@ impl AppleAccountBuilder {
/// Returns an error if the reqwest client cannot be built /// Returns an error if the reqwest client cannot be built
pub async fn build(self) -> Result<AppleAccount, Report> { pub async fn build(self) -> Result<AppleAccount, Report> {
let debug = self.debug.unwrap_or(false); let debug = self.debug.unwrap_or(false);
let anisette_provider = self let anisette_generator = self.anisette_generator.unwrap_or_else(|| {
.anisette_provider AnisetteDataGenerator::new(Arc::new(RwLock::new(RemoteV3AnisetteProvider::default())))
.unwrap_or_else(|| Box::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 /// Build the AppleAccount and log in

View File

@@ -16,7 +16,7 @@ const URL_BAG: &str = "https://gsa.apple.com/grandslam/GsService2/lookup";
pub struct GrandSlam { pub struct GrandSlam {
pub client: reqwest::Client, pub client: reqwest::Client,
pub client_info: AnisetteClientInfo, pub client_info: AnisetteClientInfo,
pub url_bag: Option<Dictionary>, url_bag: Dictionary,
} }
impl GrandSlam { impl GrandSlam {
@@ -24,64 +24,75 @@ impl GrandSlam {
/// ///
/// # Arguments /// # Arguments
/// - `client`: The reqwest client to use for requests /// - `client`: The reqwest client to use for requests
pub fn new(client_info: AnisetteClientInfo, debug: bool) -> Self { pub async fn new(client_info: AnisetteClientInfo, debug: bool) -> Result<Self, Report> {
Self { let client = Self::build_reqwest_client(debug).unwrap();
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, client_info,
url_bag: None, url_bag,
} })
} }
/// Get the URL bag from GrandSlam /// Fetch the URL bag from GrandSlam and cache it
pub async fn get_url_bag(&mut self) -> Result<&Dictionary, Report> { pub async fn fetch_url_bag(
if self.url_bag.is_none() { client: &reqwest::Client,
debug!("Fetching URL bag from GrandSlam"); base_headers: HeaderMap,
let resp = self ) -> Result<Dictionary, Report> {
.client debug!("Fetching URL bag from GrandSlam");
.get(URL_BAG) let resp = client
.headers(self.base_headers(false)?) .get(URL_BAG)
.send() .headers(base_headers)
.await .send()
.context("Failed to fetch URL Bag")? .await
.text() .context("Failed to fetch URL Bag")?
.await .text()
.context("Failed to read URL Bag response text")?; .await
.context("Failed to read URL Bag response text")?;
let dict: Dictionary = let dict: Dictionary =
plist::from_bytes(resp.as_bytes()).context("Failed to parse URL Bag plist")?; plist::from_bytes(resp.as_bytes()).context("Failed to parse URL Bag plist")?;
let urls = dict let urls = dict
.get("urls") .get("urls")
.and_then(|v| v.as_dictionary()) .and_then(|v| v.as_dictionary())
.cloned() .cloned()
.ok_or_else(|| report!("URL Bag plist missing 'urls' dictionary"))?; .ok_or_else(|| report!("URL Bag plist missing 'urls' dictionary"))?;
self.url_bag = Some(urls); Ok(urls)
}
Ok(self.url_bag.as_ref().unwrap())
} }
pub async fn get_url(&mut self, key: &str) -> Result<String, Report> { pub fn get_url(&self, key: &str) -> Result<String, Report> {
let url_bag = self.get_url_bag().await?; let url = self
let url = url_bag .url_bag
.get_string(key) .get_string(key)
.context("Unable to find key in URL bag")?; .context("Unable to find key in URL bag")?;
Ok(url) Ok(url)
} }
pub fn get(&self, url: &str) -> Result<reqwest::RequestBuilder, Report> { 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) Ok(builder)
} }
pub fn get_sms(&self, url: &str) -> Result<reqwest::RequestBuilder, Report> { 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) Ok(builder)
} }
pub fn post(&self, url: &str) -> Result<reqwest::RequestBuilder, Report> { 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) Ok(builder)
} }
@@ -121,7 +132,10 @@ impl GrandSlam {
Ok(response_plist) 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(); let mut headers = reqwest::header::HeaderMap::new();
if !sms { if !sms {
headers.insert("Content-Type", HeaderValue::from_static("text/x-xml-plist")); headers.insert("Content-Type", HeaderValue::from_static("text/x-xml-plist"));
@@ -129,11 +143,11 @@ impl GrandSlam {
} }
headers.insert( headers.insert(
"X-Mme-Client-Info", "X-Mme-Client-Info",
HeaderValue::from_str(&self.client_info.client_info)?, HeaderValue::from_str(&client_info.client_info)?,
); );
headers.insert( headers.insert(
"User-Agent", "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("X-Xcode-Version", HeaderValue::from_static("14.2 (14C18)"));
headers.insert( headers.insert(

View File

@@ -18,10 +18,10 @@ pub struct AppGroup {
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait AppGroupsApi { pub trait AppGroupsApi {
fn developer_session(&self) -> &DeveloperSession<'_>; fn developer_session(&mut self) -> &mut DeveloperSession;
async fn list_app_groups( async fn list_app_groups(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
device_type: impl Into<Option<DeveloperDeviceType>> + Send, device_type: impl Into<Option<DeveloperDeviceType>> + Send,
) -> Result<Vec<AppGroup>, Report> { ) -> Result<Vec<AppGroup>, Report> {
@@ -43,7 +43,7 @@ pub trait AppGroupsApi {
} }
async fn add_app_group( async fn add_app_group(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
name: &str, name: &str,
identifier: &str, identifier: &str,
@@ -69,7 +69,7 @@ pub trait AppGroupsApi {
} }
async fn assign_app_group( async fn assign_app_group(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
app_group: &AppGroup, app_group: &AppGroup,
app_id: &AppId, app_id: &AppId,
@@ -93,8 +93,8 @@ pub trait AppGroupsApi {
} }
} }
impl AppGroupsApi for DeveloperSession<'_> { impl AppGroupsApi for DeveloperSession {
fn developer_session(&self) -> &DeveloperSession<'_> { fn developer_session(&mut self) -> &mut DeveloperSession {
self self
} }
} }

View File

@@ -52,10 +52,10 @@ pub struct Profile {
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait AppIdsApi { pub trait AppIdsApi {
fn developer_session(&self) -> &DeveloperSession<'_>; fn developer_session(&mut self) -> &mut DeveloperSession;
async fn add_app_id( async fn add_app_id(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
name: &str, name: &str,
identifier: &str, identifier: &str,
@@ -77,7 +77,7 @@ pub trait AppIdsApi {
} }
async fn list_app_ids( async fn list_app_ids(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
device_type: impl Into<Option<DeveloperDeviceType>> + Send, device_type: impl Into<Option<DeveloperDeviceType>> + Send,
) -> Result<ListAppIdsResponse, Report> { ) -> Result<ListAppIdsResponse, Report> {
@@ -107,7 +107,7 @@ pub trait AppIdsApi {
} }
async fn update_app_id( async fn update_app_id(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
app_id: &AppId, app_id: &AppId,
features: Dictionary, features: Dictionary,
@@ -130,7 +130,7 @@ pub trait AppIdsApi {
} }
async fn delete_app_id( async fn delete_app_id(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
app_id: &AppId, app_id: &AppId,
device_type: impl Into<Option<DeveloperDeviceType>> + Send, device_type: impl Into<Option<DeveloperDeviceType>> + Send,
@@ -149,7 +149,7 @@ pub trait AppIdsApi {
} }
async fn download_team_provisioning_profile( async fn download_team_provisioning_profile(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
app_id: &AppId, app_id: &AppId,
device_type: impl Into<Option<DeveloperDeviceType>> + Send, device_type: impl Into<Option<DeveloperDeviceType>> + Send,
@@ -173,8 +173,8 @@ pub trait AppIdsApi {
} }
} }
impl AppIdsApi for DeveloperSession<'_> { impl AppIdsApi for DeveloperSession {
fn developer_session(&self) -> &DeveloperSession<'_> { fn developer_session(&mut self) -> &mut DeveloperSession {
self self
} }
} }

View File

@@ -47,10 +47,10 @@ impl std::fmt::Debug for DevelopmentCertificate {
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait CertificatesApi { pub trait CertificatesApi {
fn developer_session(&self) -> &DeveloperSession<'_>; fn developer_session(&mut self) -> &mut DeveloperSession;
async fn list_all_development_certs( async fn list_all_development_certs(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
device_type: impl Into<Option<DeveloperDeviceType>> + Send, device_type: impl Into<Option<DeveloperDeviceType>> + Send,
) -> Result<Vec<DevelopmentCertificate>, Report> { ) -> Result<Vec<DevelopmentCertificate>, Report> {
@@ -72,7 +72,7 @@ pub trait CertificatesApi {
} }
async fn revoke_development_cert( async fn revoke_development_cert(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
serial_number: &str, serial_number: &str,
device_type: impl Into<Option<DeveloperDeviceType>> + Send, device_type: impl Into<Option<DeveloperDeviceType>> + Send,
@@ -94,7 +94,7 @@ pub trait CertificatesApi {
} }
async fn submit_development_csr( async fn submit_development_csr(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
csr_content: String, csr_content: String,
machine_name: String, machine_name: String,
@@ -121,8 +121,8 @@ pub trait CertificatesApi {
} }
} }
impl CertificatesApi for DeveloperSession<'_> { impl CertificatesApi for DeveloperSession {
fn developer_session(&self) -> &DeveloperSession<'_> { fn developer_session(&mut self) -> &mut DeveloperSession {
self self
} }
} }

View File

@@ -1,4 +1,4 @@
use std::sync::{Arc, Mutex}; use std::sync::Arc;
use plist::Dictionary; use plist::Dictionary;
use plist_macro::{plist, plist_to_xml_string}; use plist_macro::{plist, plist_to_xml_string};
@@ -8,7 +8,7 @@ use tracing::{error, warn};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
anisette::{AnisetteData, AnisetteProvider}, anisette::AnisetteDataGenerator,
auth::{ auth::{
apple_account::{AppToken, AppleAccount}, apple_account::{AppToken, AppleAccount},
grandslam::GrandSlam, grandslam::GrandSlam,
@@ -23,29 +23,29 @@ pub use super::device_type::DeveloperDeviceType;
pub use super::devices::*; pub use super::devices::*;
pub use super::teams::*; pub use super::teams::*;
pub struct DeveloperSession<'a> { pub struct DeveloperSession {
token: AppToken, token: AppToken,
adsid: String, adsid: String,
client: &'a GrandSlam, client: Arc<GrandSlam>,
anisette_provider: Arc<Mutex<dyn AnisetteProvider + Send>>, anisette_generator: AnisetteDataGenerator,
} }
impl<'a> DeveloperSession<'a> { impl DeveloperSession {
pub fn new( pub fn new(
token: AppToken, token: AppToken,
adsid: String, adsid: String,
client: &'a GrandSlam, client: Arc<GrandSlam>,
anisette_provider: Arc<Mutex<dyn AnisetteProvider + Send>>, anisette_generator: AnisetteDataGenerator,
) -> Self { ) -> Self {
DeveloperSession { DeveloperSession {
token, token,
adsid, adsid,
client, 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 let token = account
.get_app_token("xcode.auth") .get_app_token("xcode.auth")
.await .await
@@ -59,13 +59,13 @@ impl<'a> DeveloperSession<'a> {
Ok(DeveloperSession::new( Ok(DeveloperSession::new(
token, token,
spd.get_string("adsid")?, spd.get_string("adsid")?,
&account.grandslam_client, account.grandslam_client.clone(),
&account.anisette_data, account.anisette_generator.clone(),
)) ))
} }
async fn send_dev_request_internal( async fn send_dev_request_internal(
&self, &mut self,
url: &str, url: &str,
body: impl Into<Option<Dictionary>>, body: impl Into<Option<Dictionary>>,
) -> Result<(Dictionary, Option<String>), Report> { ) -> Result<(Dictionary, Option<String>), Report> {
@@ -86,7 +86,12 @@ impl<'a> DeveloperSession<'a> {
.body(plist_to_xml_string(&body)) .body(plist_to_xml_string(&body))
.header("X-Apple-GS-Token", &self.token.token) .header("X-Apple-GS-Token", &self.token.token)
.header("X-Apple-I-Identity-Id", &self.adsid) .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() .send()
.await? .await?
.error_for_status() .error_for_status()
@@ -133,7 +138,7 @@ impl<'a> DeveloperSession<'a> {
} }
pub async fn send_dev_request<T: DeserializeOwned>( pub async fn send_dev_request<T: DeserializeOwned>(
&self, &mut self,
url: &str, url: &str,
body: impl Into<Option<Dictionary>>, body: impl Into<Option<Dictionary>>,
response_key: &str, response_key: &str,
@@ -152,7 +157,7 @@ impl<'a> DeveloperSession<'a> {
} }
pub async fn send_dev_request_no_response( pub async fn send_dev_request_no_response(
&self, &mut self,
url: &str, url: &str,
body: impl Into<Option<Dictionary>>, body: impl Into<Option<Dictionary>>,
) -> Result<Dictionary, Report> { ) -> Result<Dictionary, Report> {

View File

@@ -18,10 +18,10 @@ pub struct DeveloperDevice {
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait DevicesApi { pub trait DevicesApi {
fn developer_session(&self) -> &DeveloperSession<'_>; fn developer_session(&mut self) -> &mut DeveloperSession;
async fn list_devices( async fn list_devices(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
device_type: impl Into<Option<DeveloperDeviceType>> + Send, device_type: impl Into<Option<DeveloperDeviceType>> + Send,
) -> Result<Vec<DeveloperDevice>, Report> { ) -> Result<Vec<DeveloperDevice>, Report> {
@@ -39,7 +39,7 @@ pub trait DevicesApi {
} }
async fn add_device( async fn add_device(
&self, &mut self,
team: &DeveloperTeam, team: &DeveloperTeam,
name: &str, name: &str,
udid: &str, udid: &str,
@@ -61,8 +61,8 @@ pub trait DevicesApi {
} }
} }
impl DevicesApi for DeveloperSession<'_> { impl DevicesApi for DeveloperSession {
fn developer_session(&self) -> &DeveloperSession<'_> { fn developer_session(&mut self) -> &mut DeveloperSession {
self self
} }
} }

View File

@@ -16,9 +16,9 @@ pub struct DeveloperTeam {
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait TeamsApi { 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 let response: Vec<DeveloperTeam> = self
.developer_session() .developer_session()
.send_dev_request(&dev_url("listTeams", Any), None, "teams") .send_dev_request(&dev_url("listTeams", Any), None, "teams")
@@ -29,8 +29,8 @@ pub trait TeamsApi {
} }
} }
impl TeamsApi for DeveloperSession<'_> { impl TeamsApi for DeveloperSession {
fn developer_session(&self) -> &DeveloperSession<'_> { fn developer_session(&mut self) -> &mut DeveloperSession {
self self
} }
} }

View File

@@ -15,6 +15,9 @@ pub enum SideloadError {
#[error("Plist parse error: {0}")] #[error("Plist parse error: {0}")]
PlistParseError(String), PlistParseError(String),
#[error("Failed to get anisette data, anisette not provisioned")]
AnisetteNotProvisioned,
} }
struct ReqwestErrorFormatter; struct ReqwestErrorFormatter;