184 lines
5.9 KiB
Rust
184 lines
5.9 KiB
Rust
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<Self> {
|
|
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<String> {
|
|
let base = self.server.trim_end_matches('/');
|
|
Ok(format!("{}{}", base, path))
|
|
}
|
|
|
|
fn request(&self, method: reqwest::Method, path: &str) -> AppResult<reqwest::RequestBuilder> {
|
|
let url = self.url(path)?;
|
|
Ok(self
|
|
.client
|
|
.request(method, &url)
|
|
.header("Authorization", &self.auth_header))
|
|
}
|
|
|
|
pub async fn ping(&self) -> AppResult<String> {
|
|
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<MdmPushStatus> {
|
|
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": { "<udid>": { "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<MdmEnqueueResult> {
|
|
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<String> {
|
|
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<plist::Value> {
|
|
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))
|
|
}
|
|
}
|
|
}
|