ios_core/services/house_arrest/
mod.rs1use serde::{Deserialize, Serialize};
5use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
6
7use crate::services::afc::{AfcClient, AfcError};
8
9pub const SERVICE_NAME: &str = "com.apple.mobile.house_arrest";
10
11service_error!(
12 HouseArrestError,
13 #[error("house arrest error: {0}")]
14 Service(String),
15);
16
17impl From<AfcError> for HouseArrestError {
18 fn from(err: AfcError) -> Self {
19 match err {
20 AfcError::Io(e) => Self::Io(e),
21 AfcError::Status(code) => Self::Service(code.to_string()),
22 AfcError::Protocol(msg) => Self::Protocol(msg),
23 }
24 }
25}
26
27#[derive(Debug)]
28pub struct HouseArrestClient<S> {
29 stream: S,
30}
31
32impl<S: AsyncRead + AsyncWrite + Unpin> HouseArrestClient<S> {
33 pub fn new(stream: S) -> Self {
34 Self { stream }
35 }
36
37 pub async fn vend_container(self, bundle_id: &str) -> Result<AfcClient<S>, HouseArrestError> {
38 self.send_command("VendContainer", bundle_id).await
39 }
40
41 pub async fn vend_documents(self, bundle_id: &str) -> Result<AfcClient<S>, HouseArrestError> {
42 self.send_command("VendDocuments", bundle_id).await
43 }
44
45 async fn send_command(
46 mut self,
47 command: &'static str,
48 bundle_id: &str,
49 ) -> Result<AfcClient<S>, HouseArrestError> {
50 self.send_plist(&VendContainerRequest {
51 command,
52 identifier: bundle_id,
53 })
54 .await?;
55
56 let response: VendContainerResponse = self.recv_plist().await?;
57 match response.status.as_deref() {
58 Some("Complete") => Ok(AfcClient::new(self.stream)),
59 Some(status) => Err(HouseArrestError::Service(status.to_string())),
60 None => {
61 if let Some(error) = response.error {
62 Err(HouseArrestError::Service(error))
63 } else {
64 Err(HouseArrestError::Protocol(
65 "unknown house arrest response".into(),
66 ))
67 }
68 }
69 }
70 }
71
72 async fn send_plist<T: Serialize>(&mut self, value: &T) -> Result<(), HouseArrestError> {
73 let mut buf = Vec::new();
74 plist::to_writer_xml(&mut buf, value)?;
75 let len = buf.len() as u32;
76 self.stream.write_all(&len.to_be_bytes()).await?;
77 self.stream.write_all(&buf).await?;
78 self.stream.flush().await?;
79 Ok(())
80 }
81
82 async fn recv_plist<T>(&mut self) -> Result<T, HouseArrestError>
83 where
84 T: for<'de> Deserialize<'de>,
85 {
86 let mut len_buf = [0u8; 4];
87 self.stream.read_exact(&mut len_buf).await?;
88 let len = u32::from_be_bytes(len_buf) as usize;
89 const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
90 if len > MAX_PLIST_SIZE {
91 return Err(HouseArrestError::Protocol(format!(
92 "plist length {len} exceeds max {MAX_PLIST_SIZE}"
93 )));
94 }
95 let mut buf = vec![0u8; len];
96 self.stream.read_exact(&mut buf).await?;
97 Ok(plist::from_bytes(&buf)?)
98 }
99}
100
101#[derive(Serialize)]
102#[serde(rename_all = "PascalCase")]
103struct VendContainerRequest<'a> {
104 command: &'static str,
105 identifier: &'a str,
106}
107
108#[derive(Debug, Deserialize)]
109#[serde(rename_all = "PascalCase")]
110struct VendContainerResponse {
111 #[serde(default)]
112 status: Option<String>,
113 #[serde(default)]
114 error: Option<String>,
115}
116
117#[cfg(test)]
118mod tests {
119 use crate::proto::afc::{AfcHeader, AfcOpcode};
120 use zerocopy::{FromBytes, IntoBytes};
121
122 use super::*;
123
124 async fn read_plist_frame<S>(stream: &mut S) -> Vec<u8>
125 where
126 S: AsyncRead + Unpin,
127 {
128 let mut len_buf = [0u8; 4];
129 stream.read_exact(&mut len_buf).await.unwrap();
130 let len = u32::from_be_bytes(len_buf) as usize;
131 let mut buf = vec![0u8; len];
132 stream.read_exact(&mut buf).await.unwrap();
133 buf
134 }
135
136 async fn read_afc_request<S>(stream: &mut S)
137 where
138 S: AsyncRead + Unpin,
139 {
140 let mut hdr_buf = [0u8; AfcHeader::SIZE];
141 stream.read_exact(&mut hdr_buf).await.unwrap();
142 let hdr = AfcHeader::ref_from_bytes(&hdr_buf).unwrap();
143 let header_payload_len = hdr.this_len.get() as usize - AfcHeader::SIZE;
144 let payload_len = hdr.entire_len.get() as usize - hdr.this_len.get() as usize;
145
146 let mut header_payload = vec![0u8; header_payload_len];
147 let mut payload = vec![0u8; payload_len];
148 if header_payload_len > 0 {
149 stream.read_exact(&mut header_payload).await.unwrap();
150 }
151 if payload_len > 0 {
152 stream.read_exact(&mut payload).await.unwrap();
153 }
154 assert_eq!(hdr.operation.get(), AfcOpcode::ReadDir as u64);
155 }
156
157 #[test]
158 fn test_service_name_matches_house_arrest() {
159 assert_eq!(SERVICE_NAME, "com.apple.mobile.house_arrest");
160 }
161
162 #[tokio::test]
163 async fn test_vend_container_returns_afc_client_over_same_stream() {
164 let (client_side, mut server_side) = tokio::io::duplex(4096);
165
166 tokio::spawn(async move {
167 let request = read_plist_frame(&mut server_side).await;
168 let req_value: plist::Value = plist::from_bytes(&request).unwrap();
169 let dict = req_value.into_dictionary().unwrap();
170 assert_eq!(
171 dict.get("Command").and_then(|v| v.as_string()),
172 Some("VendContainer")
173 );
174 assert_eq!(
175 dict.get("Identifier").and_then(|v| v.as_string()),
176 Some("com.example.TestApp")
177 );
178
179 let response = plist::Dictionary::from_iter([(
180 "Status".to_string(),
181 plist::Value::String("Complete".into()),
182 )]);
183 let mut buf = Vec::new();
184 plist::to_writer_xml(&mut buf, &plist::Value::Dictionary(response)).unwrap();
185 let len = buf.len() as u32;
186 server_side.write_all(&len.to_be_bytes()).await.unwrap();
187 server_side.write_all(&buf).await.unwrap();
188
189 read_afc_request(&mut server_side).await;
190
191 let names = b".\0..\0Sandbox\0";
192 let hdr = AfcHeader::new(1, AfcOpcode::ReadDir, 0, names.len());
193 let mut resp = hdr.as_bytes().to_vec();
194 resp.extend_from_slice(names);
195 server_side.write_all(&resp).await.unwrap();
196 });
197
198 let client = HouseArrestClient::new(client_side);
199 let mut afc = client.vend_container("com.example.TestApp").await.unwrap();
200 let entries = afc.list_dir("/").await.unwrap();
201 assert_eq!(entries, vec!["Sandbox"]);
202 }
203
204 #[tokio::test]
205 async fn test_vend_documents_sends_expected_command() {
206 let (client_side, mut server_side) = tokio::io::duplex(4096);
207
208 tokio::spawn(async move {
209 let request = read_plist_frame(&mut server_side).await;
210 let req_value: plist::Value = plist::from_bytes(&request).unwrap();
211 let dict = req_value.into_dictionary().unwrap();
212 assert_eq!(
213 dict.get("Command").and_then(|v| v.as_string()),
214 Some("VendDocuments")
215 );
216 assert_eq!(
217 dict.get("Identifier").and_then(|v| v.as_string()),
218 Some("com.example.DocumentsApp")
219 );
220
221 let response = plist::Dictionary::from_iter([(
222 "Status".to_string(),
223 plist::Value::String("Complete".into()),
224 )]);
225 let mut buf = Vec::new();
226 plist::to_writer_xml(&mut buf, &plist::Value::Dictionary(response)).unwrap();
227 let len = buf.len() as u32;
228 server_side.write_all(&len.to_be_bytes()).await.unwrap();
229 server_side.write_all(&buf).await.unwrap();
230
231 read_afc_request(&mut server_side).await;
232
233 let names = b".\0..\0Documents\0";
234 let hdr = AfcHeader::new(1, AfcOpcode::ReadDir, 0, names.len());
235 let mut resp = hdr.as_bytes().to_vec();
236 resp.extend_from_slice(names);
237 server_side.write_all(&resp).await.unwrap();
238 });
239
240 let client = HouseArrestClient::new(client_side);
241 let mut afc = client
242 .vend_documents("com.example.DocumentsApp")
243 .await
244 .unwrap();
245 let entries = afc.list_dir("/").await.unwrap();
246 assert_eq!(entries, vec!["Documents"]);
247 }
248}