Skip to main content

ios_core/lockdown/
protocol.rs

1use serde::{Deserialize, Serialize};
2use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
3
4use crate::lockdown::LockdownError;
5
6pub const LOCKDOWN_PORT: u16 = 62078;
7
8pub fn encode_frame(payload: &[u8]) -> Vec<u8> {
9    let mut buf = Vec::with_capacity(4 + payload.len());
10    buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
11    buf.extend_from_slice(payload);
12    buf
13}
14
15pub async fn recv_frame<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Vec<u8>, LockdownError> {
16    let mut len_buf = [0u8; 4];
17    reader.read_exact(&mut len_buf).await?;
18    let length = u32::from_be_bytes(len_buf) as usize;
19    let mut payload = vec![0u8; length];
20    reader.read_exact(&mut payload).await?;
21    Ok(payload)
22}
23
24pub async fn send_lockdown<W, T>(writer: &mut W, value: &T) -> Result<(), LockdownError>
25where
26    W: AsyncWrite + Unpin,
27    T: Serialize,
28{
29    let mut bytes = Vec::new();
30    plist::to_writer_xml(&mut bytes, value).map_err(|e| LockdownError::Protocol(e.to_string()))?;
31    writer.write_all(&encode_frame(&bytes)).await?;
32    writer.flush().await?;
33    Ok(())
34}
35
36pub async fn recv_lockdown<R, T>(reader: &mut R) -> Result<T, LockdownError>
37where
38    R: AsyncRead + Unpin,
39    T: for<'de> Deserialize<'de>,
40{
41    let payload = recv_frame(reader).await?;
42    plist::from_bytes(&payload).map_err(|e| LockdownError::Protocol(e.to_string()))
43}
44
45// ── Request / Response structs ────────────────────────────
46
47#[derive(Serialize, Deserialize)]
48#[serde(rename_all = "PascalCase")]
49pub struct QueryTypeRequest {
50    pub label: &'static str,
51    pub request: &'static str,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55#[serde(rename_all = "PascalCase")]
56pub struct QueryTypeResponse {
57    #[serde(rename = "Type")]
58    pub type_: String,
59}
60
61#[derive(Serialize)]
62#[serde(rename_all = "PascalCase")]
63pub struct GetValueRequest<'a> {
64    pub label: &'static str,
65    pub request: &'static str,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub domain: Option<&'a str>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub key: Option<&'a str>,
70}
71
72#[derive(Debug, Deserialize)]
73#[serde(rename_all = "PascalCase")]
74pub struct GetValueResponse {
75    pub value: plist::Value,
76}
77
78#[derive(Serialize)]
79#[serde(rename_all = "PascalCase")]
80pub struct SetValueRequest<'a, T>
81where
82    T: Serialize,
83{
84    pub label: &'static str,
85    pub request: &'static str,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub domain: Option<&'a str>,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub key: Option<&'a str>,
90    pub value: T,
91}
92
93#[derive(Serialize)]
94#[serde(rename_all = "PascalCase")]
95pub struct RemoveValueRequest<'a> {
96    pub label: &'static str,
97    pub request: &'static str,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub domain: Option<&'a str>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub key: Option<&'a str>,
102}
103
104#[derive(Debug, Deserialize)]
105#[serde(rename_all = "PascalCase")]
106pub struct ValueOperationResponse {
107    #[serde(rename = "Error")]
108    pub error: Option<String>,
109    #[serde(rename = "Value")]
110    pub value: Option<plist::Value>,
111}
112
113#[derive(Serialize)]
114#[serde(rename_all = "PascalCase")]
115pub struct StartSessionRequest {
116    pub label: &'static str,
117    pub protocol_version: &'static str,
118    pub request: &'static str,
119    #[serde(rename = "HostID")]
120    pub host_id: String,
121    #[serde(rename = "SystemBUID")]
122    pub system_buid: String,
123}
124
125#[derive(Debug, Deserialize)]
126pub struct StartSessionResponse {
127    #[serde(rename = "SessionID")]
128    pub session_id: String,
129    #[serde(rename = "EnableSessionSSL")]
130    pub enable_session_ssl: bool,
131}
132
133#[derive(Serialize)]
134#[serde(rename_all = "PascalCase")]
135pub struct StartServiceRequest {
136    pub label: &'static str,
137    pub request: &'static str,
138    pub service: String,
139}
140
141#[derive(Debug, Serialize, Deserialize)]
142pub struct StartServiceResponse {
143    #[serde(rename = "Port")]
144    pub port: Option<u16>,
145    #[serde(rename = "EnableServiceSSL")]
146    pub enable_service_ssl: Option<bool>,
147    /// Device may return an Error field instead of Port when service is unavailable.
148    #[serde(rename = "Error")]
149    pub error: Option<String>,
150}
151
152#[derive(Serialize)]
153#[serde(rename_all = "PascalCase")]
154pub struct StopSessionRequest {
155    pub label: &'static str,
156    pub request: &'static str,
157    #[serde(rename = "SessionID")]
158    pub session_id: String,
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_encode_lockdown_frame() {
167        let payload = b"hello";
168        let frame = encode_frame(payload);
169        assert_eq!(&frame[..4], &5u32.to_be_bytes());
170        assert_eq!(&frame[4..], payload);
171    }
172
173    #[tokio::test]
174    async fn test_roundtrip_frame() {
175        let payload = b"<plist/>";
176        let frame = encode_frame(payload);
177        let mut cursor = std::io::Cursor::new(frame);
178        let decoded = recv_frame(&mut cursor).await.unwrap();
179        assert_eq!(decoded, payload);
180    }
181
182    #[tokio::test]
183    async fn test_recv_frame_empty_payload() {
184        let frame = encode_frame(b"");
185        let mut cursor = std::io::Cursor::new(frame);
186        let decoded = recv_frame(&mut cursor).await.unwrap();
187        assert!(decoded.is_empty());
188    }
189
190    #[test]
191    fn test_set_value_request_serializes_domain_key_and_value() {
192        let request = SetValueRequest {
193            label: "ios-rs",
194            request: "SetValue",
195            domain: Some("com.apple.international"),
196            key: Some("Language"),
197            value: "en",
198        };
199
200        let mut bytes = Vec::new();
201        plist::to_writer_xml(&mut bytes, &request).unwrap();
202        let xml = String::from_utf8(bytes).unwrap();
203
204        assert!(xml.contains("<key>Request</key>"));
205        assert!(xml.contains("<string>SetValue</string>"));
206        assert!(xml.contains("<key>Domain</key>"));
207        assert!(xml.contains("<string>com.apple.international</string>"));
208        assert!(xml.contains("<key>Key</key>"));
209        assert!(xml.contains("<string>Language</string>"));
210        assert!(xml.contains("<key>Value</key>"));
211        assert!(xml.contains("<string>en</string>"));
212    }
213
214    #[test]
215    fn test_remove_value_request_serializes_domain_and_key() {
216        let request = RemoveValueRequest {
217            label: "ios-rs",
218            request: "RemoveValue",
219            domain: Some("com.apple.mobile.wireless_lockdown"),
220            key: Some("EnableWifiConnections"),
221        };
222
223        let mut bytes = Vec::new();
224        plist::to_writer_xml(&mut bytes, &request).unwrap();
225        let xml = String::from_utf8(bytes).unwrap();
226
227        assert!(xml.contains("<key>Request</key>"));
228        assert!(xml.contains("<string>RemoveValue</string>"));
229        assert!(xml.contains("<key>Domain</key>"));
230        assert!(xml.contains("<string>com.apple.mobile.wireless_lockdown</string>"));
231        assert!(xml.contains("<key>Key</key>"));
232        assert!(xml.contains("<string>EnableWifiConnections</string>"));
233    }
234}