Anisette provisioning works

This commit is contained in:
nab138
2026-01-25 00:33:03 -05:00
parent 4e50c5c1d4
commit 1f61c10731
14 changed files with 1021 additions and 462 deletions

View File

@@ -16,10 +16,20 @@ install = ["dep:idevice"]
[dependencies]
idevice = { version = "0.1.51", optional = true }
plist = "1.8.0"
plist-macro = "0.1.0"
log = "0.4"
plist = "1.8"
plist-macro = "0.1.3"
reqwest = { version = "0.13.1", features = ["json", "gzip"] }
thiserror = "2.0.17"
thiserror-context = "0.1.2"
chrono = "0.4.43"
async-trait = "0.1.89"
serde = "1.0.228"
rand = "0.9.2"
uuid = "1.19.0"
sha2 = "0.10.9"
tracing = "0.1.44"
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots"] }
rootcause = "0.11.1"
futures-util = "0.3.31"
serde_json = "1.0.149"
base64 = "0.22.1"
hex = "0.4.3"

View File

@@ -1,3 +1,22 @@
pub mod remote_v3;
pub trait AnisetteProvider {}
use crate::auth::grandslam::GrandSlam;
use rootcause::prelude::*;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize, Debug, Clone)]
pub struct AnisetteClientInfo {
pub client_info: String,
pub user_agent: String,
}
#[async_trait::async_trait]
pub trait AnisetteProvider {
async fn get_anisette_headers(
&mut self,
gs: &mut GrandSlam,
) -> Result<HashMap<String, String>, Report>;
async fn get_client_info(&mut self) -> Result<AnisetteClientInfo, Report>;
}

View File

@@ -1,92 +0,0 @@
use std::path::PathBuf;
use chrono::{DateTime, SubsecRound, Utc};
use reqwest::header::{HeaderMap, HeaderValue};
use crate::SideloadResult as Result;
use crate::anisette::AnisetteProvider;
pub const DEFAULT_ANISETTE_V3_URL: &str = "https://ani.sidestore.io";
pub struct RemoteV3AnisetteProvider {
url: String,
config_path: PathBuf,
serial_number: String,
}
impl RemoteV3AnisetteProvider {
/// Create a new RemoteV3AnisetteProvider with the given URL and config path
///
/// # Arguments
/// - `url`: The URL of the remote anisette service
/// - `config_path`: The path to the config file
/// - `serial_number`: The serial number of the device
pub fn new(url: &str, config_path: PathBuf, serial_number: String) -> Self {
Self {
url: url.to_string(),
config_path,
serial_number,
}
}
pub fn set_url(mut self, url: &str) -> RemoteV3AnisetteProvider {
self.url = url.to_string();
self
}
pub fn set_config_path(mut self, config_path: PathBuf) -> RemoteV3AnisetteProvider {
self.config_path = config_path;
self
}
pub fn set_serial_number(mut self, serial_number: String) -> RemoteV3AnisetteProvider {
self.serial_number = serial_number;
self
}
}
impl Default for RemoteV3AnisetteProvider {
fn default() -> Self {
Self::new(DEFAULT_ANISETTE_V3_URL, PathBuf::new(), "0".to_string())
}
}
#[derive(Debug)]
pub struct AnisetteData {
machine_id: String,
one_time_password: String,
routing_info: String,
device_description: String,
device_unique_identifier: String,
local_user_id: String,
}
impl AnisetteData {
pub fn get_headers(&self, serial: String) -> Result<HeaderMap> {
let dt: DateTime<Utc> = Utc::now().round_subsecs(0);
let mut headers = HeaderMap::new();
for (key, value) in vec![
(
"X-Apple-I-Client-Time",
dt.format("%+").to_string().replace("+00:00", "Z"),
),
("X-Apple-I-SRL-NO", serial),
("X-Apple-I-TimeZone", "UTC".to_string()),
("X-Apple-Locale", "en_US".to_string()),
("X-Apple-I-MD-RINFO", self.routing_info.clone()),
("X-Apple-I-MD-LU", self.local_user_id.clone()),
("X-Mme-Device-Id", self.device_unique_identifier.clone()),
("X-Apple-I-MD", self.one_time_password.clone()),
("X-Apple-I-MD-M", self.machine_id.clone()),
("X-Mme-Client-Info", self.device_description.clone()),
] {
headers.insert(key, HeaderValue::from_str(&value)?);
}
Ok(headers)
}
}
impl AnisetteProvider for RemoteV3AnisetteProvider {}

