7 Commits

Author SHA1 Message Date
uncor3
c752ee92c5 Add lockdownd_pair to ffi (#75)
* Add lockdownd_pair to ffi

* fix missing param host_name
2026-02-22 19:47:02 -07:00
neo
76d847664b feat(crashreportcopymobile): ffi bindings (#71)
* feat(crashreportcopymobile): ffi bindings

* chore: update readme
2026-02-16 19:12:17 -07:00
Nicholas Sharp
1f7924b773 Add ApplicationVerificationFailed to list of known errors (#72)
* Add ApplicationVerificationFailed to list of known errors

* Add missing cfg directive
2026-02-16 19:11:54 -07:00
khcrysalis
b459eebe9d feat: support ios-arm64_x86_64-maccatalyst in xcframework (#69)
* feat: support maccatalyst in xcframework

* fix: missing ios-macabi targets in ci
2026-02-14 13:19:32 -07:00
neo
c246362f54 chore(readme): update (#68)
* feat(springboard): add get_icon subcommand

* chore: update readme
2026-02-14 13:18:37 -07:00
neo
bfe44e16e4 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 54439b85dd/idevice/src/services/bt_packet_logger.rs (L126-L138)
2026-02-14 13:16:26 -07:00
neo
54439b85dd feat(springboard): get homescreen icon metrics (#67)
* feat(springboard): get homescreen icon metrics

* chore: clippy and fmt
2026-02-13 13:00:47 -07:00
21 changed files with 1452 additions and 14 deletions

View File

@@ -35,6 +35,8 @@ jobs:
rustup target add aarch64-apple-ios-sim && \ rustup target add aarch64-apple-ios-sim && \
rustup target add aarch64-apple-darwin && \ rustup target add aarch64-apple-darwin && \
rustup target add x86_64-apple-darwin && \ rustup target add x86_64-apple-darwin && \
rustup target add aarch64-apple-ios-macabi && \
rustup target add x86_64-apple-ios-macabi && \
cargo install --force --locked bindgen-cli cargo install --force --locked bindgen-cli
- name: Build all Apple targets and examples/tools - name: Build all Apple targets and examples/tools

View File

@@ -47,6 +47,8 @@ To keep dependency bloat and compile time down, everything is contained in featu
|------------------------|-----------------------------------------------------------------------------| |------------------------|-----------------------------------------------------------------------------|
| `afc` | Apple File Conduit for file system access.| | `afc` | Apple File Conduit for file system access.|
| `amfi` | Apple mobile file integrity service | | `amfi` | Apple mobile file integrity service |
| `bt_packet_logger` | Capture Bluetooth packets. |
| `companion_proxy` | Manage paired Apple Watches. |
| `core_device_proxy` | Start a secure tunnel to access protected services. | | `core_device_proxy` | Start a secure tunnel to access protected services. |
| `crashreportcopymobile`| Copy crash reports.| | `crashreportcopymobile`| Copy crash reports.|
| `debug_proxy` | Send GDB commands to the device.| | `debug_proxy` | Send GDB commands to the device.|
@@ -55,34 +57,36 @@ To keep dependency bloat and compile time down, everything is contained in featu
| `heartbeat` | Maintain a heartbeat connection.| | `heartbeat` | Maintain a heartbeat connection.|
| `house_arrest` | Manage files in app containers | | `house_arrest` | Manage files in app containers |
| `installation_proxy` | Manage app installation and uninstallation.| | `installation_proxy` | Manage app installation and uninstallation.|
| `springboardservices` | Control SpringBoard (e.g. UI interactions). Partial support.| | `installcoordination_proxy` | Manage app installation coordination.|
| `misagent` | Manage provisioning profiles on the device.|
| `mobilebackup2` | Manage backups.|
| `mobile_image_mounter` | Manage DDI images.|
| `location_simulation` | Simulate GPS locations on the device.| | `location_simulation` | Simulate GPS locations on the device.|
| `misagent` | Manage provisioning profiles on the device.|
| `mobile_image_mounter` | Manage DDI images.|
| `mobileactivationd` | Activate/Deactivate device.|
| `mobilebackup2` | Manage backups.|
| `pair` | Pair the device.| | `pair` | Pair the device.|
| `syslog_relay` | Relay system logs from the device | | `pcapd` | Capture network packets.|
| `preboard_service` | Interface with Preboard.|
| `restore_service` | Restore service (recovery/reboot).|
| `screenshotr` | Take screenshots.|
| `springboardservices` | Control SpringBoard (icons, wallpaper, orientation, etc.).|
| `syslog_relay` | Relay system logs and OS trace logs from the device. |
| `tcp` | Connect to devices over TCP.| | `tcp` | Connect to devices over TCP.|
| `tunnel_tcp_stack` | Naive in-process TCP stack for `core_device_proxy`.| | `tunnel_tcp_stack` | Naive in-process TCP stack for `core_device_proxy`.|
| `tss` | Make requests to Apple's TSS servers. Partial support.| | `tss` | Make requests to Apple's TSS servers. Partial support.|
| `tunneld` | Interface with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3)'s tunneld. | | `tunneld` | Interface with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3)'s tunneld. |
| `usbmuxd` | Connect using the usbmuxd daemon.| | `usbmuxd` | Connect using the usbmuxd daemon.|
| `xpc` | Access protected services via XPC over RSD. | | `xpc` | Access protected services via XPC over RSD. |
| `notification_proxy` | Post and observe iOS notifications. |
### Planned/TODO ### Planned/TODO
Finish the following: Finish the following:
- springboard - webinspector
Implement the following: Implement the following:
- companion_proxy - file_relay
- diagnostics
- mobilebackup2
- notification_proxy
- screenshot
- webinspector
As this project is done in my free time within my busy schedule, there As this project is done in my free time within my busy schedule, there
is no ETA for any of these. Feel free to contribute or donate! is no ETA for any of these. Feel free to contribute or donate!

View File

@@ -0,0 +1,50 @@
// Jackson Coxson
#pragma once
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
#include <memory>
#include <string>
#include <vector>
namespace IdeviceFFI {
using CrashReportCopyMobilePtr =
std::unique_ptr<CrashReportCopyMobileHandle,
FnDeleter<CrashReportCopyMobileHandle, crash_report_client_free>>;
class CrashReportCopyMobile {
public:
// Factory: connect via Provider
static Result<CrashReportCopyMobile, FfiError> connect(Provider& provider);
// Factory: wrap an existing Idevice socket (consumes it on success)
static Result<CrashReportCopyMobile, FfiError> from_socket(Idevice&& socket);
// Static: flush crash reports from system storage
static Result<void, FfiError> flush(Provider& provider);
// Ops
Result<std::vector<std::string>, FfiError> ls(const char* dir_path = nullptr);
Result<std::vector<char>, FfiError> pull(const std::string& log_name);
Result<void, FfiError> remove(const std::string& log_name);
// RAII / moves
~CrashReportCopyMobile() noexcept = default;
CrashReportCopyMobile(CrashReportCopyMobile&&) noexcept = default;
CrashReportCopyMobile& operator=(CrashReportCopyMobile&&) noexcept = default;
CrashReportCopyMobile(const CrashReportCopyMobile&) = delete;
CrashReportCopyMobile& operator=(const CrashReportCopyMobile&) = delete;
CrashReportCopyMobileHandle* raw() const noexcept { return handle_.get(); }
static CrashReportCopyMobile adopt(CrashReportCopyMobileHandle* h) noexcept {
return CrashReportCopyMobile(h);
}
private:
explicit CrashReportCopyMobile(CrashReportCopyMobileHandle* h) noexcept : handle_(h) {}
CrashReportCopyMobilePtr handle_{};
};
} // namespace IdeviceFFI

View File

@@ -0,0 +1,46 @@
#pragma once
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
#include <memory>
#include <string>
#include <vector>
namespace IdeviceFFI {
using NotificationProxyPtr = std::unique_ptr<NotificationProxyClientHandle,
FnDeleter<NotificationProxyClientHandle, notification_proxy_client_free>>;
class NotificationProxy {
public:
// Factory: connect via Provider
static Result<NotificationProxy, FfiError> connect(Provider& provider);
// Factory: wrap an existing Idevice socket (consumes it on success)
static Result<NotificationProxy, FfiError> from_socket(Idevice&& socket);
// Ops
Result<void, FfiError> post_notification(const std::string& name);
Result<void, FfiError> observe_notification(const std::string& name);
Result<void, FfiError> observe_notifications(const std::vector<std::string>& names);
Result<std::string, FfiError> receive_notification();
Result<std::string, FfiError> 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

View File

@@ -0,0 +1,94 @@
// Jackson Coxson
#include <cstring>
#include <idevice++/bindings.hpp>
#include <idevice++/crashreportcopymobile.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
namespace IdeviceFFI {
// -------- Factory Methods --------
Result<CrashReportCopyMobile, FfiError> CrashReportCopyMobile::connect(Provider& provider) {
CrashReportCopyMobileHandle* out = nullptr;
FfiError e(::crash_report_client_connect(provider.raw(), &out));
if (e) {
return Err(e);
}
return Ok(CrashReportCopyMobile::adopt(out));
}
Result<CrashReportCopyMobile, FfiError> CrashReportCopyMobile::from_socket(Idevice&& socket) {
CrashReportCopyMobileHandle* out = nullptr;
FfiError e(::crash_report_client_new(socket.raw(), &out));
if (e) {
return Err(e);
}
socket.release();
return Ok(CrashReportCopyMobile::adopt(out));
}
Result<void, FfiError> CrashReportCopyMobile::flush(Provider& provider) {
FfiError e(::crash_report_flush(provider.raw()));
if (e) {
return Err(e);
}
return Ok();
}
// -------- Ops --------
Result<std::vector<std::string>, FfiError>
CrashReportCopyMobile::ls(const char* dir_path) {
char** entries_raw = nullptr;
size_t count = 0;
FfiError e(::crash_report_client_ls(handle_.get(), dir_path, &entries_raw, &count));
if (e) {
return Err(e);
}
std::vector<std::string> result;
if (entries_raw) {
result.reserve(count);
for (size_t i = 0; i < count; ++i) {
if (entries_raw[i]) {
result.emplace_back(entries_raw[i]);
::idevice_string_free(entries_raw[i]);
}
}
std::free(entries_raw);
}
return Ok(std::move(result));
}
Result<std::vector<char>, FfiError>
CrashReportCopyMobile::pull(const std::string& log_name) {
uint8_t* data = nullptr;
size_t length = 0;
FfiError e(::crash_report_client_pull(handle_.get(), log_name.c_str(), &data, &length));
if (e) {
return Err(e);
}
std::vector<char> result;
if (data && length > 0) {
result.assign(reinterpret_cast<char*>(data), reinterpret_cast<char*>(data) + length);
::idevice_data_free(data, length);
}
return Ok(std::move(result));
}
Result<void, FfiError> CrashReportCopyMobile::remove(const std::string& log_name) {
FfiError e(::crash_report_client_remove(handle_.get(), log_name.c_str()));
if (e) {
return Err(e);
}
return Ok();
}
} // namespace IdeviceFFI

View File

@@ -0,0 +1,82 @@
// Jackson Coxson
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/notification_proxy.hpp>
#include <idevice++/provider.hpp>
namespace IdeviceFFI {
Result<NotificationProxy, FfiError> 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, FfiError> 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<void, FfiError> 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<void, FfiError> 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<void, FfiError> NotificationProxy::observe_notifications(const std::vector<std::string>& names) {
std::vector<const char*> 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<std::string, FfiError> 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<std::string, FfiError> 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

View File

@@ -34,6 +34,7 @@ debug_proxy = ["idevice/debug_proxy"]
diagnostics_relay = ["idevice/diagnostics_relay"] diagnostics_relay = ["idevice/diagnostics_relay"]
dvt = ["idevice/dvt"] dvt = ["idevice/dvt"]
heartbeat = ["idevice/heartbeat"] heartbeat = ["idevice/heartbeat"]
notification_proxy = ["idevice/notification_proxy"]
house_arrest = ["idevice/house_arrest"] house_arrest = ["idevice/house_arrest"]
installation_proxy = ["idevice/installation_proxy"] installation_proxy = ["idevice/installation_proxy"]
springboardservices = ["idevice/springboardservices"] springboardservices = ["idevice/springboardservices"]
@@ -61,6 +62,7 @@ full = [
"diagnostics_relay", "diagnostics_relay",
"dvt", "dvt",
"heartbeat", "heartbeat",
"notification_proxy",
"house_arrest", "house_arrest",
"installation_proxy", "installation_proxy",
"misagent", "misagent",

View File

@@ -0,0 +1,325 @@
// Jackson Coxson
use std::{
ffi::{CStr, c_char},
ptr::null_mut,
};
use idevice::{
IdeviceError, IdeviceService,
provider::IdeviceProvider,
services::crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports},
};
use crate::{
IdeviceFfiError, IdeviceHandle, afc::AfcClientHandle, ffi_err, provider::IdeviceProviderHandle,
run_sync_local,
};
pub struct CrashReportCopyMobileHandle(pub CrashReportCopyMobileClient);
/// Automatically creates and connects to the crash report copy mobile service,
/// returning a client handle
///
/// # Arguments
/// * [`provider`] - An IdeviceProvider
/// * [`client`] - On success, will be set to point to a newly allocated 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 crash_report_client_connect(
provider: *mut IdeviceProviderHandle,
client: *mut *mut CrashReportCopyMobileHandle,
) -> *mut IdeviceFfiError {
if provider.is_null() || client.is_null() {
tracing::error!("Null pointer provided");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res: Result<CrashReportCopyMobileClient, IdeviceError> = run_sync_local(async move {
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
CrashReportCopyMobileClient::connect(provider_ref).await
});
match res {
Ok(r) => {
let boxed = Box::new(CrashReportCopyMobileHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Creates a new CrashReportCopyMobile client from an existing Idevice connection
///
/// # Arguments
/// * [`socket`] - An IdeviceSocket handle
/// * [`client`] - On success, will be set to point to a newly allocated handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `socket` 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 crash_report_client_new(
socket: *mut IdeviceHandle,
client: *mut *mut CrashReportCopyMobileHandle,
) -> *mut IdeviceFfiError {
if socket.is_null() || client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let socket = unsafe { Box::from_raw(socket) }.0;
let r = CrashReportCopyMobileClient::new(socket);
let boxed = Box::new(CrashReportCopyMobileHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
/// Lists crash report files in the specified directory
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle
/// * [`dir_path`] - Optional directory path (NULL for root "/")
/// * [`entries`] - Will be set to point to an array of C strings
/// * [`count`] - Will be set to the number of entries
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// All pointers must be valid and non-null
/// `dir_path` may be NULL (defaults to root)
/// Caller must free the returned array with `afc_free_directory_entries`
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_ls(
client: *mut CrashReportCopyMobileHandle,
dir_path: *const c_char,
entries: *mut *mut *mut c_char,
count: *mut libc::size_t,
) -> *mut IdeviceFfiError {
if client.is_null() || entries.is_null() || count.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let path = if dir_path.is_null() {
None
} else {
match unsafe { CStr::from_ptr(dir_path) }.to_str() {
Ok(s) => Some(s),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
}
};
let res: Result<Vec<String>, IdeviceError> = run_sync_local(async {
let client_ref = unsafe { &mut (*client).0 };
client_ref.ls(path).await
});
match res {
Ok(items) => {
let c_strings = items
.into_iter()
.filter_map(|s| std::ffi::CString::new(s).ok())
.collect::<Vec<_>>();
let string_count = c_strings.len();
// Allocate array for char pointers (with NULL terminator)
let layout = std::alloc::Layout::array::<*mut c_char>(string_count + 1).unwrap();
let ptr = unsafe { std::alloc::alloc(layout) as *mut *mut c_char };
if ptr.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
for (i, cstring) in c_strings.into_iter().enumerate() {
let string_ptr = cstring.into_raw();
unsafe { *ptr.add(i) = string_ptr };
}
// NULL terminator
unsafe { *ptr.add(string_count) = std::ptr::null_mut() };
unsafe {
*entries = ptr;
*count = string_count;
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Downloads a crash report file from the device
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle
/// * [`log_name`] - Name of the log file to download (C string)
/// * [`data`] - Will be set to point to the file contents
/// * [`length`] - Will be set to the size of the data
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// All pointers must be valid and non-null
/// `log_name` must be a valid C string
/// Caller must free the returned data with `idevice_data_free`
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_pull(
client: *mut CrashReportCopyMobileHandle,
log_name: *const c_char,
data: *mut *mut u8,
length: *mut libc::size_t,
) -> *mut IdeviceFfiError {
if client.is_null() || log_name.is_null() || data.is_null() || length.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let name = match unsafe { CStr::from_ptr(log_name) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
};
let res: Result<Vec<u8>, IdeviceError> = run_sync_local(async {
let client_ref = unsafe { &mut (*client).0 };
client_ref.pull(name).await
});
match res {
Ok(file_data) => {
let len = file_data.len();
let mut boxed = file_data.into_boxed_slice();
unsafe {
*data = boxed.as_mut_ptr();
*length = len;
}
std::mem::forget(boxed);
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Removes a crash report file from the device
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle
/// * [`log_name`] - Name of the log file to remove (C string)
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `log_name` must be a valid C string
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_remove(
client: *mut CrashReportCopyMobileHandle,
log_name: *const c_char,
) -> *mut IdeviceFfiError {
if client.is_null() || log_name.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let name = match unsafe { CStr::from_ptr(log_name) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
};
let res = run_sync_local(async {
let client_ref = unsafe { &mut (*client).0 };
client_ref.remove(name).await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Converts this client to an AFC client for advanced file operations
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle (will be consumed)
/// * [`afc_client`] - On success, will be set to an AFC client handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer (will be freed after this call)
/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_to_afc(
client: *mut CrashReportCopyMobileHandle,
afc_client: *mut *mut AfcClientHandle,
) -> *mut IdeviceFfiError {
if client.is_null() || afc_client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let crash_client = unsafe { Box::from_raw(client) }.0;
let afc = crash_client.to_afc_client();
let a = Box::into_raw(Box::new(AfcClientHandle(afc)));
unsafe { *afc_client = a };
null_mut()
}
/// Triggers a flush of crash logs from system storage
///
/// This connects to the crashreportmover service to move crash logs
/// into the AFC-accessible directory. Should be called before listing logs.
///
/// # Arguments
/// * [`provider`] - An IdeviceProvider
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `provider` must be a valid pointer to a handle allocated by this library
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_flush(
provider: *mut IdeviceProviderHandle,
) -> *mut IdeviceFfiError {
if provider.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res = run_sync_local(async {
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
flush_reports(provider_ref).await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Frees a CrashReportCopyMobile client 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 crash_report_client_free(handle: *mut CrashReportCopyMobileHandle) {
if !handle.is_null() {
tracing::debug!("Freeing crash_report_client");
let _ = unsafe { Box::from_raw(handle) };
}
}

View File

@@ -10,6 +10,8 @@ pub mod amfi;
pub mod core_device; pub mod core_device;
#[cfg(feature = "core_device_proxy")] #[cfg(feature = "core_device_proxy")]
pub mod core_device_proxy; pub mod core_device_proxy;
#[cfg(feature = "crashreportcopymobile")]
pub mod crashreportcopymobile;
#[cfg(feature = "debug_proxy")] #[cfg(feature = "debug_proxy")]
pub mod debug_proxy; pub mod debug_proxy;
#[cfg(feature = "diagnostics_relay")] #[cfg(feature = "diagnostics_relay")]
@@ -29,6 +31,8 @@ pub mod logging;
pub mod misagent; pub mod misagent;
#[cfg(feature = "mobile_image_mounter")] #[cfg(feature = "mobile_image_mounter")]
pub mod mobile_image_mounter; pub mod mobile_image_mounter;
#[cfg(feature = "notification_proxy")]
pub mod notification_proxy;
#[cfg(feature = "syslog_relay")] #[cfg(feature = "syslog_relay")]
pub mod os_trace_relay; pub mod os_trace_relay;
mod pairing_file; mod pairing_file;

View File

@@ -157,6 +157,74 @@ pub unsafe extern "C" fn lockdownd_start_service(
} }
} }
/// Pairs with the device using lockdownd
///
/// # Arguments
/// * `client` - A valid LockdowndClient handle
/// * `host_id` - The host ID (null-terminated string)
/// * `system_buid` - The system BUID (null-terminated string)
/// * `pairing_file` - On success, will be set to point to a newly allocated IdevicePairingFile handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `host_id` must be a valid null-terminated string
/// `system_buid` must be a valid null-terminated string
/// `pairing_file` must be a valid, non-null pointer to a location where the handle will be stored
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lockdownd_pair(
client: *mut LockdowndClientHandle,
host_id: *const libc::c_char,
system_buid: *const libc::c_char,
host_name: *const libc::c_char,
pairing_file: *mut *mut IdevicePairingFile,
) -> *mut IdeviceFfiError {
if client.is_null() || host_id.is_null() || system_buid.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let host_id = unsafe {
std::ffi::CStr::from_ptr(host_id)
.to_string_lossy()
.into_owned()
};
let system_buid = unsafe {
std::ffi::CStr::from_ptr(system_buid)
.to_string_lossy()
.into_owned()
};
let host_name = if host_name.is_null() {
None
} else {
Some(
match unsafe { std::ffi::CStr::from_ptr(host_name) }.to_str() {
Ok(v) => v,
Err(_) => {
return ffi_err!(IdeviceError::InvalidCString);
}
},
)
};
let res = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.pair(host_id, system_buid, host_name).await
});
match res {
Ok(pairing_file_res) => {
let boxed_pairing_file = Box::new(IdevicePairingFile(pairing_file_res));
unsafe { *pairing_file = Box::into_raw(boxed_pairing_file) };
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Gets a value from lockdownd /// Gets a value from lockdownd
/// ///
/// # Arguments /// # Arguments

View File

@@ -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<NotificationProxyClient, IdeviceError> = 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<String> = 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<String, IdeviceError> = 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<String, IdeviceError> = 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) };
}
}

View File

@@ -7,6 +7,7 @@ use idevice::{
IdeviceError, IdeviceService, provider::IdeviceProvider, IdeviceError, IdeviceService, provider::IdeviceProvider,
springboardservices::SpringBoardServicesClient, springboardservices::SpringBoardServicesClient,
}; };
use plist_ffi::plist_t;
use crate::{ use crate::{
IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync, IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync,
@@ -263,6 +264,43 @@ pub unsafe extern "C" fn springboard_services_get_interface_orientation(
} }
} }
/// Gets the home screen icon layout metrics
///
/// # Arguments
/// * `client` - A valid SpringBoardServicesClient handle
/// * `res` - On success, will point to a plist dictionary node containing the metrics
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `res` must be a valid, non-null pointer
#[unsafe(no_mangle)]
pub unsafe extern "C" fn springboard_services_get_homescreen_icon_metrics(
client: *mut SpringBoardServicesClientHandle,
res: *mut plist_t,
) -> *mut IdeviceFfiError {
if client.is_null() || res.is_null() {
tracing::error!("Invalid arguments: {client:?}, {res:?}");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let client = unsafe { &mut *client };
let output = run_sync(async { client.0.get_homescreen_icon_metrics().await });
match output {
Ok(metrics) => {
unsafe {
*res =
plist_ffi::PlistWrapper::new_node(plist::Value::Dictionary(metrics)).into_ptr();
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Frees an SpringBoardServicesClient handle /// Frees an SpringBoardServicesClient handle
/// ///
/// # Arguments /// # Arguments

View File

@@ -97,6 +97,7 @@ misagent = []
mobile_image_mounter = ["dep:sha2"] mobile_image_mounter = ["dep:sha2"]
mobileactivationd = ["dep:reqwest"] mobileactivationd = ["dep:reqwest"]
mobilebackup2 = [] mobilebackup2 = []
notification_proxy = ["tokio/macros", "tokio/time", "dep:async-stream", "dep:futures"]
location_simulation = [] location_simulation = []
pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"]
pcapd = [] pcapd = []
@@ -138,6 +139,7 @@ full = [
"mobile_image_mounter", "mobile_image_mounter",
"mobileactivationd", "mobileactivationd",
"mobilebackup2", "mobilebackup2",
"notification_proxy",
"pair", "pair",
"pcapd", "pcapd",
"preboard_service", "preboard_service",

View File

@@ -865,6 +865,14 @@ pub enum IdeviceError {
#[error("Developer mode is not enabled")] #[error("Developer mode is not enabled")]
DeveloperModeNotEnabled = -68, DeveloperModeNotEnabled = -68,
#[cfg(feature = "notification_proxy")]
#[error("notification proxy died")]
NotificationProxyDeath = -69,
#[cfg(feature = "installation_proxy")]
#[error("Application verification failed: {0}")]
ApplicationVerificationFailed(String) = -70,
} }
impl IdeviceError { impl IdeviceError {
@@ -908,6 +916,15 @@ impl IdeviceError {
Some(Self::InternalError(detailed_error)) Some(Self::InternalError(detailed_error))
} }
} }
#[cfg(feature = "installation_proxy")]
"ApplicationVerificationFailed" => {
let msg = context
.get("ErrorDescription")
.and_then(|x| x.as_string())
.unwrap_or("No context")
.to_string();
Some(Self::ApplicationVerificationFailed(msg))
}
_ => None, _ => None,
} }
} }
@@ -1030,6 +1047,12 @@ impl IdeviceError {
#[cfg(feature = "installation_proxy")] #[cfg(feature = "installation_proxy")]
IdeviceError::MalformedPackageArchive(_) => -67, IdeviceError::MalformedPackageArchive(_) => -67,
IdeviceError::DeveloperModeNotEnabled => -68, IdeviceError::DeveloperModeNotEnabled => -68,
#[cfg(feature = "notification_proxy")]
IdeviceError::NotificationProxyDeath => -69,
#[cfg(feature = "installation_proxy")]
IdeviceError::ApplicationVerificationFailed(_) => -70,
} }
} }
} }

View File

@@ -35,6 +35,8 @@ pub mod mobile_image_mounter;
pub mod mobileactivationd; pub mod mobileactivationd;
#[cfg(feature = "mobilebackup2")] #[cfg(feature = "mobilebackup2")]
pub mod mobilebackup2; pub mod mobilebackup2;
#[cfg(feature = "notification_proxy")]
pub mod notification_proxy;
#[cfg(feature = "syslog_relay")] #[cfg(feature = "syslog_relay")]
pub mod os_trace_relay; pub mod os_trace_relay;
#[cfg(feature = "pcapd")] #[cfg(feature = "pcapd")]

View File

@@ -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<Self, crate::IdeviceError> {
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<String>,
) -> 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<String>,
) -> 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<String, IdeviceError> {
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<String, IdeviceError> {
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<Box<dyn Stream<Item = Result<String, IdeviceError>> + 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(())
}
}

View File

@@ -325,4 +325,31 @@ impl SpringBoardServicesClient {
Ok(orientation) Ok(orientation)
} }
/// Gets the home screen icon layout metrics
///
/// Returns icon spacing, size, and positioning information
///
/// # Returns
/// A `plist::Dictionary` containing the icon layout metrics
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The response is malformed
///
/// # Example
/// ```rust
/// let metrics = client.get_homescreen_icon_metrics().await?;
/// println!("{:?}", metrics);
/// ```
pub async fn get_homescreen_icon_metrics(&mut self) -> Result<plist::Dictionary, IdeviceError> {
let req = crate::plist!({
"command": "getHomeScreenIconMetrics",
});
self.idevice.send_plist(req).await?;
let res = self.idevice.read_plist().await?;
Ok(res)
}
} }

View File

@@ -40,6 +40,9 @@ xcframework: apple-build
lipo -create -output swift/libs/idevice-ios-sim.a \ lipo -create -output swift/libs/idevice-ios-sim.a \
target/aarch64-apple-ios-sim/release/libidevice_ffi.a \ target/aarch64-apple-ios-sim/release/libidevice_ffi.a \
target/x86_64-apple-ios/release/libidevice_ffi.a target/x86_64-apple-ios/release/libidevice_ffi.a
lipo -create -output swift/libs/idevice-maccatalyst.a \
target/aarch64-apple-ios-macabi/release/libidevice_ffi.a \
target/x86_64-apple-ios-macabi/release/libidevice_ffi.a
lipo -create -output swift/libs/idevice-macos.a \ lipo -create -output swift/libs/idevice-macos.a \
target/aarch64-apple-darwin/release/libidevice_ffi.a \ target/aarch64-apple-darwin/release/libidevice_ffi.a \
target/x86_64-apple-darwin/release/libidevice_ffi.a target/x86_64-apple-darwin/release/libidevice_ffi.a
@@ -48,8 +51,9 @@ xcframework: apple-build
-library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \ -library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \
-library swift/libs/idevice-ios-sim.a -headers swift/include \ -library swift/libs/idevice-ios-sim.a -headers swift/include \
-library swift/libs/idevice-macos.a -headers swift/include \ -library swift/libs/idevice-macos.a -headers swift/include \
-library swift/libs/idevice-maccatalyst.a -headers swift/include \
-output swift/IDevice.xcframework -output swift/IDevice.xcframework
zip -r swift/bundle.zip swift/IDevice.xcframework zip -r swift/bundle.zip swift/IDevice.xcframework
openssl dgst -sha256 swift/bundle.zip openssl dgst -sha256 swift/bundle.zip
@@ -68,7 +72,16 @@ apple-build: # requires a Mac
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphonesimulator --show-sdk-path)" \ BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphonesimulator --show-sdk-path)" \
cargo build --release --target x86_64-apple-ios cargo build --release --target x86_64-apple-ios
# Mac Catalyst (arm64)
# AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring.
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \
cargo build --release --target aarch64-apple-ios-macabi --no-default-features --features "ring full"
# Mac Catalyst (x86_64)
# AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring.
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \
cargo build --release --target x86_64-apple-ios-macabi --no-default-features --features "ring full"
# macOS (native) no special env needed # macOS (native) no special env needed
cargo build --release --target aarch64-apple-darwin cargo build --release --target aarch64-apple-darwin
cargo build --release --target x86_64-apple-darwin cargo build --release --target x86_64-apple-darwin

View File

@@ -33,6 +33,7 @@ mod lockdown;
mod misagent; mod misagent;
mod mobilebackup2; mod mobilebackup2;
mod mounter; mod mounter;
mod notification_proxy_client;
mod notifications; mod notifications;
mod os_trace_relay; mod os_trace_relay;
mod pair; mod pair;
@@ -113,6 +114,7 @@ async fn main() {
.with_subcommand("mobilebackup2", mobilebackup2::register()) .with_subcommand("mobilebackup2", mobilebackup2::register())
.with_subcommand("mounter", mounter::register()) .with_subcommand("mounter", mounter::register())
.with_subcommand("notifications", notifications::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("os_trace_relay", os_trace_relay::register())
.with_subcommand("pair", pair::register()) .with_subcommand("pair", pair::register())
.with_subcommand("pcapd", pcapd::register()) .with_subcommand("pcapd", pcapd::register())
@@ -214,6 +216,9 @@ async fn main() {
"notifications" => { "notifications" => {
notifications::main(sub_args, provider).await; notifications::main(sub_args, provider).await;
} }
"notification_proxy" => {
notification_proxy_client::main(sub_args, provider).await;
}
"os_trace_relay" => { "os_trace_relay" => {
os_trace_relay::main(sub_args, provider).await; os_trace_relay::main(sub_args, provider).await;
} }

View File

@@ -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<dyn IdeviceProvider>) {
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::<String>()
.expect("No notification ID passed");
let notifications: Vec<&str> = input.split_whitespace().collect();
client
.observe_notifications(&notifications)
.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::<String>()
.expect("No notification ID passed");
client
.post_notification(&notification)
.await
.expect("Failed to post notification");
}
_ => unreachable!(),
}
}

View File

@@ -49,6 +49,25 @@ pub fn register() -> JkCommand {
"get_interface_orientation", "get_interface_orientation",
JkCommand::new().help("Gets the device's current screen orientation"), JkCommand::new().help("Gets the device's current screen orientation"),
) )
.with_subcommand(
"get_homescreen_icon_metrics",
JkCommand::new().help("Gets home screen icon layout metrics"),
)
.with_subcommand(
"get_icon",
JkCommand::new()
.help("Gets an app's icon as PNG")
.with_argument(
JkArgument::new()
.with_help("Bundle identifier (e.g. com.apple.Maps)")
.required(true),
)
.with_flag(
JkFlag::new("save")
.with_help("Path to save the icon PNG file, or icon.png by default")
.with_argument(JkArgument::new().required(true)),
),
)
.subcommand_required(true) .subcommand_required(true)
} }
@@ -113,6 +132,30 @@ pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvi
.expect("Failed to get interface orientation"); .expect("Failed to get interface orientation");
println!("{:?}", orientation); println!("{:?}", orientation);
} }
"get_homescreen_icon_metrics" => {
let metrics = sbc
.get_homescreen_icon_metrics()
.await
.expect("Failed to get homescreen icon metrics");
let metrics_value = plist::Value::Dictionary(metrics);
println!("{}", pretty_print_plist(&metrics_value));
}
"get_icon" => {
let bundle_id = sub_args.next_argument::<String>().unwrap();
let icon_data = sbc
.get_icon_pngdata(bundle_id)
.await
.expect("Failed to get icon");
let save_path = sub_args
.get_flag::<String>("save")
.unwrap_or("icon.png".to_string());
tokio::fs::write(&save_path, icon_data)
.await
.expect("Failed to save icon");
}
_ => unreachable!(), _ => unreachable!(),
} }
} }