From 189dd5caf2118bb720e0f349ce59dba2a6322f4e Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 3 Jan 2026 16:58:33 -0700 Subject: [PATCH] Refactor idevice tools into single binary --- Cargo.lock | 60 ++++ tools/Cargo.toml | 128 +------- tools/src/activation.rs | 115 ++------ tools/src/afc.rs | 304 +++++++++---------- tools/src/amfi.rs | 160 ++++------ tools/src/app_service.rs | 358 ++++++++++------------- tools/src/bt_packet_logger.rs | 69 +---- tools/src/companion_proxy.rs | 222 +++++++------- tools/src/crash_logs.rs | 145 ++++------ tools/src/debug_proxy.rs | 65 +---- tools/src/diagnostics.rs | 206 ++++++------- tools/src/diagnosticsservice.rs | 65 +---- tools/src/dvt_packet_parser.rs | 20 +- tools/src/heartbeat_client.rs | 58 +--- tools/src/ideviceinfo.rs | 62 +--- tools/src/ideviceinstaller.rs | 158 ++++------ tools/src/installcoordination_proxy.rs | 148 ++++------ tools/src/instproxy.rs | 162 +++++------ tools/src/location_simulation.rs | 198 ++++++------- tools/src/lockdown.rs | 144 ++++----- tools/src/main.rs | 328 +++++++++++++++++++++ tools/src/misagent.rs | 132 ++++----- tools/src/mobilebackup2.rs | 386 ++++++++++++------------- tools/src/mounter.rs | 330 ++++++++++----------- tools/src/notifications.rs | 63 +--- tools/src/os_trace_relay.rs | 57 +--- tools/src/pair.rs | 39 +-- tools/src/pcapd.rs | 53 +--- tools/src/preboard.rs | 90 ++---- tools/src/process_control.rs | 78 +---- tools/src/remotexpc.rs | 62 +--- tools/src/restore_service.rs | 163 +++++------ tools/src/screenshot.rs | 75 +---- tools/src/syslog_relay.rs | 57 +--- 34 files changed, 1983 insertions(+), 2777 deletions(-) create mode 100644 tools/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 4d56cb9..4944606 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -546,6 +559,18 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dialoguer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -580,6 +605,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1121,6 +1152,7 @@ dependencies = [ "clap", "futures-util", "idevice", + "jkcli", "ns-keyed-archive", "plist", "plist-macro", @@ -1202,6 +1234,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jkcli" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80352668d7fb7afdf101bcedf2ec2ae77887f550114dd38502a3c9365189c06f" +dependencies = [ + "dialoguer", + "owo-colors", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -1599,6 +1641,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + [[package]] name = "parking" version = "2.2.1" @@ -2154,6 +2202,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" @@ -2674,6 +2728,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 106977b..28cc33c 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -8,26 +8,7 @@ license = "MIT" documentation = "https://docs.rs/idevice" repository = "https://github.com/jkcoxson/idevice" keywords = ["lockdownd", "ios"] - -[[bin]] -name = "ideviceinfo" -path = "src/ideviceinfo.rs" - -[[bin]] -name = "heartbeat_client" -path = "src/heartbeat_client.rs" - -[[bin]] -name = "instproxy" -path = "src/instproxy.rs" - -[[bin]] -name = "ideviceinstaller" -path = "src/ideviceinstaller.rs" - -[[bin]] -name = "mounter" -path = "src/mounter.rs" +default-run = "idevice-tools" # [[bin]] # name = "core_device_proxy_tun" @@ -37,112 +18,6 @@ path = "src/mounter.rs" name = "idevice_id" path = "src/idevice_id.rs" -[[bin]] -name = "process_control" -path = "src/process_control.rs" - -[[bin]] -name = "dvt_packet_parser" -path = "src/dvt_packet_parser.rs" - -[[bin]] -name = "remotexpc" -path = "src/remotexpc.rs" - -[[bin]] -name = "debug_proxy" -path = "src/debug_proxy.rs" - -[[bin]] -name = "misagent" -path = "src/misagent.rs" - -[[bin]] -name = "location_simulation" -path = "src/location_simulation.rs" - -[[bin]] -name = "afc" -path = "src/afc.rs" - -[[bin]] -name = "crash_logs" -path = "src/crash_logs.rs" - -[[bin]] -name = "amfi" -path = "src/amfi.rs" - -[[bin]] -name = "pair" -path = "src/pair.rs" - -[[bin]] -name = "syslog_relay" -path = "src/syslog_relay.rs" - -[[bin]] -name = "os_trace_relay" -path = "src/os_trace_relay.rs" - -[[bin]] -name = "app_service" -path = "src/app_service.rs" - -[[bin]] -name = "lockdown" -path = "src/lockdown.rs" - -[[bin]] -name = "restore_service" -path = "src/restore_service.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" - - -[[bin]] -name = "screenshot" -path = "src/screenshot.rs" - -[[bin]] -name = "activation" -path = "src/activation.rs" - -[[bin]] -name = "notifications" -path = "src/notifications.rs" - - -[[bin]] -name = "installcoordination_proxy" -path = "src/installcoordination_proxy.rs" - [[bin]] name = "iproxy" path = "src/iproxy.rs" @@ -156,6 +31,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } sha2 = { version = "0.10" } ureq = { version = "3" } clap = { version = "4.5" } +jkcli = { version = "0.1" } plist = { version = "1.7" } plist-macro = { version = "0.1" } ns-keyed-archive = "0.1.2" diff --git a/tools/src/activation.rs b/tools/src/activation.rs index 86f65e5..f9dee47 100644 --- a/tools/src/activation.rs +++ b/tools/src/activation.rs @@ -1,65 +1,23 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("activation") - .about("mobileactivationd") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage activation status on an iOS device") + .with_subcommand("state", JkCommand::new().help("Gets the activation state")) + .with_subcommand( + "deactivate", + JkCommand::new().help("Deactivates 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("state").about("Gets the activation state")) - .subcommand(Command::new("deactivate").about("Deactivates the device")) - .get_matches(); - - if matches.get_flag("about") { - println!("activation - activate the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "activation-jkcoxson").await - { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let activation_client = MobileActivationdClient::new(&*provider); let mut lc = LockdownClient::connect(&*provider) .await @@ -74,40 +32,19 @@ async fn main() { .into_string() .unwrap(); - if matches.subcommand_matches("state").is_some() { - let s = activation_client.state().await.expect("no state"); - println!("Activation State: {s}"); - } else if matches.subcommand_matches("deactivate").is_some() { - println!("CAUTION: You are deactivating {udid}, press enter to continue."); - let mut input = String::new(); - std::io::stdin().read_line(&mut input).ok(); - activation_client.deactivate().await.expect("no deactivate"); - // } else if matches.subcommand_matches("accept").is_some() { - // amfi_client - // .accept_developer_mode() - // .await - // .expect("Failed to show"); - // } else if matches.subcommand_matches("status").is_some() { - // let status = amfi_client - // .get_developer_mode_status() - // .await - // .expect("Failed to get status"); - // println!("Enabled: {status}"); - // } else if let Some(matches) = matches.subcommand_matches("state") { - // let uuid: &String = match matches.get_one("uuid") { - // Some(u) => u, - // None => { - // eprintln!("No UUID passed. Invalid usage, pass -h for help"); - // return; - // } - // }; - // let status = amfi_client - // .trust_app_signer(uuid) - // .await - // .expect("Failed to get state"); - // println!("Enabled: {status}"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, _sub_args) = arguments.first_subcommand().expect("no subarg passed"); + + match sub_name.as_str() { + "state" => { + let s = activation_client.state().await.expect("no state"); + println!("Activation State: {s}"); + } + "deactivate" => { + println!("CAUTION: You are deactivating {udid}, press enter to continue."); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).ok(); + activation_client.deactivate().await.expect("no deactivate"); + } + _ => unreachable!(), } - return; } diff --git a/tools/src/afc.rs b/tools/src/afc.rs index e1fbb5f..f08719b 100644 --- a/tools/src/afc.rs +++ b/tools/src/afc.rs @@ -2,130 +2,119 @@ use std::path::PathBuf; -use clap::{Arg, Command, value_parser}; use idevice::{ IdeviceService, afc::{AfcClient, opcode::AfcFopenMode}, house_arrest::HouseArrestClient, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; -mod common; +const DOCS_HELP: &str = "Read the documents from a bundle. Note that when vending documents, you can only access files in /Documents"; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("afc") - .about("Manage files on the device") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_flag( + JkFlag::new("documents") + .with_help(DOCS_HELP) + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_flag( + JkFlag::new("container") + .with_help("Read the container contents of a bundle") + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("udid") - .long("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)"), - ) - .arg( - Arg::new("documents") - .long("documents") - .value_name("BUNDLE_ID") - .help("Read the documents from a bundle. Note that when vending documents, you can only access files in /Documents") - .global(true), - ) - .arg( - Arg::new("container") - .long("container") - .value_name("BUNDLE_ID") - .help("Read the container contents of a bundle") - .global(true), - ) - .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") - .arg(Arg::new("path").required(true).index(1)), - ) - .subcommand( - Command::new("download") - .about("Downloads a file") - .arg(Arg::new("path").required(true).index(1)) - .arg(Arg::new("save").required(true).index(2)), - ) - .subcommand( - Command::new("upload") - .about("Creates a directory") - .arg( - Arg::new("file") + .with_subcommand( + "list", + JkCommand::new() + .help("Lists the items in the directory") + .with_argument( + JkArgument::new() .required(true) - .index(1) - .value_parser(value_parser!(PathBuf)), + .with_help("The directory to list in"), + ), + ) + .with_subcommand( + "download", + JkCommand::new() + .help("Download a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path in the AFC jail"), ) - .arg(Arg::new("path").required(true).index(2)), + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to save file to"), + ), ) - .subcommand( - Command::new("mkdir") - .about("Creates a directory") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "upload", + JkCommand::new() + .help("Upload a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the file to upload"), + ) + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to save file to in the AFC jail"), + ), ) - .subcommand( - Command::new("remove") - .about("Remove a provisioning profile") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "mkdir", + JkCommand::new().help("Create a folder").with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the folder to create in the AFC jail"), + ), ) - .subcommand( - Command::new("remove_all") - .about("Remove a provisioning profile") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "remove", + JkCommand::new().help("Remove a file").with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the file to remove"), + ), ) - .subcommand( - Command::new("info") - .about("Get info about a file") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "remove_all", + JkCommand::new().help("Remove a folder").with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the folder to remove"), + ), ) - .subcommand(Command::new("device_info").about("Get info about the device")) - .get_matches(); + .with_subcommand( + "info", + JkCommand::new() + .help("Get info about a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the file to get info for"), + ), + ) + .with_subcommand( + "device_info", + JkCommand::new().help("Get info about the device"), + ) + .subcommand_required(true) +} - if matches.get_flag("about") { - println!("afc"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("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 afc_client = if let Some(bundle_id) = matches.get_one::("container") { +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let mut afc_client = if let Some(bundle_id) = arguments.get_flag::("container") { let h = HouseArrestClient::connect(&*provider) .await .expect("Failed to connect to house arrest"); h.vend_container(bundle_id) .await .expect("Failed to vend container") - } else if let Some(bundle_id) = matches.get_one::("documents") { + } else if let Some(bundle_id) = arguments.get_flag::("documents") { let h = HouseArrestClient::connect(&*provider) .await .expect("Failed to connect to house arrest"); @@ -138,59 +127,72 @@ async fn main() { .expect("Unable to connect to misagent") }; - if let Some(matches) = matches.subcommand_matches("list") { - let path = matches.get_one::("path").expect("No path passed"); - let res = afc_client.list_dir(path).await.expect("Failed to read dir"); - println!("{path}\n{res:#?}"); - } else if let Some(matches) = matches.subcommand_matches("mkdir") { - let path = matches.get_one::("path").expect("No path passed"); - afc_client.mk_dir(path).await.expect("Failed to mkdir"); - } else if let Some(matches) = matches.subcommand_matches("download") { - let path = matches.get_one::("path").expect("No path passed"); - let save = matches.get_one::("save").expect("No path passed"); + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + match sub_name.as_str() { + "list" => { + let path = sub_args.next_argument::().expect("No path passed"); + let res = afc_client + .list_dir(&path) + .await + .expect("Failed to read dir"); + println!("{path}\n{res:#?}"); + } + "mkdir" => { + let path = sub_args.next_argument::().expect("No path passed"); + afc_client.mk_dir(path).await.expect("Failed to mkdir"); + } + "download" => { + let path = sub_args.next_argument::().expect("No path passed"); + let save = sub_args.next_argument::().expect("No path passed"); - let mut file = afc_client - .open(path, AfcFopenMode::RdOnly) - .await - .expect("Failed to open"); + let mut file = afc_client + .open(path, AfcFopenMode::RdOnly) + .await + .expect("Failed to open"); - let res = file.read_entire().await.expect("Failed to read"); - tokio::fs::write(save, res) - .await - .expect("Failed to write to file"); - } else if let Some(matches) = matches.subcommand_matches("upload") { - let file = matches.get_one::("file").expect("No path passed"); - let path = matches.get_one::("path").expect("No path passed"); + let res = file.read_entire().await.expect("Failed to read"); + tokio::fs::write(save, res) + .await + .expect("Failed to write to file"); + } + "upload" => { + let file = sub_args.next_argument::().expect("No path passed"); + let path = sub_args.next_argument::().expect("No path passed"); - let bytes = tokio::fs::read(file).await.expect("Failed to read file"); - let mut file = afc_client - .open(path, AfcFopenMode::WrOnly) - .await - .expect("Failed to open"); + let bytes = tokio::fs::read(file).await.expect("Failed to read file"); + let mut file = afc_client + .open(path, AfcFopenMode::WrOnly) + .await + .expect("Failed to open"); - file.write_entire(&bytes) - .await - .expect("Failed to upload bytes"); - } else if let Some(matches) = matches.subcommand_matches("remove") { - let path = matches.get_one::("path").expect("No path passed"); - afc_client.remove(path).await.expect("Failed to remove"); - } else if let Some(matches) = matches.subcommand_matches("remove_all") { - let path = matches.get_one::("path").expect("No path passed"); - afc_client.remove_all(path).await.expect("Failed to remove"); - } else if let Some(matches) = matches.subcommand_matches("info") { - let path = matches.get_one::("path").expect("No path passed"); - let res = afc_client - .get_file_info(path) - .await - .expect("Failed to get file info"); - println!("{res:#?}"); - } else if matches.subcommand_matches("device_info").is_some() { - let res = afc_client - .get_device_info() - .await - .expect("Failed to get file info"); - println!("{res:#?}"); - } else { - eprintln!("Invalid usage, pass -h for help"); + file.write_entire(&bytes) + .await + .expect("Failed to upload bytes"); + } + "remove" => { + let path = sub_args.next_argument::().expect("No path passed"); + afc_client.remove(path).await.expect("Failed to remove"); + } + "remove_all" => { + let path = sub_args.next_argument::().expect("No path passed"); + afc_client.remove_all(path).await.expect("Failed to remove"); + } + "info" => { + let path = sub_args.next_argument::().expect("No path passed"); + let res = afc_client + .get_file_info(path) + .await + .expect("Failed to get file info"); + println!("{res:#?}"); + } + "device_info" => { + let res = afc_client + .get_device_info() + .await + .expect("Failed to get file info"); + println!("{res:#?}"); + } + _ => unreachable!(), } } diff --git a/tools/src/amfi.rs b/tools/src/amfi.rs index bff4847..0298da1 100644 --- a/tools/src/amfi.rs +++ b/tools/src/amfi.rs @@ -1,109 +1,77 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, amfi::AmfiClient}; +use idevice::{IdeviceService, amfi::AmfiClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("amfi") - .about("Mess with developer mode") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Mess with devleoper mode") + .with_subcommand( + "show", + JkCommand::new().help("Shows the developer mode option in settings"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand("enable", JkCommand::new().help("Enables developer mode")) + .with_subcommand( + "accept", + JkCommand::new().help("Shows the accept dialogue for developer mode"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), + .with_subcommand( + "status", + JkCommand::new().help("Gets the developer mode status"), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("show").about("Shows the developer mode option in settings")) - .subcommand(Command::new("enable").about("Enables developer mode")) - .subcommand(Command::new("accept").about("Shows the accept dialogue for developer mode")) - .subcommand(Command::new("status").about("Gets the developer mode status")) - .subcommand( - Command::new("trust") - .about("Trusts an app signer") - .arg(Arg::new("uuid").required(true)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("amfi - manage developer mode"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .with_subcommand("trust", JkCommand::new().help("Trusts an app signer")) + .with_argument(JkArgument::new().with_help("UUID").required(true)) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut amfi_client = AmfiClient::connect(&*provider) .await .expect("Failed to connect to amfi"); - if matches.subcommand_matches("show").is_some() { - amfi_client - .reveal_developer_mode_option_in_ui() - .await - .expect("Failed to show"); - } else if matches.subcommand_matches("enable").is_some() { - amfi_client - .enable_developer_mode() - .await - .expect("Failed to show"); - } else if matches.subcommand_matches("accept").is_some() { - amfi_client - .accept_developer_mode() - .await - .expect("Failed to show"); - } else if matches.subcommand_matches("status").is_some() { - let status = amfi_client - .get_developer_mode_status() - .await - .expect("Failed to get status"); - println!("Enabled: {status}"); - } else if let Some(matches) = matches.subcommand_matches("state") { - let uuid: &String = match matches.get_one("uuid") { - Some(u) => u, - None => { - eprintln!("No UUID passed. Invalid usage, pass -h for help"); - return; - } - }; - let status = amfi_client - .trust_app_signer(uuid) - .await - .expect("Failed to get state"); - println!("Enabled: {status}"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed"); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "show" => { + amfi_client + .reveal_developer_mode_option_in_ui() + .await + .expect("Failed to show"); + } + "enable" => { + amfi_client + .enable_developer_mode() + .await + .expect("Failed to show"); + } + "accept" => { + amfi_client + .accept_developer_mode() + .await + .expect("Failed to show"); + } + "status" => { + let status = amfi_client + .get_developer_mode_status() + .await + .expect("Failed to get status"); + println!("Enabled: {status}"); + } + "trust" => { + let uuid: String = match sub_args.next_argument() { + Some(u) => u, + None => { + eprintln!("No UUID passed. Invalid usage, pass -h for help"); + return; + } + }; + let status = amfi_client + .trust_app_signer(uuid) + .await + .expect("Failed to get state"); + println!("Enabled: {status}"); + } + _ => unreachable!(), } - return; } diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index c286e4e..5cc5032 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -1,111 +1,72 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, RsdService, core_device::{AppServiceClient, OpenStdioSocketClient}, core_device_proxy::CoreDeviceProxy, + provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("remotexpc") - .about("Get services from RemoteXPC") - .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), - ) - .subcommand(Command::new("list").about("Lists the images mounted on the device")) - .subcommand( - Command::new("launch") - .about("Launch the app on the device") - .arg( - Arg::new("bundle_id") - .required(true) - .help("The bundle ID to launch"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the RemoteXPC app service on the device") + .with_subcommand("list", JkCommand::new().help("List apps on the device")) + .with_subcommand( + "launch", + JkCommand::new() + .help("Launch an app on the device") + .with_argument( + JkArgument::new() + .with_help("Bundle ID to launch") + .required(true), ), ) - .subcommand(Command::new("processes").about("List the processes running")) - .subcommand( - Command::new("uninstall").about("Uninstall an app").arg( - Arg::new("bundle_id") - .required(true) - .help("The bundle ID to uninstall"), + .with_subcommand( + "processes", + JkCommand::new().help("List the processes running"), + ) + .with_subcommand( + "uninstall", + JkCommand::new().help("Uninstall an app").with_argument( + JkArgument::new() + .with_help("Bundle ID to uninstall") + .required(true), ), ) - .subcommand( - Command::new("signal") - .about("Send a signal to an app") - .arg(Arg::new("pid").required(true).help("PID to send to")) - .arg(Arg::new("signal").required(true).help("Signal to send")), + .with_subcommand( + "signal", + JkCommand::new() + .help("Uninstall an app") + .with_argument(JkArgument::new().with_help("PID to signal").required(true)) + .with_argument(JkArgument::new().with_help("Signal to send").required(true)), ) - .subcommand( - Command::new("icon") - .about("Send a signal to an app") - .arg( - Arg::new("bundle_id") - .required(true) - .help("The bundle ID to fetch"), + .with_subcommand( + "icon", + JkCommand::new() + .help("Fetch an icon for an app") + .with_argument( + JkArgument::new() + .with_help("Bundle ID for the app") + .required(true), ) - .arg( - Arg::new("path") - .required(true) - .help("The path to save the icon to"), + .with_argument( + JkArgument::new() + .with_help("Path to save it to") + .required(true), ) - .arg(Arg::new("hw").required(false).help("The height and width")) - .arg(Arg::new("scale").required(false).help("The scale")), + .with_argument( + JkArgument::new() + .with_help("Height and width") + .required(true), + ) + .with_argument(JkArgument::new().with_help("Scale").required(true)), ) - .get_matches(); + .subcommand_required(true) +} - 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, "app_service-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -123,121 +84,122 @@ async fn main() { .await .expect("no connect"); - if matches.subcommand_matches("list").is_some() { - let apps = asc - .list_apps(true, true, true, true, true) - .await - .expect("Failed to get apps"); - println!("{apps:#?}"); - } else if let Some(matches) = matches.subcommand_matches("launch") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand"); + let mut sub_args = sub_args.clone(); - 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, &[], true, false, None, None, Some(stdio_uuid)) - .await - .expect("no launch"); - - 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); + match sub_name.as_str() { + "list" => { + let apps = asc + .list_apps(true, true, true, true, true) + .await + .expect("Failed to get apps"); + println!("{apps:#?}"); + } + "launch" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; } - 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); + }; + + 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, &[], true, false, None, None, Some(stdio_uuid)) + .await + .expect("no launch"); + + 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."); + } + // 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."); } - println!("\nLocal stdin closed."); - return; } } - } else if matches.subcommand_matches("processes").is_some() { - let p = asc.list_processes().await.expect("no processes?"); - println!("{p:#?}"); - } else if let Some(matches) = matches.subcommand_matches("uninstall") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; + "processes" => { + let p = asc.list_processes().await.expect("no processes?"); + println!("{p:#?}"); + } + "uninstall" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; - asc.uninstall_app(bundle_id).await.expect("no launch") - } else if let Some(matches) = matches.subcommand_matches("signal") { - let pid: u32 = match matches.get_one::("pid") { - Some(b) => b.parse().expect("failed to parse PID as u32"), - None => { - eprintln!("No bundle PID passed"); - return; - } - }; - let signal: u32 = match matches.get_one::("signal") { - Some(b) => b.parse().expect("failed to parse signal as u32"), - None => { - eprintln!("No bundle signal passed"); - return; - } - }; + asc.uninstall_app(bundle_id).await.expect("no launch") + } + "signal" => { + let pid: u32 = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle PID passed"); + return; + } + }; + let signal: u32 = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle signal passed"); + return; + } + }; - let res = asc.send_signal(pid, signal).await.expect("no signal"); - println!("{res:#?}"); - } else if let Some(matches) = matches.subcommand_matches("icon") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; - let save_path: &String = match matches.get_one("path") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; - let hw: f32 = match matches.get_one::("hw") { - Some(b) => b.parse().expect("failed to parse PID as f32"), - None => 1.0, - }; - let scale: f32 = match matches.get_one::("scale") { - Some(b) => b.parse().expect("failed to parse signal as f32"), - None => 1.0, - }; + let res = asc.send_signal(pid, signal).await.expect("no signal"); + println!("{res:#?}"); + } + "icon" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + let save_path: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + let hw: f32 = sub_args.next_argument().unwrap_or(1.0); + let scale: f32 = sub_args.next_argument().unwrap_or(1.0); - let res = asc - .fetch_app_icon(bundle_id, hw, hw, scale, true) - .await - .expect("no signal"); - println!("{res:?}"); - tokio::fs::write(save_path, res.data) - .await - .expect("failed to save"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let res = asc + .fetch_app_icon(bundle_id, hw, hw, scale, true) + .await + .expect("no signal"); + println!("{res:?}"); + tokio::fs::write(save_path, res.data) + .await + .expect("failed to save"); + } + _ => unreachable!(), } } diff --git a/tools/src/bt_packet_logger.rs b/tools/src/bt_packet_logger.rs index 7529200..346f115 100644 --- a/tools/src/bt_packet_logger.rs +++ b/tools/src/bt_packet_logger.rs @@ -1,71 +1,20 @@ // Jackson Coxson -use clap::{Arg, Command}; use futures_util::StreamExt; -use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient}; +use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use tokio::io::AsyncWrite; use crate::pcap::{write_pcap_header, write_pcap_record}; -mod common; -mod pcap; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Writes Bluetooth pcap data") + .with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)")) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - let out = matches.get_one::("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; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let out: Option = arguments.clone().next_argument(); let logger_client = BtPacketLoggerClient::connect(&*provider) .await diff --git a/tools/src/companion_proxy.rs b/tools/src/companion_proxy.rs index 8e02c99..eec9699 100644 --- a/tools/src/companion_proxy.rs +++ b/tools/src/companion_proxy.rs @@ -1,83 +1,60 @@ // Jackson Coxson -use clap::{Arg, Command, arg}; use idevice::{ IdeviceService, RsdService, companion_proxy::CompanionProxy, - core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, + core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use plist_macro::{pretty_print_dictionary, pretty_print_plist}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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 "the device udid to get from").required(true)) - .arg(arg!(-v --value "the value to get").required(true)), - ) - .subcommand( - Command::new("start") - .about("Starts a service") - .arg(arg!(-p --port "the port").required(true)) - .arg(arg!(-n --name "the optional service name").required(false)), - ) - .subcommand( - Command::new("stop") - .about("Starts a service") - .arg(arg!(-p --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::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Apple Watch proxy") + .with_subcommand( + "list", + JkCommand::new().help("List the companions on the device"), + ) + .with_subcommand("listen", JkCommand::new().help("Listen for devices")) + .with_subcommand( + "get", + JkCommand::new() + .help("Gets a value from an AW") + .with_argument( + JkArgument::new() + .with_help("The AW UDID to get from") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("The value to get") + .required(true), + ), + ) + .with_subcommand( + "start", + JkCommand::new() + .help("Starts a service on the Apple Watch") + .with_argument( + JkArgument::new() + .with_help("The port to listen on") + .required(true), + ) + .with_argument(JkArgument::new().with_help("The service name")), + ) + .with_subcommand( + "stop", + JkCommand::new() + .help("Stops a service on the Apple Watch") + .with_argument( + JkArgument::new() + .with_help("The port to stop") + .required(true), + ), + ) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core_device_proxy"); @@ -97,55 +74,72 @@ async fn main() { // .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::("value").expect("no value passed"); - let udid = matches - .get_one::("device_udid") - .expect("no AW udid passed"); + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); - match proxy.get_value(udid, key).await { - Ok(value) => { - println!("{}", pretty_print_plist(&value)); - } - Err(e) => { - eprintln!("Error getting value: {e}"); + match sub_name.as_str() { + "list" => { + proxy.get_device_registry().await.expect("Failed to show"); + } + "listen" => { + 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("start") { - let port: u16 = matches - .get_one::("port") - .expect("no port passed") - .parse() - .expect("not a number"); - let name = matches.get_one::("name").map(|x| x.as_str()); + "get" => { + let key: String = sub_args.next_argument::().expect("no value passed"); + let udid = sub_args + .next_argument::() + .expect("no AW udid passed"); - match proxy.start_forwarding_service_port(port, name, None).await { - Ok(value) => { - println!("started on port {value}"); + match proxy.get_value(udid, key).await { + Ok(value) => { + println!("{}", pretty_print_plist(&value)); + } + Err(e) => { + eprintln!("Error getting value: {e}"); + } } - Err(e) => { + } + "start" => { + let port: u16 = sub_args + .next_argument::() + .expect("no port passed") + .parse() + .expect("not a number"); + let name = sub_args.next_argument::(); + + match proxy + .start_forwarding_service_port( + port, + match &name { + Some(n) => Some(n.as_str()), + None => None, + }, + None, + ) + .await + { + Ok(value) => { + println!("started on port {value}"); + } + Err(e) => { + eprintln!("Error starting: {e}"); + } + } + } + "stop" => { + let port: u16 = sub_args + .next_argument::() + .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 if let Some(matches) = matches.subcommand_matches("stop") { - let port: u16 = matches - .get_one::("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"); + _ => unreachable!(), } - return; } diff --git a/tools/src/crash_logs.rs b/tools/src/crash_logs.rs index b17ef13..7265647 100644 --- a/tools/src/crash_logs.rs +++ b/tools/src/crash_logs.rs @@ -1,96 +1,79 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports}, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage crash logs") + .with_subcommand( + "list", + JkCommand::new() + .help("List crash logs in the directory") + .with_argument( + JkArgument::new() + .with_help("Path to list in") + .required(true), + ), + ) + .with_subcommand( + "flush", + JkCommand::new().help("Flushes reports to the directory"), + ) + .with_subcommand( + "pull", + JkCommand::new() + .help("Check the capabilities") + .with_argument( + JkArgument::new() + .with_help("Path to the log to pull") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("Path to save the log to") + .required(true), + ), + ) + .subcommand_required(true) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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") - .arg(Arg::new("dir").required(false).index(1)), - ) - .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)) - .arg(Arg::new("dir").required(false).index(3)), - ) - .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::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "afc-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut crash_client = CrashReportCopyMobileClient::connect(&*provider) .await .expect("Unable to connect to misagent"); - if let Some(matches) = matches.subcommand_matches("list") { - let dir_path: Option<&String> = matches.get_one("dir"); - let res = crash_client - .ls(dir_path.map(|x| x.as_str())) - .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::("path").expect("No path passed"); - let save = matches.get_one::("save").expect("No path passed"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub command passed"); + let mut sub_args = sub_args.clone(); - 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"); + match sub_name.as_str() { + "list" => { + let dir_path: Option = sub_args.next_argument(); + let res = crash_client + .ls(match &dir_path { + Some(d) => Some(d.as_str()), + None => None, + }) + .await + .expect("Failed to read dir"); + println!("{res:#?}"); + } + "flush" => { + flush_reports(&*provider).await.expect("Failed to flush"); + } + "pull" => { + let path = sub_args.next_argument::().expect("No path passed"); + let save = sub_args.next_argument::().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"); + } + _ => unreachable!(), } } diff --git a/tools/src/debug_proxy.rs b/tools/src/debug_proxy.rs index f965bd8..4d09d4b 100644 --- a/tools/src/debug_proxy.rs +++ b/tools/src/debug_proxy.rs @@ -2,70 +2,17 @@ use std::io::Write; -use clap::{Arg, Command}; use idevice::{ IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, - rsd::RsdHandshake, + provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Start a debug proxy shell") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("remotexpc") - .about("Get services from RemoteXPC") - .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, "debug-proxy-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); diff --git a/tools/src/diagnostics.rs b/tools/src/diagnostics.rs index 674f757..bb7d1c5 100644 --- a/tools/src/diagnostics.rs +++ b/tools/src/diagnostics.rs @@ -1,106 +1,71 @@ // Jackson Coxson // idevice Rust implementation of libimobiledevice's idevicediagnostics -use clap::{Arg, ArgMatches, Command}; -use idevice::{IdeviceService, services::diagnostics_relay::DiagnosticsRelayClient}; +use idevice::{ + IdeviceService, provider::IdeviceProvider, services::diagnostics_relay::DiagnosticsRelayClient, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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)"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the diagnostics interface of a device") + .with_subcommand( + "ioregistry", + JkCommand::new() + .help("Print IORegistry information") + .with_flag( + JkFlag::new("plane") + .with_help("IORegistry plane to query (e.g., IODeviceTree, IOService)") + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("name") - .long("name") - .value_name("NAME") - .help("Entry name to filter by"), + .with_flag( + JkFlag::new("name") + .with_help("Entry name to filter by") + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("class") - .long("class") - .value_name("CLASS") - .help("Entry class to filter by"), + .with_flag( + JkFlag::new("class") + .with_help("Entry class to filter by") + .with_argument(JkArgument::new().required(true)), ), ) - .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..), + .with_subcommand( + "mobilegestalt", + JkCommand::new() + .help("Print MobileGestalt information") + .with_argument( + JkArgument::new() + .with_help("Comma-separated list of keys to query") + .required(true), ), ) - .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::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "idevicediagnostics-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .with_subcommand( + "gasguage", + JkCommand::new().help("Print gas gauge (battery) information"), + ) + .with_subcommand( + "nand", + JkCommand::new().help("Print NAND flash information"), + ) + .with_subcommand( + "all", + JkCommand::new().help("Print all available diagnostics information"), + ) + .with_subcommand( + "wifi", + JkCommand::new().help("Print WiFi diagnostics information"), + ) + .with_subcommand( + "goodbye", + JkCommand::new().help("Send Goodbye to diagnostics relay"), + ) + .with_subcommand("restart", JkCommand::new().help("Restart the device")) + .with_subcommand("shutdown", JkCommand::new().help("Shutdown the device")) + .with_subcommand("sleep", JkCommand::new().help("Put the device to sleep")) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await { Ok(client) => client, Err(e) => { @@ -109,47 +74,52 @@ async fn main() { } }; - match matches.subcommand() { - Some(("ioregistry", sub_matches)) => { - handle_ioregistry(&mut diagnostics_client, sub_matches).await; + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_matches = sub_args.clone(); + + match sub_name.as_str() { + "ioregistry" => { + handle_ioregistry(&mut diagnostics_client, &sub_matches).await; } - Some(("mobilegestalt", sub_matches)) => { - handle_mobilegestalt(&mut diagnostics_client, sub_matches).await; + "mobilegestalt" => { + handle_mobilegestalt(&mut diagnostics_client, &mut sub_matches).await; } - Some(("gasguage", _)) => { + "gasguage" => { handle_gasguage(&mut diagnostics_client).await; } - Some(("nand", _)) => { + "nand" => { handle_nand(&mut diagnostics_client).await; } - Some(("all", _)) => { + "all" => { handle_all(&mut diagnostics_client).await; } - Some(("wifi", _)) => { + "wifi" => { handle_wifi(&mut diagnostics_client).await; } - Some(("restart", _)) => { + "restart" => { handle_restart(&mut diagnostics_client).await; } - Some(("shutdown", _)) => { + "shutdown" => { handle_shutdown(&mut diagnostics_client).await; } - Some(("sleep", _)) => { + "sleep" => { handle_sleep(&mut diagnostics_client).await; } - Some(("goodbye", _)) => { + "goodbye" => { handle_goodbye(&mut diagnostics_client).await; } - _ => { - eprintln!("No subcommand specified. Use --help for usage information."); - } + _ => unreachable!(), } } -async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { - let plane = matches.get_one::("plane").map(|s| s.as_str()); - let name = matches.get_one::("name").map(|s| s.as_str()); - let class = matches.get_one::("class").map(|s| s.as_str()); +async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &CollectedArguments) { + let plane = matches.get_flag::("plane"); + let name = matches.get_flag::("name"); + let class = matches.get_flag::("class"); + + let plane = plane.as_deref(); + let name = name.as_deref(); + let class = class.as_deref(); match client.ioregistry(plane, name, class).await { Ok(Some(data)) => { @@ -164,12 +134,14 @@ async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMat } } -async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { - let keys = matches - .get_many::("keys") - .map(|values| values.map(|s| s.to_string()).collect::>()); +async fn handle_mobilegestalt( + client: &mut DiagnosticsRelayClient, + matches: &mut CollectedArguments, +) { + let keys = matches.next_argument::().unwrap(); + let keys = keys.split(',').map(|x| x.to_string()).collect(); - match client.mobilegestalt(keys).await { + match client.mobilegestalt(Some(keys)).await { Ok(Some(data)) => { println!("{data:#?}"); } diff --git a/tools/src/diagnosticsservice.rs b/tools/src/diagnosticsservice.rs index 74e6ca4..337e232 100644 --- a/tools/src/diagnosticsservice.rs +++ b/tools/src/diagnosticsservice.rs @@ -1,71 +1,18 @@ // Jackson Coxson -use clap::{Arg, Command}; use futures_util::StreamExt; use idevice::{ IdeviceService, RsdService, core_device::DiagnostisServiceClient, - core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, + core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkCommand}; use tokio::io::AsyncWriteExt; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Retrieve a sysdiagnose") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); diff --git a/tools/src/dvt_packet_parser.rs b/tools/src/dvt_packet_parser.rs index 7249520..17d1e35 100644 --- a/tools/src/dvt_packet_parser.rs +++ b/tools/src/dvt_packet_parser.rs @@ -1,10 +1,22 @@ // Jackson Coxson -use idevice::dvt::message::Message; +use idevice::{dvt::message::Message, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -#[tokio::main] -async fn main() { - let file = std::env::args().nth(1).expect("No file passed"); +pub fn register() -> JkCommand { + JkCommand::new() + .help("Parse a DVT packet from a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path the the packet file"), + ) +} + +pub async fn main(arguments: &CollectedArguments, _provider: Box) { + let mut arguments = arguments.clone(); + + let file: String = arguments.next_argument().expect("No file passed"); let mut bytes = tokio::fs::File::open(file).await.unwrap(); let message = Message::from_reader(&mut bytes).await.unwrap(); diff --git a/tools/src/heartbeat_client.rs b/tools/src/heartbeat_client.rs index b40ba08..b1e709d 100644 --- a/tools/src/heartbeat_client.rs +++ b/tools/src/heartbeat_client.rs @@ -1,60 +1,14 @@ // Jackson Coxson // Heartbeat client -use clap::{Arg, Command}; -use idevice::{IdeviceService, heartbeat::HeartbeatClient}; +use idevice::{IdeviceService, heartbeat::HeartbeatClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("heartbeat a device") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .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), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("heartbeat_client - heartbeat a device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "heartbeat_client-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let mut heartbeat_client = HeartbeatClient::connect(&*provider) .await .expect("Unable to connect to heartbeat"); diff --git a/tools/src/ideviceinfo.rs b/tools/src/ideviceinfo.rs index d9f0d85..3ce7086 100644 --- a/tools/src/ideviceinfo.rs +++ b/tools/src/ideviceinfo.rs @@ -1,64 +1,14 @@ // Jackson Coxson // idevice Rust implementation of libimobiledevice's ideviceinfo -use clap::{Arg, Command}; -use idevice::{IdeviceService, lockdown::LockdownClient}; +use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .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), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new().help("ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary.") +} +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let mut lockdown_client = match LockdownClient::connect(&*provider).await { Ok(l) => l, Err(e) => { diff --git a/tools/src/ideviceinstaller.rs b/tools/src/ideviceinstaller.rs index a1c94cf..d7a8290 100644 --- a/tools/src/ideviceinstaller.rs +++ b/tools/src/ideviceinstaller.rs @@ -1,103 +1,73 @@ // A minimal ideviceinstaller-like CLI to install/upgrade apps -use clap::{Arg, ArgAction, Command}; -use idevice::utils::installation; +use idevice::{provider::IdeviceProvider, utils::installation}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_subcommand( + "install", + JkCommand::new() + .help("Install a local .ipa or directory") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the .ipa or directory containing the app"), + ), + ) + .with_subcommand( + "upgrade", + JkCommand::new() + .help("Install a local .ipa or directory") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the .ipa or directory containing the app"), + ), + ) + .subcommand_required(true) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg"); + let mut sub_args = sub_args.clone(); - 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::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "ideviceinstaller").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; + match sub_name.as_str() { + "install" => { + let path: String = sub_args.next_argument().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}"), + } } - }; - - 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}"), + "upgrade" => { + let path: String = sub_args.next_argument().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 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"); + _ => unreachable!(), } } diff --git a/tools/src/installcoordination_proxy.rs b/tools/src/installcoordination_proxy.rs index d00e867..08fb2f0 100644 --- a/tools/src/installcoordination_proxy.rs +++ b/tools/src/installcoordination_proxy.rs @@ -1,87 +1,39 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, - installcoordination_proxy::InstallcoordinationProxy, rsd::RsdHandshake, + installcoordination_proxy::InstallcoordinationProxy, provider::IdeviceProvider, + rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("installationcoordination_proxy") - .about("") - .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), - ) - .subcommand( - Command::new("info") - .about("Get info about an app on the device") - .arg( - Arg::new("bundle_id") +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the RemoteXPC installation coordination proxy") + .with_subcommand( + "info", + JkCommand::new() + .help("Get info about an app on the device") + .with_argument( + JkArgument::new() .required(true) - .help("The bundle ID to query"), + .with_help("The bundle ID to query"), ), ) - .subcommand( - Command::new("uninstall") - .about("Get info about an app on the device") - .arg( - Arg::new("bundle_id") + .with_subcommand( + "uninstall", + JkCommand::new() + .help("Uninstalls an app on the device") + .with_argument( + JkArgument::new() .required(true) - .help("The bundle ID to query"), + .with_help("The bundle ID to delete"), ), ) - .get_matches(); + .subcommand_required(true) +} - 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, "app_service-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -103,30 +55,38 @@ async fn main() { .await .expect("no connect"); - if let Some(matches) = matches.subcommand_matches("info") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); - let res = icp.query_app_path(bundle_id).await.expect("no info"); - println!("Path: {res}"); - } else if let Some(matches) = matches.subcommand_matches("uninstall") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; + match sub_name.as_str() { + "info" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; - icp.uninstall_app(bundle_id) - .await - .expect("uninstall failed"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let res = icp + .query_app_path(bundle_id.as_str()) + .await + .expect("no info"); + println!("Path: {res}"); + } + "uninstall" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + + icp.uninstall_app(bundle_id.as_str()) + .await + .expect("uninstall failed"); + } + _ => unreachable!(), } } diff --git a/tools/src/instproxy.rs b/tools/src/instproxy.rs index 54eb64e..f126519 100644 --- a/tools/src/instproxy.rs +++ b/tools/src/instproxy.rs @@ -1,108 +1,84 @@ // Jackson Coxson // Just lists apps for now -use clap::{Arg, Command}; -use idevice::{IdeviceService, installation_proxy::InstallationProxyClient}; +use idevice::{ + IdeviceService, installation_proxy::InstallationProxyClient, provider::IdeviceProvider, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_subcommand( + "lookup", + JkCommand::new().help("Gets the apps on the device"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand( + "browse", + JkCommand::new().help("Browses the apps on the device"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), + .with_subcommand( + "check_capabilities", + JkCommand::new().help("Check the capabilities"), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), + .with_subcommand( + "install", + JkCommand::new() + .help("Install an app in the AFC jail") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path in the AFC jail"), + ), ) - .subcommand(Command::new("lookup").about("Gets the apps on the device")) - .subcommand(Command::new("browse").about("Browses the apps on the device")) - .subcommand(Command::new("check_capabilities").about("Check the capabilities")) - .subcommand( - Command::new("install") - .about("Install an app in the AFC jail") - .arg(Arg::new("path")), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "instproxy-jkcoxson").await - { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut instproxy_client = InstallationProxyClient::connect(&*provider) .await .expect("Unable to connect to instproxy"); - if matches.subcommand_matches("lookup").is_some() { - let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap(); - for app in apps.keys() { - println!("{app}"); - } - } else if matches.subcommand_matches("browse").is_some() { - instproxy_client.browse(None).await.expect("browse failed"); - } else if matches.subcommand_matches("check_capabilities").is_some() { - instproxy_client - .check_capabilities_match(Vec::new(), None) - .await - .expect("check failed"); - } else if let Some(matches) = matches.subcommand_matches("install") { - let path: &String = match matches.get_one("path") { - Some(p) => p, - None => { - eprintln!("No path passed, pass -h for help"); - return; - } - }; - instproxy_client - .install_with_callback( - path, - None, - async |(percentage, _)| { - println!("Installing: {percentage}"); - }, - (), - ) - .await - .expect("Failed to install") - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg"); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "lookup" => { + let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap(); + for app in apps.keys() { + println!("{app}"); + } + } + "browse" => { + instproxy_client.browse(None).await.expect("browse failed"); + } + "check_capabilities" => { + instproxy_client + .check_capabilities_match(Vec::new(), None) + .await + .expect("check failed"); + } + "install" => { + let path: String = match sub_args.next_argument() { + Some(p) => p, + None => { + eprintln!("No path passed, pass -h for help"); + return; + } + }; + + instproxy_client + .install_with_callback( + path, + None, + async |(percentage, _)| { + println!("Installing: {percentage}"); + }, + (), + ) + .await + .expect("Failed to install") + } + _ => unreachable!(), } } diff --git a/tools/src/location_simulation.rs b/tools/src/location_simulation.rs index a38fda9..ca364e1 100644 --- a/tools/src/location_simulation.rs +++ b/tools/src/location_simulation.rs @@ -1,70 +1,33 @@ // Jackson Coxson // Just lists apps for now -use clap::{Arg, Command}; +use idevice::provider::IdeviceProvider; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; use idevice::dvt::location_simulation::LocationSimulationClient; use idevice::services::simulate_location::LocationSimulationService; -mod common; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); +pub fn register() -> JkCommand { + JkCommand::new() + .help("Simulate device location") + .with_subcommand( + "clear", + JkCommand::new().help("Clears the location set on the device"), + ) + .with_subcommand( + "set", + JkCommand::new() + .help("Set the location on the device") + .with_argument(JkArgument::new().with_help("latitude").required(true)) + .with_argument(JkArgument::new().with_help("longitutde").required(true)), + ) + .subcommand_required(true) +} - let matches = Command::new("simulate_location") - .about("Simulate device location") - .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("clear").about("Clears the location set on the device")) - .subcommand( - Command::new("set") - .about("Set the location on the device") - .arg(Arg::new("latitude").required(true)) - .arg(Arg::new("longitude").required(true)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("simulate_location - Sets the simulated location on an iOS device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "simulate_location-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub arg passed"); + let mut sub_args = sub_args.clone(); if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { let rsd_port = proxy.handshake.server_rsd_port; @@ -86,42 +49,44 @@ async fn main() { let mut ls_client = LocationSimulationClient::new(&mut ls_client) .await .expect("Unable to get channel for location simulation"); - if matches.subcommand_matches("clear").is_some() { - ls_client.clear().await.expect("Unable to clear"); - println!("Location cleared!"); - } else if let Some(matches) = matches.subcommand_matches("set") { - let latitude: &String = match matches.get_one("latitude") { - Some(l) => l, - None => { - eprintln!("No latitude passed! Pass -h for help"); - return; - } - }; - let latitude: f64 = latitude.parse().expect("Failed to parse as float"); - let longitude: &String = match matches.get_one("longitude") { - Some(l) => l, - None => { - eprintln!("No longitude passed! Pass -h for help"); - return; - } - }; - let longitude: f64 = longitude.parse().expect("Failed to parse as float"); - ls_client - .set(latitude, longitude) - .await - .expect("Failed to set location"); - - println!("Location set!"); - println!("Press ctrl-c to stop"); - loop { + match sub_name.as_str() { + "clear" => { + ls_client.clear().await.expect("Unable to clear"); + println!("Location cleared!"); + } + "set" => { + let latitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No latitude passed! Pass -h for help"); + return; + } + }; + let latitude: f64 = latitude.parse().expect("Failed to parse as float"); + let longitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No longitude passed! Pass -h for help"); + return; + } + }; + let longitude: f64 = longitude.parse().expect("Failed to parse as float"); ls_client .set(latitude, longitude) .await .expect("Failed to set location"); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + println!("Location set!"); + println!("Press ctrl-c to stop"); + loop { + 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"); + _ => unreachable!(), } } else { let mut location_client = match LocationSimulationService::connect(&*provider).await { @@ -133,35 +98,36 @@ async fn main() { return; } }; - if matches.subcommand_matches("clear").is_some() { - location_client.clear().await.expect("Unable to clear"); - println!("Location cleared!"); - } else if let Some(matches) = matches.subcommand_matches("set") { - let latitude: &String = match matches.get_one("latitude") { - Some(l) => l, - None => { - eprintln!("No latitude passed! Pass -h for help"); - return; - } - }; - let longitude: &String = match matches.get_one("longitude") { - Some(l) => l, - None => { - eprintln!("No longitude passed! Pass -h for help"); - return; - } - }; - location_client - .set(latitude, longitude) - .await - .expect("Failed to set location"); + match sub_name.as_str() { + "clear" => { + location_client.clear().await.expect("Unable to clear"); + println!("Location cleared!"); + } + "set" => { + let latitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No latitude passed! Pass -h for help"); + return; + } + }; - println!("Location set!"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let longitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No longitude passed! Pass -h for help"); + return; + } + }; + location_client + .set(latitude.as_str(), longitude.as_str()) + .await + .expect("Failed to set location"); + + println!("Location set!"); + } + _ => unreachable!(), } }; - - return; } diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 8f654f1..7e60224 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -1,78 +1,40 @@ // Jackson Coxson -use clap::{Arg, Command, arg}; -use idevice::{IdeviceService, lockdown::LockdownClient}; +use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use plist::Value; use plist_macro::pretty_print_plist; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("lockdown") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with lockdown") + .with_subcommand( + "get", + JkCommand::new() + .help("Gets a value from lockdown") + .with_argument(JkArgument::new().with_help("The value to get")) + .with_argument(JkArgument::new().with_help("The domain to get in")), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand( + "set", + JkCommand::new() + .help("Gets a value from lockdown") + .with_argument( + JkArgument::new() + .with_help("The value to set") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("The value key to set") + .required(true), + ) + .with_argument(JkArgument::new().with_help("The domain to set in")), ) - .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("get") - .about("Gets a value") - .arg(arg!(-v --value "the value to get").required(false)) - .arg(arg!(-d --domain "the domain to get in").required(false)), - ) - .subcommand( - Command::new("set") - .about("Sets a lockdown value") - .arg(arg!(-k --key "the key to set").required(true)) - .arg(arg!(-v --value "the value to set the key to").required(true)) - .arg(arg!(-d --domain "the domain to get in").required(false)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut lockdown_client = LockdownClient::connect(&*provider) .await .expect("Unable to connect to lockdown"); @@ -82,12 +44,27 @@ async fn main() { .await .expect("no session"); - match matches.subcommand() { - Some(("get", sub_m)) => { - let key = sub_m.get_one::("value").map(|x| x.as_str()); - let domain = sub_m.get_one::("domain").map(|x| x.as_str()); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand"); + let mut sub_args = sub_args.clone(); - match lockdown_client.get_value(key, domain).await { + match sub_name.as_str() { + "get" => { + let key: Option = sub_args.next_argument(); + let domain: Option = sub_args.next_argument(); + + match lockdown_client + .get_value( + match &key { + Some(k) => Some(k.as_str()), + None => None, + }, + match &domain { + Some(d) => Some(d.as_str()), + None => None, + }, + ) + .await + { Ok(value) => { println!("{}", pretty_print_plist(&value)); } @@ -96,25 +73,28 @@ async fn main() { } } } - - Some(("set", sub_m)) => { - let key = sub_m.get_one::("key").unwrap(); - let value_str = sub_m.get_one::("value").unwrap(); - let domain = sub_m.get_one::("domain"); + "set" => { + let value_str: String = sub_args.next_argument().unwrap(); + let key: String = sub_args.next_argument().unwrap(); + let domain: Option = sub_args.next_argument(); let value = Value::String(value_str.clone()); match lockdown_client - .set_value(key, value, domain.map(|x| x.as_str())) + .set_value( + key, + value, + match &domain { + Some(d) => Some(d.as_str()), + None => None, + }, + ) .await { Ok(()) => println!("Successfully set"), Err(e) => eprintln!("Error setting value: {e}"), } } - - _ => { - eprintln!("No subcommand provided. Try `--help` for usage."); - } + _ => unreachable!(), } } diff --git a/tools/src/main.rs b/tools/src/main.rs new file mode 100644 index 0000000..5e215b4 --- /dev/null +++ b/tools/src/main.rs @@ -0,0 +1,328 @@ +// Jackson Coxson + +use std::{ + net::{IpAddr, SocketAddr}, + str::FromStr, +}; + +use idevice::{ + pairing_file::PairingFile, + provider::{IdeviceProvider, TcpProvider}, + usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection, UsbmuxdDevice}, +}; +use jkcli::{JkArgument, JkCommand, JkFlag}; + +mod activation; +mod afc; +mod amfi; +mod app_service; +mod bt_packet_logger; +mod companion_proxy; +mod crash_logs; +mod debug_proxy; +mod diagnostics; +mod diagnosticsservice; +mod dvt_packet_parser; +mod heartbeat_client; +mod ideviceinfo; +mod ideviceinstaller; +mod installcoordination_proxy; +mod instproxy; +mod location_simulation; +mod lockdown; +mod misagent; +mod mobilebackup2; +mod mounter; +mod notifications; +mod os_trace_relay; +mod pair; +mod pcapd; +mod preboard; +mod process_control; +mod remotexpc; +mod restore_service; +mod screenshot; +mod syslog_relay; + +mod pcap; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + // Set the base CLI + let arguments = JkCommand::new() + .with_flag( + JkFlag::new("about") + .with_help("Prints the about message") + .with_short_curcuit(|| { + eprintln!("idevice-rs-tools - Jackson Coxson\n"); + eprintln!("Tools to manage and manipulate iOS devices"); + eprintln!("Version {}", env!("CARGO_PKG_VERSION")); + eprintln!("https://github.com/jkcoxson/idevice"); + eprintln!("\nOn to eternal perfection!"); + std::process::exit(0); + }), + ) + .with_flag( + JkFlag::new("version") + .with_help("Prints the version") + .with_short_curcuit(|| { + println!("{}", env!("CARGO_PKG_VERSION")); + std::process::exit(0); + }), + ) + .with_flag( + JkFlag::new("pairing-file") + .with_argument(JkArgument::new().required(true)) + .with_help("The path to the pairing file to use"), + ) + .with_flag( + JkFlag::new("host") + .with_argument(JkArgument::new().required(true)) + .with_help("The host to connect to"), + ) + .with_flag( + JkFlag::new("udid") + .with_argument(JkArgument::new().required(true)) + .with_help("The UDID to use"), + ) + .with_subcommand("activation", activation::register()) + .with_subcommand("afc", afc::register()) + .with_subcommand("amfi", amfi::register()) + .with_subcommand("app_service", app_service::register()) + .with_subcommand("bt_packet_logger", bt_packet_logger::register()) + .with_subcommand("companion_proxy", companion_proxy::register()) + .with_subcommand("crash_logs", crash_logs::register()) + .with_subcommand("debug_proxy", debug_proxy::register()) + .with_subcommand("diagnostics", diagnostics::register()) + .with_subcommand("diagnosticsservice", diagnosticsservice::register()) + .with_subcommand("dvt_packet_parser", dvt_packet_parser::register()) + .with_subcommand("heartbeat_client", heartbeat_client::register()) + .with_subcommand("ideviceinfo", ideviceinfo::register()) + .with_subcommand("ideviceinstaller", ideviceinstaller::register()) + .with_subcommand( + "installcoordination_proxy", + installcoordination_proxy::register(), + ) + .with_subcommand("instproxy", instproxy::register()) + .with_subcommand("location_simulation", location_simulation::register()) + .with_subcommand("lockdown", lockdown::register()) + .with_subcommand("misagent", misagent::register()) + .with_subcommand("mobilebackup2", mobilebackup2::register()) + .with_subcommand("mounter", mounter::register()) + .with_subcommand("notifications", notifications::register()) + .with_subcommand("os_trace_relay", os_trace_relay::register()) + .with_subcommand("pair", pair::register()) + .with_subcommand("pcapd", pcapd::register()) + .with_subcommand("preboard", preboard::register()) + .with_subcommand("process_control", process_control::register()) + .with_subcommand("remotexpc", remotexpc::register()) + .with_subcommand("restore_service", restore_service::register()) + .with_subcommand("screenshot", screenshot::register()) + .with_subcommand("syslog_relay", syslog_relay::register()) + .subcommand_required(true) + .collect() + .expect("Failed to collect CLI args"); + + let udid = arguments.get_flag::("udid"); + let host = arguments.get_flag::("host"); + let pairing_file = arguments.get_flag::("pairing-file"); + + let provider = match get_provider(udid, host, pairing_file, "idevice-rs-tools").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let (subcommand, sub_args) = match arguments.first_subcommand() { + Some(s) => s, + None => { + eprintln!("No subcommand passed, pass -h for help"); + return; + } + }; + + match subcommand.as_str() { + "activation" => { + activation::main(sub_args, provider).await; + } + "afc" => { + afc::main(sub_args, provider).await; + } + "amfi" => { + amfi::main(sub_args, provider).await; + } + "app_service" => { + app_service::main(sub_args, provider).await; + } + "bt_packet_logger" => { + bt_packet_logger::main(sub_args, provider).await; + } + "companion_proxy" => { + companion_proxy::main(sub_args, provider).await; + } + "crash_logs" => { + crash_logs::main(sub_args, provider).await; + } + "debug_proxy" => { + debug_proxy::main(sub_args, provider).await; + } + "diagnostics" => { + diagnostics::main(sub_args, provider).await; + } + "diagnosticsservice" => { + diagnosticsservice::main(sub_args, provider).await; + } + "dvt_packet_parser" => { + dvt_packet_parser::main(sub_args, provider).await; + } + "heartbeat_client" => { + heartbeat_client::main(sub_args, provider).await; + } + "ideviceinfo" => { + ideviceinfo::main(sub_args, provider).await; + } + "ideviceinstaller" => { + ideviceinstaller::main(sub_args, provider).await; + } + "installcoordination_proxy" => { + installcoordination_proxy::main(sub_args, provider).await; + } + "instproxy" => { + instproxy::main(sub_args, provider).await; + } + "location_simulation" => { + location_simulation::main(sub_args, provider).await; + } + "lockdown" => { + lockdown::main(sub_args, provider).await; + } + "misagent" => { + misagent::main(sub_args, provider).await; + } + "mobilebackup2" => { + mobilebackup2::main(sub_args, provider).await; + } + "mounter" => { + mounter::main(sub_args, provider).await; + } + "notifications" => { + notifications::main(sub_args, provider).await; + } + "os_trace_relay" => { + os_trace_relay::main(sub_args, provider).await; + } + "pair" => { + pair::main(sub_args, provider).await; + } + "pcapd" => { + pcapd::main(sub_args, provider).await; + } + "preboard" => { + preboard::main(sub_args, provider).await; + } + "process_control" => { + process_control::main(sub_args, provider).await; + } + "remotexpc" => { + remotexpc::main(sub_args, provider).await; + } + "restore_service" => { + restore_service::main(sub_args, provider).await; + } + "screenshot" => { + screenshot::main(sub_args, provider).await; + } + "syslog_relay" => { + syslog_relay::main(sub_args, provider).await; + } + _ => unreachable!(), + } +} + +async fn get_provider( + udid: Option, + host: Option, + pairing_file: Option, + label: &str, +) -> Result, String> { + let provider: Box = 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) + .await + .expect("unable to connect to socket address"); + UsbmuxdConnection::new(Box::new(socket), 1) + } else { + UsbmuxdConnection::default() + .await + .expect("Unable to connect to usbmxud") + }; + + let dev = match usbmuxd.get_device(udid.as_str()).await { + Ok(d) => d, + Err(e) => { + return Err(format!("Device not found: {e:?}")); + } + }; + Box::new(dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label)) + } else if let Some(host) = host + && let Some(pairing_file) = pairing_file + { + let host = match IpAddr::from_str(host.as_str()) { + Ok(h) => h, + Err(e) => { + return Err(format!("Invalid host: {e:?}")); + } + }; + let pairing_file = match PairingFile::read_from_file(pairing_file) { + Ok(p) => p, + Err(e) => { + return Err(format!("Unable to read pairing file: {e:?}")); + } + }; + + Box::new(TcpProvider { + addr: host, + pairing_file, + label: label.to_string(), + }) + } else { + 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) + .await + .expect("unable to connect to socket address"); + UsbmuxdConnection::new(Box::new(socket), 1) + } else { + UsbmuxdConnection::default() + .await + .expect("Unable to connect to usbmxud") + }; + let devs = match usbmuxd.get_devices().await { + Ok(d) => d, + Err(e) => { + return Err(format!("Unable to get devices from usbmuxd: {e:?}")); + } + }; + let usb_devs: Vec<&UsbmuxdDevice> = devs + .iter() + .filter(|x| x.connection_type == Connection::Usb) + .collect(); + + if devs.is_empty() { + return Err("No devices connected!".to_string()); + } + + let chosen_dev = if !usb_devs.is_empty() { + usb_devs[0] + } else { + &devs[0] + }; + Box::new(chosen_dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label)) + }; + Ok(provider) +} diff --git a/tools/src/misagent.rs b/tools/src/misagent.rs index 443607f..39c1971 100644 --- a/tools/src/misagent.rs +++ b/tools/src/misagent.rs @@ -2,99 +2,71 @@ use std::path::PathBuf; -use clap::{Arg, Command, arg, value_parser}; -use idevice::{IdeviceService, misagent::MisagentClient}; +use idevice::{IdeviceService, misagent::MisagentClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .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 images mounted on the device") - .arg( - arg!(-s --save "the folder to save the profiles to") - .value_parser(value_parser!(PathBuf)), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage provisioning profiles on the device") + .with_subcommand( + "list", + JkCommand::new() + .help("List profiles installed on the device") + .with_argument( + JkArgument::new() + .with_help("Path to save profiles from the device") + .required(false), ), ) - .subcommand( - Command::new("remove") - .about("Remove a provisioning profile") - .arg(Arg::new("id").required(true).index(1)), + .with_subcommand( + "remove", + JkCommand::new() + .help("Remove a profile installed on the device") + .with_argument( + JkArgument::new() + .with_help("ID of the profile to remove") + .required(true), + ), ) - .get_matches(); + .subcommand_required(true) +} - if matches.get_flag("about") { - println!( - "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } +pub async fn main(arguments: &CollectedArguments, provider: Box) { + tracing_subscriber::fmt::init(); - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; let mut misagent_client = MisagentClient::connect(&*provider) .await .expect("Unable to connect to misagent"); - if let Some(matches) = matches.subcommand_matches("list") { - let images = misagent_client - .copy_all() - .await - .expect("Unable to get images"); - if let Some(path) = matches.get_one::("save") { - tokio::fs::create_dir_all(path) - .await - .expect("Unable to create save DIR"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed"); + let mut sub_args = sub_args.clone(); - for (index, image) in images.iter().enumerate() { - let f = path.join(format!("{index}.pem")); - tokio::fs::write(f, image) + match sub_name.as_str() { + "list" => { + let images = misagent_client + .copy_all() + .await + .expect("Unable to get images"); + if let Some(path) = sub_args.next_argument::() { + tokio::fs::create_dir_all(&path) .await - .expect("Failed to write image"); + .expect("Unable to create save DIR"); + + for (index, image) in images.iter().enumerate() { + let f = path.join(format!("{index}.pem")); + tokio::fs::write(f, image) + .await + .expect("Failed to write image"); + } } } - } else if let Some(matches) = matches.subcommand_matches("remove") { - let id = matches.get_one::("id").expect("No ID passed"); - misagent_client.remove(id).await.expect("Failed to remove"); - } else { - eprintln!("Invalid usage, pass -h for help"); + "remove" => { + let id = sub_args.next_argument::().expect("No ID passed"); + misagent_client + .remove(id.as_str()) + .await + .expect("Failed to remove"); + } + _ => unreachable!(), } } diff --git a/tools/src/mobilebackup2.rs b/tools/src/mobilebackup2.rs index 18749e8..15c827b 100644 --- a/tools/src/mobilebackup2.rs +++ b/tools/src/mobilebackup2.rs @@ -1,191 +1,135 @@ // Jackson Coxson // Mobile Backup 2 tool for iOS devices -use clap::{Arg, Command}; use idevice::{ IdeviceService, mobilebackup2::{MobileBackup2Client, RestoreOptions}, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; use plist::Dictionary; use std::fs; use std::io::{Read, Write}; use std::path::Path; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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") +pub fn register() -> JkCommand { + JkCommand::new() + .help("Mobile Backup 2 tool for iOS devices") + .with_subcommand( + "info", + JkCommand::new() + .help("Get backup information from a local backup directory") + .with_argument( + JkArgument::new() + .with_help("Backup DIR to read from") .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"), + .with_argument( + JkArgument::new() + .with_help("Source identifier (defaults to current UDID)") + .required(true), ), ) - .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"), + .with_subcommand( + "list", + JkCommand::new() + .help("List files of the last backup from a local backup directory") + .with_argument( + JkArgument::new() + .with_help("Backup DIR to read from") + .required(true), ) - .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), + .with_argument( + JkArgument::new() + .with_help("Source identifier (defaults to current UDID)") + .required(true), ), ) - .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") + .with_subcommand( + "backup", + JkCommand::new() + .help("Start a backup operation") + .with_argument( + JkArgument::new() + .with_help("Backup directory on host") .required(true), ) - .arg( - Arg::new("path") - .long("path") - .value_name("REL_PATH") + .with_argument( + JkArgument::new() + .with_help("Target identifier for the backup") .required(true), ) - .arg(Arg::new("password").long("password").value_name("PWD")), + .with_argument( + JkArgument::new() + .with_help("Source identifier for the backup") + .required(true), + ), ) - .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")), + .with_subcommand( + "restore", + JkCommand::new() + .help("Restore from a local backup directory (DeviceLink)") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument( + JkArgument::new() + .with_help("Source UDID; defaults to current device UDID") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("Backup password if encrypted") + .required(true), + ) + .with_flag(JkFlag::new("no-reboot")) + .with_flag(JkFlag::new("no-copy")) + .with_flag(JkFlag::new("no-settings")) + .with_flag(JkFlag::new("system")) + .with_flag(JkFlag::new("remove")), ) - .subcommand( - Command::new("erase-device") - .about("Erase the device via mobilebackup2") - .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)), + .with_subcommand( + "unback", + JkCommand::new() + .help("Unpack a complete backup to device hierarchy") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument(JkArgument::new().with_help("Source")) + .with_argument(JkArgument::new().with_help("Password")), ) - .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::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("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; - } - }; + .with_subcommand( + "extract", + JkCommand::new() + .help("Extract a file from a previous backup") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument(JkArgument::new().with_help("Source").required(true)) + .with_argument(JkArgument::new().with_help("Domain").required(true)) + .with_argument(JkArgument::new().with_help("Path").required(true)) + .with_argument(JkArgument::new().with_help("Password").required(true)), + ) + .with_subcommand( + "change-password", + JkCommand::new() + .help("Change backup password") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument(JkArgument::new().with_help("Old password").required(true)) + .with_argument(JkArgument::new().with_help("New password").required(true)), + ) + .with_subcommand( + "erase-device", + JkCommand::new() + .help("Erase the device via mobilebackup2") + .with_argument(JkArgument::new().with_help("DIR").required(true)), + ) + .with_subcommand( + "freespace", + JkCommand::new().help("Get free space information"), + ) + .with_subcommand( + "encryption", + JkCommand::new().help("Check backup encryption status"), + ) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut backup_client = match MobileBackup2Client::connect(&*provider).await { Ok(client) => client, Err(e) => { @@ -194,11 +138,16 @@ async fn main() { } }; - match matches.subcommand() { - Some(("info", sub)) => { - let dir = sub.get_one::("dir").unwrap(); - let source = sub.get_one::("source").map(|s| s.as_str()); - match backup_client.info_from_path(Path::new(dir), source).await { + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "info" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + + match backup_client.info_from_path(Path::new(&dir), source).await { Ok(dict) => { println!("Backup Information:"); for (k, v) in dict { @@ -208,10 +157,12 @@ async fn main() { Err(e) => eprintln!("Failed to get info: {e}"), } } - Some(("list", sub)) => { - let dir = sub.get_one::("dir").unwrap(); - let source = sub.get_one::("source").map(|s| s.as_str()); - match backup_client.list_from_path(Path::new(dir), source).await { + "list" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + + match backup_client.list_from_path(Path::new(&dir), source).await { Ok(dict) => { println!("List Response:"); for (k, v) in dict { @@ -221,12 +172,12 @@ async fn main() { Err(e) => eprintln!("Failed to list: {e}"), } } - Some(("backup", sub_matches)) => { - let target = sub_matches.get_one::("target").map(|s| s.as_str()); - let source = sub_matches.get_one::("source").map(|s| s.as_str()); - let dir = sub_matches - .get_one::("dir") - .expect("dir is required"); + "backup" => { + let target = sub_args.next_argument::(); + let target = target.as_deref(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let dir = sub_args.next_argument::().expect("dir is required"); println!("Starting backup operation..."); let res = backup_client @@ -234,95 +185,112 @@ async fn main() { .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 { + } 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::("dir").unwrap(); - let source = sub.get_one::("source").map(|s| s.as_str()); + "restore" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let mut ropts = RestoreOptions::new(); - if sub.get_flag("no-reboot") { + if sub_args.has_flag("no-reboot") { ropts = ropts.with_reboot(false); } - if sub.get_flag("no-copy") { + if sub_args.has_flag("no-copy") { ropts = ropts.with_copy(false); } - if sub.get_flag("no-settings") { + if sub_args.has_flag("no-settings") { ropts = ropts.with_preserve_settings(false); } - if sub.get_flag("system") { + if sub_args.has_flag("system") { ropts = ropts.with_system_files(true); } - if sub.get_flag("remove") { + if sub_args.has_flag("remove") { ropts = ropts.with_remove_items_not_restored(true); } - if let Some(pw) = sub.get_one::("password") { + if let Some(pw) = sub_args.next_argument::() { ropts = ropts.with_password(pw); } match backup_client - .restore_from_path(Path::new(dir), source, Some(ropts)) + .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::("dir").unwrap(); - let source = sub.get_one::("source").map(|s| s.as_str()); - let password = sub.get_one::("password").map(|s| s.as_str()); + "unback" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let password = sub_args.next_argument::(); + let password = password.as_deref(); + match backup_client - .unback_from_path(Path::new(dir), password, source) + .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::("dir").unwrap(); - let source = sub.get_one::("source").map(|s| s.as_str()); - let domain = sub.get_one::("domain").unwrap(); - let rel = sub.get_one::("path").unwrap(); - let password = sub.get_one::("password").map(|s| s.as_str()); + "extract" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let domain = sub_args.next_argument::().unwrap(); + let rel = sub_args.next_argument::().unwrap(); + let password = sub_args.next_argument::(); + let password = password.as_deref(); + match backup_client - .extract_from_path(domain, rel, Path::new(dir), password, source) + .extract_from_path( + domain.as_str(), + rel.as_str(), + 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::("dir").unwrap(); - let old = sub.get_one::("old").map(|s| s.as_str()); - let newv = sub.get_one::("new").map(|s| s.as_str()); + "change-password" => { + let dir = sub_args.next_argument::().unwrap(); + let old = sub_args.next_argument::(); + let old = old.as_deref(); + let newv = sub_args.next_argument::(); + let newv = newv.as_deref(); + match backup_client - .change_password_from_path(Path::new(dir), old, newv) + .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::("dir").unwrap(); - match backup_client.erase_device_from_path(Path::new(dir)).await { + "erase-device" => { + let dir = sub_args.next_argument::().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 { + "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 { + "encryption" => match backup_client.check_backup_encryption().await { Ok(is_encrypted) => { println!( "Backup encryption: {}", diff --git a/tools/src/mounter.rs b/tools/src/mounter.rs index baeb4ee..4674294 100644 --- a/tools/src/mounter.rs +++ b/tools/src/mounter.rs @@ -3,88 +3,58 @@ use std::{io::Write, path::PathBuf}; -use clap::{Arg, Command, arg, value_parser}; -use idevice::{IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter}; +use idevice::{ + IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter, + provider::IdeviceProvider, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; use plist_macro::pretty_print_plist; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage mounts on an iOS device") + .with_subcommand( + "list", + JkCommand::new().help("Lists the images mounted on the device"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand( + "unmount", + JkCommand::new().help("Unmounts the developer disk image"), ) - .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("Lists the images mounted on the device")) - .subcommand(Command::new("unmount").about("Unmounts the developer disk image")) - .subcommand( - Command::new("mount") - .about("Mounts the developer disk image") - .arg( - arg!(-i --image "the developer disk image to mount") - .value_parser(value_parser!(PathBuf)) + .with_subcommand( + "mount", + JkCommand::new() + .help("Mounts the developer disk image") + .with_flag( + JkFlag::new("image") + .with_short("i") + .with_argument(JkArgument::new().required(true)) + .with_help("A path to the image to mount") .required(true), ) - .arg( - arg!(-b --manifest "the build manifest (iOS 17+)") - .value_parser(value_parser!(PathBuf)), + .with_flag( + JkFlag::new("manifest") + .with_short("b") + .with_argument(JkArgument::new()) + .with_help("the build manifest (iOS 17+)"), ) - .arg( - arg!(-t --trustcache "the trust cache (iOS 17+)") - .value_parser(value_parser!(PathBuf)), + .with_flag( + JkFlag::new("trustcache") + .with_short("t") + .with_argument(JkArgument::new()) + .with_help("the trust cache (iOS 17+)"), ) - .arg( - arg!(-s --signature "the image signature (iOS < 17.0") - .value_parser(value_parser!(PathBuf)), + .with_flag( + JkFlag::new("signature") + .with_short("s") + .with_argument(JkArgument::new()) + .with_help("the image signature (iOS < 17.0"), ), ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut lockdown_client = LockdownClient::connect(&*provider) .await .expect("Unable to connect to lockdown"); @@ -117,114 +87,120 @@ async fn main() { .await .expect("Unable to connect to image mounter"); - if matches.subcommand_matches("list").is_some() { - let images = mounter_client - .copy_devices() - .await - .expect("Unable to get images"); - for i in images { - println!("{}", pretty_print_plist(&i)); - } - } else if matches.subcommand_matches("unmount").is_some() { - if product_version < 17 { - mounter_client - .unmount_image("/Developer") + let (subcommand, sub_args) = arguments + .first_subcommand() + .expect("No subcommand passed! Pass -h for help"); + + match subcommand.as_str() { + "list" => { + let images = mounter_client + .copy_devices() .await - .expect("Failed to unmount"); - } else { - mounter_client - .unmount_image("/System/Developer") - .await - .expect("Failed to unmount"); - } - } else if let Some(matches) = matches.subcommand_matches("mount") { - let image: &PathBuf = match matches.get_one("image") { - Some(i) => i, - None => { - eprintln!("No image was passed! Pass -h for help"); - return; + .expect("Unable to get images"); + for i in images { + println!("{}", pretty_print_plist(&i)); } - }; - let image = tokio::fs::read(image).await.expect("Unable to read image"); - if product_version < 17 { - let signature: &PathBuf = match matches.get_one("signature") { - Some(s) => s, - None => { - eprintln!("No signature was passed! Pass -h for help"); - return; - } - }; - let signature = tokio::fs::read(signature) - .await - .expect("Unable to read signature"); - - mounter_client - .mount_developer(&image, signature) - .await - .expect("Unable to mount"); - } else { - let manifest: &PathBuf = match matches.get_one("manifest") { - Some(s) => s, - None => { - eprintln!("No build manifest was passed! Pass -h for help"); - return; - } - }; - let build_manifest = &tokio::fs::read(manifest) - .await - .expect("Unable to read signature"); - - let trust_cache: &PathBuf = match matches.get_one("trustcache") { - Some(s) => s, - None => { - eprintln!("No trust cache was passed! Pass -h for help"); - return; - } - }; - let trust_cache = tokio::fs::read(trust_cache) - .await - .expect("Unable to read signature"); - - 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"); - - mounter_client - .mount_personalized_with_callback( - &*provider, - image, - trust_cache, - build_manifest, - None, - unique_chip_id, - async |((n, d), _)| { - let percent = (n as f64 / d as f64) * 100.0; - print!("\rProgress: {percent:.2}%"); - std::io::stdout().flush().unwrap(); // Make sure it prints immediately - if n == d { - println!(); - } - }, - (), - ) - .await - .expect("Unable to mount"); } - } else { - eprintln!("Invalid usage, pass -h for help"); + "unmount" => { + if product_version < 17 { + mounter_client + .unmount_image("/Developer") + .await + .expect("Failed to unmount"); + } else { + mounter_client + .unmount_image("/System/Developer") + .await + .expect("Failed to unmount"); + } + } + "mount" => { + let image: PathBuf = match sub_args.get_flag("image") { + Some(i) => i, + None => { + eprintln!("No image was passed! Pass -h for help"); + return; + } + }; + let image = tokio::fs::read(image).await.expect("Unable to read image"); + if product_version < 17 { + let signature: PathBuf = match sub_args.get_flag("signature") { + Some(s) => s, + None => { + eprintln!("No signature was passed! Pass -h for help"); + return; + } + }; + let signature = tokio::fs::read(signature) + .await + .expect("Unable to read signature"); + + mounter_client + .mount_developer(&image, signature) + .await + .expect("Unable to mount"); + } else { + let manifest: PathBuf = match sub_args.get_flag("manifest") { + Some(s) => s, + None => { + eprintln!("No build manifest was passed! Pass -h for help"); + return; + } + }; + let build_manifest = &tokio::fs::read(manifest) + .await + .expect("Unable to read signature"); + + let trust_cache: PathBuf = match sub_args.get_flag("trustcache") { + Some(s) => s, + None => { + eprintln!("No trust cache was passed! Pass -h for help"); + return; + } + }; + let trust_cache = tokio::fs::read(trust_cache) + .await + .expect("Unable to read signature"); + + 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"); + + mounter_client + .mount_personalized_with_callback( + &*provider, + image, + trust_cache, + build_manifest, + None, + unique_chip_id, + async |((n, d), _)| { + let percent = (n as f64 / d as f64) * 100.0; + print!("\rProgress: {percent:.2}%"); + std::io::stdout().flush().unwrap(); // Make sure it prints immediately + if n == d { + println!(); + } + }, + (), + ) + .await + .expect("Unable to mount"); + } + } + _ => unreachable!(), } - return; } diff --git a/tools/src/notifications.rs b/tools/src/notifications.rs index 703eb9d..7026489 100644 --- a/tools/src/notifications.rs +++ b/tools/src/notifications.rs @@ -1,59 +1,16 @@ // Monitor memory and app notifications -use clap::{Arg, Command}; -use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; -mod common; +use idevice::{ + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, + rsd::RsdHandshake, +}; +use jkcli::{CollectedArguments, JkCommand}; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let matches = Command::new("notifications") - .about("start notifications") - .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), - ) - .get_matches(); - - if matches.get_flag("about") { - print!("notifications - start notifications to ios device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "notifications-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new().help("Notification proxy") +} +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -80,7 +37,6 @@ async fn main() { .await .expect("Failed to start notifications"); - // Handle Ctrl+C gracefully loop { tokio::select! { _ = tokio::signal::ctrl_c() => { @@ -88,7 +44,6 @@ async fn main() { break; } - // Branch 2: Wait for the next batch of notifications. result = notification_client.get_notification() => { if let Err(e) = result { eprintln!("Failed to get notifications: {}", e); diff --git a/tools/src/os_trace_relay.rs b/tools/src/os_trace_relay.rs index f85f396..f3daf44 100644 --- a/tools/src/os_trace_relay.rs +++ b/tools/src/os_trace_relay.rs @@ -1,58 +1,13 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient}; +use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Relay OS logs") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("os_trace_relay") - .about("Relay system 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), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("Relay logs on the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let log_client = OsTraceRelayClient::connect(&*provider) .await .expect("Unable to connect to misagent"); diff --git a/tools/src/pair.rs b/tools/src/pair.rs index fae58e8..1cfc23d 100644 --- a/tools/src/pair.rs +++ b/tools/src/pair.rs @@ -1,46 +1,29 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, lockdown::LockdownClient, + provider::IdeviceProvider, usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection}, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_argument(JkArgument::new().with_help("A UDID to override and pair with")) +} - let matches = Command::new("pair") - .about("Pair with the device") - .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), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("pair - pair with the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); +pub async fn main(arguments: &CollectedArguments, _provider: Box) { + let mut arguments = arguments.clone(); + let udid: Option = arguments.next_argument(); let mut u = UsbmuxdConnection::default() .await .expect("Failed to connect to usbmuxd"); let dev = match udid { Some(udid) => u - .get_device(udid) + .get_device(udid.as_str()) .await .expect("Failed to get device with specific udid"), None => u diff --git a/tools/src/pcapd.rs b/tools/src/pcapd.rs index d489f45..2fca403 100644 --- a/tools/src/pcapd.rs +++ b/tools/src/pcapd.rs @@ -1,55 +1,20 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, pcapd::{PcapFileWriter, PcapdClient}, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Writes pcap network data") + .with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)")) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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::("udid"); - let out = matches.get_one::("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; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let out = arguments.clone().next_argument::(); let mut logger_client = PcapdClient::connect(&*provider) .await diff --git a/tools/src/preboard.rs b/tools/src/preboard.rs index 5655db3..a6bfdb3 100644 --- a/tools/src/preboard.rs +++ b/tools/src/preboard.rs @@ -1,76 +1,34 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, preboard_service::PreboardServiceClient}; +use idevice::{IdeviceService, preboard_service::PreboardServiceClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the preboard service") + .with_subcommand("create", JkCommand::new().help("Create a stashbag??")) + .with_subcommand("commit", JkCommand::new().help("Commit a stashbag??")) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { 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"); + let (sub_name, _) = arguments.first_subcommand().unwrap(); + + match sub_name.as_str() { + "create" => { + pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) + .await + .expect("Failed to create"); + } + "commit" => { + pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) + .await + .expect("Failed to create"); + } + _ => unreachable!(), } - return; } diff --git a/tools/src/process_control.rs b/tools/src/process_control.rs index d0939b4..743d498 100644 --- a/tools/src/process_control.rs +++ b/tools/src/process_control.rs @@ -1,76 +1,26 @@ // Jackson Coxson -use clap::{Arg, Command}; +use idevice::provider::IdeviceProvider; use idevice::services::lockdown::LockdownClient; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Launch an app with process control") + .with_argument( + JkArgument::new() + .required(true) + .with_help("The bundle ID to launch"), + ) +} -#[tokio::main] -async fn main() { +pub async fn main(arguments: &CollectedArguments, provider: Box) { tracing_subscriber::fmt::init(); - let matches = Command::new("process_control") - .about("Query process control") - .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(2), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("tunneld") - .long("tunneld") - .help("Use tunneld for connection") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("bundle_id") - .value_name("Bundle ID") - .help("Bundle ID of the app to launch") - .index(1), - ) - .get_matches(); + let mut arguments = arguments.clone(); - if matches.get_flag("about") { - println!("process_control - launch and manage processes on the device"); - 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 bundle_id = matches - .get_one::("bundle_id") - .expect("No bundle ID specified"); - - let provider = - match common::get_provider(udid, host, pairing_file, "process_control-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + let bundle_id: String = arguments.next_argument().expect("No bundle ID specified"); let mut rs_client_opt: Option< idevice::dvt::remote_server::RemoteServerClient>, diff --git a/tools/src/remotexpc.rs b/tools/src/remotexpc.rs index 31df4b6..5b25573 100644 --- a/tools/src/remotexpc.rs +++ b/tools/src/remotexpc.rs @@ -1,65 +1,17 @@ // Jackson Coxson // Print out all the RemoteXPC services -use clap::{Arg, Command}; use idevice::{ - IdeviceService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, - tcp::stream::AdapterStream, + IdeviceService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, + rsd::RsdHandshake, tcp::stream::AdapterStream, }; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("remotexpc") - .about("Get services from RemoteXPC") - .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), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("remotexpc - get info from RemoteXPC"); - 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, "remotexpc-jkcoxson").await - { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new().help("Get services from RemoteXPC") +} +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); diff --git a/tools/src/restore_service.rs b/tools/src/restore_service.rs index 37b5a93..3daecf6 100644 --- a/tools/src/restore_service.rs +++ b/tools/src/restore_service.rs @@ -1,77 +1,41 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ - IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, restore_service::RestoreServiceClient, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use plist_macro::pretty_print_dictionary; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("restore_service") - .about("Interact with the Restore Service service") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the Restore Service service") + .with_subcommand("delay", JkCommand::new().help("Delay recovery image")) + .with_subcommand("recovery", JkCommand::new().help("Enter recovery mode")) + .with_subcommand("reboot", JkCommand::new().help("Reboots the device")) + .with_subcommand( + "preflightinfo", + JkCommand::new().help("Gets the preflight info"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand("nonces", JkCommand::new().help("Gets the nonces")) + .with_subcommand( + "app_parameters", + JkCommand::new().help("Gets the app parameters"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)"), + .with_subcommand( + "restore_lang", + JkCommand::new() + .help("Restores the language") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Language to restore"), + ), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("delay").about("Delay recovery image")) - .subcommand(Command::new("recovery").about("Enter recovery mode")) - .subcommand(Command::new("reboot").about("Reboots the device")) - .subcommand(Command::new("preflightinfo").about("Gets the preflight info")) - .subcommand(Command::new("nonces").about("Gets the nonces")) - .subcommand(Command::new("app_parameters").about("Gets the app parameters")) - .subcommand( - Command::new("restore_lang") - .about("Restores the language") - .arg(Arg::new("language").required(true).index(1)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." - ); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = - match common::get_provider(udid, host, pairing_file, "restore_service-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -89,37 +53,46 @@ async fn main() { .await .expect("Unable to connect to service"); - if matches.subcommand_matches("recovery").is_some() { - restore_client - .enter_recovery() - .await - .expect("command failed"); - } else if matches.subcommand_matches("reboot").is_some() { - restore_client.reboot().await.expect("command failed"); - } else if matches.subcommand_matches("preflightinfo").is_some() { - let info = restore_client - .get_preflightinfo() - .await - .expect("command failed"); - pretty_print_dictionary(&info); - } else if matches.subcommand_matches("nonces").is_some() { - let nonces = restore_client.get_nonces().await.expect("command failed"); - pretty_print_dictionary(&nonces); - } else if matches.subcommand_matches("app_parameters").is_some() { - let params = restore_client - .get_app_parameters() - .await - .expect("command failed"); - pretty_print_dictionary(¶ms); - } else if let Some(matches) = matches.subcommand_matches("restore_lang") { - let lang = matches - .get_one::("language") - .expect("No language passed"); - restore_client - .restore_lang(lang) - .await - .expect("failed to restore lang"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "recovery" => { + restore_client + .enter_recovery() + .await + .expect("command failed"); + } + "reboot" => { + restore_client.reboot().await.expect("command failed"); + } + "preflightinfo" => { + let info = restore_client + .get_preflightinfo() + .await + .expect("command failed"); + println!("{}", pretty_print_dictionary(&info)); + } + "nonces" => { + let nonces = restore_client.get_nonces().await.expect("command failed"); + println!("{}", pretty_print_dictionary(&nonces)); + } + "app_parameters" => { + let params = restore_client + .get_app_parameters() + .await + .expect("command failed"); + println!("{}", pretty_print_dictionary(¶ms)); + } + "restore_lang" => { + let lang: String = sub_args + .next_argument::() + .expect("No language passed"); + restore_client + .restore_lang(lang) + .await + .expect("failed to restore lang"); + } + _ => unreachable!(), } } diff --git a/tools/src/screenshot.rs b/tools/src/screenshot.rs index db55613..82d1a17 100644 --- a/tools/src/screenshot.rs +++ b/tools/src/screenshot.rs @@ -1,69 +1,20 @@ -use clap::{Arg, Command}; -use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; +use idevice::{ + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, + rsd::RsdHandshake, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use std::fs; use idevice::screenshotr::ScreenshotService; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Take a screenshot") + .with_argument(JkArgument::new().with_help("Output path").required(true)) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let matches = Command::new("screen_shot") - .about("take screenshot") - .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("output") - .short('o') - .long("output") - .value_name("FILE") - .help("Output file path for the screenshot (default: ./screenshot.png)") - .default_value("screenshot.png"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - print!("screen_shot - take screenshot from ios device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - let output_path = matches.get_one::("output").unwrap(); - - let provider = - match common::get_provider(udid, host, pairing_file, "take_screenshot-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let output_path = arguments.clone().next_argument::().unwrap(); let res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { println!("Using DVT over CoreDeviceProxy"); @@ -104,7 +55,7 @@ async fn main() { screenshot_client.take_screenshot().await.unwrap() }; - match fs::write(output_path, res) { + match fs::write(&output_path, res) { Ok(_) => println!("Screenshot saved to: {}", output_path), Err(e) => eprintln!("Failed to write screenshot to file: {}", e), } diff --git a/tools/src/syslog_relay.rs b/tools/src/syslog_relay.rs index 0552579..11b116c 100644 --- a/tools/src/syslog_relay.rs +++ b/tools/src/syslog_relay.rs @@ -1,58 +1,13 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, syslog_relay::SyslogRelayClient}; +use idevice::{IdeviceService, provider::IdeviceProvider, syslog_relay::SyslogRelayClient}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Relay system logs") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("syslog_relay") - .about("Relay system 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), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("Relay logs on the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); - let host = matches.get_one::("host"); - let pairing_file = matches.get_one::("pairing_file"); - - let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let mut log_client = SyslogRelayClient::connect(&*provider) .await .expect("Unable to connect to misagent");