ios_core/services/notificationproxy/
mod.rs1use std::collections::HashSet;
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
11use tokio::time::Instant;
12
13pub const SERVICE_NAME: &str = "com.apple.mobile.notification_proxy";
14pub const SPRINGBOARD_FINISHED_STARTUP: &str = "com.apple.springboard.finishedstartup";
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum NotificationEvent {
18 Notification(String),
19 ProxyDeath,
20}
21
22service_error!(
23 NotificationProxyError,
24 #[error("proxy closed before notification arrived")]
25 ProxyDeath,
26 #[error("timed out waiting for notification")]
27 Timeout,
28);
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum NotificationProxyEvent {
32 Notification(String),
33 ProxyDeath,
34}
35
36#[derive(Debug)]
37pub struct NotificationProxyClient<S> {
38 stream: S,
39 observing: HashSet<String>,
40}
41
42impl<S: AsyncRead + AsyncWrite + Unpin> NotificationProxyClient<S> {
43 pub fn new(stream: S) -> Self {
44 Self {
45 stream,
46 observing: HashSet::new(),
47 }
48 }
49
50 pub async fn observe(&mut self, notification: &str) -> Result<(), NotificationProxyError> {
51 if self.observing.contains(notification) {
52 return Ok(());
53 }
54
55 self.send_request(NotificationProxyRequest {
56 command: "ObserveNotification",
57 name: Some(notification),
58 })
59 .await?;
60 self.observing.insert(notification.to_string());
61 Ok(())
62 }
63
64 pub async fn post(&mut self, notification: &str) -> Result<(), NotificationProxyError> {
65 self.send_request(NotificationProxyRequest {
66 command: "PostNotification",
67 name: Some(notification),
68 })
69 .await
70 }
71
72 pub async fn wait_for(
73 &mut self,
74 notification: &str,
75 timeout: Duration,
76 ) -> Result<(), NotificationProxyError> {
77 self.observe(notification).await?;
78
79 let deadline = Instant::now() + timeout;
80 loop {
81 let remaining = deadline.saturating_duration_since(Instant::now());
82 if remaining.is_zero() {
83 return Err(NotificationProxyError::Timeout);
84 }
85
86 let event = tokio::time::timeout(remaining, self.recv_event())
87 .await
88 .map_err(|_| NotificationProxyError::Timeout)??;
89
90 match event {
91 NotificationEvent::Notification(name) if name == notification => return Ok(()),
92 NotificationEvent::ProxyDeath => return Err(NotificationProxyError::ProxyDeath),
93 NotificationEvent::Notification(_) => {}
94 }
95 }
96 }
97
98 pub async fn wait_for_springboard(
99 &mut self,
100 timeout: Duration,
101 ) -> Result<(), NotificationProxyError> {
102 self.wait_for(SPRINGBOARD_FINISHED_STARTUP, timeout).await
103 }
104
105 pub async fn next_event(
106 &mut self,
107 timeout: Duration,
108 ) -> Result<NotificationProxyEvent, NotificationProxyError> {
109 let message = tokio::time::timeout(timeout, self.recv_message())
110 .await
111 .map_err(|_| NotificationProxyError::Timeout)??;
112
113 match message.command.as_deref() {
114 Some("RelayNotification") => message
115 .name
116 .map(NotificationProxyEvent::Notification)
117 .ok_or_else(|| {
118 NotificationProxyError::Protocol("RelayNotification missing Name field".into())
119 }),
120 Some("ProxyDeath") => Ok(NotificationProxyEvent::ProxyDeath),
121 other => Err(NotificationProxyError::Protocol(format!(
122 "unexpected notification proxy command: {}",
123 other.unwrap_or("<missing>")
124 ))),
125 }
126 }
127
128 pub async fn shutdown(&mut self) -> Result<(), NotificationProxyError> {
129 self.send_request(NotificationProxyRequest {
130 command: "Shutdown",
131 name: None,
132 })
133 .await
134 }
135
136 pub async fn recv_event(&mut self) -> Result<NotificationEvent, NotificationProxyError> {
137 let message = self.recv_message().await?;
138 match message.command.as_deref() {
139 Some("RelayNotification") => Ok(NotificationEvent::Notification(
140 message.name.ok_or_else(|| {
141 NotificationProxyError::Protocol("RelayNotification missing Name".to_string())
142 })?,
143 )),
144 Some("ProxyDeath") => Ok(NotificationEvent::ProxyDeath),
145 Some(other) => Err(NotificationProxyError::Protocol(format!(
146 "unexpected notification proxy command: {other}"
147 ))),
148 None => Err(NotificationProxyError::Protocol(
149 "notification proxy message missing Command".to_string(),
150 )),
151 }
152 }
153
154 async fn send_request(
155 &mut self,
156 request: NotificationProxyRequest<'_>,
157 ) -> Result<(), NotificationProxyError> {
158 let mut buf = Vec::new();
159 plist::to_writer_xml(&mut buf, &request)?;
160 self.stream
161 .write_all(&(buf.len() as u32).to_be_bytes())
162 .await?;
163 self.stream.write_all(&buf).await?;
164 self.stream.flush().await?;
165 Ok(())
166 }
167
168 async fn recv_message(&mut self) -> Result<NotificationProxyMessage, NotificationProxyError> {
169 let mut len_buf = [0u8; 4];
170 self.stream.read_exact(&mut len_buf).await?;
171 let len = u32::from_be_bytes(len_buf) as usize;
172 const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
173 if len > MAX_PLIST_SIZE {
174 return Err(NotificationProxyError::Protocol(format!(
175 "plist length {len} exceeds max {MAX_PLIST_SIZE}"
176 )));
177 }
178 let mut buf = vec![0u8; len];
179 self.stream.read_exact(&mut buf).await?;
180 Ok(plist::from_bytes(&buf)?)
181 }
182}
183
184#[derive(Serialize)]
185#[serde(rename_all = "PascalCase")]
186struct NotificationProxyRequest<'a> {
187 command: &'static str,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 name: Option<&'a str>,
190}
191
192#[derive(Debug, Deserialize)]
193#[serde(rename_all = "PascalCase")]
194struct NotificationProxyMessage {
195 #[serde(default)]
196 command: Option<String>,
197 #[serde(default)]
198 name: Option<String>,
199}
200
201#[cfg(test)]
202mod tests {
203 use crate::test_util::MockStream;
204
205 use super::*;
206
207 fn plist_frame(value: plist::Value) -> Vec<u8> {
208 let mut buf = Vec::new();
209 plist::to_writer_xml(&mut buf, &value).unwrap();
210 buf
211 }
212
213 #[tokio::test]
214 async fn observe_encodes_notification_request() {
215 let mut stream = MockStream::default();
216 let mut client = NotificationProxyClient::new(&mut stream);
217 client.observe("com.apple.example.ready").await.unwrap();
218
219 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
220 let payload = &stream.written[4..4 + len];
221 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
222 assert_eq!(dict["Command"].as_string(), Some("ObserveNotification"));
223 assert_eq!(dict["Name"].as_string(), Some("com.apple.example.ready"));
224 }
225
226 #[tokio::test]
227 async fn post_encodes_notification_request() {
228 let mut stream = MockStream::default();
229 let mut client = NotificationProxyClient::new(&mut stream);
230 client.post("com.apple.example.trigger").await.unwrap();
231
232 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
233 let payload = &stream.written[4..4 + len];
234 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
235 assert_eq!(dict["Command"].as_string(), Some("PostNotification"));
236 assert_eq!(dict["Name"].as_string(), Some("com.apple.example.trigger"));
237 }
238
239 #[tokio::test]
240 async fn wait_for_matches_relay_notification() {
241 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([
242 (
243 "Command".to_string(),
244 plist::Value::String("RelayNotification".into()),
245 ),
246 (
247 "Name".to_string(),
248 plist::Value::String("com.apple.example.ready".into()),
249 ),
250 ])));
251 let mut stream = MockStream::with_frames(vec![frame]);
252 let mut client = NotificationProxyClient::new(&mut stream);
253
254 client
255 .wait_for("com.apple.example.ready", Duration::from_millis(100))
256 .await
257 .unwrap();
258 }
259
260 #[tokio::test]
261 async fn recv_event_decodes_relay_notification() {
262 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([
263 (
264 "Command".to_string(),
265 plist::Value::String("RelayNotification".into()),
266 ),
267 (
268 "Name".to_string(),
269 plist::Value::String("com.apple.example.ready".into()),
270 ),
271 ])));
272 let mut stream = MockStream::with_frames(vec![frame]);
273 let mut client = NotificationProxyClient::new(&mut stream);
274
275 let event = client.recv_event().await.unwrap();
276 assert_eq!(
277 event,
278 NotificationEvent::Notification("com.apple.example.ready".into())
279 );
280 }
281
282 #[tokio::test]
283 async fn recv_event_decodes_proxy_death() {
284 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([(
285 "Command".to_string(),
286 plist::Value::String("ProxyDeath".into()),
287 )])));
288 let mut stream = MockStream::with_frames(vec![frame]);
289 let mut client = NotificationProxyClient::new(&mut stream);
290
291 let event = client.recv_event().await.unwrap();
292 assert_eq!(event, NotificationEvent::ProxyDeath);
293 }
294
295 #[tokio::test]
296 async fn wait_for_springboard_uses_expected_name() {
297 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([
298 (
299 "Command".to_string(),
300 plist::Value::String("RelayNotification".into()),
301 ),
302 (
303 "Name".to_string(),
304 plist::Value::String(SPRINGBOARD_FINISHED_STARTUP.into()),
305 ),
306 ])));
307 let mut stream = MockStream::with_frames(vec![frame]);
308 let mut client = NotificationProxyClient::new(&mut stream);
309
310 client
311 .wait_for_springboard(Duration::from_millis(100))
312 .await
313 .unwrap();
314 }
315
316 #[tokio::test]
317 async fn next_event_returns_notification_name() {
318 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([
319 (
320 "Command".to_string(),
321 plist::Value::String("RelayNotification".into()),
322 ),
323 (
324 "Name".to_string(),
325 plist::Value::String("com.apple.example.stream".into()),
326 ),
327 ])));
328 let mut stream = MockStream::with_frames(vec![frame]);
329 let mut client = NotificationProxyClient::new(&mut stream);
330
331 let event = client.next_event(Duration::from_millis(100)).await.unwrap();
332 assert_eq!(
333 event,
334 NotificationProxyEvent::Notification("com.apple.example.stream".into())
335 );
336 }
337
338 #[tokio::test]
339 async fn next_event_maps_proxy_death() {
340 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([(
341 "Command".to_string(),
342 plist::Value::String("ProxyDeath".into()),
343 )])));
344 let mut stream = MockStream::with_frames(vec![frame]);
345 let mut client = NotificationProxyClient::new(&mut stream);
346
347 let event = client.next_event(Duration::from_millis(100)).await.unwrap();
348 assert_eq!(event, NotificationProxyEvent::ProxyDeath);
349 }
350
351 #[tokio::test]
352 async fn wait_for_times_out_when_no_notification_arrives() {
353 let (client_side, _server_side) = tokio::io::duplex(1024);
354 let mut client = NotificationProxyClient::new(client_side);
355
356 let err = client
357 .wait_for("com.apple.example.ready", Duration::from_millis(10))
358 .await
359 .unwrap_err();
360 assert!(matches!(err, NotificationProxyError::Timeout));
361 }
362}