use crate::config::AppConfig; use crate::error::{AppError, AppResult}; use serde::{Deserialize, Serialize}; const HTTP_TIMEOUT_SECONDS: u64 = 30; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MdmPushStatus { pub udid: String, pub push_result: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MdmEnqueueResult { pub command_uuid: String, pub response_body: String, } pub struct MdmClient { client: reqwest::Client, server: String, auth_header: String, } impl MdmClient { pub fn new() -> AppResult { let cfg = AppConfig::load_binder_config()?; let creds = format!("{}:{}", cfg.mdm_user, cfg.mdm_api_key); use base64::{engine::general_purpose::STANDARD, Engine}; let auth_header = format!("Basic {}", STANDARD.encode(creds)); Ok(Self { client: reqwest::Client::builder() .timeout(std::time::Duration::from_secs(HTTP_TIMEOUT_SECONDS)) .build() .map_err(|e| AppError::new(format!("reqwest client build: {}", e)))?, server: cfg.mdm_server, auth_header, }) } fn url(&self, path: &str) -> AppResult { let base = self.server.trim_end_matches('/'); Ok(format!("{}{}", base, path)) } fn request(&self, method: reqwest::Method, path: &str) -> AppResult { let url = self.url(path)?; Ok(self .client .request(method, &url) .header("Authorization", &self.auth_header)) } pub async fn ping(&self) -> AppResult { let resp = self .request(reqwest::Method::GET, "/version")? .send() .await .map_err(|e| AppError::new(format!("MDM ping failed: {}", e)))?; let status = resp.status(); let body = resp .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); if !status.is_success() { return Err(AppError::new(format!("MDM ping HTTP {}: {}", status, body))); } Ok(body) } pub async fn push(&self, udid: &str) -> AppResult { let resp = self .request(reqwest::Method::GET, &format!("/v1/push/{}", udid))? .send() .await .map_err(|e| AppError::new(format!("MDM push failed: {}", e)))?; let status = resp.status(); let body = resp .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); if !status.is_success() { return Err(AppError::new(format!("MDM push HTTP {}: {}", status, body))); } // Parse NanoMDM response: { "status": { "": { "push_result": "..." } } } let parsed: serde_json::Value = serde_json::from_str(&body) .map_err(|e| AppError::new(format!("MDM push parse error: {} — body: {}", e, body)))?; let push_result = parsed .get("status") .and_then(|s| s.get(udid)) .and_then(|d| d.get("push_result")) .and_then(|v| v.as_str()) .ok_or_else(|| AppError::new(format!("MDM push response unerwartet: {}", body)))? .to_string(); Ok(MdmPushStatus { udid: udid.to_string(), push_result, }) } pub async fn enqueue(&self, udid: &str, command: serde_json::Value) -> AppResult { let command_uuid = uuid::Uuid::new_v4().to_string(); let envelope = serde_json::json!({ "CommandUUID": command_uuid, "Command": command, }); // NanoMDM expects plist XML, not JSON. Serialize to plist XML. let plist_body = json_to_plist_xml(&envelope)?; let resp = self .request(reqwest::Method::PUT, &format!("/v1/enqueue/{}?push=1", udid))? .header("Content-Type", "application/xml") .body(plist_body) .send() .await .map_err(|e| AppError::new(format!("MDM enqueue failed: {}", e)))?; let status = resp.status(); let body = resp .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); if !status.is_success() { return Err(AppError::new(format!("MDM enqueue HTTP {}: {}", status, body))); } Ok(MdmEnqueueResult { command_uuid, response_body: body, }) } } fn json_to_plist_xml(value: &serde_json::Value) -> AppResult { let plist_value = json_value_to_plist(value)?; let mut buf = Vec::new(); plist::Value::from(plist_value) .to_writer_xml(&mut buf) .map_err(|e| AppError::new(format!("plist serialize failed: {}", e)))?; String::from_utf8(buf).map_err(|e| AppError::new(format!("plist utf8 failed: {}", e))) } fn json_value_to_plist(value: &serde_json::Value) -> AppResult { use plist::Value as Pv; match value { serde_json::Value::Null => Ok(Pv::String(String::new())), serde_json::Value::Bool(b) => Ok(Pv::Boolean(*b)), serde_json::Value::Number(n) => { if let Some(i) = n.as_i64() { Ok(Pv::Integer(i.into())) } else if let Some(f) = n.as_f64() { Ok(Pv::Real(f)) } else { Err(AppError::new("unsupported json number".to_string())) } } serde_json::Value::String(s) => Ok(Pv::String(s.clone())), serde_json::Value::Array(arr) => { let mut out = Vec::new(); for item in arr { out.push(json_value_to_plist(item)?); } Ok(Pv::Array(out.into())) } serde_json::Value::Object(map) => { let mut out = plist::Dictionary::new(); for (k, v) in map { out.insert(k.clone(), json_value_to_plist(v)?); } Ok(Pv::Dictionary(out)) } } }