View File

@@ -0,0 +1,393 @@
mod state;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use base64::prelude::*;
use chrono::{DateTime, SubsecRound, Utc};
use plist_macro::plist;
use reqwest::header::{HeaderMap, HeaderValue};
use rootcause::prelude::*;
use serde::Deserialize;
use tokio_tungstenite::tungstenite::Message;
use tracing::{debug, info};
use crate::anisette::remote_v3::state::AnisetteState;
use crate::anisette::{AnisetteClientInfo, AnisetteProvider};
use crate::auth::grandslam::GrandSlam;
use crate::util::plist::plist_to_xml_string;
use futures_util::{SinkExt, StreamExt};
pub const DEFAULT_ANISETTE_V3_URL: &str = "https://ani.sidestore.io";
#[derive(Debug)]
pub struct AnisetteData {
machine_id: String,
one_time_password: String,
routing_info: String,
device_description: String,
device_unique_identifier: String,
local_user_id: String,
}
impl AnisetteData {
pub fn get_headers(&self, serial: String) -> 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 struct RemoteV3AnisetteProvider {
pub state: Option<AnisetteState>,
url: String,
config_path: PathBuf,
serial_number: String,
client_info: Option<AnisetteClientInfo>,
client: reqwest::Client,
}
impl RemoteV3AnisetteProvider {
/// Create a new RemoteV3AnisetteProvider with the given URL and config path
///
/// # Arguments
/// - `url`: The URL of the remote anisette service
/// - `config_path`: The path to the config file
/// - `serial_number`: The serial number of the device
///
pub fn new(url: &str, config_path: PathBuf, serial_number: String) -> Self {
Self {
state: None,
url: url.to_string(),
config_path,
serial_number,
client_info: None,
client: reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()
.unwrap(),
}
}
pub fn set_url(mut self, url: &str) -> RemoteV3AnisetteProvider {
self.url = url.to_string();
self
}
pub fn set_config_path(mut self, config_path: PathBuf) -> RemoteV3AnisetteProvider {
self.config_path = config_path;
self
}
pub fn set_serial_number(mut self, serial_number: String) -> RemoteV3AnisetteProvider {
self.serial_number = serial_number;
self
}
}
impl Default for RemoteV3AnisetteProvider {
fn default() -> Self {
Self::new(DEFAULT_ANISETTE_V3_URL, PathBuf::new(), "0".to_string())
}
}
#[async_trait::async_trait]
impl AnisetteProvider for RemoteV3AnisetteProvider {
async fn get_anisette_headers(
&mut self,
gs: &mut GrandSlam,
) -> Result<HashMap<String, String>, Report> {
let state = self.get_state(gs).await?;
unimplemented!()
}
async fn get_client_info(&mut self) -> Result<AnisetteClientInfo, Report> {
self.ensure_client_info().await?;
Ok(self.client_info.as_ref().unwrap().clone())
}
}
impl RemoteV3AnisetteProvider {
async fn ensure_client_info(&mut self) -> Result<(), Report> {
if self.client_info.is_none() {
let resp = self
.client
.get(format!("{}/v3/client_info", self.url))
.send()
.await?
.error_for_status()?
.json::<AnisetteClientInfo>()
.await?;
self.client_info = Some(resp);
}
debug!("Got client client_info: {:?}", self.client_info);
Ok(())
}
async fn get_state(&mut self, gs: &mut 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() {
if let Ok(state) = plist::from_file(&state_path) {
debug!("Loaded existing anisette state from {:?}", state_path);
self.state = Some(state);
} else {
debug!("No existing anisette state found");
self.state = Some(AnisetteState::new());
}
}
let state = self.state.as_mut().unwrap();
if !state.is_provisioned() {
info!("Provisioning required...");
Self::provision(state, gs, &self.url)
.await
.context("Failed to provision")?;
}
plist::to_file_xml(&state_path, &state)?;
Ok(state)
}
async fn provisioning_headers(
state: &AnisetteState,
gs: &mut GrandSlam,
) -> Result<HeaderMap, Report> {
let mut headers = HeaderMap::new();
headers.insert(
"X-Apple-I-MD-LU",
HeaderValue::from_str(&hex::encode(state.get_md_lu()))?,
);
headers.insert(
"X-Apple-I-Client-Time",
HeaderValue::from_str(
&Utc::now()
.round_subsecs(0)
.format("%+")
.to_string()
.replace("+00:00", "Z"),
)?,
);
headers.insert("X-Apple-I-TimeZone", HeaderValue::from_static("UTC"));
headers.insert("X-Apple-Locale", HeaderValue::from_static("en_US"));
headers.insert(
"X-Mme-Device-Id",
HeaderValue::from_str(&state.get_device_id())?,
);
Ok(headers)
}
async fn provision(
state: &mut AnisetteState,
gs: &mut GrandSlam,
url: &str,
) -> Result<(), Report> {
info!("Starting provisioning");
let urls = gs.get_url_bag().await?;
let start_provisioning = urls
.get("midStartProvisioning")
.and_then(|v| v.as_string())
.ok_or(report!("Missing URL bag entry for midStartProvisioning"))?
.to_string();
let end_provisioning = urls
.get("midFinishProvisioning")
.and_then(|v| v.as_string())
.ok_or(report!("Missing URL bag entry for midFinishProvisioning"))?
.to_string();
let websocket_url = format!("{}/v3/provisioning_session", url)
.replace("https://", "wss://")
.replace("http://", "ws://");
let (mut ws_stream, _) = tokio_tungstenite::connect_async(&websocket_url)
.await
.context("Failed to connect anisette provisioning socket")?;
loop {
let Some(msg) = ws_stream.next().await else {
continue;
};
let msg = msg.context("Failed to read anisette provisioning socket message")?;
if msg.is_close() {
bail!("Anisette provisioning socket closed unexpectedly");
}
let msg = msg
.into_text()
.context("Failed to parse provisioning message")?;
debug!("Received provisioning message: {}", msg);
let provision_msg: ProvisioningMessage =
serde_json::from_str(&msg).context("Unknown provisioning message")?;
match provision_msg {
ProvisioningMessage::GiveIdentifier => {
ws_stream
.send(Message::Text(
serde_json::json!({
"identifier": BASE64_STANDARD.encode(&state.keychain_identifier),
})
.to_string()
.into(),
))
.await
.context("Failed to send identifier")?;
}
ProvisioningMessage::GiveStartProvisioningData => {
let body = plist!(dict {
"Header": {},
"Request": {}
});
let resp = gs
.post(&start_provisioning)?
.headers(Self::provisioning_headers(state, gs).await?)
.body(plist_to_xml_string(&body))
.send()
.await
.context("Failed to send start provisioning request")?
.error_for_status()
.context("Start provisioning request returned error")?
.text()
.await
.context("Failed to read start provisioning response text")?;
let resp_plist: plist::Dictionary = plist::from_bytes(resp.as_bytes())
.context("Failed to parse start provisioning response plist")?;
let spim = resp_plist
.get("Response")
.and_then(|v| v.as_dictionary())
.and_then(|d| d.get("spim"))
.and_then(|v| v.as_string())
.ok_or(report!("Start provisioning response missing spim"))?;
ws_stream
.send(Message::Text(
serde_json::json!({
"spim": spim,
})
.to_string()
.into(),
))
.await
.context("Failed to send start provisioning data")?;
}
ProvisioningMessage::GiveEndProvisioningData { cpim } => {
let body = plist!(dict {
"Header": {},
"Request": {
"cpim": cpim,
}
});
let resp = gs
.post(&end_provisioning)?
.headers(Self::provisioning_headers(state, gs).await?)
.body(plist_to_xml_string(&body))
.send()
.await
.context("Failed to send end provisioning request")?
.error_for_status()
.context("End provisioning request returned error")?
.text()
.await
.context("Failed to read end provisioning response text")?;
let resp_plist: plist::Dictionary = plist::from_bytes(resp.as_bytes())
.context("Failed to parse end provisioning response plist")?;
let response = resp_plist
.get("Response")
.and_then(|v| v.as_dictionary())
.ok_or(report!(
"End provisioning response missing Response dictionary"
))?;
ws_stream
.send(Message::Text(
serde_json::json!({
"ptm": response
.get("ptm")
.and_then(|v| v.as_string())
.ok_or(report!("End provisioning response missing ptm"))?,
"tk": response
.get("tk")
.and_then(|v| v.as_string())
.ok_or(report!("End provisioning response missing tk"))?,
})
.to_string()
.into(),
))
.await
.context("Failed to send start provisioning data")?;
}
ProvisioningMessage::ProvisioningSuccess { adi_pb } => {
state.adi_pb = Some(BASE64_STANDARD.decode(adi_pb)?);
ws_stream.close(None).await?;
info!("Provisioning successful");
break;
}
ProvisioningMessage::Timeout => bail!("Anisette provisioning timed out"),
ProvisioningMessage::InvalidIdentifier => {
bail!("Anisette provisioning failed: invalid identifier")
}
ProvisioningMessage::StartProvisioningError { message } => {
return Err(
report!("Anisette provisioning failed: start provisioning error")
.attach(message)
.into(),
);
}
ProvisioningMessage::EndProvisioningError { message } => {
return Err(
report!("Anisette provisioning failed: end provisioning error")
.attach(message)
.into(),
);
}
}
}
Ok(())
}
}
#[derive(Deserialize)]
#[serde(tag = "result")]
enum ProvisioningMessage {
GiveIdentifier,
GiveStartProvisioningData,
GiveEndProvisioningData { cpim: String },
ProvisioningSuccess { adi_pb: String },
Timeout,
InvalidIdentifier,
StartProvisioningError { message: String },
EndProvisioningError { message: String },
}

View File

@@ -0,0 +1,81 @@
// Serialization/Desieralization borrowed from https://github.com/SideStore/apple-private-apis/blob/master/omnisette/src/remote_anisette_v3.rs
use plist::Data;
use rand::Rng;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sha2::{Digest, Sha256};
use uuid::Uuid;
fn bin_serialize<S>(x: &[u8], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_bytes(x)
}
fn bin_serialize_opt<S>(x: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
x.clone().map(|i| Data::new(i)).serialize(s)
}
fn bin_deserialize_opt<'de, D>(d: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<Data> = Deserialize::deserialize(d)?;
Ok(s.map(|i| i.into()))
}
fn bin_deserialize_16<'de, D>(d: D) -> Result<[u8; 16], D::Error>
where
D: Deserializer<'de>,
{
let s: Data = Deserialize::deserialize(d)?;
let s: Vec<u8> = s.into();
Ok(s.try_into().unwrap())
}
#[derive(Serialize, Deserialize)]
pub struct AnisetteState {
#[serde(
serialize_with = "bin_serialize",
deserialize_with = "bin_deserialize_16"
)]
pub keychain_identifier: [u8; 16],
#[serde(
serialize_with = "bin_serialize_opt",
deserialize_with = "bin_deserialize_opt"
)]
pub adi_pb: Option<Vec<u8>>,
}
impl Default for AnisetteState {
fn default() -> Self {
AnisetteState {
keychain_identifier: rand::rng().random::<[u8; 16]>(),
adi_pb: None,
}
}
}
impl AnisetteState {
pub fn new() -> AnisetteState {
AnisetteState::default()
}
pub fn is_provisioned(&self) -> bool {
self.adi_pb.is_some()
}
pub fn get_md_lu(&self) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(&self.keychain_identifier);
hasher.finalize().into()
}
pub fn get_device_id(&self) -> String {
Uuid::from_bytes(self.keychain_identifier).to_string()
}
}

