ios_core/lockdown/
client.rs1use serde::Serialize;
2use tokio::io::{AsyncRead, AsyncWrite};
3
4use crate::lockdown::pair_record::PairRecord;
5use crate::lockdown::protocol::*;
6use crate::lockdown::session::start_lockdown_session;
7use crate::lockdown::{LockdownError, ServiceInfo};
8
9pub struct LockdownClient {
11 reader: Box<dyn AsyncRead + Unpin + Send>,
12 writer: Box<dyn AsyncWrite + Unpin + Send>,
13 session_id: Option<String>,
14}
15
16impl LockdownClient {
17 pub async fn connect_with_stream<S>(
19 stream: S,
20 pair_record: &PairRecord,
21 ) -> Result<Self, LockdownError>
22 where
23 S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
24 {
25 let (session_id, reader, writer) = start_lockdown_session(stream, pair_record).await?;
26 Ok(Self {
27 reader: Box::new(reader),
28 writer: Box::new(writer),
29 session_id: Some(session_id),
30 })
31 }
32
33 pub async fn get_value(
35 &mut self,
36 domain: Option<&str>,
37 key: Option<&str>,
38 ) -> Result<plist::Value, LockdownError> {
39 send_lockdown(
40 &mut self.writer,
41 &GetValueRequest {
42 label: "ios-rs",
43 request: "GetValue",
44 domain,
45 key,
46 },
47 )
48 .await?;
49 let resp: plist::Value = recv_lockdown(&mut self.reader).await?;
50 extract_get_value(resp, domain, key)
51 }
52
53 pub async fn set_value<T>(
55 &mut self,
56 domain: Option<&str>,
57 key: Option<&str>,
58 value: T,
59 ) -> Result<(), LockdownError>
60 where
61 T: Serialize,
62 {
63 send_lockdown(
64 &mut self.writer,
65 &SetValueRequest {
66 label: "ios-rs",
67 request: "SetValue",
68 domain,
69 key,
70 value,
71 },
72 )
73 .await?;
74 let resp: ValueOperationResponse = recv_lockdown(&mut self.reader).await?;
75 if let Some(err) = resp.error {
76 return Err(LockdownError::Protocol(format!(
77 "SetValue failed for domain={domain:?} key={key:?}: {err}"
78 )));
79 }
80 Ok(())
81 }
82
83 pub async fn remove_value(
85 &mut self,
86 domain: Option<&str>,
87 key: Option<&str>,
88 ) -> Result<(), LockdownError> {
89 send_lockdown(
90 &mut self.writer,
91 &RemoveValueRequest {
92 label: "ios-rs",
93 request: "RemoveValue",
94 domain,
95 key,
96 },
97 )
98 .await?;
99 let resp: ValueOperationResponse = recv_lockdown(&mut self.reader).await?;
100 if let Some(err) = resp.error {
101 return Err(LockdownError::Protocol(format!(
102 "RemoveValue failed for domain={domain:?} key={key:?}: {err}"
103 )));
104 }
105 Ok(())
106 }
107
108 pub async fn start_service(&mut self, service: &str) -> Result<ServiceInfo, LockdownError> {
110 send_lockdown(
111 &mut self.writer,
112 &StartServiceRequest {
113 label: "ios-rs",
114 request: "StartService",
115 service: service.to_string(),
116 },
117 )
118 .await?;
119 let resp: StartServiceResponse = recv_lockdown(&mut self.reader).await?;
120 if let Some(err) = resp.error {
121 return Err(LockdownError::Protocol(format!(
122 "StartService '{service}' failed: {err}"
123 )));
124 }
125 let port = resp.port.ok_or_else(|| {
126 LockdownError::Protocol(format!("StartService '{service}': missing Port field"))
127 })?;
128 Ok(ServiceInfo {
129 port,
130 enable_service_ssl: resp.enable_service_ssl.unwrap_or(false),
131 })
132 }
133
134 pub async fn stop_session(&mut self) -> Result<(), LockdownError> {
136 if let Some(sid) = self.session_id.take() {
137 send_lockdown(
138 &mut self.writer,
139 &StopSessionRequest {
140 label: "ios-rs",
141 request: "StopSession",
142 session_id: sid,
143 },
144 )
145 .await?;
146 }
147 Ok(())
148 }
149
150 pub async fn product_version(&mut self) -> Result<semver::Version, LockdownError> {
152 let val = self.get_value(None, Some("ProductVersion")).await?;
153 let s = val
154 .as_string()
155 .ok_or_else(|| LockdownError::Protocol("ProductVersion is not a string".into()))?;
156 let normalized = match s.matches('.').count() {
158 0 => format!("{s}.0.0"),
159 1 => format!("{s}.0"),
160 _ => s.to_string(),
161 };
162 semver::Version::parse(&normalized)
163 .map_err(|e| LockdownError::Protocol(format!("invalid version '{s}': {e}")))
164 }
165}
166
167fn extract_get_value(
168 response: plist::Value,
169 domain: Option<&str>,
170 key: Option<&str>,
171) -> Result<plist::Value, LockdownError> {
172 if let plist::Value::Dictionary(mut values) = response {
173 if let Some(plist::Value::String(error)) = values.remove("Error") {
174 return Err(LockdownError::Protocol(format!(
175 "GetValue failed for domain={domain:?} key={key:?}: {error}"
176 )));
177 }
178
179 if let Some(value) = values.remove("Value") {
180 return Ok(value);
181 }
182
183 return Err(LockdownError::Protocol(format!(
184 "GetValue missing Value for domain={domain:?} key={key:?}: {:?}",
185 plist::Value::Dictionary(values)
186 )));
187 }
188
189 Err(LockdownError::Protocol(format!(
190 "GetValue returned non-dictionary response for domain={domain:?} key={key:?}: {response:?}"
191 )))
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn missing_get_value_payload_reports_context() {
200 let response = plist::Value::Dictionary(plist::Dictionary::from_iter([(
201 "Status".to_string(),
202 plist::Value::String("Success".into()),
203 )]));
204
205 let err = extract_get_value(
206 response,
207 Some("com.apple.mobile.wireless_lockdown"),
208 Some("EnableWifiConnections"),
209 )
210 .expect_err("missing value should error");
211
212 let rendered = err.to_string();
213 assert!(rendered.contains("EnableWifiConnections"));
214 assert!(rendered.contains("com.apple.mobile.wireless_lockdown"));
215 assert!(rendered.contains("Status"));
216 }
217}