diff --git a/Cargo.lock b/Cargo.lock index 3176418..c82aeb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,28 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "async-task" version = "4.7.1" @@ -1112,6 +1134,7 @@ dependencies = [ name = "idevice" version = "0.1.38" dependencies = [ + "async-stream", "base64", "byteorder", "bytes", @@ -1163,6 +1186,7 @@ version = "0.1.0" dependencies = [ "clap", "env_logger", + "futures-util", "idevice", "log", "ns-keyed-archive", diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 1102aca..86836b1 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -42,6 +42,7 @@ reqwest = { version = "0.12", features = [ ], optional = true, default-features = false } rand = { version = "0.9", optional = true } futures = { version = "0.3", optional = true } +async-stream = { version = "0.3.6", optional = true } sha2 = { version = "0.10", optional = true, features = ["oid"] } @@ -96,7 +97,7 @@ tunnel_tcp_stack = [ tss = ["dep:uuid", "dep:reqwest"] tunneld = ["dep:serde_json", "dep:json", "dep:reqwest"] usbmuxd = ["tokio/net"] -xpc = ["dep:indexmap", "dep:uuid"] +xpc = ["dep:indexmap", "dep:uuid", "dep:async-stream"] full = [ "afc", "amfi", diff --git a/idevice/src/services/core_device/diagnosticsservice.rs b/idevice/src/services/core_device/diagnosticsservice.rs new file mode 100644 index 0000000..084a390 --- /dev/null +++ b/idevice/src/services/core_device/diagnosticsservice.rs @@ -0,0 +1,72 @@ +// Jackson Coxson + +use std::pin::Pin; + +use futures::Stream; +use log::warn; + +use crate::{IdeviceError, ReadWrite, RsdService, obf}; + +impl RsdService for DiagnostisServiceClient> { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.coredevice.diagnosticsservice") + } + + async fn from_stream(stream: Box) -> Result { + Ok(Self { + inner: super::CoreDeviceServiceClient::new(stream).await?, + }) + } +} + +pub struct DiagnostisServiceClient { + inner: super::CoreDeviceServiceClient, +} + +pub struct SysdiagnoseResponse<'a> { + pub preferred_filename: String, + pub stream: Pin, IdeviceError>> + 'a>>, + pub expected_length: usize, +} + +impl DiagnostisServiceClient { + pub async fn capture_sysdiagnose<'a>( + &'a mut self, + dry_run: bool, + ) -> Result, IdeviceError> { + let req = crate::plist!({ + "options": { + "collectFullLogs": true + }, + "isDryRun": dry_run + }) + .into_dictionary() + .unwrap(); + + let res = self + .inner + .invoke("com.apple.coredevice.feature.capturesysdiagnose", Some(req)) + .await?; + + if let Some(len) = res + .as_dictionary() + .and_then(|x| x.get("fileTransfer")) + .and_then(|x| x.as_dictionary()) + .and_then(|x| x.get("expectedLength")) + .and_then(|x| x.as_unsigned_integer()) + && let Some(name) = res + .as_dictionary() + .and_then(|x| x.get("preferredFilename")) + .and_then(|x| x.as_string()) + { + Ok(SysdiagnoseResponse { + stream: Box::pin(self.inner.inner.iter_file_chunks(len as usize, 0)), + preferred_filename: name.to_string(), + expected_length: len as usize, + }) + } else { + warn!("Did not get expected responses from RemoteXPC"); + Err(IdeviceError::UnexpectedResponse) + } + } +} diff --git a/idevice/src/services/core_device/mod.rs b/idevice/src/services/core_device/mod.rs index 6114a0e..e6ef7f7 100644 --- a/idevice/src/services/core_device/mod.rs +++ b/idevice/src/services/core_device/mod.rs @@ -9,7 +9,9 @@ use crate::{ }; mod app_service; +mod diagnosticsservice; pub use app_service::*; +pub use diagnosticsservice::*; const CORE_SERVICE_VERSION: &str = "443.18"; diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 2cc126e..54d0d79 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -105,6 +105,10 @@ path = "src/diagnostics.rs" name = "mobilebackup2" path = "src/mobilebackup2.rs" +[[bin]] +name = "diagnosticsservice" +path = "src/diagnosticsservice.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } @@ -117,6 +121,7 @@ clap = { version = "4.5" } plist = { version = "1.7" } ns-keyed-archive = "0.1.2" uuid = "1.16" +futures-util = { version = "0.3" } [features] default = ["aws-lc"] diff --git a/tools/src/diagnosticsservice.rs b/tools/src/diagnosticsservice.rs new file mode 100644 index 0000000..e043381 --- /dev/null +++ b/tools/src/diagnosticsservice.rs @@ -0,0 +1,106 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use futures_util::StreamExt; +use idevice::{ + IdeviceService, RsdService, core_device::DiagnostisServiceClient, + core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, +}; +use tokio::io::AsyncWriteExt; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("remotexpc") + .about("Gets a sysdiagnose") + .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)") + .index(1), + ) + .arg( + Arg::new("tunneld") + .long("tunneld") + .help("Use tunneld") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("debug_proxy - connect to the debug proxy and run commands"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let pairing_file = matches.get_one::("pairing_file"); + let host = matches.get_one::("host"); + + let provider = + match common::get_provider(udid, host, pairing_file, "diagnosticsservice-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + let proxy = CoreDeviceProxy::connect(&*provider) + .await + .expect("no core proxy"); + let rsd_port = proxy.handshake.server_rsd_port; + + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + + let mut dsc = DiagnostisServiceClient::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("no connect"); + + println!("Getting sysdiagnose, this takes a while! iOS is slow..."); + let mut res = dsc + .capture_sysdiagnose(false) + .await + .expect("no sysdiagnose"); + println!("Got sysdaignose! Saving to file"); + + let mut written = 0usize; + let mut out = tokio::fs::File::create(&res.preferred_filename) + .await + .expect("no file?"); + while let Some(chunk) = res.stream.next().await { + let buf = chunk.expect("stream stopped?"); + if !buf.is_empty() { + out.write_all(&buf).await.expect("no write all?"); + written += buf.len(); + } + println!("wrote {written}/{} bytes", res.expected_length); + } + println!("Done! Saved to {}", res.preferred_filename); +}