mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 06:26:16 +01:00
Logging in (with trusted device 2fa only)
This commit is contained in:
121
Cargo.lock
generated
121
Cargo.lock
generated
@@ -8,6 +8,17 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -92,7 +103,16 @@ version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -107,6 +127,15 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.53"
|
||||
@@ -150,6 +179,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.57"
|
||||
@@ -236,7 +275,7 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"generic-array 0.14.7",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
@@ -263,6 +302,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -397,6 +437,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "1.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -455,6 +505,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -711,6 +770,16 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -731,12 +800,17 @@ dependencies = [
|
||||
name = "isideload"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"cbc",
|
||||
"chrono",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hmac",
|
||||
"idevice",
|
||||
"nab138_srp",
|
||||
"pbkdf2",
|
||||
"plist",
|
||||
"plist-macro",
|
||||
"rand",
|
||||
@@ -872,6 +946,20 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nab138_srp"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "587a7a2ae38ab9a818f42c12b02a7ad5d738006f78f3b53a9f28da91fe13411d"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"digest",
|
||||
"generic-array 1.3.5",
|
||||
"lazy_static",
|
||||
"num-bigint",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@@ -881,12 +969,31 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -908,6 +1015,16 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
|
||||
@@ -8,8 +8,9 @@ use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
isideload::init().expect("Failed to initialize error reporting");
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_max_level(Level::DEBUG)
|
||||
.with_max_level(Level::INFO)
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||
|
||||
@@ -31,13 +32,12 @@ async fn main() {
|
||||
};
|
||||
|
||||
let account = AppleAccountBuilder::new(apple_id)
|
||||
.danger_debug(true)
|
||||
.anisette(RemoteV3AnisetteProvider::default().set_serial_number("2".to_string()))
|
||||
.anisette_provider(RemoteV3AnisetteProvider::default().set_serial_number("2".to_string()))
|
||||
.login(apple_password, get_2fa_code)
|
||||
.await;
|
||||
|
||||
match account {
|
||||
Ok(_account) => println!("Successfully logged in to Apple ID"),
|
||||
Err(e) => eprintln!("Failed to log in to Apple ID: {}", e),
|
||||
Err(e) => eprintln!("Failed to log in to Apple ID: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,3 +33,8 @@ futures-util = "0.3.31"
|
||||
serde_json = "1.0.149"
|
||||
base64 = "0.22.1"
|
||||
hex = "0.4.3"
|
||||
srp = { package = "nab138_srp", version = "0.6.0" }
|
||||
pbkdf2 = "0.12.2"
|
||||
hmac = "0.12.1"
|
||||
cbc = { version = "0.1.2", features = ["std"] }
|
||||
aes = "0.8.4"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
pub mod remote_v3;
|
||||
|
||||
use crate::auth::grandslam::GrandSlam;
|
||||
use chrono::{DateTime, SubsecRound, Utc};
|
||||
use plist::Dictionary;
|
||||
use plist_macro::plist;
|
||||
use reqwest::header::HeaderMap;
|
||||
use rootcause::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -11,12 +15,83 @@ pub struct AnisetteClientInfo {
|
||||
pub user_agent: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
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 fn get_header_map(&self, serial: String) -> HeaderMap {
|
||||
let headers_map = self.get_headers(serial);
|
||||
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, serial: String) -> Dictionary {
|
||||
let headers = self.get_headers(serial);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[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_anisette_data(&mut self, gs: &mut GrandSlam) -> Result<AnisetteData, Report>;
|
||||
|
||||
async fn get_client_info(&mut self) -> Result<AnisetteClientInfo, Report>;
|
||||
}
|
||||
|
||||
@@ -1,67 +1,25 @@
|
||||
mod state;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use base64::prelude::*;
|
||||
use chrono::{DateTime, SubsecRound, Utc};
|
||||
use chrono::{SubsecRound, Utc};
|
||||
use plist_macro::plist;
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use reqwest::header::{CONTENT_TYPE, 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::anisette::{AnisetteClientInfo, AnisetteData, AnisetteProvider};
|
||||
use crate::auth::grandslam::GrandSlam;
|
||||
use crate::util::plist::plist_to_xml_string;
|
||||
use crate::util::plist::PlistDataExtract;
|
||||
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,
|
||||
@@ -117,23 +75,57 @@ impl Default for RemoteV3AnisetteProvider {
|
||||
|
||||
#[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?;
|
||||
async fn get_anisette_data(&mut self, gs: &mut GrandSlam) -> Result<AnisetteData, Report> {
|
||||
let state = self.get_state(gs).await?.clone();
|
||||
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();
|
||||
|
||||
unimplemented!()
|
||||
let headers = self
|
||||
.client
|
||||
.post(format!("{}/v3/get_headers", self.url))
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
serde_json::json!({
|
||||
"identifier": BASE64_STANDARD.encode(&state.keychain_identifier),
|
||||
"adi_pb": BASE64_STANDARD.encode(adi_pb)
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<AnisetteHeaders>()
|
||||
.await?;
|
||||
|
||||
match headers {
|
||||
AnisetteHeaders::Headers {
|
||||
machine_id,
|
||||
one_time_password,
|
||||
routing_info,
|
||||
} => {
|
||||
let data = AnisetteData {
|
||||
machine_id,
|
||||
one_time_password,
|
||||
routing_info,
|
||||
device_description: client_info,
|
||||
device_unique_identifier: state.get_device_id(),
|
||||
local_user_id: hex::encode(&state.get_md_lu()),
|
||||
};
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
AnisetteHeaders::GetHeadersError { message } => {
|
||||
Err(report!("Failed to get anisette headers")
|
||||
.attach(message)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -147,20 +139,20 @@ impl RemoteV3AnisetteProvider {
|
||||
self.client_info = Some(resp);
|
||||
}
|
||||
|
||||
debug!("Got client client_info: {:?}", self.client_info);
|
||||
|
||||
Ok(())
|
||||
Ok(self.client_info.as_ref().unwrap().clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteV3AnisetteProvider {
|
||||
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);
|
||||
info!("Loaded existing anisette state from {:?}", state_path);
|
||||
self.state = Some(state);
|
||||
} else {
|
||||
debug!("No existing anisette state found");
|
||||
info!("No existing anisette state found");
|
||||
self.state = Some(AnisetteState::new());
|
||||
}
|
||||
}
|
||||
@@ -177,10 +169,7 @@ impl RemoteV3AnisetteProvider {
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
async fn provisioning_headers(
|
||||
state: &AnisetteState,
|
||||
gs: &mut GrandSlam,
|
||||
) -> Result<HeaderMap, Report> {
|
||||
async fn provisioning_headers(state: &AnisetteState) -> Result<HeaderMap, Report> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"X-Apple-I-MD-LU",
|
||||
@@ -211,25 +200,14 @@ impl RemoteV3AnisetteProvider {
|
||||
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 start_provisioning = gs.get_url("midStartProvisioning").await?;
|
||||
let end_provisioning = gs.get_url("midFinishProvisioning").await?;
|
||||
|
||||
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")?;
|
||||
let (mut ws_stream, _) = tokio_tungstenite::connect_async(&websocket_url).await?;
|
||||
|
||||
loop {
|
||||
let Some(msg) = ws_stream.next().await else {
|
||||
@@ -266,28 +244,18 @@ impl RemoteV3AnisetteProvider {
|
||||
"Request": {}
|
||||
});
|
||||
|
||||
let resp = gs
|
||||
.post(&start_provisioning)?
|
||||
.headers(Self::provisioning_headers(state, gs).await?)
|
||||
.body(plist_to_xml_string(&body))
|
||||
.send()
|
||||
let response = gs
|
||||
.plist_request(
|
||||
&start_provisioning,
|
||||
&body,
|
||||
Some(Self::provisioning_headers(state).await?),
|
||||
)
|
||||
.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")?;
|
||||
.context("Failed to send start provisioning request")?;
|
||||
|
||||
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"))?;
|
||||
let spim = response
|
||||
.get_str("spim")
|
||||
.context("Start provisioning response missing spim")?;
|
||||
|
||||
ws_stream
|
||||
.send(Message::Text(
|
||||
@@ -308,39 +276,24 @@ impl RemoteV3AnisetteProvider {
|
||||
}
|
||||
});
|
||||
|
||||
let resp = gs
|
||||
.post(&end_provisioning)?
|
||||
.headers(Self::provisioning_headers(state, gs).await?)
|
||||
.body(plist_to_xml_string(&body))
|
||||
.send()
|
||||
let response = gs
|
||||
.plist_request(
|
||||
&end_provisioning,
|
||||
&body,
|
||||
Some(Self::provisioning_headers(state).await?),
|
||||
)
|
||||
.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"
|
||||
))?;
|
||||
.context("Failed to send end provisioning request")?;
|
||||
|
||||
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"))?,
|
||||
.get_str("ptm")
|
||||
.context("End provisioning response missing ptm")?,
|
||||
"tk": response
|
||||
.get("tk")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(report!("End provisioning response missing tk"))?,
|
||||
.get_str("tk")
|
||||
.context("End provisioning response missing tk")?,
|
||||
})
|
||||
.to_string()
|
||||
.into(),
|
||||
@@ -391,3 +344,19 @@ enum ProvisioningMessage {
|
||||
StartProvisioningError { message: String },
|
||||
EndProvisioningError { message: String },
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "result")]
|
||||
enum AnisetteHeaders {
|
||||
GetHeadersError {
|
||||
message: String,
|
||||
},
|
||||
Headers {
|
||||
#[serde(rename = "X-Apple-I-MD-M")]
|
||||
machine_id: String,
|
||||
#[serde(rename = "X-Apple-I-MD")]
|
||||
one_time_password: String,
|
||||
#[serde(rename = "X-Apple-I-MD-RINFO")]
|
||||
routing_info: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ where
|
||||
Ok(s.try_into().unwrap())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct AnisetteState {
|
||||
#[serde(
|
||||
serialize_with = "bin_serialize",
|
||||
|
||||
@@ -1,21 +1,48 @@
|
||||
use crate::{
|
||||
anisette::{AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
|
||||
auth::grandslam::GrandSlam,
|
||||
anisette::{AnisetteData, AnisetteProvider, remote_v3::RemoteV3AnisetteProvider},
|
||||
auth::grandslam::{GrandSlam, GrandSlamErrorChecker},
|
||||
util::plist::PlistDataExtract,
|
||||
};
|
||||
use aes::cipher::block_padding::Pkcs7;
|
||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||
use cbc::cipher::{BlockDecryptMut, KeyIvInit};
|
||||
use hmac::{Hmac, Mac};
|
||||
use plist::Dictionary;
|
||||
use plist_macro::plist;
|
||||
use reqwest::header::HeaderMap;
|
||||
use rootcause::prelude::*;
|
||||
use tracing::{info, warn};
|
||||
use sha2::{Digest, Sha256};
|
||||
use srp::{
|
||||
client::{SrpClient, SrpClientVerifier},
|
||||
groups::G_2048,
|
||||
};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
const SERIAL_NUMBER: &str = "2";
|
||||
|
||||
pub struct AppleAccount {
|
||||
pub email: String,
|
||||
pub spd: Option<plist::Dictionary>,
|
||||
pub anisette: Box<dyn AnisetteProvider>,
|
||||
pub anisette_provider: Box<dyn AnisetteProvider>,
|
||||
pub anisette_data: AnisetteData,
|
||||
pub grandslam_client: GrandSlam,
|
||||
login_state: LoginState,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
pub struct AppleAccountBuilder {
|
||||
email: String,
|
||||
debug: Option<bool>,
|
||||
anisette: Option<Box<dyn AnisetteProvider>>,
|
||||
anisette_provider: Option<Box<dyn AnisetteProvider>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoginState {
|
||||
LoggedIn,
|
||||
NeedsDevice2FA,
|
||||
NeedsSMS2FA,
|
||||
NeedsExtraStep(String),
|
||||
NeedsLogin,
|
||||
}
|
||||
|
||||
impl AppleAccountBuilder {
|
||||
@@ -27,7 +54,7 @@ impl AppleAccountBuilder {
|
||||
Self {
|
||||
email: email.to_string(),
|
||||
debug: None,
|
||||
anisette: None,
|
||||
anisette_provider: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +67,24 @@ impl AppleAccountBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn anisette(mut self, anisette: impl AnisetteProvider + 'static) -> Self {
|
||||
self.anisette = Some(Box::new(anisette));
|
||||
pub fn anisette_provider(mut self, anisette_provider: impl AnisetteProvider + 'static) -> Self {
|
||||
self.anisette_provider = Some(Box::new(anisette_provider));
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the AppleAccount
|
||||
///
|
||||
/// # Errors
|
||||
/// 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()));
|
||||
|
||||
AppleAccount::new(&self.email, anisette_provider, debug).await
|
||||
}
|
||||
|
||||
/// Build the AppleAccount and log in
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -60,12 +100,9 @@ impl AppleAccountBuilder {
|
||||
where
|
||||
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, password, two_factor_callback, anisette, debug).await
|
||||
let mut account = self.build().await?;
|
||||
account.login(password, two_factor_callback).await?;
|
||||
Ok(account)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,41 +115,321 @@ impl AppleAccount {
|
||||
AppleAccountBuilder::new(email)
|
||||
}
|
||||
|
||||
/// Log in to an Apple account with the given email
|
||||
/// Build the apple account with the given email
|
||||
///
|
||||
/// Reccomended to use the AppleAccountBuilder instead
|
||||
pub async fn login(
|
||||
pub async fn new(
|
||||
email: &str,
|
||||
password: &str,
|
||||
two_factor_callback: impl Fn() -> Option<String>,
|
||||
mut anisette: Box<dyn AnisetteProvider>,
|
||||
mut anisette_provider: Box<dyn AnisetteProvider>,
|
||||
debug: bool,
|
||||
) -> Result<Self, Report> {
|
||||
info!("Logging in to apple ID: {}", email);
|
||||
info!("Initializing apple account");
|
||||
if debug {
|
||||
warn!("Debug mode enabled: this is a security risk!");
|
||||
}
|
||||
|
||||
let client_info = anisette
|
||||
let client_info = anisette_provider
|
||||
.get_client_info()
|
||||
.await
|
||||
.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)
|
||||
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 headers for login")?;
|
||||
.context("Failed to get anisette data for login")?;
|
||||
|
||||
Ok(AppleAccount {
|
||||
email: email.to_string(),
|
||||
spd: None,
|
||||
anisette,
|
||||
anisette_provider,
|
||||
anisette_data,
|
||||
grandslam_client,
|
||||
debug,
|
||||
login_state: LoginState::NeedsLogin,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
&mut self,
|
||||
password: &str,
|
||||
two_factor_callback: impl Fn() -> Option<String>,
|
||||
) -> Result<(), Report> {
|
||||
info!("Logging in to apple ID: {}", self.email);
|
||||
if self.debug {
|
||||
warn!("Debug mode enabled: this is a security risk!");
|
||||
}
|
||||
|
||||
self.login_state = self
|
||||
.login_inner(password)
|
||||
.await
|
||||
.context("Failed to log in to Apple ID")?;
|
||||
|
||||
debug!("Initial login successful");
|
||||
|
||||
let mut attempts = 0;
|
||||
|
||||
loop {
|
||||
attempts += 1;
|
||||
if attempts > 10 {
|
||||
bail!(
|
||||
"Couldn't login after 10 attempts, aborting (current state: {:?})",
|
||||
self.login_state
|
||||
);
|
||||
}
|
||||
match self.login_state {
|
||||
LoginState::LoggedIn => {
|
||||
info!("Successfully logged in to Apple ID");
|
||||
return Ok(());
|
||||
}
|
||||
LoginState::NeedsDevice2FA => {
|
||||
debug!("Trusted device 2FA required");
|
||||
let request_code_url = self
|
||||
.grandslam_client
|
||||
.get_url("trustedDeviceSecondaryAuth")
|
||||
.await?;
|
||||
|
||||
let submit_code_url = self.grandslam_client.get_url("validateCode").await?;
|
||||
|
||||
self.grandslam_client
|
||||
.get(&request_code_url)?
|
||||
.headers(self.build_2fa_headers().await?)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to request trusted device 2fa")?
|
||||
.error_for_status()
|
||||
.context("Trusted device 2FA request failed")?;
|
||||
|
||||
info!("Trusted device 2FA request sent");
|
||||
|
||||
let code = two_factor_callback()
|
||||
.ok_or_else(|| report!("No 2FA code provided, aborting"))?;
|
||||
|
||||
let res = self
|
||||
.grandslam_client
|
||||
.get(&submit_code_url)?
|
||||
.headers(self.build_2fa_headers().await?)
|
||||
.header("security-code", code)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to submit trusted device 2fa code")?
|
||||
.error_for_status()
|
||||
.context("Trusted device 2FA code submission failed")?
|
||||
.text()
|
||||
.await
|
||||
.context("Failed to read trusted device 2FA response text")?;
|
||||
|
||||
let plist: Dictionary = plist::from_bytes(res.as_bytes())
|
||||
.context("Failed to parse trusted device response plist")
|
||||
.attach_with(|| res.clone())?;
|
||||
plist
|
||||
.check_grandslam_error()
|
||||
.context("Trusted device 2FA rejected")?;
|
||||
|
||||
debug!("Trusted device 2FA completed, need to login again");
|
||||
self.login_state = LoginState::NeedsLogin;
|
||||
}
|
||||
LoginState::NeedsSMS2FA => {
|
||||
info!("SMS 2FA required");
|
||||
todo!();
|
||||
}
|
||||
LoginState::NeedsExtraStep(_) => todo!(),
|
||||
LoginState::NeedsLogin => {
|
||||
debug!("Logging in again...");
|
||||
self.login_state = self
|
||||
.login_inner(password)
|
||||
.await
|
||||
.context("Failed to login again")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_2fa_headers(&mut self) -> Result<HeaderMap, Report> {
|
||||
let mut headers = self.anisette_data.get_header_map(SERIAL_NUMBER.to_string());
|
||||
|
||||
let spd = self
|
||||
.spd
|
||||
.as_ref()
|
||||
.ok_or_else(|| report!("SPD data not available, cannot build 2FA headers"))?;
|
||||
|
||||
let adsid = spd
|
||||
.get_str("adsid")
|
||||
.context("Failed to build 2FA headers")?;
|
||||
let token = spd
|
||||
.get_str("GsIdmsToken")
|
||||
.context("Failed to build 2FA headers")?;
|
||||
let identity = BASE64_STANDARD.encode(format!("{}:{}", adsid, token));
|
||||
|
||||
headers.insert(
|
||||
"X-Apple-Identity-Token",
|
||||
reqwest::header::HeaderValue::from_str(&identity)?,
|
||||
);
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
async fn login_inner(&mut self, password: &str) -> Result<LoginState, Report> {
|
||||
let gs_service_url = self.grandslam_client.get_url("gsService").await?;
|
||||
|
||||
debug!("GrandSlam service URL: {}", gs_service_url);
|
||||
|
||||
let cpd = self
|
||||
.anisette_data
|
||||
.get_client_provided_data(SERIAL_NUMBER.to_string());
|
||||
|
||||
let srp_client = SrpClient::<Sha256>::new(&G_2048);
|
||||
let a: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
|
||||
let a_pub = srp_client.compute_public_ephemeral(&a);
|
||||
|
||||
let req1 = plist!(dict {
|
||||
"Header": {
|
||||
"Version": "1.0.1"
|
||||
},
|
||||
"Request": {
|
||||
"A2k": a_pub, // A2k = client public ephemeral
|
||||
"cpd": cpd.clone(), // cpd = client provided data
|
||||
"o": "init", // o = operation
|
||||
"ps": [ // ps = protocols supported
|
||||
"s2k",
|
||||
"s2k_fo"
|
||||
],
|
||||
"u": self.email.clone(), // u = username
|
||||
}
|
||||
});
|
||||
|
||||
debug!("Sending initial login request");
|
||||
|
||||
let response = self
|
||||
.grandslam_client
|
||||
.plist_request(&gs_service_url, &req1, None)
|
||||
.await
|
||||
.context("Failed to send initial login request")?
|
||||
.check_grandslam_error()
|
||||
.context("GrandSlam error during initial login request")?;
|
||||
|
||||
debug!("Login step 1 completed");
|
||||
|
||||
let salt = response
|
||||
.get_data("s")
|
||||
.context("Failed to parse initial login response")?;
|
||||
let b_pub = response
|
||||
.get_data("B")
|
||||
.context("Failed to parse initial login response")?;
|
||||
let iters = response
|
||||
.get_signed_integer("i")
|
||||
.context("Failed to parse initial login response")?;
|
||||
let c = response
|
||||
.get_str("c")
|
||||
.context("Failed to parse initial login response")?;
|
||||
let selected_protocol = response
|
||||
.get_str("sp")
|
||||
.context("Failed to parse initial login response")?;
|
||||
|
||||
debug!(
|
||||
"Selected SRP protocol: {}, iterations: {}",
|
||||
selected_protocol, iters
|
||||
);
|
||||
|
||||
if selected_protocol != "s2k" && selected_protocol != "s2k_fo" {
|
||||
bail!("Unsupported SRP protocol selected: {}", selected_protocol);
|
||||
}
|
||||
|
||||
let hashed_password = Sha256::digest(password.as_bytes());
|
||||
|
||||
let password_hash = if selected_protocol == "s2k_fo" {
|
||||
hex::encode(&hashed_password).into_bytes()
|
||||
} else {
|
||||
hashed_password.to_vec()
|
||||
};
|
||||
|
||||
let mut password_buf = [0u8; 32];
|
||||
pbkdf2::pbkdf2::<hmac::Hmac<Sha256>>(&password_hash, salt, iters as u32, &mut password_buf)
|
||||
.context("Failed to derive password using PBKDF2")?;
|
||||
|
||||
let verifier: SrpClientVerifier<Sha256> = srp_client
|
||||
.process_reply(&a, &self.email.as_bytes(), &password_buf, salt, b_pub)
|
||||
.unwrap();
|
||||
|
||||
let req2 = plist!(dict {
|
||||
"Header": {
|
||||
"Version": "1.0.1"
|
||||
},
|
||||
"Request": {
|
||||
"M1": verifier.proof().to_vec(), // A2k = client public ephemeral
|
||||
"c": c, // c = client proof from step 1
|
||||
"cpd": cpd, // cpd = client provided data
|
||||
"o": "complete", // o = operation
|
||||
"u": self.email.clone(), // u = username
|
||||
}
|
||||
});
|
||||
|
||||
debug!("Sending proof login request");
|
||||
|
||||
let response2 = self
|
||||
.grandslam_client
|
||||
.plist_request(&gs_service_url, &req2, None)
|
||||
.await
|
||||
.context("Failed to send proof login request")?
|
||||
.check_grandslam_error()
|
||||
.context("GrandSlam error during proof login request")?;
|
||||
|
||||
debug!("Login step 2 response received, verifying server proof");
|
||||
|
||||
let m2 = response2
|
||||
.get_data("M2")
|
||||
.context("Failed to parse proof login response")?;
|
||||
verifier
|
||||
.verify_server(m2)
|
||||
.map_err(|e| report!("Negotiation failed, server proof mismatch: {}", e))?;
|
||||
|
||||
debug!("Server proof verified");
|
||||
|
||||
let spd_encrypted = response2
|
||||
.get_data("spd")
|
||||
.context("Failed to get SPD from login response")?;
|
||||
|
||||
let spd_decrypted = Self::decrypt_cbc(&verifier, &spd_encrypted)
|
||||
.context("Failed to decrypt SPD from login response")?;
|
||||
let spd: plist::Dictionary =
|
||||
plist::from_bytes(&spd_decrypted).context("Failed to parse decrypted SPD plist")?;
|
||||
|
||||
self.spd = Some(spd);
|
||||
|
||||
let status = response2
|
||||
.get_dict("Status")
|
||||
.context("Failed to parse proof login response")?;
|
||||
|
||||
debug!("Login step 2 completed");
|
||||
|
||||
if let Some(plist::Value::String(s)) = status.get("au") {
|
||||
return Ok(match s.as_str() {
|
||||
"trustedDeviceSecondaryAuth" => LoginState::NeedsDevice2FA,
|
||||
"secondaryAuth" => LoginState::NeedsSMS2FA,
|
||||
unknown => LoginState::NeedsExtraStep(unknown.to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(LoginState::LoggedIn)
|
||||
}
|
||||
|
||||
fn create_session_key(usr: &SrpClientVerifier<Sha256>, name: &str) -> Result<Vec<u8>, Report> {
|
||||
Ok(Hmac::<Sha256>::new_from_slice(&usr.key())?
|
||||
.chain_update(name.as_bytes())
|
||||
.finalize()
|
||||
.into_bytes()
|
||||
.to_vec())
|
||||
}
|
||||
|
||||
fn decrypt_cbc(usr: &SrpClientVerifier<Sha256>, data: &[u8]) -> Result<Vec<u8>, Report> {
|
||||
let extra_data_key = Self::create_session_key(usr, "extra data key:")?;
|
||||
let extra_data_iv = Self::create_session_key(usr, "extra data iv:")?;
|
||||
let extra_data_iv = &extra_data_iv[..16];
|
||||
|
||||
Ok(
|
||||
cbc::Decryptor::<aes::Aes256>::new_from_slices(&extra_data_key, extra_data_iv)?
|
||||
.decrypt_padded_vec_mut::<Pkcs7>(&data)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use plist::Dictionary;
|
||||
use reqwest::{Certificate, ClientBuilder, header::HeaderValue};
|
||||
use plist_macro::pretty_print_dictionary;
|
||||
use reqwest::{
|
||||
Certificate, ClientBuilder,
|
||||
header::{HeaderMap, HeaderValue},
|
||||
};
|
||||
use rootcause::prelude::*;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::anisette::AnisetteClientInfo;
|
||||
use crate::{
|
||||
anisette::AnisetteClientInfo,
|
||||
util::plist::{PlistDataExtract, plist_to_xml_string},
|
||||
};
|
||||
|
||||
const APPLE_ROOT: &[u8] = include_bytes!("./apple_root.der");
|
||||
const URL_BAG: &str = "https://gsa.apple.com/grandslam/GsService2/lookup";
|
||||
@@ -55,12 +62,61 @@ impl GrandSlam {
|
||||
Ok(self.url_bag.as_ref().unwrap())
|
||||
}
|
||||
|
||||
pub async fn get_url(&mut self, key: &str) -> Result<String, Report> {
|
||||
let url_bag = self.get_url_bag().await?;
|
||||
let url = 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()?);
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
|
||||
pub fn post(&self, url: &str) -> Result<reqwest::RequestBuilder, Report> {
|
||||
let builder = self.client.post(url).headers(self.base_headers()?);
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
|
||||
pub async fn plist_request(
|
||||
&self,
|
||||
url: &str,
|
||||
body: &Dictionary,
|
||||
additional_headers: Option<HeaderMap>,
|
||||
) -> Result<Dictionary, Report> {
|
||||
let resp = self
|
||||
.post(url)?
|
||||
.headers(additional_headers.unwrap_or_else(|| reqwest::header::HeaderMap::new()))
|
||||
.body(plist_to_xml_string(body))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send grandslam request")?
|
||||
.error_for_status()
|
||||
.context("Received error response from grandslam")?
|
||||
.text()
|
||||
.await
|
||||
.context("Failed to read grandslam response as text")?;
|
||||
|
||||
let dict: Dictionary = plist::from_bytes(resp.as_bytes())
|
||||
.context("Failed to parse grandslam response plist")
|
||||
.attach_with(|| resp.clone())?;
|
||||
|
||||
let response_plist = dict
|
||||
.get("Response")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
report!("grandslam response missing 'Response'")
|
||||
.attach(pretty_print_dictionary(&dict))
|
||||
})?;
|
||||
|
||||
Ok(response_plist)
|
||||
}
|
||||
|
||||
fn base_headers(&self) -> Result<reqwest::header::HeaderMap, Report> {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("Content-Type", HeaderValue::from_static("text/x-xml-plist"));
|
||||
@@ -100,3 +156,31 @@ impl GrandSlam {
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GrandSlamErrorChecker {
|
||||
fn check_grandslam_error(self) -> Result<Dictionary, Report<GrandSlamError>>;
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GrandSlamError {
|
||||
#[error("Auth error {0}: {1}")]
|
||||
AuthWithMessage(i64, String),
|
||||
}
|
||||
|
||||
impl GrandSlamErrorChecker for Dictionary {
|
||||
fn check_grandslam_error(self) -> Result<Self, Report<GrandSlamError>> {
|
||||
let result = match self.get("Status") {
|
||||
Some(plist::Value::Dictionary(d)) => d,
|
||||
_ => &self,
|
||||
};
|
||||
|
||||
if result.get_signed_integer("ec").unwrap_or(0) != 0 {
|
||||
bail!(GrandSlamError::AuthWithMessage(
|
||||
result.get_signed_integer("ec").unwrap_or(-1),
|
||||
result.get_str("em").unwrap_or("Unknown error").to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
use rootcause::{
|
||||
hooks::{Hooks, context_formatter::ContextFormatterHook},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
pub mod anisette;
|
||||
pub mod auth;
|
||||
pub mod util;
|
||||
|
||||
struct ReqwestErrorFormatter;
|
||||
|
||||
impl ContextFormatterHook<reqwest::Error> for ReqwestErrorFormatter {
|
||||
fn display(
|
||||
&self,
|
||||
report: rootcause::ReportRef<'_, reqwest::Error, markers::Uncloneable, markers::Local>,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
writeln!(f, "{}", report.format_current_context_unhooked())?;
|
||||
let mut source = report.current_context_error_source();
|
||||
while let Some(s) = source {
|
||||
writeln!(f, "Caused by: {:?}", s)?;
|
||||
source = s.source();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() -> Result<(), Report> {
|
||||
Hooks::new()
|
||||
.context_formatter::<reqwest::Error, _>(ReqwestErrorFormatter)
|
||||
.install()
|
||||
.context("Failed to install error reporting hooks")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use plist_macro::{plist_to_xml_bytes, plist_value_to_xml_bytes};
|
||||
use plist_macro::{plist_to_xml_bytes, plist_value_to_xml_bytes, pretty_print_dictionary};
|
||||
use rootcause::prelude::*;
|
||||
|
||||
pub fn plist_to_xml_string(p: &plist::Dictionary) -> String {
|
||||
String::from_utf8(plist_to_xml_bytes(p)).unwrap()
|
||||
@@ -7,3 +8,53 @@ pub fn plist_to_xml_string(p: &plist::Dictionary) -> String {
|
||||
pub fn plist_value_to_xml_string(p: &plist::Value) -> String {
|
||||
String::from_utf8(plist_value_to_xml_bytes(p)).unwrap()
|
||||
}
|
||||
|
||||
pub trait PlistDataExtract {
|
||||
fn get_data(&self, key: &str) -> Result<&[u8], Report>;
|
||||
fn get_str(&self, key: &str) -> Result<&str, Report>;
|
||||
fn get_string(&self, key: &str) -> Result<String, Report>;
|
||||
fn get_signed_integer(&self, key: &str) -> Result<i64, Report>;
|
||||
fn get_dict(&self, key: &str) -> Result<&plist::Dictionary, Report>;
|
||||
}
|
||||
|
||||
impl PlistDataExtract for plist::Dictionary {
|
||||
fn get_data(&self, key: &str) -> Result<&[u8], Report> {
|
||||
self.get(key).and_then(|v| v.as_data()).ok_or_else(|| {
|
||||
report!("Plist missing data for key '{}'", key).attach(pretty_print_dictionary(self))
|
||||
})
|
||||
}
|
||||
|
||||
fn get_str(&self, key: &str) -> Result<&str, Report> {
|
||||
self.get(key).and_then(|v| v.as_string()).ok_or_else(|| {
|
||||
report!("Plist missing string for key '{}'", key).attach(pretty_print_dictionary(self))
|
||||
})
|
||||
}
|
||||
|
||||
fn get_string(&self, key: &str) -> Result<String, Report> {
|
||||
self.get(key)
|
||||
.and_then(|v| v.as_string())
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| {
|
||||
report!("Plist missing string for key '{}'", key)
|
||||
.attach(pretty_print_dictionary(self))
|
||||
})
|
||||
}
|
||||
|
||||
fn get_signed_integer(&self, key: &str) -> Result<i64, Report> {
|
||||
self.get(key)
|
||||
.and_then(|v| v.as_signed_integer())
|
||||
.ok_or_else(|| {
|
||||
report!("Plist missing signed integer for key '{}'", key)
|
||||
.attach(pretty_print_dictionary(self))
|
||||
})
|
||||
}
|
||||
|
||||
fn get_dict(&self, key: &str) -> Result<&plist::Dictionary, Report> {
|
||||
self.get(key)
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or_else(|| {
|
||||
report!("Plist missing dictionary for key '{}'", key)
|
||||
.attach(pretty_print_dictionary(self))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user