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 .map_err(|e| NotificationProxyError::Plist(e.to_string()))?;
161 self.stream
162 .write_all(&(buf.len() as u32).to_be_bytes())
163 .await?;
164 self.stream.write_all(&buf).await?;
165 self.stream.flush().await?;
166 Ok(())
167 }
168
169 async fn recv_message(&mut self) -> Result<NotificationProxyMessage, NotificationProxyError> {
170 let mut len_buf = [0u8; 4];
171 self.stream.read_exact(&mut len_buf).await?;
172 let len = u32::from_be_bytes(len_buf) as usize;
173 const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
174 if len > MAX_PLIST_SIZE {
175 return Err(NotificationProxyError::Protocol(format!(
176 "plist length {len} exceeds max {MAX_PLIST_SIZE}"
177 )));
178 }
179 let mut buf = vec![0u8; len];
180 self.stream.read_exact(&mut buf).await?;
181 plist::from_bytes(&buf).map_err(|e| NotificationProxyError::Plist(e.to_string()))
182 }
183}
184
185#[derive(Serialize)]
186#[serde(rename_all = "PascalCase")]
187struct NotificationProxyRequest<'a> {
188 command: &'static str,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 name: Option<&'a str>,
191}
192
193#[derive(Debug, Deserialize)]
194#[serde(rename_all = "PascalCase")]
195struct NotificationProxyMessage {
196 #[serde(default)]
197 command: Option<String>,
198 #[serde(default)]
199 name: Option<String>,
200}
201
202#[cfg(test)]
203mod tests {
204 use crate::test_util::MockStream;
205
206 use super::*;
207
208 fn plist_frame(value: plist::Value) -> Vec<u8> {
209 let mut buf = Vec::new();
210 plist::to_writer_xml(&mut buf, &value).unwrap();
211 buf
212 }
213
214 #[tokio::test]
215 async fn observe_encodes_notification_request() {
216 let mut stream = MockStream::default();
217 let mut client = NotificationProxyClient::new(&mut stream);
218 client.observe("com.apple.example.ready").await.unwrap();
219
220 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
221 let payload = &stream.written[4..4 + len];
222 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
223 assert_eq!(dict["Command"].as_string(), Some("ObserveNotification"));
224 assert_eq!(dict["Name"].as_string(), Some("com.apple.example.ready"));
225 }
226
227 #[tokio::test]
228 async fn post_encodes_notification_request() {
229 let mut stream = MockStream::default();
230 let mut client = NotificationProxyClient::new(&mut stream);
231 client.post("com.apple.example.trigger").await.unwrap();
232
233 let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
234 let payload = &stream.written[4..4 + len];
235 let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
236 assert_eq!(dict["Command"].as_string(), Some("PostNotification"));
237 assert_eq!(dict["Name"].as_string(), Some("com.apple.example.trigger"));
238 }
239
240 #[tokio::test]
241 async fn wait_for_matches_relay_notification() {
242 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([
243 (
244 "Command".to_string(),
245 plist::Value::String("RelayNotification".into()),
246 ),
247 (
248 "Name".to_string(),
249 plist::Value::String("com.apple.example.ready".into()),
250 ),
251 ])));
252 let mut stream = MockStream::with_frames(vec![frame]);
253 let mut client = NotificationProxyClient::new(&mut stream);
254
255 client
256 .wait_for("com.apple.example.ready", Duration::from_millis(100))
257 .await
258 .unwrap();
259 }
260
261 #[tokio::test]
262 async fn recv_event_decodes_relay_notification() {
263 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([
264 (
265 "Command".to_string(),
266 plist::Value::String("RelayNotification".into()),
267 ),
268 (
269 "Name".to_string(),
270 plist::Value::String("com.apple.example.ready".into()),
271 ),
272 ])));
273 let mut stream = MockStream::with_frames(vec![frame]);
274 let mut client = NotificationProxyClient::new(&mut stream);
275
276 let event = client.recv_event().await.unwrap();
277 assert_eq!(
278 event,
279 NotificationEvent::Notification("com.apple.example.ready".into())
280 );
281 }
282
283 #[tokio::test]
284 async fn recv_event_decodes_proxy_death() {
285 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([(
286 "Command".to_string(),
287 plist::Value::String("ProxyDeath".into()),
288 )])));
289 let mut stream = MockStream::with_frames(vec![frame]);
290 let mut client = NotificationProxyClient::new(&mut stream);
291
292 let event = client.recv_event().await.unwrap();
293 assert_eq!(event, NotificationEvent::ProxyDeath);
294 }
295
296 #[tokio::test]
297 async fn wait_for_springboard_uses_expected_name() {
298 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([
299 (
300 "Command".to_string(),
301 plist::Value::String("RelayNotification".into()),
302 ),
303 (
304 "Name".to_string(),
305 plist::Value::String(SPRINGBOARD_FINISHED_STARTUP.into()),
306 ),
307 ])));
308 let mut stream = MockStream::with_frames(vec![frame]);
309 let mut client = NotificationProxyClient::new(&mut stream);
310
311 client
312 .wait_for_springboard(Duration::from_millis(100))
313 .await
314 .unwrap();
315 }
316
317 #[tokio::test]
318 async fn next_event_returns_notification_name() {
319 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([
320 (
321 "Command".to_string(),
322 plist::Value::String("RelayNotification".into()),
323 ),
324 (
325 "Name".to_string(),
326 plist::Value::String("com.apple.example.stream".into()),
327 ),
328 ])));
329 let mut stream = MockStream::with_frames(vec![frame]);
330 let mut client = NotificationProxyClient::new(&mut stream);
331
332 let event = client.next_event(Duration::from_millis(100)).await.unwrap();
333 assert_eq!(
334 event,
335 NotificationProxyEvent::Notification("com.apple.example.stream".into())
336 );
337 }
338
339 #[tokio::test]
340 async fn next_event_maps_proxy_death() {
341 let frame = plist_frame(plist::Value::Dictionary(plist::Dictionary::from_iter([(
342 "Command".to_string(),
343 plist::Value::String("ProxyDeath".into()),
344 )])));
345 let mut stream = MockStream::with_frames(vec![frame]);
346 let mut client = NotificationProxyClient::new(&mut stream);
347
348 let event = client.next_event(Duration::from_millis(100)).await.unwrap();
349 assert_eq!(event, NotificationProxyEvent::ProxyDeath);
350 }
351
352 #[tokio::test]
353 async fn wait_for_times_out_when_no_notification_arrives() {
354 let (client_side, _server_side) = tokio::io::duplex(1024);
355 let mut client = NotificationProxyClient::new(client_side);
356
357 let err = client
358 .wait_for("com.apple.example.ready", Duration::from_millis(10))
359 .await
360 .unwrap_err();
361 assert!(matches!(err, NotificationProxyError::Timeout));
362 }
363}