View File

@@ -1,19 +1,15 @@
use crate::{
SideloadResult as Result,
anisette::{AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
auth::grandslam::GrandSlam,
};
use log::{info, warn};
use reqwest::{Certificate, ClientBuilder};
use thiserror_context::Context;
const APPLE_ROOT: &[u8] = include_bytes!("./apple_root.der");
use rootcause::prelude::*;
use tracing::{info, warn};
pub struct AppleAccount {
pub email: String,
pub spd: Option<plist::Dictionary>,
pub client: reqwest::Client,
pub anisette: Box<dyn AnisetteProvider>,
pub grandslam_client: GrandSlam,
}
pub struct AppleAccountBuilder {
@@ -56,16 +52,20 @@ impl AppleAccountBuilder {
/// - `two_factor_callback`: A callback function that returns the two-factor authentication code
/// # Errors
/// Returns an error if the reqwest client cannot be built
pub async fn login<F>(self, _password: &str, _two_factor_callback: F) -> Result<AppleAccount>
pub async fn login<F>(
self,
password: &str,
two_factor_callback: F,
) -> Result<AppleAccount, Report>
where
F: Fn() -> Result<String>,
F: Fn() -> Option<String>,
{
let debug = self.debug.unwrap_or(false);
let anisette = self
.anisette
.unwrap_or_else(|| Box::new(RemoteV3AnisetteProvider::default()));
AppleAccount::login(&self.email, debug, anisette).await
AppleAccount::login(&self.email, password, two_factor_callback, anisette, debug).await
}
}
@@ -83,43 +83,36 @@ impl AppleAccount {
/// Reccomended to use the AppleAccountBuilder instead
pub async fn login(
email: &str,
password: &str,
two_factor_callback: impl Fn() -> Option<String>,
mut anisette: Box<dyn AnisetteProvider>,
debug: bool,
anisette: Box<dyn AnisetteProvider>,
) -> Result<Self> {
) -> Result<Self, Report> {
info!("Logging in to apple ID: {}", email);
if debug {
warn!("Debug mode enabled: this is a security risk!");
}
let client = Self::build_client(debug)?;
let mut gs = GrandSlam::new(&client);
gs.get_url_bag()
let client_info = anisette
.get_client_info()
.await
.context("Failed to get URL bag from GrandSlam")?;
.context("Failed to get anisette client info")?;
let mut grandslam_client = GrandSlam::new(client_info, debug);
let url_bag = grandslam_client
.get_url_bag()
.await
.context("Failed to get URL bag for login")?;
let headers = anisette
.get_anisette_headers(&mut grandslam_client)
.await
.context("Failed to get anisette headers for login")?;
Ok(AppleAccount {
email: email.to_string(),
spd: None,
client,
anisette,
grandslam_client,
})
}
/// Build a reqwest client with the Apple root certificate
///
/// # Arguments
/// - `debug`: DANGER, If true, accept invalid certificates and enable verbose connection logging
/// # Errors
/// Returns an error if the reqwest client cannot be built
pub fn build_client(debug: bool) -> Result<reqwest::Client> {
let cert = Certificate::from_der(APPLE_ROOT)?;
let client = ClientBuilder::new()
.add_root_certificate(cert)
.http1_title_case_headers()
.danger_accept_invalid_certs(debug)
.connection_verbose(debug)
.build()?;
Ok(client)
}
}

View File

@@ -1,64 +1,102 @@
use crate::SideloadResult as Result;
use log::debug;
use plist::Dictionary;
use plist_macro::pretty_print_dictionary;
use reqwest::header::HeaderValue;
use reqwest::{Certificate, ClientBuilder, header::HeaderValue};
use rootcause::prelude::*;
use tracing::debug;
use crate::anisette::AnisetteClientInfo;
const APPLE_ROOT: &[u8] = include_bytes!("./apple_root.der");
const URL_BAG: &str = "https://gsa.apple.com/grandslam/GsService2/lookup";
pub struct GrandSlam<'a> {
client: &'a reqwest::Client,
url_bag: Option<Dictionary>,
pub struct GrandSlam {
pub client: reqwest::Client,
pub client_info: AnisetteClientInfo,
pub url_bag: Option<Dictionary>,
}
impl<'a> GrandSlam<'a> {
impl GrandSlam {
/// Create a new GrandSlam instance
///
/// # Arguments
/// - `client`: The reqwest client to use for requests
pub fn new(client: &'a reqwest::Client) -> Self {
pub fn new(client_info: AnisetteClientInfo, debug: bool) -> Self {
Self {
client,
client: Self::build_reqwest_client(debug).unwrap(),
client_info,
url_bag: None,
}
}
/// Get the URL bag from GrandSlam
pub async fn get_url_bag(&mut self) -> Result<&Dictionary> {
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())
.headers(self.base_headers()?)
.send()
.await?
.await
.context("Failed to fetch URL Bag")?
.text()
.await?;
let dict: Dictionary = plist::from_bytes(resp.as_bytes())?;
debug!("{}", pretty_print_dictionary(&dict));
self.url_bag = Some(dict);
.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"))?;
self.url_bag = Some(urls);
}
Ok(self.url_bag.as_ref().unwrap())
}
fn base_headers() -> reqwest::header::HeaderMap {
pub fn post(&self, url: &str) -> Result<reqwest::RequestBuilder, Report> {
let builder = self.client.post(url).headers(self.base_headers()?);
Ok(builder)
}
fn base_headers(&self) -> Result<reqwest::header::HeaderMap, Report> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("Context-Type", HeaderValue::from_static("text/x-xml-plist"));
headers.insert("Content-Type", HeaderValue::from_static("text/x-xml-plist"));
headers.insert("Accept", HeaderValue::from_static("text/x-xml-plist"));
headers.insert(
"X-Mme-Client-Info",
HeaderValue::from_static(
"<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
),
HeaderValue::from_str(&self.client_info.client_info)?,
);
headers.insert(
"User-Agent",
HeaderValue::from_str(&self.client_info.user_agent)?,
);
headers.insert("User-Agent", HeaderValue::from_static("Xcode"));
headers.insert("X-Xcode-Version", HeaderValue::from_static("14.2 (14C18)"));
headers.insert(
"X-Apple-App-Info",
HeaderValue::from_static("com.apple.gs.xcode.auth"),
);
headers
Ok(headers)
}
/// Build a reqwest client with the Apple root certificate
///
/// # Arguments
/// - `debug`: DANGER, If true, accept invalid certificates and enable verbose connection logging
/// # Errors
/// Returns an error if the reqwest client cannot be built
pub fn build_reqwest_client(debug: bool) -> Result<reqwest::Client, Report> {
let cert = Certificate::from_der(APPLE_ROOT)?;
let client = ClientBuilder::new()
.add_root_certificate(cert)
.http1_title_case_headers()
.danger_accept_invalid_certs(debug)
.connection_verbose(debug)
.build()?;
Ok(client)
}
}

View File

@@ -1,21 +1,3 @@
use thiserror::Error as ThisError;
use thiserror_context::{Context, impl_context};
pub mod anisette;
pub mod auth;
#[derive(Debug, ThisError)]
pub enum ErrorInner {
#[error("Failed sending request: {0}")]
Reqwest(#[from] reqwest::Error),
#[error("Failed parsing plist: {0}")]
Plist(#[from] plist::Error),
#[error("Invalid Header: {0}")]
InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
}
impl_context!(Error(ErrorInner));
pub type SideloadResult<T> = std::result::Result<T, Error>;
pub mod util;

View File

@@ -0,0 +1 @@
pub mod plist;

View File

@@ -0,0 +1,9 @@
use plist_macro::{plist_to_xml_bytes, plist_value_to_xml_bytes};
pub fn plist_to_xml_string(p: &plist::Dictionary) -> String {
String::from_utf8(plist_to_xml_bytes(p)).unwrap()
}
pub fn plist_value_to_xml_string(p: &plist::Value) -> String {
String::from_utf8(plist_value_to_xml_bytes(p)).unwrap()
}