mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
Implement crash report services
This commit is contained in:
@@ -57,6 +57,7 @@ bytes = "1.10.1"
|
||||
afc = ["dep:chrono"]
|
||||
amfi = []
|
||||
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
|
||||
crashreportcopymobile = ["afc"]
|
||||
debug_proxy = []
|
||||
dvt = ["dep:byteorder", "dep:ns-keyed-archive"]
|
||||
heartbeat = ["tokio/macros", "tokio/time"]
|
||||
@@ -82,6 +83,7 @@ full = [
|
||||
"afc",
|
||||
"amfi",
|
||||
"core_device_proxy",
|
||||
"crashreportcopymobile",
|
||||
"debug_proxy",
|
||||
"dvt",
|
||||
"heartbeat",
|
||||
|
||||
182
idevice/src/crashreportcopymobile.rs
Normal file
182
idevice/src/crashreportcopymobile.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
//! iOS Crash Logs
|
||||
//!
|
||||
//! Provides functionality for managing crash logs on a connected iOS device.
|
||||
//!
|
||||
//! This module enables clients to list, pull, and remove crash logs via the
|
||||
//! `CrashReportCopyMobile` service using the AFC protocol. It also includes a
|
||||
//! function to trigger a flush of crash logs from system storage into the
|
||||
//! crash reports directory by connecting to the `com.apple.crashreportmover` service.
|
||||
|
||||
use log::{debug, warn};
|
||||
|
||||
use crate::{afc::AfcClient, lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
/// Client for managing crash logs on an iOS device.
|
||||
///
|
||||
/// This client wraps access to the `com.apple.crashreportcopymobile` service,
|
||||
/// which exposes crash logs through the Apple File Conduit (AFC).
|
||||
pub struct CrashReportCopyMobileClient {
|
||||
/// The underlying AFC client connected to the crash logs directory.
|
||||
pub afc_client: AfcClient,
|
||||
}
|
||||
|
||||
impl IdeviceService for CrashReportCopyMobileClient {
|
||||
/// Returns the name of the CrashReportCopyMobile service.
|
||||
fn service_name() -> &'static str {
|
||||
"com.apple.crashreportcopymobile"
|
||||
}
|
||||
|
||||
/// Connects to the CrashReportCopyMobile service on the device.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - The provider used to access the device and pairing info.
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected `CrashReportCopyMobileClient`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if the connection fails at any stage.
|
||||
///
|
||||
/// # Process
|
||||
/// 1. Connects to the lockdownd service.
|
||||
/// 2. Starts a lockdown session.
|
||||
/// 3. Requests the CrashReportCopyMobile service.
|
||||
/// 4. Establishes a connection to the service.
|
||||
/// 5. Performs SSL handshake if required.
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
|
||||
let (port, ssl) = lockdown.start_service(Self::service_name()).await?;
|
||||
|
||||
let mut idevice = provider.connect(port).await?;
|
||||
if ssl {
|
||||
idevice
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
afc_client: AfcClient::new(idevice),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CrashReportCopyMobileClient {
|
||||
/// Creates a new client from an existing AFC-capable device connection.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `idevice` - A pre-established connection to the device.
|
||||
pub fn new(idevice: Idevice) -> Self {
|
||||
Self {
|
||||
afc_client: AfcClient::new(idevice),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists crash report files in the root of the crash logs directory.
|
||||
///
|
||||
/// # Returns
|
||||
/// A list of filenames.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if listing the directory fails.
|
||||
pub async fn ls(&mut self) -> Result<Vec<String>, IdeviceError> {
|
||||
let mut res = self.afc_client.list_dir("/").await?;
|
||||
if res.len() > 2 {
|
||||
if &res[0] == "." {
|
||||
res.swap_remove(0);
|
||||
}
|
||||
if &res[1] == ".." {
|
||||
res.swap_remove(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Retrieves the contents of a specified crash log file.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `log` - Name of the log file to retrieve.
|
||||
///
|
||||
/// # Returns
|
||||
/// A byte vector containing the file contents.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if the file cannot be opened or read.
|
||||
pub async fn pull(&mut self, log: impl Into<String>) -> Result<Vec<u8>, IdeviceError> {
|
||||
let log = log.into();
|
||||
let mut f = self
|
||||
.afc_client
|
||||
.open(format!("/{log}"), crate::afc::opcode::AfcFopenMode::RdOnly)
|
||||
.await?;
|
||||
|
||||
f.read().await
|
||||
}
|
||||
|
||||
/// Removes a specified crash log file from the device.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `log` - Name of the log file to remove.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if the file could not be deleted.
|
||||
pub async fn remove(&mut self, log: impl Into<String>) -> Result<(), IdeviceError> {
|
||||
let log = log.into();
|
||||
self.afc_client.remove(format!("/{log}")).await
|
||||
}
|
||||
|
||||
/// Consumes this client and returns the inner AFC client.
|
||||
pub fn to_afc_client(self) -> AfcClient {
|
||||
self.afc_client
|
||||
}
|
||||
}
|
||||
|
||||
const EXPECTED_FLUSH: [u8; 4] = [0x70, 0x69, 0x6E, 0x67]; // 'ping'
|
||||
|
||||
/// Triggers a flush of crash logs from system storage.
|
||||
///
|
||||
/// This connects to the `com.apple.crashreportmover` service,
|
||||
/// which moves crash logs into the AFC-accessible directory.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - The device provider used for connection and pairing info.
|
||||
///
|
||||
/// # Returns
|
||||
/// `Ok(())` if the service responds with a valid flush indicator.
|
||||
/// `Err(IdeviceError)` if the service responds with unexpected data
|
||||
/// or the connection fails.
|
||||
pub async fn flush_reports(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<(), IdeviceError> {
|
||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
|
||||
let (port, ssl) = lockdown.start_service("com.apple.crashreportmover").await?;
|
||||
|
||||
let mut idevice = provider.connect(port).await?;
|
||||
if ssl {
|
||||
idevice
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let res = idevice.read_raw(4).await?;
|
||||
debug!(
|
||||
"Flush reports response: {:?}",
|
||||
String::from_utf8_lossy(&res)
|
||||
);
|
||||
|
||||
if res[..4] == EXPECTED_FLUSH {
|
||||
Ok(())
|
||||
} else {
|
||||
warn!("crashreportmover sent wrong bytes: {res:02X?}");
|
||||
Err(IdeviceError::CrashReportMoverBadResponse(res))
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ pub mod amfi;
|
||||
mod ca;
|
||||
#[cfg(feature = "core_device_proxy")]
|
||||
pub mod core_device_proxy;
|
||||
#[cfg(feature = "crashreportcopymobile")]
|
||||
pub mod crashreportcopymobile;
|
||||
#[cfg(feature = "debug_proxy")]
|
||||
pub mod debug_proxy;
|
||||
#[cfg(feature = "dvt")]
|
||||
@@ -451,6 +453,10 @@ pub enum IdeviceError {
|
||||
#[error("missing file attribute")]
|
||||
AfcMissingAttribute,
|
||||
|
||||
#[cfg(feature = "crashreportcopymobile")]
|
||||
#[error("crash report mover sent the wrong response")]
|
||||
CrashReportMoverBadResponse(Vec<u8>),
|
||||
|
||||
#[cfg(any(feature = "tss", feature = "tunneld"))]
|
||||
#[error("http reqwest error")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
|
||||
@@ -61,6 +61,10 @@ path = "src/location_simulation.rs"
|
||||
name = "afc"
|
||||
path = "src/afc.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "crash_logs"
|
||||
path = "src/crash_logs.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "amfi"
|
||||
path = "src/amfi.rs"
|
||||
|
||||
87
tools/src/crash_logs.rs
Normal file
87
tools/src/crash_logs.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
crashreportcopymobile::{flush_reports, CrashReportCopyMobileClient},
|
||||
IdeviceService,
|
||||
};
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("crash_logs")
|
||||
.about("Manage crash logs")
|
||||
.arg(
|
||||
Arg::new("host")
|
||||
.long("host")
|
||||
.value_name("HOST")
|
||||
.help("IP address of the device"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pairing_file")
|
||||
.long("pairing-file")
|
||||
.value_name("PATH")
|
||||
.help("Path to the pairing file"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("udid")
|
||||
.value_name("UDID")
|
||||
.help("UDID of the device (overrides host/pairing file)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.subcommand(Command::new("list").about("Lists the items in the directory"))
|
||||
.subcommand(Command::new("flush").about("Flushes reports to the directory"))
|
||||
.subcommand(
|
||||
Command::new("pull")
|
||||
.about("Pulls a log")
|
||||
.arg(Arg::new("path").required(true).index(1))
|
||||
.arg(Arg::new("save").required(true).index(2)),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("crash_logs - manage crash logs on the device");
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
|
||||
let udid = matches.get_one::<String>("udid");
|
||||
let host = matches.get_one::<String>("host");
|
||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
||||
|
||||
let provider = match common::get_provider(udid, host, pairing_file, "afc-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut crash_client = CrashReportCopyMobileClient::connect(&*provider)
|
||||
.await
|
||||
.expect("Unable to connect to misagent");
|
||||
|
||||
if let Some(_matches) = matches.subcommand_matches("list") {
|
||||
let res = crash_client.ls().await.expect("Failed to read dir");
|
||||
println!("{res:#?}");
|
||||
} else if matches.subcommand_matches("flush").is_some() {
|
||||
flush_reports(&*provider).await.expect("Failed to flush");
|
||||
} else if let Some(matches) = matches.subcommand_matches("pull") {
|
||||
let path = matches.get_one::<String>("path").expect("No path passed");
|
||||
let save = matches.get_one::<String>("save").expect("No path passed");
|
||||
|
||||
let res = crash_client.pull(path).await.expect("Failed to pull log");
|
||||
tokio::fs::write(save, res)
|
||||
.await
|
||||
.expect("Failed to write to file");
|
||||
} else {
|
||||
eprintln!("Invalid usage, pass -h for help");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user