mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 14:36:16 +01:00
Merge branch 'master' into rppairing
a
This commit is contained in:
@@ -3,7 +3,7 @@ name = "idevice-tools"
|
||||
description = "Rust binary tools to interact with services on iOS devices."
|
||||
authors = ["Jackson Coxson"]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
documentation = "https://docs.rs/idevice"
|
||||
repository = "https://github.com/jkcoxson/idevice"
|
||||
@@ -21,13 +21,17 @@ path = "src/heartbeat_client.rs"
|
||||
name = "instproxy"
|
||||
path = "src/instproxy.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "ideviceinstaller"
|
||||
path = "src/ideviceinstaller.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "mounter"
|
||||
path = "src/mounter.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "core_device_proxy_tun"
|
||||
path = "src/core_device_proxy_tun.rs"
|
||||
# [[bin]]
|
||||
# name = "core_device_proxy_tun"
|
||||
# path = "src/core_device_proxy_tun.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "idevice_id"
|
||||
@@ -97,15 +101,49 @@ path = "src/restore_service.rs"
|
||||
name = "remote_pairing"
|
||||
path = "src/remote_pairing.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "companion_proxy"
|
||||
path = "src/companion_proxy.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "diagnostics"
|
||||
path = "src/diagnostics.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "mobilebackup2"
|
||||
path = "src/mobilebackup2.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "diagnosticsservice"
|
||||
path = "src/diagnosticsservice.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "bt_packet_logger"
|
||||
path = "src/bt_packet_logger.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "pcapd"
|
||||
path = "src/pcapd.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "preboard"
|
||||
path = "src/preboard.rs"
|
||||
|
||||
[dependencies]
|
||||
idevice = { path = "../idevice", features = ["full"] }
|
||||
tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] }
|
||||
idevice = { path = "../idevice", features = ["full"], default-features = false }
|
||||
tokio = { version = "1.43", features = ["full"] }
|
||||
log = { version = "0.4" }
|
||||
env_logger = { version = "0.11" }
|
||||
tun-rs = { version = "1.5", features = ["async"] }
|
||||
# tun-rs = { version = "1.5", features = ["async"] }
|
||||
sha2 = { version = "0.10" }
|
||||
ureq = { version = "3" }
|
||||
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"]
|
||||
aws-lc = ["idevice/aws-lc"]
|
||||
ring = ["idevice/ring"]
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{value_parser, Arg, Command};
|
||||
use clap::{Arg, Command, value_parser};
|
||||
use idevice::{
|
||||
afc::{opcode::AfcFopenMode, AfcClient},
|
||||
house_arrest::HouseArrestClient,
|
||||
IdeviceService,
|
||||
afc::{AfcClient, opcode::AfcFopenMode},
|
||||
house_arrest::HouseArrestClient,
|
||||
};
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{amfi::AmfiClient, IdeviceService};
|
||||
use idevice::{IdeviceService, amfi::AmfiClient};
|
||||
|
||||
mod common;
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
core_device::AppServiceClient, core_device_proxy::CoreDeviceProxy,
|
||||
debug_proxy::DebugProxyClient, rsd::RsdHandshake, tcp::stream::AdapterStream, IdeviceService,
|
||||
RsdService,
|
||||
IdeviceService, RsdService,
|
||||
core_device::{AppServiceClient, OpenStdioSocketClient},
|
||||
core_device_proxy::CoreDeviceProxy,
|
||||
rsd::RsdHandshake,
|
||||
};
|
||||
|
||||
mod common;
|
||||
@@ -100,7 +99,7 @@ async fn main() {
|
||||
let host = matches.get_one::<String>("host");
|
||||
|
||||
let provider =
|
||||
match common::get_provider(udid, host, pairing_file, "debug-proxy-jkcoxson").await {
|
||||
match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
@@ -112,15 +111,10 @@ async fn main() {
|
||||
.expect("no core proxy");
|
||||
let rsd_port = proxy.handshake.server_rsd_port;
|
||||
|
||||
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
||||
adapter
|
||||
.pcap("/Users/jacksoncoxson/Desktop/rs_xpc.pcap")
|
||||
.await
|
||||
.unwrap();
|
||||
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
||||
let mut adapter = adapter.to_async_handle();
|
||||
|
||||
let stream = AdapterStream::connect(&mut adapter, rsd_port)
|
||||
.await
|
||||
.expect("no RSD connect");
|
||||
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
|
||||
|
||||
// Make the connection to RemoteXPC
|
||||
let mut handshake = RsdHandshake::new(stream).await.unwrap();
|
||||
@@ -144,12 +138,42 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
let mut stdio_conn = OpenStdioSocketClient::connect_rsd(&mut adapter, &mut handshake)
|
||||
.await
|
||||
.expect("no stdio");
|
||||
|
||||
let stdio_uuid = stdio_conn.read_uuid().await.expect("no uuid");
|
||||
println!("stdio uuid: {stdio_uuid:?}");
|
||||
|
||||
let res = asc
|
||||
.launch_application(bundle_id, &[], false, false, None, None)
|
||||
.launch_application(bundle_id, &[], true, false, None, None, Some(stdio_uuid))
|
||||
.await
|
||||
.expect("no launch");
|
||||
|
||||
println!("{res:#?}");
|
||||
println!("Launch response {res:#?}");
|
||||
|
||||
let (mut remote_reader, mut remote_writer) = tokio::io::split(stdio_conn.inner);
|
||||
let mut local_stdin = tokio::io::stdin();
|
||||
let mut local_stdout = tokio::io::stdout();
|
||||
|
||||
tokio::select! {
|
||||
// Task 1: Copy data from the remote process to local stdout
|
||||
res = tokio::io::copy(&mut remote_reader, &mut local_stdout) => {
|
||||
if let Err(e) = res {
|
||||
eprintln!("Error copying from remote to local: {}", e);
|
||||
}
|
||||
println!("\nRemote connection closed.");
|
||||
return;
|
||||
}
|
||||
// Task 2: Copy data from local stdin to the remote process
|
||||
res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => {
|
||||
if let Err(e) = res {
|
||||
eprintln!("Error copying from local to remote: {}", e);
|
||||
}
|
||||
println!("\nLocal stdin closed.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if matches.subcommand_matches("processes").is_some() {
|
||||
let p = asc.list_processes().await.expect("no processes?");
|
||||
println!("{p:#?}");
|
||||
|
||||
104
tools/src/bt_packet_logger.rs
Normal file
104
tools/src/bt_packet_logger.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use futures_util::StreamExt;
|
||||
use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient};
|
||||
use tokio::io::AsyncWrite;
|
||||
|
||||
use crate::pcap::{write_pcap_header, write_pcap_record};
|
||||
|
||||
mod common;
|
||||
mod pcap;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("amfi")
|
||||
.about("Capture Bluetooth packets")
|
||||
.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("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("out")
|
||||
.long("out")
|
||||
.value_name("PCAP")
|
||||
.help("Write PCAP to this file (use '-' for stdout)"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("bt_packet_logger - capture bluetooth packets");
|
||||
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 out = matches.get_one::<String>("out").map(String::to_owned);
|
||||
|
||||
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let logger_client = BtPacketLoggerClient::connect(&*provider)
|
||||
.await
|
||||
.expect("Failed to connect to amfi");
|
||||
|
||||
let mut s = logger_client.into_stream();
|
||||
|
||||
// Open output (default to stdout if --out omitted)
|
||||
let mut out_writer: Box<dyn AsyncWrite + Unpin + Send> = match out.as_deref() {
|
||||
Some("-") | None => Box::new(tokio::io::stdout()),
|
||||
Some(path) => Box::new(tokio::fs::File::create(path).await.expect("open pcap")),
|
||||
};
|
||||
|
||||
// Write global header
|
||||
write_pcap_header(&mut out_writer)
|
||||
.await
|
||||
.expect("pcap header");
|
||||
|
||||
// Drain stream to PCAP
|
||||
while let Some(res) = s.next().await {
|
||||
match res {
|
||||
Ok(frame) => {
|
||||
write_pcap_record(
|
||||
&mut out_writer,
|
||||
frame.hdr.ts_secs,
|
||||
frame.hdr.ts_usecs,
|
||||
frame.kind,
|
||||
&frame.h4,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|e| eprintln!("pcap write error: {e}"));
|
||||
}
|
||||
Err(e) => eprintln!("Failed to get next packet: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,7 @@ pub async fn get_provider(
|
||||
pairing_file: Option<&String>,
|
||||
label: &str,
|
||||
) -> Result<Box<dyn IdeviceProvider>, String> {
|
||||
let provider: Box<dyn IdeviceProvider> = if udid.is_some() {
|
||||
let udid = udid.unwrap();
|
||||
|
||||
let provider: Box<dyn IdeviceProvider> = if let Some(udid) = udid {
|
||||
let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") {
|
||||
let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS");
|
||||
let socket = tokio::net::TcpStream::connect(socket)
|
||||
@@ -40,14 +38,16 @@ pub async fn get_provider(
|
||||
}
|
||||
};
|
||||
Box::new(dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label))
|
||||
} else if host.is_some() && pairing_file.is_some() {
|
||||
let host = match IpAddr::from_str(host.unwrap()) {
|
||||
} else if let Some(host) = host
|
||||
&& let Some(pairing_file) = pairing_file
|
||||
{
|
||||
let host = match IpAddr::from_str(host) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
return Err(format!("Invalid host: {e:?}"));
|
||||
}
|
||||
};
|
||||
let pairing_file = match PairingFile::read_from_file(pairing_file.unwrap()) {
|
||||
let pairing_file = match PairingFile::read_from_file(pairing_file) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err(format!("Unable to read pairing file: {e:?}"));
|
||||
|
||||
151
tools/src/companion_proxy.rs
Normal file
151
tools/src/companion_proxy.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{Arg, Command, arg};
|
||||
use idevice::{
|
||||
IdeviceService, RsdService, companion_proxy::CompanionProxy,
|
||||
core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, pretty_print_plist,
|
||||
rsd::RsdHandshake,
|
||||
};
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("companion_proxy")
|
||||
.about("Apple Watch things")
|
||||
.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("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.subcommand(Command::new("list").about("List the companions on the device"))
|
||||
.subcommand(Command::new("listen").about("Listen for devices"))
|
||||
.subcommand(
|
||||
Command::new("get")
|
||||
.about("Gets a value")
|
||||
.arg(arg!(-d --device_udid <STRING> "the device udid to get from").required(true))
|
||||
.arg(arg!(-v --value <STRING> "the value to get").required(true)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("start")
|
||||
.about("Starts a service")
|
||||
.arg(arg!(-p --port <PORT> "the port").required(true))
|
||||
.arg(arg!(-n --name <STRING> "the optional service name").required(false)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("stop")
|
||||
.about("Starts a service")
|
||||
.arg(arg!(-p --port <PORT> "the port").required(true)),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("companion_proxy");
|
||||
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, "amfi-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||
.await
|
||||
.expect("no core_device_proxy");
|
||||
let rsd_port = proxy.handshake.server_rsd_port;
|
||||
let mut provider = proxy
|
||||
.create_software_tunnel()
|
||||
.expect("no tunnel")
|
||||
.to_async_handle();
|
||||
let mut handshake = RsdHandshake::new(provider.connect(rsd_port).await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut proxy = CompanionProxy::connect_rsd(&mut provider, &mut handshake)
|
||||
.await
|
||||
.expect("no companion proxy connect");
|
||||
|
||||
// let mut proxy = CompanionProxy::connect(&*provider)
|
||||
// .await
|
||||
// .expect("Failed to connect to companion proxy");
|
||||
|
||||
if matches.subcommand_matches("list").is_some() {
|
||||
proxy.get_device_registry().await.expect("Failed to show");
|
||||
} else if matches.subcommand_matches("listen").is_some() {
|
||||
let mut stream = proxy.listen_for_devices().await.expect("Failed to show");
|
||||
while let Ok(v) = stream.next().await {
|
||||
println!("{}", pretty_print_dictionary(&v));
|
||||
}
|
||||
} else if let Some(matches) = matches.subcommand_matches("get") {
|
||||
let key = matches.get_one::<String>("value").expect("no value passed");
|
||||
let udid = matches
|
||||
.get_one::<String>("device_udid")
|
||||
.expect("no AW udid passed");
|
||||
|
||||
match proxy.get_value(udid, key).await {
|
||||
Ok(value) => {
|
||||
println!("{}", pretty_print_plist(&value));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error getting value: {e}");
|
||||
}
|
||||
}
|
||||
} else if let Some(matches) = matches.subcommand_matches("start") {
|
||||
let port: u16 = matches
|
||||
.get_one::<String>("port")
|
||||
.expect("no port passed")
|
||||
.parse()
|
||||
.expect("not a number");
|
||||
let name = matches.get_one::<String>("name").map(|x| x.as_str());
|
||||
|
||||
match proxy.start_forwarding_service_port(port, name, None).await {
|
||||
Ok(value) => {
|
||||
println!("started on port {value}");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error starting: {e}");
|
||||
}
|
||||
}
|
||||
} else if let Some(matches) = matches.subcommand_matches("stop") {
|
||||
let port: u16 = matches
|
||||
.get_one::<String>("port")
|
||||
.expect("no port passed")
|
||||
.parse()
|
||||
.expect("not a number");
|
||||
|
||||
if let Err(e) = proxy.stop_forwarding_service_port(port).await {
|
||||
eprintln!("Error starting: {e}");
|
||||
}
|
||||
} else {
|
||||
eprintln!("Invalid usage, pass -h for help");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
crashreportcopymobile::{flush_reports, CrashReportCopyMobileClient},
|
||||
IdeviceService,
|
||||
crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports},
|
||||
};
|
||||
|
||||
mod common;
|
||||
@@ -37,7 +37,11 @@ async fn main() {
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.subcommand(Command::new("list").about("Lists the items in the directory"))
|
||||
.subcommand(
|
||||
Command::new("list")
|
||||
.about("Lists the items in the directory")
|
||||
.arg(Arg::new("dir").required(false).index(1)),
|
||||
)
|
||||
.subcommand(Command::new("flush").about("Flushes reports to the directory"))
|
||||
.subcommand(
|
||||
Command::new("pull")
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::io::Write;
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, rsd::RsdHandshake,
|
||||
tcp::stream::AdapterStream, IdeviceService, RsdService,
|
||||
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient,
|
||||
rsd::RsdHandshake,
|
||||
};
|
||||
|
||||
mod common;
|
||||
@@ -71,10 +71,9 @@ async fn main() {
|
||||
.expect("no core proxy");
|
||||
let rsd_port = proxy.handshake.server_rsd_port;
|
||||
|
||||
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
||||
let stream = AdapterStream::connect(&mut adapter, rsd_port)
|
||||
.await
|
||||
.expect("no RSD connect");
|
||||
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();
|
||||
|
||||
279
tools/src/diagnostics.rs
Normal file
279
tools/src/diagnostics.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
// Jackson Coxson
|
||||
// idevice Rust implementation of libimobiledevice's idevicediagnostics
|
||||
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use idevice::{IdeviceService, services::diagnostics_relay::DiagnosticsRelayClient};
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("idevicediagnostics")
|
||||
.about("Interact with the diagnostics interface of a device")
|
||||
.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("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("ioregistry")
|
||||
.about("Print IORegistry information")
|
||||
.arg(
|
||||
Arg::new("plane")
|
||||
.long("plane")
|
||||
.value_name("PLANE")
|
||||
.help("IORegistry plane to query (e.g., IODeviceTree, IOService)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("name")
|
||||
.long("name")
|
||||
.value_name("NAME")
|
||||
.help("Entry name to filter by"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("class")
|
||||
.long("class")
|
||||
.value_name("CLASS")
|
||||
.help("Entry class to filter by"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("mobilegestalt")
|
||||
.about("Print MobileGestalt information")
|
||||
.arg(
|
||||
Arg::new("keys")
|
||||
.long("keys")
|
||||
.value_name("KEYS")
|
||||
.help("Comma-separated list of keys to query")
|
||||
.value_delimiter(',')
|
||||
.num_args(1..),
|
||||
),
|
||||
)
|
||||
.subcommand(Command::new("gasguage").about("Print gas gauge (battery) information"))
|
||||
.subcommand(Command::new("nand").about("Print NAND flash information"))
|
||||
.subcommand(Command::new("all").about("Print all available diagnostics information"))
|
||||
.subcommand(Command::new("wifi").about("Print WiFi diagnostics information"))
|
||||
.subcommand(Command::new("goodbye").about("Send Goodbye to diagnostics relay"))
|
||||
.subcommand(Command::new("restart").about("Restart the device"))
|
||||
.subcommand(Command::new("shutdown").about("Shutdown the device"))
|
||||
.subcommand(Command::new("sleep").about("Put the device to sleep"))
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!(
|
||||
"idevicediagnostics - interact with the diagnostics interface of a device. Reimplementation of libimobiledevice's binary."
|
||||
);
|
||||
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, "idevicediagnostics-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
eprintln!("Unable to connect to diagnostics relay: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match matches.subcommand() {
|
||||
Some(("ioregistry", sub_matches)) => {
|
||||
handle_ioregistry(&mut diagnostics_client, sub_matches).await;
|
||||
}
|
||||
Some(("mobilegestalt", sub_matches)) => {
|
||||
handle_mobilegestalt(&mut diagnostics_client, sub_matches).await;
|
||||
}
|
||||
Some(("gasguage", _)) => {
|
||||
handle_gasguage(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("nand", _)) => {
|
||||
handle_nand(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("all", _)) => {
|
||||
handle_all(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("wifi", _)) => {
|
||||
handle_wifi(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("restart", _)) => {
|
||||
handle_restart(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("shutdown", _)) => {
|
||||
handle_shutdown(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("sleep", _)) => {
|
||||
handle_sleep(&mut diagnostics_client).await;
|
||||
}
|
||||
Some(("goodbye", _)) => {
|
||||
handle_goodbye(&mut diagnostics_client).await;
|
||||
}
|
||||
_ => {
|
||||
eprintln!("No subcommand specified. Use --help for usage information.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) {
|
||||
let plane = matches.get_one::<String>("plane").map(|s| s.as_str());
|
||||
let name = matches.get_one::<String>("name").map(|s| s.as_str());
|
||||
let class = matches.get_one::<String>("class").map(|s| s.as_str());
|
||||
|
||||
match client.ioregistry(plane, name, class).await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No IORegistry data returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get IORegistry data: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) {
|
||||
let keys = matches
|
||||
.get_many::<String>("keys")
|
||||
.map(|values| values.map(|s| s.to_string()).collect::<Vec<_>>());
|
||||
|
||||
match client.mobilegestalt(keys).await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No MobileGestalt data returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get MobileGestalt data: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_gasguage(client: &mut DiagnosticsRelayClient) {
|
||||
match client.gasguage().await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No gas gauge data returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get gas gauge data: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_nand(client: &mut DiagnosticsRelayClient) {
|
||||
match client.nand().await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No NAND data returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get NAND data: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_all(client: &mut DiagnosticsRelayClient) {
|
||||
match client.all().await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No diagnostics data returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get all diagnostics data: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_wifi(client: &mut DiagnosticsRelayClient) {
|
||||
match client.wifi().await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{data:#?}");
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No WiFi diagnostics returned");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get WiFi diagnostics: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_restart(client: &mut DiagnosticsRelayClient) {
|
||||
match client.restart().await {
|
||||
Ok(()) => {
|
||||
println!("Device restart command sent successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to restart device: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_shutdown(client: &mut DiagnosticsRelayClient) {
|
||||
match client.shutdown().await {
|
||||
Ok(()) => {
|
||||
println!("Device shutdown command sent successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to shutdown device: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_sleep(client: &mut DiagnosticsRelayClient) {
|
||||
match client.sleep().await {
|
||||
Ok(()) => {
|
||||
println!("Device sleep command sent successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to put device to sleep: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_goodbye(client: &mut DiagnosticsRelayClient) {
|
||||
match client.goodbye().await {
|
||||
Ok(()) => println!("Goodbye acknowledged by device"),
|
||||
Err(e) => eprintln!("Goodbye failed: {e:?}"),
|
||||
}
|
||||
}
|
||||
106
tools/src/diagnosticsservice.rs
Normal file
106
tools/src/diagnosticsservice.rs
Normal file
@@ -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::<String>("udid");
|
||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
||||
let host = matches.get_one::<String>("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);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Heartbeat client
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{heartbeat::HeartbeatClient, IdeviceService};
|
||||
use idevice::{IdeviceService, heartbeat::HeartbeatClient};
|
||||
|
||||
mod common;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// idevice Rust implementation of libimobiledevice's ideviceinfo
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{lockdown::LockdownClient, IdeviceService};
|
||||
use idevice::{IdeviceService, lockdown::LockdownClient};
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -39,7 +39,9 @@ async fn main() {
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary.");
|
||||
println!(
|
||||
"ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary."
|
||||
);
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
@@ -67,7 +69,9 @@ async fn main() {
|
||||
|
||||
println!(
|
||||
"{:?}",
|
||||
lockdown_client.get_value("ProductVersion", None).await
|
||||
lockdown_client
|
||||
.get_value(Some("ProductVersion"), None)
|
||||
.await
|
||||
);
|
||||
|
||||
println!(
|
||||
@@ -82,5 +86,5 @@ async fn main() {
|
||||
.await
|
||||
);
|
||||
println!("{:?}", lockdown_client.idevice.get_type().await.unwrap());
|
||||
println!("{:#?}", lockdown_client.get_all_values(None).await);
|
||||
println!("{:#?}", lockdown_client.get_value(None, None).await);
|
||||
}
|
||||
|
||||
103
tools/src/ideviceinstaller.rs
Normal file
103
tools/src/ideviceinstaller.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
// A minimal ideviceinstaller-like CLI to install/upgrade apps
|
||||
|
||||
use clap::{Arg, ArgAction, Command};
|
||||
use idevice::utils::installation;
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("ideviceinstaller")
|
||||
.about("Install/upgrade apps on an iOS device (AFC + InstallationProxy)")
|
||||
.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("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("install")
|
||||
.about("Install a local .ipa or directory")
|
||||
.arg(Arg::new("path").required(true).value_name("PATH")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("upgrade")
|
||||
.about("Upgrade from a local .ipa or directory")
|
||||
.arg(Arg::new("path").required(true).value_name("PATH")),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("ideviceinstaller - install/upgrade apps using AFC + InstallationProxy (Rust)");
|
||||
println!("Copyright (c) 2025");
|
||||
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, "ideviceinstaller").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(matches) = matches.subcommand_matches("install") {
|
||||
let path: &String = matches.get_one("path").expect("required");
|
||||
match installation::install_package_with_callback(
|
||||
&*provider,
|
||||
path,
|
||||
None,
|
||||
|(percentage, _)| async move {
|
||||
println!("Installing: {percentage}%");
|
||||
},
|
||||
(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => println!("install success"),
|
||||
Err(e) => eprintln!("Install failed: {e}"),
|
||||
}
|
||||
} else if let Some(matches) = matches.subcommand_matches("upgrade") {
|
||||
let path: &String = matches.get_one("path").expect("required");
|
||||
match installation::upgrade_package_with_callback(
|
||||
&*provider,
|
||||
path,
|
||||
None,
|
||||
|(percentage, _)| async move {
|
||||
println!("Upgrading: {percentage}%");
|
||||
},
|
||||
(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => println!("upgrade success"),
|
||||
Err(e) => eprintln!("Upgrade failed: {e}"),
|
||||
}
|
||||
} else {
|
||||
eprintln!("Invalid usage, pass -h for help");
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Just lists apps for now
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{installation_proxy::InstallationProxyClient, IdeviceService};
|
||||
use idevice::{IdeviceService, installation_proxy::InstallationProxyClient};
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -47,7 +47,9 @@ async fn main() {
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary.");
|
||||
println!(
|
||||
"instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary."
|
||||
);
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
@@ -69,10 +71,7 @@ async fn main() {
|
||||
.await
|
||||
.expect("Unable to connect to instproxy");
|
||||
if matches.subcommand_matches("lookup").is_some() {
|
||||
let apps = instproxy_client
|
||||
.get_apps(Some("User".to_string()), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap();
|
||||
for app in apps.keys() {
|
||||
println!("{app}");
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
// Just lists apps for now
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, tcp::stream::AdapterStream,
|
||||
IdeviceService, RsdService,
|
||||
};
|
||||
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -71,10 +68,9 @@ async fn main() {
|
||||
.expect("no core proxy");
|
||||
let rsd_port = proxy.handshake.server_rsd_port;
|
||||
|
||||
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
||||
let stream = AdapterStream::connect(&mut adapter, rsd_port)
|
||||
.await
|
||||
.expect("no RSD connect");
|
||||
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();
|
||||
@@ -118,7 +114,11 @@ async fn main() {
|
||||
println!("Location set!");
|
||||
println!("Press ctrl-c to stop");
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
ls_client
|
||||
.set(latitude, longitude)
|
||||
.await
|
||||
.expect("Failed to set location");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
} else {
|
||||
eprintln!("Invalid usage, pass -h for help");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{arg, Arg, Command};
|
||||
use idevice::{lockdown::LockdownClient, pretty_print_plist, IdeviceService};
|
||||
use clap::{Arg, Command, arg};
|
||||
use idevice::{IdeviceService, lockdown::LockdownClient, pretty_print_plist};
|
||||
use plist::Value;
|
||||
|
||||
mod common;
|
||||
@@ -39,12 +39,7 @@ async fn main() {
|
||||
.subcommand(
|
||||
Command::new("get")
|
||||
.about("Gets a value")
|
||||
.arg(arg!(-v --value <STRING> "the value to get").required(true))
|
||||
.arg(arg!(-d --domain <STRING> "the domain to get in").required(false)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("get_all")
|
||||
.about("Gets all")
|
||||
.arg(arg!(-v --value <STRING> "the value to get").required(false))
|
||||
.arg(arg!(-d --domain <STRING> "the domain to get in").required(false)),
|
||||
)
|
||||
.subcommand(
|
||||
@@ -57,7 +52,9 @@ async fn main() {
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary.");
|
||||
println!(
|
||||
"lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary."
|
||||
);
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
@@ -86,8 +83,8 @@ async fn main() {
|
||||
|
||||
match matches.subcommand() {
|
||||
Some(("get", sub_m)) => {
|
||||
let key = sub_m.get_one::<String>("value").unwrap();
|
||||
let domain = sub_m.get_one::<String>("domain").cloned();
|
||||
let key = sub_m.get_one::<String>("value").map(|x| x.as_str());
|
||||
let domain = sub_m.get_one::<String>("domain").map(|x| x.as_str());
|
||||
|
||||
match lockdown_client.get_value(key, domain).await {
|
||||
Ok(value) => {
|
||||
@@ -98,27 +95,18 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(("get_all", sub_m)) => {
|
||||
let domain = sub_m.get_one::<String>("domain").cloned();
|
||||
|
||||
match lockdown_client.get_all_values(domain).await {
|
||||
Ok(value) => {
|
||||
println!("{}", pretty_print_plist(&plist::Value::Dictionary(value)));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error getting value: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(("set", sub_m)) => {
|
||||
let key = sub_m.get_one::<String>("key").unwrap();
|
||||
let value_str = sub_m.get_one::<String>("value").unwrap();
|
||||
let domain = sub_m.get_one::<String>("domain").cloned();
|
||||
let domain = sub_m.get_one::<String>("domain");
|
||||
|
||||
let value = Value::String(value_str.clone());
|
||||
|
||||
match lockdown_client.set_value(key, value, domain).await {
|
||||
match lockdown_client
|
||||
.set_value(key, value, domain.map(|x| x.as_str()))
|
||||
.await
|
||||
{
|
||||
Ok(()) => println!("Successfully set"),
|
||||
Err(e) => eprintln!("Error setting value: {e}"),
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{arg, value_parser, Arg, Command};
|
||||
use idevice::{misagent::MisagentClient, IdeviceService};
|
||||
use clap::{Arg, Command, arg, value_parser};
|
||||
use idevice::{IdeviceService, misagent::MisagentClient};
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -52,7 +52,9 @@ async fn main() {
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary.");
|
||||
println!(
|
||||
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
|
||||
);
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
|
||||
668
tools/src/mobilebackup2.rs
Normal file
668
tools/src/mobilebackup2.rs
Normal file
@@ -0,0 +1,668 @@
|
||||
// Jackson Coxson
|
||||
// Mobile Backup 2 tool for iOS devices
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
IdeviceService,
|
||||
mobilebackup2::{MobileBackup2Client, RestoreOptions},
|
||||
};
|
||||
use plist::Dictionary;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("mobilebackup2")
|
||||
.about("Mobile Backup 2 tool for iOS devices")
|
||||
.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("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("info")
|
||||
.about("Get backup information from a local backup directory")
|
||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
||||
.arg(
|
||||
Arg::new("source")
|
||||
.long("source")
|
||||
.value_name("SOURCE")
|
||||
.help("Source identifier (defaults to current UDID)"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("list")
|
||||
.about("List files of the last backup from a local backup directory")
|
||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
||||
.arg(Arg::new("source").long("source").value_name("SOURCE")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("backup")
|
||||
.about("Start a backup operation")
|
||||
.arg(
|
||||
Arg::new("dir")
|
||||
.long("dir")
|
||||
.value_name("DIR")
|
||||
.help("Backup directory on host")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("target")
|
||||
.long("target")
|
||||
.value_name("TARGET")
|
||||
.help("Target identifier for the backup"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("source")
|
||||
.long("source")
|
||||
.value_name("SOURCE")
|
||||
.help("Source identifier for the backup"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("restore")
|
||||
.about("Restore from a local backup directory (DeviceLink)")
|
||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
||||
.arg(
|
||||
Arg::new("source")
|
||||
.long("source")
|
||||
.value_name("SOURCE")
|
||||
.help("Source UDID; defaults to current device UDID"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("password")
|
||||
.long("password")
|
||||
.value_name("PWD")
|
||||
.help("Backup password if encrypted"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-reboot")
|
||||
.long("no-reboot")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-copy")
|
||||
.long("no-copy")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-settings")
|
||||
.long("no-settings")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("system")
|
||||
.long("system")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("remove")
|
||||
.long("remove")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("unback")
|
||||
.about("Unpack a complete backup to device hierarchy")
|
||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
||||
.arg(Arg::new("source").long("source").value_name("SOURCE"))
|
||||
.arg(Arg::new("password").long("password").value_name("PWD")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("extract")
|
||||
.about("Extract a file from a previous backup")
|
||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
||||
.arg(Arg::new("source").long("source").value_name("SOURCE"))
|
||||
.arg(
|
||||
Arg::new("domain")
|
||||
.long("domain")
|
||||
.value_name("DOMAIN")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("path")
|
||||
.long("path")
|
||||
.value_name("REL_PATH")
|
||||
.required(true),
|
||||
)
|
||||
.arg(Arg::new("password").long("password").value_name("PWD")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("change-password")
|
||||
.about("Change backup password")
|
||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
||||
.arg(Arg::new("old").long("old").value_name("OLD"))
|
||||
.arg(Arg::new("new").long("new").value_name("NEW")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("erase-device")
|
||||
.about("Erase the device via mobilebackup2")
|
||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)),
|
||||
)
|
||||
.subcommand(Command::new("freespace").about("Get free space information"))
|
||||
.subcommand(Command::new("encryption").about("Check backup encryption status"))
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("mobilebackup2 - manage device backups using Mobile Backup 2 service");
|
||||
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, "mobilebackup2-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Error creating provider: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut backup_client = match MobileBackup2Client::connect(&*provider).await {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
eprintln!("Unable to connect to mobilebackup2 service: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match matches.subcommand() {
|
||||
Some(("info", sub)) => {
|
||||
let dir = sub.get_one::<String>("dir").unwrap();
|
||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
||||
match backup_client.info_from_path(Path::new(dir), source).await {
|
||||
Ok(dict) => {
|
||||
println!("Backup Information:");
|
||||
for (k, v) in dict {
|
||||
println!(" {k}: {v:?}");
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Failed to get info: {e}"),
|
||||
}
|
||||
}
|
||||
Some(("list", sub)) => {
|
||||
let dir = sub.get_one::<String>("dir").unwrap();
|
||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
||||
match backup_client.list_from_path(Path::new(dir), source).await {
|
||||
Ok(dict) => {
|
||||
println!("List Response:");
|
||||
for (k, v) in dict {
|
||||
println!(" {k}: {v:?}");
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Failed to list: {e}"),
|
||||
}
|
||||
}
|
||||
Some(("backup", sub_matches)) => {
|
||||
let target = sub_matches.get_one::<String>("target").map(|s| s.as_str());
|
||||
let source = sub_matches.get_one::<String>("source").map(|s| s.as_str());
|
||||
let dir = sub_matches
|
||||
.get_one::<String>("dir")
|
||||
.expect("dir is required");
|
||||
|
||||
println!("Starting backup operation...");
|
||||
let res = backup_client
|
||||
.send_request("Backup", target, source, None::<Dictionary>)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
eprintln!("Failed to send backup request: {e}");
|
||||
} else if let Err(e) = process_dl_loop(&mut backup_client, Path::new(dir)).await {
|
||||
eprintln!("Backup failed during DL loop: {e}");
|
||||
} else {
|
||||
println!("Backup flow finished");
|
||||
}
|
||||
}
|
||||
Some(("restore", sub)) => {
|
||||
let dir = sub.get_one::<String>("dir").unwrap();
|
||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
||||
let mut ropts = RestoreOptions::new();
|
||||
if sub.get_flag("no-reboot") {
|
||||
ropts = ropts.with_reboot(false);
|
||||
}
|
||||
if sub.get_flag("no-copy") {
|
||||
ropts = ropts.with_copy(false);
|
||||
}
|
||||
if sub.get_flag("no-settings") {
|
||||
ropts = ropts.with_preserve_settings(false);
|
||||
}
|
||||
if sub.get_flag("system") {
|
||||
ropts = ropts.with_system_files(true);
|
||||
}
|
||||
if sub.get_flag("remove") {
|
||||
ropts = ropts.with_remove_items_not_restored(true);
|
||||
}
|
||||
if let Some(pw) = sub.get_one::<String>("password") {
|
||||
ropts = ropts.with_password(pw);
|
||||
}
|
||||
match backup_client
|
||||
.restore_from_path(Path::new(dir), source, Some(ropts))
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Restore flow finished"),
|
||||
Err(e) => eprintln!("Restore failed: {e}"),
|
||||
}
|
||||
}
|
||||
Some(("unback", sub)) => {
|
||||
let dir = sub.get_one::<String>("dir").unwrap();
|
||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
||||
let password = sub.get_one::<String>("password").map(|s| s.as_str());
|
||||
match backup_client
|
||||
.unback_from_path(Path::new(dir), password, source)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Unback finished"),
|
||||
Err(e) => eprintln!("Unback failed: {e}"),
|
||||
}
|
||||
}
|
||||
Some(("extract", sub)) => {
|
||||
let dir = sub.get_one::<String>("dir").unwrap();
|
||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
||||
let domain = sub.get_one::<String>("domain").unwrap();
|
||||
let rel = sub.get_one::<String>("path").unwrap();
|
||||
let password = sub.get_one::<String>("password").map(|s| s.as_str());
|
||||
match backup_client
|
||||
.extract_from_path(domain, rel, Path::new(dir), password, source)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Extract finished"),
|
||||
Err(e) => eprintln!("Extract failed: {e}"),
|
||||
}
|
||||
}
|
||||
Some(("change-password", sub)) => {
|
||||
let dir = sub.get_one::<String>("dir").unwrap();
|
||||
let old = sub.get_one::<String>("old").map(|s| s.as_str());
|
||||
let newv = sub.get_one::<String>("new").map(|s| s.as_str());
|
||||
match backup_client
|
||||
.change_password_from_path(Path::new(dir), old, newv)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Change password finished"),
|
||||
Err(e) => eprintln!("Change password failed: {e}"),
|
||||
}
|
||||
}
|
||||
Some(("erase-device", sub)) => {
|
||||
let dir = sub.get_one::<String>("dir").unwrap();
|
||||
match backup_client.erase_device_from_path(Path::new(dir)).await {
|
||||
Ok(_) => println!("Erase device command sent"),
|
||||
Err(e) => eprintln!("Erase device failed: {e}"),
|
||||
}
|
||||
}
|
||||
Some(("freespace", _)) => match backup_client.get_freespace().await {
|
||||
Ok(freespace) => {
|
||||
let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||
println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)");
|
||||
}
|
||||
Err(e) => eprintln!("Failed to get free space: {e}"),
|
||||
},
|
||||
Some(("encryption", _)) => match backup_client.check_backup_encryption().await {
|
||||
Ok(is_encrypted) => {
|
||||
println!(
|
||||
"Backup encryption: {}",
|
||||
if is_encrypted { "Enabled" } else { "Disabled" }
|
||||
);
|
||||
}
|
||||
Err(e) => eprintln!("Failed to check backup encryption: {e}"),
|
||||
},
|
||||
_ => {
|
||||
println!("No subcommand provided. Use --help for available commands.");
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect from the service
|
||||
if let Err(e) = backup_client.disconnect().await {
|
||||
eprintln!("Warning: Failed to disconnect cleanly: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
use idevice::services::mobilebackup2::{
|
||||
DL_CODE_ERROR_LOCAL as CODE_ERROR_LOCAL, DL_CODE_FILE_DATA as CODE_FILE_DATA,
|
||||
DL_CODE_SUCCESS as CODE_SUCCESS,
|
||||
};
|
||||
|
||||
async fn process_dl_loop(
|
||||
client: &mut MobileBackup2Client,
|
||||
host_dir: &Path,
|
||||
) -> Result<Option<Dictionary>, idevice::IdeviceError> {
|
||||
loop {
|
||||
let (tag, value) = client.receive_dl_message().await?;
|
||||
match tag.as_str() {
|
||||
"DLMessageDownloadFiles" => {
|
||||
handle_download_files(client, &value, host_dir).await?;
|
||||
}
|
||||
"DLMessageUploadFiles" => {
|
||||
handle_upload_files(client, &value, host_dir).await?;
|
||||
}
|
||||
"DLMessageGetFreeDiskSpace" => {
|
||||
// Minimal implementation: report unknown/zero with success
|
||||
client
|
||||
.send_status_response(0, None, Some(plist::Value::Integer(0u64.into())))
|
||||
.await?;
|
||||
}
|
||||
"DLContentsOfDirectory" => {
|
||||
// Minimal: return empty listing
|
||||
let empty = plist::Value::Dictionary(Dictionary::new());
|
||||
client.send_status_response(0, None, Some(empty)).await?;
|
||||
}
|
||||
"DLMessageCreateDirectory" => {
|
||||
let status = create_directory_from_message(&value, host_dir);
|
||||
client.send_status_response(status, None, None).await?;
|
||||
}
|
||||
"DLMessageMoveFiles" | "DLMessageMoveItems" => {
|
||||
let status = move_files_from_message(&value, host_dir);
|
||||
client
|
||||
.send_status_response(
|
||||
status,
|
||||
None,
|
||||
Some(plist::Value::Dictionary(Dictionary::new())),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"DLMessageRemoveFiles" | "DLMessageRemoveItems" => {
|
||||
let status = remove_files_from_message(&value, host_dir);
|
||||
client
|
||||
.send_status_response(
|
||||
status,
|
||||
None,
|
||||
Some(plist::Value::Dictionary(Dictionary::new())),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"DLMessageCopyItem" => {
|
||||
let status = copy_item_from_message(&value, host_dir);
|
||||
client
|
||||
.send_status_response(
|
||||
status,
|
||||
None,
|
||||
Some(plist::Value::Dictionary(Dictionary::new())),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"DLMessageProcessMessage" => {
|
||||
// Final status/content: return inner dict
|
||||
if let plist::Value::Array(arr) = value
|
||||
&& let Some(plist::Value::Dictionary(dict)) = arr.get(1)
|
||||
{
|
||||
return Ok(Some(dict.clone()));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
"DLMessageDisconnect" => {
|
||||
return Ok(None);
|
||||
}
|
||||
other => {
|
||||
eprintln!("Unsupported DL message: {other}");
|
||||
client
|
||||
.send_status_response(-1, Some("Operation not supported"), None)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_download_files(
|
||||
client: &mut MobileBackup2Client,
|
||||
dl_value: &plist::Value,
|
||||
host_dir: &Path,
|
||||
) -> Result<(), idevice::IdeviceError> {
|
||||
// dl_value is an array: ["DLMessageDownloadFiles", [paths...], progress?]
|
||||
let mut err_any = false;
|
||||
if let plist::Value::Array(arr) = dl_value
|
||||
&& arr.len() >= 2
|
||||
&& let Some(plist::Value::Array(files)) = arr.get(1)
|
||||
{
|
||||
for pv in files {
|
||||
if let Some(path) = pv.as_string()
|
||||
&& let Err(e) = send_single_file(client, host_dir, path).await
|
||||
{
|
||||
eprintln!("Failed to send file {path}: {e}");
|
||||
err_any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// terminating zero dword
|
||||
client.idevice.send_raw(&0u32.to_be_bytes()).await?;
|
||||
// status response
|
||||
if err_any {
|
||||
client
|
||||
.send_status_response(
|
||||
-13,
|
||||
Some("Multi status"),
|
||||
Some(plist::Value::Dictionary(Dictionary::new())),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
client
|
||||
.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new())))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_single_file(
|
||||
client: &mut MobileBackup2Client,
|
||||
host_dir: &Path,
|
||||
rel_path: &str,
|
||||
) -> Result<(), idevice::IdeviceError> {
|
||||
let full = host_dir.join(rel_path);
|
||||
let path_bytes = rel_path.as_bytes().to_vec();
|
||||
let nlen = (path_bytes.len() as u32).to_be_bytes();
|
||||
client.idevice.send_raw(&nlen).await?;
|
||||
client.idevice.send_raw(&path_bytes).await?;
|
||||
|
||||
let mut f = match std::fs::File::open(&full) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
// send error
|
||||
let desc = e.to_string();
|
||||
let size = (desc.len() as u32 + 1).to_be_bytes();
|
||||
let mut hdr = Vec::with_capacity(5);
|
||||
hdr.extend_from_slice(&size);
|
||||
hdr.push(CODE_ERROR_LOCAL);
|
||||
client.idevice.send_raw(&hdr).await?;
|
||||
client.idevice.send_raw(desc.as_bytes()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let mut buf = [0u8; 32768];
|
||||
loop {
|
||||
let read = f.read(&mut buf).unwrap_or(0);
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
let size = ((read as u32) + 1).to_be_bytes();
|
||||
let mut hdr = Vec::with_capacity(5);
|
||||
hdr.extend_from_slice(&size);
|
||||
hdr.push(CODE_FILE_DATA);
|
||||
client.idevice.send_raw(&hdr).await?;
|
||||
client.idevice.send_raw(&buf[..read]).await?;
|
||||
}
|
||||
// success trailer
|
||||
let mut ok = [0u8; 5];
|
||||
ok[..4].copy_from_slice(&1u32.to_be_bytes());
|
||||
ok[4] = CODE_SUCCESS;
|
||||
client.idevice.send_raw(&ok).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_upload_files(
|
||||
client: &mut MobileBackup2Client,
|
||||
_dl_value: &plist::Value,
|
||||
host_dir: &Path,
|
||||
) -> Result<(), idevice::IdeviceError> {
|
||||
// Minimal receiver: read pairs of (dir, filename) and block stream
|
||||
// Receive dir name
|
||||
loop {
|
||||
let dlen = read_be_u32(client).await?;
|
||||
if dlen == 0 {
|
||||
break;
|
||||
}
|
||||
let dname = read_exact_string(client, dlen as usize).await?;
|
||||
let flen = read_be_u32(client).await?;
|
||||
if flen == 0 {
|
||||
break;
|
||||
}
|
||||
let fname = read_exact_string(client, flen as usize).await?;
|
||||
let dst = host_dir.join(&fname);
|
||||
if let Some(parent) = dst.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
let mut file = std::fs::File::create(&dst)
|
||||
.map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?;
|
||||
loop {
|
||||
let nlen = read_be_u32(client).await?;
|
||||
if nlen == 0 {
|
||||
break;
|
||||
}
|
||||
let code = read_one(client).await?;
|
||||
if code == CODE_FILE_DATA {
|
||||
let size = (nlen - 1) as usize;
|
||||
let data = read_exact(client, size).await?;
|
||||
file.write_all(&data)
|
||||
.map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?;
|
||||
} else {
|
||||
let _ = read_exact(client, (nlen - 1) as usize).await?;
|
||||
}
|
||||
}
|
||||
let _ = dname; // not used
|
||||
}
|
||||
client
|
||||
.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new())))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn read_be_u32(client: &mut MobileBackup2Client) -> Result<u32, idevice::IdeviceError> {
|
||||
let buf = client.idevice.read_raw(4).await?;
|
||||
Ok(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]))
|
||||
}
|
||||
|
||||
async fn read_one(client: &mut MobileBackup2Client) -> Result<u8, idevice::IdeviceError> {
|
||||
let buf = client.idevice.read_raw(1).await?;
|
||||
Ok(buf[0])
|
||||
}
|
||||
|
||||
async fn read_exact(
|
||||
client: &mut MobileBackup2Client,
|
||||
size: usize,
|
||||
) -> Result<Vec<u8>, idevice::IdeviceError> {
|
||||
client.idevice.read_raw(size).await
|
||||
}
|
||||
|
||||
async fn read_exact_string(
|
||||
client: &mut MobileBackup2Client,
|
||||
size: usize,
|
||||
) -> Result<String, idevice::IdeviceError> {
|
||||
let buf = client.idevice.read_raw(size).await?;
|
||||
Ok(String::from_utf8_lossy(&buf).to_string())
|
||||
}
|
||||
|
||||
fn create_directory_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
|
||||
if let plist::Value::Array(arr) = dl_value
|
||||
&& arr.len() >= 2
|
||||
&& let Some(plist::Value::String(dir)) = arr.get(1)
|
||||
{
|
||||
let path = host_dir.join(dir);
|
||||
return match fs::create_dir_all(&path) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
};
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
fn move_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
|
||||
if let plist::Value::Array(arr) = dl_value
|
||||
&& arr.len() >= 2
|
||||
&& let Some(plist::Value::Dictionary(map)) = arr.get(1)
|
||||
{
|
||||
for (from, to_v) in map.iter() {
|
||||
if let Some(to) = to_v.as_string() {
|
||||
let old = host_dir.join(from);
|
||||
let newp = host_dir.join(to);
|
||||
if let Some(parent) = newp.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
if fs::rename(&old, &newp).is_err() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
fn remove_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
|
||||
if let plist::Value::Array(arr) = dl_value
|
||||
&& arr.len() >= 2
|
||||
&& let Some(plist::Value::Array(items)) = arr.get(1)
|
||||
{
|
||||
for it in items {
|
||||
if let Some(p) = it.as_string() {
|
||||
let path = host_dir.join(p);
|
||||
if path.is_dir() {
|
||||
if fs::remove_dir_all(&path).is_err() {
|
||||
return -1;
|
||||
}
|
||||
} else if path.exists() && fs::remove_file(&path).is_err() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
fn copy_item_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
|
||||
if let plist::Value::Array(arr) = dl_value
|
||||
&& arr.len() >= 3
|
||||
&& let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) =
|
||||
(arr.get(1), arr.get(2))
|
||||
{
|
||||
let from = host_dir.join(src);
|
||||
let to = host_dir.join(dst);
|
||||
if let Some(parent) = to.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
if from.is_dir() {
|
||||
// shallow copy: create dir
|
||||
return match fs::create_dir_all(&to) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
};
|
||||
} else {
|
||||
return match fs::copy(&from, &to) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
};
|
||||
}
|
||||
}
|
||||
-1
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
use std::{io::Write, path::PathBuf};
|
||||
|
||||
use clap::{arg, value_parser, Arg, Command};
|
||||
use clap::{Arg, Command, arg, value_parser};
|
||||
use idevice::{
|
||||
lockdown::LockdownClient, mobile_image_mounter::ImageMounter, pretty_print_plist,
|
||||
IdeviceService,
|
||||
IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter,
|
||||
pretty_print_plist,
|
||||
};
|
||||
|
||||
mod common;
|
||||
@@ -67,7 +67,9 @@ async fn main() {
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary.");
|
||||
println!(
|
||||
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
|
||||
);
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
@@ -89,7 +91,10 @@ async fn main() {
|
||||
.await
|
||||
.expect("Unable to connect to lockdown");
|
||||
|
||||
let product_version = match lockdown_client.get_value("ProductVersion", None).await {
|
||||
let product_version = match lockdown_client
|
||||
.get_value(Some("ProductVersion"), None)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
lockdown_client
|
||||
@@ -97,7 +102,7 @@ async fn main() {
|
||||
.await
|
||||
.unwrap();
|
||||
lockdown_client
|
||||
.get_value("ProductVersion", None)
|
||||
.get_value(Some("ProductVersion"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
@@ -182,21 +187,22 @@ async fn main() {
|
||||
.await
|
||||
.expect("Unable to read signature");
|
||||
|
||||
let unique_chip_id = match lockdown_client.get_value("UniqueChipID", None).await {
|
||||
Ok(u) => u,
|
||||
Err(_) => {
|
||||
lockdown_client
|
||||
.start_session(&provider.get_pairing_file().await.unwrap())
|
||||
.await
|
||||
.expect("Unable to start session");
|
||||
lockdown_client
|
||||
.get_value("UniqueChipID", None)
|
||||
.await
|
||||
.expect("Unable to get UniqueChipID")
|
||||
let unique_chip_id =
|
||||
match lockdown_client.get_value(Some("UniqueChipID"), None).await {
|
||||
Ok(u) => u,
|
||||
Err(_) => {
|
||||
lockdown_client
|
||||
.start_session(&provider.get_pairing_file().await.unwrap())
|
||||
.await
|
||||
.expect("Unable to start session");
|
||||
lockdown_client
|
||||
.get_value(Some("UniqueChipID"), None)
|
||||
.await
|
||||
.expect("Unable to get UniqueChipID")
|
||||
}
|
||||
}
|
||||
}
|
||||
.as_unsigned_integer()
|
||||
.expect("Unexpected value for chip IP");
|
||||
.as_unsigned_integer()
|
||||
.expect("Unexpected value for chip IP");
|
||||
|
||||
mounter_client
|
||||
.mount_personalized_with_callback(
|
||||
@@ -208,7 +214,7 @@ async fn main() {
|
||||
unique_chip_id,
|
||||
async |((n, d), _)| {
|
||||
let percent = (n as f64 / d as f64) * 100.0;
|
||||
print!("\rProgress: {:.2}%", percent);
|
||||
print!("\rProgress: {percent:.2}%");
|
||||
std::io::stdout().flush().unwrap(); // Make sure it prints immediately
|
||||
if n == d {
|
||||
println!();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{os_trace_relay::OsTraceRelayClient, IdeviceService};
|
||||
use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient};
|
||||
|
||||
mod common;
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
IdeviceService,
|
||||
lockdown::LockdownClient,
|
||||
usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection},
|
||||
IdeviceService,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
@@ -74,10 +74,13 @@ async fn main() {
|
||||
.expect("Pairing file test failed");
|
||||
|
||||
// Add the UDID (jitterbug spec)
|
||||
pairing_file.udid = Some(dev.udid);
|
||||
pairing_file.udid = Some(dev.udid.clone());
|
||||
let pairing_file = pairing_file.serialize().expect("failed to serialize");
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
String::from_utf8(pairing_file.serialize().unwrap()).unwrap()
|
||||
);
|
||||
println!("{}", String::from_utf8(pairing_file.clone()).unwrap());
|
||||
|
||||
// Save with usbmuxd
|
||||
u.save_pair_record(dev.device_id, &dev.udid, pairing_file)
|
||||
.await
|
||||
.expect("no save");
|
||||
}
|
||||
|
||||
60
tools/src/pcap.rs
Normal file
60
tools/src/pcap.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use idevice::bt_packet_logger::BtPacketKind;
|
||||
use tokio::io::{AsyncWrite, AsyncWriteExt};
|
||||
|
||||
// Classic PCAP (big-endian) global header for DLT_BLUETOOTH_HCI_H4_WITH_PHDR (201)
|
||||
const PCAP_GLOBAL_HEADER_BE: [u8; 24] = [
|
||||
0xA1, 0xB2, 0xC3, 0xD4, // magic (big-endian stream)
|
||||
0x00, 0x02, // version maj
|
||||
0x00, 0x04, // version min
|
||||
0x00, 0x00, 0x00, 0x00, // thiszone
|
||||
0x00, 0x00, 0x00, 0x00, // sigfigs
|
||||
0x00, 0x00, 0x08, 0x00, // snaplen = 2048
|
||||
0x00, 0x00, 0x00, 201, // network = 201 (HCI_H4_WITH_PHDR)
|
||||
];
|
||||
|
||||
#[inline]
|
||||
fn be32(x: u32) -> [u8; 4] {
|
||||
[(x >> 24) as u8, (x >> 16) as u8, (x >> 8) as u8, x as u8]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn dir_flag(kind: BtPacketKind) -> Option<u32> {
|
||||
use BtPacketKind::*;
|
||||
Some(match kind {
|
||||
HciCmd | AclSent | ScoSent => 0,
|
||||
HciEvt | AclRecv | ScoRecv => 1,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn write_pcap_header<W: AsyncWrite + Unpin>(w: &mut W) -> std::io::Result<()> {
|
||||
w.write_all(&PCAP_GLOBAL_HEADER_BE).await
|
||||
}
|
||||
|
||||
pub async fn write_pcap_record<W: AsyncWrite + Unpin>(
|
||||
w: &mut W,
|
||||
ts_sec: u32,
|
||||
ts_usec: u32,
|
||||
kind: BtPacketKind,
|
||||
h4_payload: &[u8], // starts with H4 type followed by HCI bytes
|
||||
) -> std::io::Result<()> {
|
||||
// Prepend 4-byte direction flag to the packet body
|
||||
let Some(dir) = dir_flag(kind) else {
|
||||
return Ok(());
|
||||
};
|
||||
let cap_len = 4u32 + h4_payload.len() as u32;
|
||||
|
||||
// PCAP record header (big-endian fields to match magic above)
|
||||
// ts_sec, ts_usec, incl_len, orig_len
|
||||
let mut rec = [0u8; 16];
|
||||
rec[0..4].copy_from_slice(&be32(ts_sec));
|
||||
rec[4..8].copy_from_slice(&be32(ts_usec));
|
||||
rec[8..12].copy_from_slice(&be32(cap_len));
|
||||
rec[12..16].copy_from_slice(&be32(cap_len));
|
||||
|
||||
// Write: rec hdr, dir flag (as 4 BE bytes), then H4 bytes
|
||||
w.write_all(&rec).await?;
|
||||
w.write_all(&be32(dir)).await?;
|
||||
w.write_all(h4_payload).await?;
|
||||
Ok(())
|
||||
}
|
||||
83
tools/src/pcapd.rs
Normal file
83
tools/src/pcapd.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
IdeviceService,
|
||||
pcapd::{PcapFileWriter, PcapdClient},
|
||||
};
|
||||
|
||||
mod common;
|
||||
mod pcap;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("pcapd")
|
||||
.about("Capture IP packets")
|
||||
.arg(
|
||||
Arg::new("udid")
|
||||
.value_name("UDID")
|
||||
.help("UDID of the device (overrides host/pairing file)")
|
||||
.index(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("out")
|
||||
.long("out")
|
||||
.value_name("PCAP")
|
||||
.help("Write PCAP to this file (use '-' for stdout)"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("bt_packet_logger - capture bluetooth packets");
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
|
||||
let udid = matches.get_one::<String>("udid");
|
||||
let out = matches.get_one::<String>("out").map(String::to_owned);
|
||||
|
||||
let provider = match common::get_provider(udid, None, None, "pcapd-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut logger_client = PcapdClient::connect(&*provider)
|
||||
.await
|
||||
.expect("Failed to connect to pcapd! This service is only available over USB!");
|
||||
|
||||
logger_client.next_packet().await.unwrap();
|
||||
|
||||
// Open output (default to stdout if --out omitted)
|
||||
let mut out_writer = match out.as_deref() {
|
||||
Some(path) => Some(
|
||||
PcapFileWriter::new(tokio::fs::File::create(path).await.expect("open pcap"))
|
||||
.await
|
||||
.expect("write header"),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
println!("Starting packet stream");
|
||||
loop {
|
||||
let packet = logger_client
|
||||
.next_packet()
|
||||
.await
|
||||
.expect("failed to read next packet");
|
||||
if let Some(writer) = &mut out_writer {
|
||||
writer.write_packet(&packet).await.expect("write packet");
|
||||
} else {
|
||||
println!("{packet:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
76
tools/src/preboard.rs
Normal file
76
tools/src/preboard.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{IdeviceService, preboard_service::PreboardServiceClient};
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("preboard")
|
||||
.about("Mess with developer mode")
|
||||
.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("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.subcommand(Command::new("create").about("Create a stashbag??"))
|
||||
.subcommand(Command::new("commit").about("Commit a stashbag??"))
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("preboard - no idea what this does");
|
||||
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, "amfi-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut pc = PreboardServiceClient::connect(&*provider)
|
||||
.await
|
||||
.expect("Failed to connect to Preboard");
|
||||
|
||||
if matches.subcommand_matches("create").is_some() {
|
||||
pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
|
||||
.await
|
||||
.expect("Failed to create");
|
||||
} else if matches.subcommand_matches("commit").is_some() {
|
||||
pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
|
||||
.await
|
||||
.expect("Failed to create");
|
||||
} else {
|
||||
eprintln!("Invalid usage, pass -h for help");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, tcp::stream::AdapterStream,
|
||||
IdeviceService, RsdService,
|
||||
};
|
||||
use idevice::services::lockdown::LockdownClient;
|
||||
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -74,30 +72,70 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||
let mut rs_client_opt: Option<
|
||||
idevice::dvt::remote_server::RemoteServerClient<Box<dyn idevice::ReadWrite>>,
|
||||
> = None;
|
||||
|
||||
if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
|
||||
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 (iOS 17+)
|
||||
let mut handshake = RsdHandshake::new(stream).await.unwrap();
|
||||
let mut rs_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd(
|
||||
&mut adapter,
|
||||
&mut handshake,
|
||||
)
|
||||
.await
|
||||
.expect("no core proxy");
|
||||
let rsd_port = proxy.handshake.server_rsd_port;
|
||||
.expect("no connect");
|
||||
rs_client.read_message(0).await.expect("no read??");
|
||||
rs_client_opt = Some(rs_client);
|
||||
}
|
||||
|
||||
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
||||
let stream = AdapterStream::connect(&mut adapter, rsd_port)
|
||||
.await
|
||||
.expect("no RSD connect");
|
||||
|
||||
// Make the connection to RemoteXPC
|
||||
let mut handshake = RsdHandshake::new(stream).await.unwrap();
|
||||
|
||||
let mut rs_client =
|
||||
idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake)
|
||||
let mut rs_client = if let Some(c) = rs_client_opt {
|
||||
c
|
||||
} else {
|
||||
// Read iOS version to decide whether we can fallback to remoteserver
|
||||
let mut lockdown = LockdownClient::connect(&*provider)
|
||||
.await
|
||||
.expect("no connect");
|
||||
.expect("lockdown connect failed");
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await.expect("pairing file"))
|
||||
.await
|
||||
.expect("lockdown start_session failed");
|
||||
let pv = lockdown
|
||||
.get_value(Some("ProductVersion"), None)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|v| v.as_string().map(|s| s.to_string()))
|
||||
.unwrap_or_default();
|
||||
let major: u32 = pv
|
||||
.split('.')
|
||||
.next()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
if major >= 17 {
|
||||
// iOS 17+ with no CoreDeviceProxy: do not attempt remoteserver (would return InvalidService)
|
||||
panic!("iOS {pv} detected and CoreDeviceProxy unavailable. RemoteXPC tunnel required.");
|
||||
}
|
||||
|
||||
// iOS 16 and earlier: fallback to Lockdown remoteserver (or DVTSecureSocketProxy)
|
||||
idevice::dvt::remote_server::RemoteServerClient::connect(&*provider)
|
||||
.await
|
||||
.expect("failed to connect to Instruments Remote Server over Lockdown (iOS16-). Ensure Developer Disk Image is mounted.")
|
||||
};
|
||||
|
||||
// Note: On both transports, protocol requires reading the initial message on root channel (0)
|
||||
rs_client.read_message(0).await.expect("no read??");
|
||||
let mut pc_client = idevice::dvt::process_control::ProcessControlClient::new(&mut rs_client)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pid = pc_client
|
||||
.launch_app(bundle_id, None, None, true, false)
|
||||
.launch_app(bundle_id, None, None, false, false)
|
||||
.await
|
||||
.expect("no launch??");
|
||||
pc_client
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
core_device_proxy::CoreDeviceProxy, tcp::stream::AdapterStream, xpc::RemoteXpcClient,
|
||||
IdeviceService,
|
||||
IdeviceService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake,
|
||||
tcp::stream::AdapterStream,
|
||||
};
|
||||
|
||||
mod common;
|
||||
@@ -66,13 +66,12 @@ async fn main() {
|
||||
let rsd_port = proxy.handshake.server_rsd_port;
|
||||
|
||||
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
||||
adapter.pcap("new_xpc.pcap").await.unwrap();
|
||||
let conn = AdapterStream::connect(&mut adapter, rsd_port)
|
||||
|
||||
let stream = AdapterStream::connect(&mut adapter, rsd_port)
|
||||
.await
|
||||
.expect("no RSD connect");
|
||||
|
||||
// Make the connection to RemoteXPC
|
||||
let mut client = RemoteXpcClient::new(Box::new(conn)).await.unwrap();
|
||||
|
||||
println!("{:#?}", client.do_handshake().await);
|
||||
let handshake = RsdHandshake::new(stream).await.unwrap();
|
||||
println!("{:#?}", handshake.services);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{
|
||||
core_device_proxy::CoreDeviceProxy, pretty_print_dictionary,
|
||||
restore_service::RestoreServiceClient, rsd::RsdHandshake, tcp::stream::AdapterStream,
|
||||
IdeviceService, RsdService,
|
||||
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, pretty_print_dictionary,
|
||||
restore_service::RestoreServiceClient, rsd::RsdHandshake,
|
||||
};
|
||||
|
||||
mod common;
|
||||
@@ -52,7 +51,9 @@ async fn main() {
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary.");
|
||||
println!(
|
||||
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
|
||||
);
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
@@ -75,11 +76,9 @@ async fn main() {
|
||||
.expect("no core proxy");
|
||||
let rsd_port = proxy.handshake.server_rsd_port;
|
||||
|
||||
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
||||
|
||||
let stream = AdapterStream::connect(&mut adapter, rsd_port)
|
||||
.await
|
||||
.expect("no RSD connect");
|
||||
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();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{syslog_relay::SyslogRelayClient, IdeviceService};
|
||||
use idevice::{IdeviceService, syslog_relay::SyslogRelayClient};
|
||||
|
||||
mod common;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user