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