From bfe44e16e4de59c5728078ad50199fda248951d1 Mon Sep 17 00:00:00 2001 From: neo Date: Sat, 14 Feb 2026 15:16:26 -0500 Subject: [PATCH] feat: notification proxy (#70) * init * chore: clippy and fmt * feat: ffi wrapper * feat: multi-observe and timeout to notification proxy * fix: nitpicks 1. proxy death its onw error in emun #69 2. make returned stream actual stream, copied from https://github.com/jkcoxson/idevice/blob/54439b85dd48663a4562ad01f63fbc57351e1f3d/idevice/src/services/bt_packet_logger.rs#L126-L138 --- cpp/include/idevice++/notification_proxy.hpp | 46 +++ cpp/src/notification_proxy.cpp | 82 +++++ ffi/Cargo.toml | 2 + ffi/src/lib.rs | 2 + ffi/src/notification_proxy.rs | 311 +++++++++++++++++++ idevice/Cargo.toml | 2 + idevice/src/lib.rs | 7 + idevice/src/services/mod.rs | 2 + idevice/src/services/notification_proxy.rs | 212 +++++++++++++ tools/src/main.rs | 5 + tools/src/notification_proxy_client.rs | 85 +++++ 11 files changed, 756 insertions(+) create mode 100644 cpp/include/idevice++/notification_proxy.hpp create mode 100644 cpp/src/notification_proxy.cpp create mode 100644 ffi/src/notification_proxy.rs create mode 100644 idevice/src/services/notification_proxy.rs create mode 100644 tools/src/notification_proxy_client.rs diff --git a/cpp/include/idevice++/notification_proxy.hpp b/cpp/include/idevice++/notification_proxy.hpp new file mode 100644 index 0000000..f4bdcf8 --- /dev/null +++ b/cpp/include/idevice++/notification_proxy.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using NotificationProxyPtr = std::unique_ptr>; + +class NotificationProxy { + public: + // Factory: connect via Provider + static Result connect(Provider& provider); + + // Factory: wrap an existing Idevice socket (consumes it on success) + static Result from_socket(Idevice&& socket); + + // Ops + Result post_notification(const std::string& name); + Result observe_notification(const std::string& name); + Result observe_notifications(const std::vector& names); + Result receive_notification(); + Result receive_notification_with_timeout(u_int64_t interval); + + // RAII / moves + ~NotificationProxy() noexcept = default; + NotificationProxy(NotificationProxy&&) noexcept = default; + NotificationProxy& operator=(NotificationProxy&&) noexcept = default; + NotificationProxy(const NotificationProxy&) = delete; + NotificationProxy& operator=(const NotificationProxy&) = delete; + + NotificationProxyClientHandle* raw() const noexcept { return handle_.get(); } + static NotificationProxy adopt(NotificationProxyClientHandle* h) noexcept { + return NotificationProxy(h); + } + + private: + explicit NotificationProxy(NotificationProxyClientHandle* h) noexcept : handle_(h) {} + NotificationProxyPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/notification_proxy.cpp b/cpp/src/notification_proxy.cpp new file mode 100644 index 0000000..dea5664 --- /dev/null +++ b/cpp/src/notification_proxy.cpp @@ -0,0 +1,82 @@ +// Jackson Coxson + +#include +#include +#include +#include + +namespace IdeviceFFI { + +Result NotificationProxy::connect(Provider& provider) { + NotificationProxyClientHandle* out = nullptr; + FfiError e(::notification_proxy_connect(provider.raw(), &out)); + if (e) { + provider.release(); + return Err(e); + } + return Ok(NotificationProxy::adopt(out)); +} + +Result NotificationProxy::from_socket(Idevice&& socket) { + NotificationProxyClientHandle* out = nullptr; + FfiError e(::notification_proxy_new(socket.raw(), &out)); + if (e) { + return Err(e); + } + socket.release(); + return Ok(NotificationProxy::adopt(out)); +} + +Result NotificationProxy::post_notification(const std::string& name) { + FfiError e(::notification_proxy_post(handle_.get(), name.c_str())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result NotificationProxy::observe_notification(const std::string& name) { + FfiError e(::notification_proxy_observe(handle_.get(), name.c_str())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result NotificationProxy::observe_notifications(const std::vector& names) { + std::vector ptrs; + ptrs.reserve(names.size() + 1); + for (const auto& n : names) { + ptrs.push_back(n.c_str()); + } + ptrs.push_back(nullptr); + FfiError e(::notification_proxy_observe_multiple(handle_.get(), ptrs.data())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result NotificationProxy::receive_notification() { + char* name_ptr = nullptr; + FfiError e(::notification_proxy_receive(handle_.get(), &name_ptr)); + if (e) { + return Err(e); + } + std::string name(name_ptr); + ::notification_proxy_free_string(name_ptr); + return Ok(std::move(name)); +} + +Result NotificationProxy::receive_notification_with_timeout(u_int64_t interval) { + char* name_ptr = nullptr; + FfiError e(::notification_proxy_receive_with_timeout(handle_.get(), interval, &name_ptr)); + if (e) { + return Err(e); + } + std::string name(name_ptr); + ::notification_proxy_free_string(name_ptr); + return Ok(std::move(name)); +} + +} // namespace IdeviceFFI diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 4e3d8f7..6b8e70e 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -34,6 +34,7 @@ debug_proxy = ["idevice/debug_proxy"] diagnostics_relay = ["idevice/diagnostics_relay"] dvt = ["idevice/dvt"] heartbeat = ["idevice/heartbeat"] +notification_proxy = ["idevice/notification_proxy"] house_arrest = ["idevice/house_arrest"] installation_proxy = ["idevice/installation_proxy"] springboardservices = ["idevice/springboardservices"] @@ -61,6 +62,7 @@ full = [ "diagnostics_relay", "dvt", "heartbeat", + "notification_proxy", "house_arrest", "installation_proxy", "misagent", diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 31e0147..6b8d004 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -29,6 +29,8 @@ pub mod logging; pub mod misagent; #[cfg(feature = "mobile_image_mounter")] pub mod mobile_image_mounter; +#[cfg(feature = "notification_proxy")] +pub mod notification_proxy; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; mod pairing_file; diff --git a/ffi/src/notification_proxy.rs b/ffi/src/notification_proxy.rs new file mode 100644 index 0000000..d883f88 --- /dev/null +++ b/ffi/src/notification_proxy.rs @@ -0,0 +1,311 @@ +// Jackson Coxson + +use std::ffi::{CStr, CString, c_char}; +use std::ptr::null_mut; + +use idevice::{ + IdeviceError, IdeviceService, notification_proxy::NotificationProxyClient, + provider::IdeviceProvider, +}; + +use crate::{ + IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync_local, +}; + +pub struct NotificationProxyClientHandle(pub NotificationProxyClient); + +/// Automatically creates and connects to Notification Proxy, returning a client handle +/// +/// # Arguments +/// * [`provider`] - An IdeviceProvider +/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_connect( + provider: *mut IdeviceProviderHandle, + client: *mut *mut NotificationProxyClientHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || client.is_null() { + tracing::error!("Null pointer provided"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 }; + NotificationProxyClient::connect(provider_ref).await + }); + + match res { + Ok(r) => { + let boxed = Box::new(NotificationProxyClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => { + ffi_err!(e) + } + } +} + +/// Creates a new NotificationProxyClient from an existing Idevice connection +/// +/// # Arguments +/// * [`socket`] - An IdeviceSocket handle +/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `socket` must be a valid pointer to a handle allocated by this library. The socket is consumed, +/// and may not be used again. +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_new( + socket: *mut IdeviceHandle, + client: *mut *mut NotificationProxyClientHandle, +) -> *mut IdeviceFfiError { + if socket.is_null() || client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let socket = unsafe { Box::from_raw(socket) }.0; + let r = NotificationProxyClient::new(socket); + let boxed = Box::new(NotificationProxyClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() +} + +/// Posts a notification to the device +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `name` - C string containing the notification name +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name` must be a valid null-terminated C string +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_post( + client: *mut NotificationProxyClientHandle, + name: *const c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let name_str = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + }; + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.post_notification(name_str).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Observes a specific notification +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `name` - C string containing the notification name to observe +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name` must be a valid null-terminated C string +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_observe( + client: *mut NotificationProxyClientHandle, + name: *const c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let name_str = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + }; + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.observe_notification(name_str).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Observes multiple notifications at once +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `names` - A null-terminated array of C strings containing notification names +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `names` must be a valid pointer to a null-terminated array of null-terminated C strings +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_observe_multiple( + client: *mut NotificationProxyClientHandle, + names: *const *const c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || names.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let mut notification_names: Vec = Vec::new(); + let mut i = 0; + loop { + let ptr = unsafe { *names.add(i) }; + if ptr.is_null() { + break; + } + match unsafe { CStr::from_ptr(ptr) }.to_str() { + Ok(s) => notification_names.push(s.to_string()), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + } + i += 1; + } + + let refs: Vec<&str> = notification_names.iter().map(|s| s.as_str()).collect(); + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.observe_notifications(&refs).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Receives the next notification from the device +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_receive( + client: *mut NotificationProxyClientHandle, + name_out: *mut *mut c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name_out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.receive_notification().await + }); + + match res { + Ok(name) => match CString::new(name) { + Ok(c_string) => { + unsafe { *name_out = c_string.into_raw() }; + null_mut() + } + Err(_) => ffi_err!(IdeviceError::FfiInvalidString), + }, + Err(e) => ffi_err!(e), + } +} + +/// Receives the next notification with a timeout +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `interval` - Timeout in seconds to wait for a notification +/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_receive_with_timeout( + client: *mut NotificationProxyClientHandle, + interval: u64, + name_out: *mut *mut c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name_out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.receive_notification_with_timeout(interval).await + }); + + match res { + Ok(name) => match CString::new(name) { + Ok(c_string) => { + unsafe { *name_out = c_string.into_raw() }; + null_mut() + } + Err(_) => ffi_err!(IdeviceError::FfiInvalidString), + }, + Err(e) => ffi_err!(e), + } +} + +/// Frees a string returned by notification_proxy_receive +/// +/// # Safety +/// `s` must be a valid pointer returned from `notification_proxy_receive` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_free_string(s: *mut c_char) { + if !s.is_null() { + let _ = unsafe { CString::from_raw(s) }; + } +} + +/// Frees a handle +/// +/// # Arguments +/// * [`handle`] - The handle to free +/// +/// # Safety +/// `handle` must be a valid pointer to the handle that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_client_free( + handle: *mut NotificationProxyClientHandle, +) { + if !handle.is_null() { + tracing::debug!("Freeing notification_proxy_client"); + let _ = unsafe { Box::from_raw(handle) }; + } +} diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index c929127..3a448f8 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -97,6 +97,7 @@ misagent = [] mobile_image_mounter = ["dep:sha2"] mobileactivationd = ["dep:reqwest"] mobilebackup2 = [] +notification_proxy = ["tokio/macros", "tokio/time", "dep:async-stream", "dep:futures"] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] pcapd = [] @@ -138,6 +139,7 @@ full = [ "mobile_image_mounter", "mobileactivationd", "mobilebackup2", + "notification_proxy", "pair", "pcapd", "preboard_service", diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 5081f33..ba62ed0 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -865,6 +865,10 @@ pub enum IdeviceError { #[error("Developer mode is not enabled")] DeveloperModeNotEnabled = -68, + + #[cfg(feature = "notification_proxy")] + #[error("notification proxy died")] + NotificationProxyDeath = -69, } impl IdeviceError { @@ -1030,6 +1034,9 @@ impl IdeviceError { #[cfg(feature = "installation_proxy")] IdeviceError::MalformedPackageArchive(_) => -67, IdeviceError::DeveloperModeNotEnabled => -68, + + #[cfg(feature = "notification_proxy")] + IdeviceError::NotificationProxyDeath => -69, } } } diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index e795578..802b837 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -35,6 +35,8 @@ pub mod mobile_image_mounter; pub mod mobileactivationd; #[cfg(feature = "mobilebackup2")] pub mod mobilebackup2; +#[cfg(feature = "notification_proxy")] +pub mod notification_proxy; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; #[cfg(feature = "pcapd")] diff --git a/idevice/src/services/notification_proxy.rs b/idevice/src/services/notification_proxy.rs new file mode 100644 index 0000000..e389a43 --- /dev/null +++ b/idevice/src/services/notification_proxy.rs @@ -0,0 +1,212 @@ +//! iOS Device Notification Proxy Service +//! +//! Based on libimobiledevice's notification_proxy implementation +//! +//! Common notification identifiers: +//! Full list: include/libimobiledevice/notification_proxy.h +//! +//! - Notifications that can be sent (PostNotification): +//! - `com.apple.itunes-mobdev.syncWillStart` - Sync will start +//! - `com.apple.itunes-mobdev.syncDidStart` - Sync started +//! - `com.apple.itunes-mobdev.syncDidFinish` - Sync finished +//! - `com.apple.itunes-mobdev.syncLockRequest` - Request sync lock +//! +//! - Notifications that can be observed (ObserveNotification): +//! - `com.apple.itunes-client.syncCancelRequest` - Cancel sync request +//! - `com.apple.itunes-client.syncSuspendRequest` - Suspend sync +//! - `com.apple.itunes-client.syncResumeRequest` - Resume sync +//! - `com.apple.mobile.lockdown.phone_number_changed` - Phone number changed +//! - `com.apple.mobile.lockdown.device_name_changed` - Device name changed +//! - `com.apple.mobile.lockdown.timezone_changed` - Timezone changed +//! - `com.apple.mobile.lockdown.trusted_host_attached` - Trusted host attached +//! - `com.apple.mobile.lockdown.host_detached` - Host detached +//! - `com.apple.mobile.lockdown.host_attached` - Host attached +//! - `com.apple.mobile.lockdown.registration_failed` - Registration failed +//! - `com.apple.mobile.lockdown.activation_state` - Activation state +//! - `com.apple.mobile.lockdown.brick_state` - Brick state +//! - `com.apple.mobile.lockdown.disk_usage_changed` - Disk usage (iOS 4.0+) +//! - `com.apple.mobile.data_sync.domain_changed` - Data sync domain changed +//! - `com.apple.mobile.application_installed` - App installed +//! - `com.apple.mobile.application_uninstalled` - App uninstalled + +use std::pin::Pin; + +use futures::Stream; +use tracing::warn; + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; + +/// Client for interacting with the iOS notification proxy service +/// +/// The notification proxy service provides a mechanism to observe and post +/// system notifications. +/// +/// Use `observe_notification` to register for events, then `receive_notification` +/// to wait for them. +#[derive(Debug)] +pub struct NotificationProxyClient { + /// The underlying device connection with established notification_proxy service + pub idevice: Idevice, +} + +impl IdeviceService for NotificationProxyClient { + /// Returns the notification proxy service name as registered with lockdownd + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.mobile.notification_proxy") + } + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl NotificationProxyClient { + /// Creates a new notification proxy client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + /// Posts a notification to the device + /// + /// # Arguments + /// * `notification_name` - Name of the notification to post + /// + /// # Errors + /// Returns `IdeviceError` if the notification fails to send + pub async fn post_notification( + &mut self, + notification_name: impl Into, + ) -> Result<(), IdeviceError> { + let request = crate::plist!({ + "Command": "PostNotification", + "Name": notification_name.into() + }); + self.idevice.send_plist(request).await + } + + /// Registers to observe a specific notification + /// + /// After calling this, use `receive_notification` to wait for events. + /// + /// # Arguments + /// * `notification_name` - Name of the notification to observe + /// + /// # Errors + /// Returns `IdeviceError` if the registration fails + pub async fn observe_notification( + &mut self, + notification_name: impl Into, + ) -> Result<(), IdeviceError> { + let request = crate::plist!({ + "Command": "ObserveNotification", + "Name": notification_name.into() + }); + self.idevice.send_plist(request).await + } + + /// Registers to observe multiple notifications at once + /// + /// # Arguments + /// * `notification_names` - Slice of notification names to observe + /// + /// # Errors + /// Returns `IdeviceError` if any registration fails + pub async fn observe_notifications( + &mut self, + notification_names: &[&str], + ) -> Result<(), IdeviceError> { + for name in notification_names { + self.observe_notification(*name).await?; + } + Ok(()) + } + + /// Waits for and receives the next notification from the device + /// + /// # Returns + /// The name of the received notification + /// + /// # Errors + /// - `NotificationProxyDeath` if the proxy connection died + /// - `UnexpectedResponse` if the response format is invalid + pub async fn receive_notification(&mut self) -> Result { + let response = self.idevice.read_plist().await?; + + match response.get("Command").and_then(|c| c.as_string()) { + Some("RelayNotification") => match response.get("Name").and_then(|n| n.as_string()) { + Some(name) => Ok(name.to_string()), + None => Err(IdeviceError::UnexpectedResponse), + }, + Some("ProxyDeath") => { + warn!("NotificationProxy died!"); + Err(IdeviceError::NotificationProxyDeath) + } + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Waits for a notification with a timeout + /// + /// # Arguments + /// * `interval` - Timeout in seconds to wait for a notification + /// + /// # Returns + /// The name of the received notification + /// + /// # Errors + /// - `NotificationProxyDeath` if the proxy connection died + /// - `UnexpectedResponse` if the response format is invalid + /// - `HeartbeatTimeout` if no notification received before interval + pub async fn receive_notification_with_timeout( + &mut self, + interval: u64, + ) -> Result { + tokio::select! { + result = self.receive_notification() => result, + _ = tokio::time::sleep(tokio::time::Duration::from_secs(interval)) => { + Err(IdeviceError::HeartbeatTimeout) + } + } + } + + /// Continuous stream of notifications. + pub fn into_stream( + mut self, + ) -> Pin> + Send>> { + Box::pin(async_stream::try_stream! { + loop { + let response = self.idevice.read_plist().await?; + + match response.get("Command").and_then(|c| c.as_string()) { + Some("RelayNotification") => { + match response.get("Name").and_then(|n| n.as_string()) { + Some(name) => yield name.to_string(), + None => Err(IdeviceError::UnexpectedResponse)?, + } + } + Some("ProxyDeath") => { + warn!("NotificationProxy died!"); + Err(IdeviceError::NotificationProxyDeath)?; + } + _ => Err(IdeviceError::UnexpectedResponse)?, + } + } + }) + } + + /// Shuts down the notification proxy connection + /// + /// # Errors + /// Returns `IdeviceError` if the shutdown command fails to send + pub async fn shutdown(&mut self) -> Result<(), IdeviceError> { + let request = crate::plist!({ + "Command": "Shutdown" + }); + self.idevice.send_plist(request).await?; + // Best-effort: wait for ProxyDeath ack + let _ = self.idevice.read_plist().await; + Ok(()) + } +} diff --git a/tools/src/main.rs b/tools/src/main.rs index 4bd3d77..db70ea4 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -33,6 +33,7 @@ mod lockdown; mod misagent; mod mobilebackup2; mod mounter; +mod notification_proxy_client; mod notifications; mod os_trace_relay; mod pair; @@ -113,6 +114,7 @@ async fn main() { .with_subcommand("mobilebackup2", mobilebackup2::register()) .with_subcommand("mounter", mounter::register()) .with_subcommand("notifications", notifications::register()) + .with_subcommand("notification_proxy", notification_proxy_client::register()) .with_subcommand("os_trace_relay", os_trace_relay::register()) .with_subcommand("pair", pair::register()) .with_subcommand("pcapd", pcapd::register()) @@ -214,6 +216,9 @@ async fn main() { "notifications" => { notifications::main(sub_args, provider).await; } + "notification_proxy" => { + notification_proxy_client::main(sub_args, provider).await; + } "os_trace_relay" => { os_trace_relay::main(sub_args, provider).await; } diff --git a/tools/src/notification_proxy_client.rs b/tools/src/notification_proxy_client.rs new file mode 100644 index 0000000..9559d62 --- /dev/null +++ b/tools/src/notification_proxy_client.rs @@ -0,0 +1,85 @@ +// Jackson Coxson + +use idevice::{ + IdeviceService, notification_proxy::NotificationProxyClient, provider::IdeviceProvider, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; + +pub fn register() -> JkCommand { + JkCommand::new() + .help("Notification proxy") + .with_subcommand( + "observe", + JkCommand::new() + .help("Observe notifications from the device") + .with_argument( + JkArgument::new() + .with_help("The notification ID to observe") + .required(true), + ), + ) + .with_subcommand( + "post", + JkCommand::new() + .help("Post a notification to the device") + .with_argument( + JkArgument::new() + .with_help("The notification ID to post") + .required(true), + ), + ) + .subcommand_required(true) +} + +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let mut client = NotificationProxyClient::connect(&*provider) + .await + .expect("Unable to connect to notification proxy"); + + let (subcommand, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + + match subcommand.as_str() { + "observe" => { + let input: String = sub_args + .next_argument::() + .expect("No notification ID passed"); + + let notifications: Vec<&str> = input.split_whitespace().collect(); + client + .observe_notifications(¬ifications) + .await + .expect("Failed to observe notifications"); + + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + println!("\nShutdown signal received, exiting."); + break; + } + + result = client.receive_notification() => { + match result { + Ok(notif) => println!("Received notification: {}", notif), + Err(e) => { + eprintln!("Failed to receive notification: {}", e); + break; + } + } + } + } + } + } + "post" => { + let notification: String = sub_args + .next_argument::() + .expect("No notification ID passed"); + + client + .post_notification(¬ification) + .await + .expect("Failed to post notification"); + } + _ => unreachable!(), + } +}