mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 14:36:16 +01:00
Implement crash report services
This commit is contained in:
@@ -57,6 +57,7 @@ bytes = "1.10.1"
|
|||||||
afc = ["dep:chrono"]
|
afc = ["dep:chrono"]
|
||||||
amfi = []
|
amfi = []
|
||||||
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
|
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
|
||||||
|
crashreportcopymobile = ["afc"]
|
||||||
debug_proxy = []
|
debug_proxy = []
|
||||||
dvt = ["dep:byteorder", "dep:ns-keyed-archive"]
|
dvt = ["dep:byteorder", "dep:ns-keyed-archive"]
|
||||||
heartbeat = ["tokio/macros", "tokio/time"]
|
heartbeat = ["tokio/macros", "tokio/time"]
|
||||||
@@ -82,6 +83,7 @@ full = [
|
|||||||
"afc",
|
"afc",
|
||||||
"amfi",
|
"amfi",
|
||||||
"core_device_proxy",
|
"core_device_proxy",
|
||||||
|
"crashreportcopymobile",
|
||||||
"debug_proxy",
|
"debug_proxy",
|
||||||
"dvt",
|
"dvt",
|
||||||
"heartbeat",
|
"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;
|
mod ca;
|
||||||
#[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 = "dvt")]
|
#[cfg(feature = "dvt")]
|
||||||
@@ -451,6 +453,10 @@ pub enum IdeviceError {
|
|||||||
#[error("missing file attribute")]
|
#[error("missing file attribute")]
|
||||||
AfcMissingAttribute,
|
AfcMissingAttribute,
|
||||||
|
|
||||||
|
#[cfg(feature = "crashreportcopymobile")]
|
||||||
|
#[error("crash report mover sent the wrong response")]
|
||||||
|
CrashReportMoverBadResponse(Vec<u8>),
|
||||||
|
|
||||||
#[cfg(any(feature = "tss", feature = "tunneld"))]
|
#[cfg(any(feature = "tss", feature = "tunneld"))]
|
||||||
#[error("http reqwest error")]
|
#[error("http reqwest error")]
|
||||||
Reqwest(#[from] reqwest::Error),
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ path = "src/location_simulation.rs"
|
|||||||
name = "afc"
|
name = "afc"
|
||||||
path = "src/afc.rs"
|
path = "src/afc.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "crash_logs"
|
||||||
|
path = "src/crash_logs.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "amfi"
|
name = "amfi"
|
||||||
path = "src/amfi.rs"
|
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