Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
grandslam
src
http_session.rs
Refactor and unify a lot of things for another project and implement airdrop support
Dadoum
commited
a74924e
at 2026-10-25 22:15:58
http_session.rs
Blame
History
Raw
use crate::bundle_information::{AUTH_KIT_BUNDLE_INFORMATION, BundleInformation}; use crate::device::Device; use adi::proxy::{ADIProxy, ADIResult}; use base64::Engine; use base64::prelude::BASE64_STANDARD; use chrono::{Local, SecondsFormat}; use plist::{Dictionary, Value}; use reqwest::header::{HeaderMap, InvalidHeaderValue}; use reqwest::{Certificate, Client, Method, RequestBuilder}; use thiserror::Error; const SIMULATED_AUTHENTICATION_FRAMEWORK_INFORMATION: BundleInformation = AUTH_KIT_BUNDLE_INFORMATION; /// Error returned by Apple servers' endpoints #[derive(Debug, Error)] #[error("{message} ({code})")] pub struct AppleError { // description: String, pub code: i64, pub subcode: Option<i64>, pub message: String, } pub type URLBag = Dictionary; #[derive(Debug, Error)] pub enum URLBagError { #[error("The provided URL bag is invalid")] InvalidUrlBag, #[error("Unable to parse the provided URL bag")] Parsing, } /// Parse the Status field of an Apple server Response. /// Returns the AppleError if there is one reported. pub fn parse_status(status_dict: &Dictionary) -> Result<(), AppleError> { // Some responses have no status code at all, and errors don't always come with a message. match status_dict.get("ec").and_then(Value::as_signed_integer) { None | Some(0) => Ok(()), Some(code) => Err(AppleError { code, subcode: status_dict.get("sec").and_then(Value::as_signed_integer), message: status_dict .get("em") .and_then(Value::as_string) .unwrap_or("Unknown error") .to_string(), }), } } /// Abstracts a session with Apple servers. #[derive(Clone)] pub struct BasicHTTPSession<'lt> { pub client: Client, pub device: Device, pub application_info: BundleInformation<'lt>, } #[derive(Clone)] pub struct HTTPSession<'lt> { pub http_session: BasicHTTPSession<'lt>, pub url_bag: URLBag, } #[derive(Debug, Error)] pub enum HTTPSessionCreationError { #[error("Cannot build the client identification headers: {0}")] InvalidHeader(#[from] InvalidHeaderValue), #[error("Network error: {0}")] Reqwest(#[from] reqwest::Error), #[error("Cannot parse the URL bag: {0}")] InvalidURLBagResponsePlist(URLBagError), #[error("Invalid URL bag response")] InvalidURLBagResponseStructure, } impl<'lt> BasicHTTPSession<'lt> { pub fn new( device: Device, client_bundle_info: BundleInformation<'lt>, custom_certificate: Option<Certificate>, ) -> Result<Self, HTTPSessionCreationError> { let client_builder = Client::builder(); let client_builder = match custom_certificate { Some(certificate) => client_builder.add_root_certificate(certificate), None => client_builder, }; let headers = { let mut headers = HeaderMap::new(); headers.insert( "User-Agent", format!( "{}/{}", client_bundle_info.bundle_name, client_bundle_info.bundle_version ) .parse()?, ); headers.insert( "X-MMe-Client-Info", device .server_friendly_description( &SIMULATED_AUTHENTICATION_FRAMEWORK_INFORMATION, &client_bundle_info, ) .parse()?, ); headers.insert("X-Mme-Device-Id", device.device_uuid.parse()?); headers.insert( "X-Apple-Client-App-Name", client_bundle_info.bundle_name.parse()?, ); headers }; let client = client_builder .redirect(reqwest::redirect::Policy::none()) .default_headers(headers) .connection_verbose(false) .build()?; Ok(BasicHTTPSession { client, device, application_info: client_bundle_info, }) } pub fn device(&self) -> &Device { &self.device } pub fn application_information(&self) -> &BundleInformation<'_> { &self.application_info } pub fn request_builder(&self, method: Method, url: &str) -> RequestBuilder { let client_time = Local::now() .to_utc() .to_rfc3339_opts(SecondsFormat::Secs, true); self.client .request(method, url) .header("X-Apple-I-Client-Time", client_time) } } impl<'lt> HTTPSession<'lt> { pub fn new( http_session: BasicHTTPSession<'lt>, url_bag: URLBag, ) -> Result<Self, HTTPSessionCreationError> { Ok(Self { http_session, url_bag, }) } pub fn url_switch(&mut self, url_bag: URLBag) { self.url_bag = url_bag; } pub fn device(&self) -> &Device { self.http_session.device() } pub fn application_information(&self) -> &BundleInformation<'_> { self.http_session.application_information() } pub fn url_bag(&self) -> &URLBag { &self.url_bag } pub fn request_builder(&self, method: Method, url: &str) -> RequestBuilder { self.http_session.request_builder(method, url) } } #[derive(Clone)] pub struct AnisetteHTTPSession<'lt, 'adi> { pub http_session: HTTPSession<'lt>, pub adi_proxy: &'adi dyn ADIProxy, } impl<'lt, 'adi> AnisetteHTTPSession<'lt, 'adi> { pub fn new(http_session: HTTPSession<'lt>, adi_proxy: &'adi dyn ADIProxy) -> Self { Self { http_session, adi_proxy, } } pub fn url_bag(&self) -> &URLBag { self.http_session.url_bag() } pub fn simple_request_builder(&self, method: Method, url: &str) -> RequestBuilder { self.http_session.request_builder(method, url) } pub fn anisette_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> { let mut request_builder = self.simple_request_builder(method, url); if self.adi_proxy.is_machine_provisioned(-2)? { let ds_id = -2; let (mid_b, otp_b) = self.adi_proxy.request_otp(ds_id)?; let mid = BASE64_STANDARD.encode(mid_b); let otp = BASE64_STANDARD.encode(otp_b); let rinfo = self.adi_proxy.get_idms_routing(ds_id)?; request_builder = request_builder .header("X-Apple-I-MD", otp) .header("X-Apple-I-MD-M", mid) .header("X-Apple-I-MD-RINFO", rinfo); } if self.adi_proxy.is_machine_provisioned(-1)? { let ds_id = -1; let (mid_b, otp_b) = self.adi_proxy.request_otp(ds_id)?; let mid = BASE64_STANDARD.encode(mid_b); let otp = BASE64_STANDARD.encode(otp_b); request_builder = request_builder .header("X-Apple-I-AMD", otp) .header("X-Apple-I-AMD-M", mid); } Ok(request_builder) } pub fn device(&self) -> &Device { self.http_session.device() } pub fn application_information(&self) -> &BundleInformation<'_> { self.http_session.application_information() } pub fn adi_proxy(&self) -> &dyn ADIProxy { self.adi_proxy } pub fn url_switch(&mut self, url_bag: URLBag) { self.http_session.url_switch(url_bag) } } #[cfg(test)] mod tests { use super::*; use plist_macros::dict; #[test] fn status_codes_report_apple_errors() { assert!(parse_status(&Dictionary::new()).is_ok()); assert!(parse_status(&dict! { "ec": 0, "em": "" }).is_ok()); let error = parse_status(&dict! { "ec": -22406, "em": "Wrong password", "sec": 2 }).unwrap_err(); assert_eq!( (error.code, error.subcode, error.message.as_str()), (-22406, Some(2), "Wrong password") ); // Errors without a message are still errors. let error = parse_status(&dict! { "ec": -1 }).unwrap_err(); assert_eq!(error.message, "Unknown error"); } }