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;
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#[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 #[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}