This commit is contained in:
2026-02-26 16:59:30 +08:00
commit 3339111ff2
31 changed files with 4635 additions and 0 deletions

241
src/provisioning_wasm.rs Normal file
View File

@@ -0,0 +1,241 @@
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::io::Cursor;
use std::path::PathBuf;
use anyhow::{Context, Result, anyhow, bail};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use chrono::Utc;
use plist::Value;
use serde::Deserialize;
use serde_json::json;
use crate::Adi;
use crate::device::DeviceData;
#[derive(Debug, Deserialize)]
struct JsHttpResponse {
status: u16,
body: String,
#[serde(default)]
error: String,
}
pub struct ProvisioningSession<'a> {
adi: &'a mut Adi,
device: &'a DeviceData,
url_bag: HashMap<String, String>,
}
impl<'a> ProvisioningSession<'a> {
pub fn new(
adi: &'a mut Adi,
device: &'a DeviceData,
_apple_root_pem: Option<PathBuf>,
) -> Result<Self> {
Ok(Self {
adi,
device,
url_bag: HashMap::new(),
})
}
pub fn provision(&mut self, dsid: u64) -> Result<()> {
println!("ProvisioningSession.provision");
if self.url_bag.is_empty() {
self.load_url_bag()?;
}
let start_url = self
.url_bag
.get("midStartProvisioning")
.cloned()
.ok_or_else(|| anyhow!("url bag missing midStartProvisioning"))?;
let finish_url = self
.url_bag
.get("midFinishProvisioning")
.cloned()
.ok_or_else(|| anyhow!("url bag missing midFinishProvisioning"))?;
let start_body = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Header</key>
<dict/>
<key>Request</key>
<dict/>
</dict>
</plist>"#;
let start_bytes = self.post_with_time(&start_url, start_body)?;
let start_plist = parse_plist(&start_bytes)?;
let spim_b64 = plist_get_string_in_response(&start_plist, "spim")?;
let spim = STANDARD.decode(spim_b64.as_bytes())?;
let start = self.adi.start_provisioning(dsid, &spim)?;
let cpim_b64 = STANDARD.encode(&start.cpim);
let finish_body = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Header</key>\n <dict/>\n <key>Request</key>\n <dict>\n <key>cpim</key>\n <string>{}</string>\n </dict>\n</dict>\n</plist>",
cpim_b64
);
let finish_bytes = self.post_with_time(&finish_url, &finish_body)?;
let finish_plist = parse_plist(&finish_bytes)?;
let ptm_b64 = plist_get_string_in_response(&finish_plist, "ptm")?;
let tk_b64 = plist_get_string_in_response(&finish_plist, "tk")?;
let ptm = STANDARD.decode(ptm_b64.as_bytes())?;
let tk = STANDARD.decode(tk_b64.as_bytes())?;
self.adi.end_provisioning(start.session, &ptm, &tk)?;
Ok(())
}
fn load_url_bag(&mut self) -> Result<()> {
let bytes = self.get("https://gsa.apple.com/grandslam/GsService2/lookup")?;
let plist = parse_plist(&bytes)?;
let root = plist
.as_dictionary()
.ok_or_else(|| anyhow!("lookup plist root is not a dictionary"))?;
let urls = root
.get("urls")
.and_then(Value::as_dictionary)
.ok_or_else(|| anyhow!("lookup plist missing urls dictionary"))?;
self.url_bag.clear();
for (name, value) in urls {
if let Some(url) = value.as_string() {
self.url_bag.insert(name.to_string(), url.to_string());
}
}
Ok(())
}
fn get(&self, url: &str) -> Result<Vec<u8>> {
let request = json!({
"url": url,
"headers": self.common_headers(None),
});
self.call_http("anisette_http_get", request)
}
fn post_with_time(&self, url: &str, body: &str) -> Result<Vec<u8>> {
let client_time = current_client_time();
let request = json!({
"url": url,
"headers": self.common_headers(Some(&client_time)),
"body": body,
});
self.call_http("anisette_http_post", request)
}
fn common_headers(&self, client_time: Option<&str>) -> HashMap<&'static str, String> {
let mut headers = HashMap::from([
(
"User-Agent",
"akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0".to_string(),
),
(
"Content-Type",
"application/x-www-form-urlencoded".to_string(),
),
("Connection", "keep-alive".to_string()),
(
"X-Mme-Device-Id",
self.device.unique_device_identifier.clone(),
),
(
"X-MMe-Client-Info",
self.device.server_friendly_description.clone(),
),
("X-Apple-I-MD-LU", self.device.local_user_uuid.clone()),
("X-Apple-Client-App-Name", "Setup".to_string()),
]);
if let Some(time) = client_time {
headers.insert("X-Apple-I-Client-Time", time.to_string());
}
headers
}
fn call_http(&self, name: &str, payload: serde_json::Value) -> Result<Vec<u8>> {
// JS callback must return JSON: { status: number, body: base64, error?: string }.
let payload_json = serde_json::to_string(&payload)?;
let script = format!(
"(function(){{var fn = (typeof {name} === 'function') ? {name} : (typeof Module !== 'undefined' ? Module.{name} : null); return fn ? fn({payload_json}) : '';}})();"
);
let response_json = run_script_string(&script)?;
if response_json.trim().is_empty() {
bail!("missing JS http callback {name}");
}
let response: JsHttpResponse = serde_json::from_str(&response_json)
.with_context(|| format!("invalid JS http response for {name}"))?;
if !response.error.trim().is_empty() {
bail!("js http error: {}", response.error);
}
if response.status >= 400 {
bail!("js http status {} for {}", response.status, name);
}
let bytes = STANDARD
.decode(response.body.as_bytes())
.map_err(|e| anyhow!("base64 decode failed: {e}"))?;
Ok(bytes)
}
}
#[cfg(target_os = "emscripten")]
unsafe extern "C" {
fn emscripten_run_script_string(script: *const core::ffi::c_char) -> *mut core::ffi::c_char;
}
#[cfg(target_os = "emscripten")]
fn run_script_string(script: &str) -> Result<String> {
let script = CString::new(script).map_err(|e| anyhow!("invalid JS script: {e}"))?;
let ptr = unsafe { emscripten_run_script_string(script.as_ptr()) };
if ptr.is_null() {
return Err(anyhow!("emscripten_run_script_string returned null"));
}
let text = unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned();
Ok(text)
}
fn parse_plist(bytes: &[u8]) -> Result<Value> {
Ok(Value::from_reader_xml(Cursor::new(bytes))?)
}
fn plist_get_string_in_response<'a>(plist: &'a Value, key: &str) -> Result<&'a str> {
let root = plist
.as_dictionary()
.ok_or_else(|| anyhow!("plist root is not a dictionary"))?;
let response = root
.get("Response")
.and_then(Value::as_dictionary)
.ok_or_else(|| anyhow!("plist missing Response dictionary"))?;
let value = response
.get(key)
.ok_or_else(|| anyhow!("plist Response missing {key}"))?;
if let Some(text) = value.as_string() {
return Ok(text);
}
bail!("plist Response field {key} is not a string")
}
fn current_client_time() -> String {
Utc::now().format("%Y-%m-%dT%H:%M:%S%:z").to_string()
}