Refactor idevice tools into single binary

This commit is contained in:
Jackson Coxson
2026-01-03 16:58:33 -07:00
parent 2eebbff172
commit 189dd5caf2
34 changed files with 1983 additions and 2777 deletions

60
Cargo.lock generated
View File

@@ -427,6 +427,19 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@@ -546,6 +559,18 @@ dependencies = [
"powerfmt", "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]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -580,6 +605,12 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -1121,6 +1152,7 @@ dependencies = [
"clap", "clap",
"futures-util", "futures-util",
"idevice", "idevice",
"jkcli",
"ns-keyed-archive", "ns-keyed-archive",
"plist", "plist",
"plist-macro", "plist-macro",
@@ -1202,6 +1234,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 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]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.34" version = "0.1.34"
@@ -1599,6 +1641,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "owo-colors"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.2.1" version = "2.2.1"
@@ -2154,6 +2202,12 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shell-words"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -2674,6 +2728,12 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"

View File

@@ -8,26 +8,7 @@ license = "MIT"
documentation = "https://docs.rs/idevice" documentation = "https://docs.rs/idevice"
repository = "https://github.com/jkcoxson/idevice" repository = "https://github.com/jkcoxson/idevice"
keywords = ["lockdownd", "ios"] keywords = ["lockdownd", "ios"]
default-run = "idevice-tools"
[[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"
# [[bin]] # [[bin]]
# name = "core_device_proxy_tun" # name = "core_device_proxy_tun"
@@ -37,112 +18,6 @@ path = "src/mounter.rs"
name = "idevice_id" name = "idevice_id"
path = "src/idevice_id.rs" 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]] [[bin]]
name = "iproxy" name = "iproxy"
path = "src/iproxy.rs" path = "src/iproxy.rs"
@@ -156,6 +31,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sha2 = { version = "0.10" } sha2 = { version = "0.10" }
ureq = { version = "3" } ureq = { version = "3" }
clap = { version = "4.5" } clap = { version = "4.5" }
jkcli = { version = "0.1" }
plist = { version = "1.7" } plist = { version = "1.7" }
plist-macro = { version = "0.1" } plist-macro = { version = "0.1" }
ns-keyed-archive = "0.1.2" ns-keyed-archive = "0.1.2"

View File

@@ -1,65 +1,23 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient, IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient,
provider::IdeviceProvider,
}; };
use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Manage activation status on an iOS device")
async fn main() { .with_subcommand("state", JkCommand::new().help("Gets the activation state"))
tracing_subscriber::fmt::init(); .with_subcommand(
"deactivate",
let matches = Command::new("activation") JkCommand::new().help("Deactivates the device"),
.about("mobileactivationd")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .subcommand_required(true)
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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "activation-jkcoxson").await
{
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let activation_client = MobileActivationdClient::new(&*provider); let activation_client = MobileActivationdClient::new(&*provider);
let mut lc = LockdownClient::connect(&*provider) let mut lc = LockdownClient::connect(&*provider)
.await .await
@@ -74,40 +32,19 @@ async fn main() {
.into_string() .into_string()
.unwrap(); .unwrap();
if matches.subcommand_matches("state").is_some() { 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"); let s = activation_client.state().await.expect("no state");
println!("Activation State: {s}"); println!("Activation State: {s}");
} else if matches.subcommand_matches("deactivate").is_some() { }
"deactivate" => {
println!("CAUTION: You are deactivating {udid}, press enter to continue."); println!("CAUTION: You are deactivating {udid}, press enter to continue.");
let mut input = String::new(); let mut input = String::new();
std::io::stdin().read_line(&mut input).ok(); std::io::stdin().read_line(&mut input).ok();
activation_client.deactivate().await.expect("no deactivate"); 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");
} }
return; _ => unreachable!(),
}
} }

View File

@@ -2,130 +2,119 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Arg, Command, value_parser};
use idevice::{ use idevice::{
IdeviceService, IdeviceService,
afc::{AfcClient, opcode::AfcFopenMode}, afc::{AfcClient, opcode::AfcFopenMode},
house_arrest::HouseArrestClient, 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] pub fn register() -> JkCommand {
async fn main() { JkCommand::new()
tracing_subscriber::fmt::init(); .help("Manage files in the AFC jail of a device")
.with_flag(
let matches = Command::new("afc") JkFlag::new("documents")
.about("Manage files on the device") .with_help(DOCS_HELP)
.arg( .with_argument(JkArgument::new().required(true)),
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .with_flag(
Arg::new("pairing_file") JkFlag::new("container")
.long("pairing-file") .with_help("Read the container contents of a bundle")
.value_name("PATH") .with_argument(JkArgument::new().required(true)),
.help("Path to the pairing file"),
) )
.arg( .with_subcommand(
Arg::new("udid") "list",
.long("udid") JkCommand::new()
.value_name("UDID") .help("Lists the items in the directory")
.help("UDID of the device (overrides host/pairing file)"), .with_argument(
) JkArgument::new()
.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")
.required(true) .required(true)
.index(1) .with_help("The directory to list in"),
.value_parser(value_parser!(PathBuf)), ),
) )
.arg(Arg::new("path").required(true).index(2)), .with_subcommand(
"download",
JkCommand::new()
.help("Download a file")
.with_argument(
JkArgument::new()
.required(true)
.with_help("Path in the AFC jail"),
) )
.subcommand( .with_argument(
Command::new("mkdir") JkArgument::new()
.about("Creates a directory") .required(true)
.arg(Arg::new("path").required(true).index(1)), .with_help("Path to save file to"),
),
) )
.subcommand( .with_subcommand(
Command::new("remove") "upload",
.about("Remove a provisioning profile") JkCommand::new()
.arg(Arg::new("path").required(true).index(1)), .help("Upload a file")
.with_argument(
JkArgument::new()
.required(true)
.with_help("Path to the file to upload"),
) )
.subcommand( .with_argument(
Command::new("remove_all") JkArgument::new()
.about("Remove a provisioning profile") .required(true)
.arg(Arg::new("path").required(true).index(1)), .with_help("Path to save file to in the AFC jail"),
),
) )
.subcommand( .with_subcommand(
Command::new("info") "mkdir",
.about("Get info about a file") JkCommand::new().help("Create a folder").with_argument(
.arg(Arg::new("path").required(true).index(1)), JkArgument::new()
.required(true)
.with_help("Path to the folder to create in the AFC jail"),
),
) )
.subcommand(Command::new("device_info").about("Get info about the device")) .with_subcommand(
.get_matches(); "remove",
JkCommand::new().help("Remove a file").with_argument(
JkArgument::new()
.required(true)
.with_help("Path to the file to remove"),
),
)
.with_subcommand(
"remove_all",
JkCommand::new().help("Remove a folder").with_argument(
JkArgument::new()
.required(true)
.with_help("Path to the folder to remove"),
),
)
.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") { pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
println!("afc"); let mut afc_client = if let Some(bundle_id) = arguments.get_flag::<String>("container") {
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "afc-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut afc_client = if let Some(bundle_id) = matches.get_one::<String>("container") {
let h = HouseArrestClient::connect(&*provider) let h = HouseArrestClient::connect(&*provider)
.await .await
.expect("Failed to connect to house arrest"); .expect("Failed to connect to house arrest");
h.vend_container(bundle_id) h.vend_container(bundle_id)
.await .await
.expect("Failed to vend container") .expect("Failed to vend container")
} else if let Some(bundle_id) = matches.get_one::<String>("documents") { } else if let Some(bundle_id) = arguments.get_flag::<String>("documents") {
let h = HouseArrestClient::connect(&*provider) let h = HouseArrestClient::connect(&*provider)
.await .await
.expect("Failed to connect to house arrest"); .expect("Failed to connect to house arrest");
@@ -138,16 +127,24 @@ async fn main() {
.expect("Unable to connect to misagent") .expect("Unable to connect to misagent")
}; };
if let Some(matches) = matches.subcommand_matches("list") { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
let path = matches.get_one::<String>("path").expect("No path passed"); let mut sub_args = sub_args.clone();
let res = afc_client.list_dir(path).await.expect("Failed to read dir"); match sub_name.as_str() {
"list" => {
let path = sub_args.next_argument::<String>().expect("No path passed");
let res = afc_client
.list_dir(&path)
.await
.expect("Failed to read dir");
println!("{path}\n{res:#?}"); println!("{path}\n{res:#?}");
} else if let Some(matches) = matches.subcommand_matches("mkdir") { }
let path = matches.get_one::<String>("path").expect("No path passed"); "mkdir" => {
let path = sub_args.next_argument::<String>().expect("No path passed");
afc_client.mk_dir(path).await.expect("Failed to mkdir"); afc_client.mk_dir(path).await.expect("Failed to mkdir");
} else if let Some(matches) = matches.subcommand_matches("download") { }
let path = matches.get_one::<String>("path").expect("No path passed"); "download" => {
let save = matches.get_one::<String>("save").expect("No path passed"); let path = sub_args.next_argument::<String>().expect("No path passed");
let save = sub_args.next_argument::<String>().expect("No path passed");
let mut file = afc_client let mut file = afc_client
.open(path, AfcFopenMode::RdOnly) .open(path, AfcFopenMode::RdOnly)
@@ -158,9 +155,10 @@ async fn main() {
tokio::fs::write(save, res) tokio::fs::write(save, res)
.await .await
.expect("Failed to write to file"); .expect("Failed to write to file");
} else if let Some(matches) = matches.subcommand_matches("upload") { }
let file = matches.get_one::<PathBuf>("file").expect("No path passed"); "upload" => {
let path = matches.get_one::<String>("path").expect("No path passed"); let file = sub_args.next_argument::<PathBuf>().expect("No path passed");
let path = sub_args.next_argument::<String>().expect("No path passed");
let bytes = tokio::fs::read(file).await.expect("Failed to read file"); let bytes = tokio::fs::read(file).await.expect("Failed to read file");
let mut file = afc_client let mut file = afc_client
@@ -171,26 +169,30 @@ async fn main() {
file.write_entire(&bytes) file.write_entire(&bytes)
.await .await
.expect("Failed to upload bytes"); .expect("Failed to upload bytes");
} else if let Some(matches) = matches.subcommand_matches("remove") { }
let path = matches.get_one::<String>("path").expect("No path passed"); "remove" => {
let path = sub_args.next_argument::<String>().expect("No path passed");
afc_client.remove(path).await.expect("Failed to remove"); afc_client.remove(path).await.expect("Failed to remove");
} else if let Some(matches) = matches.subcommand_matches("remove_all") { }
let path = matches.get_one::<String>("path").expect("No path passed"); "remove_all" => {
let path = sub_args.next_argument::<String>().expect("No path passed");
afc_client.remove_all(path).await.expect("Failed to remove"); afc_client.remove_all(path).await.expect("Failed to remove");
} else if let Some(matches) = matches.subcommand_matches("info") { }
let path = matches.get_one::<String>("path").expect("No path passed"); "info" => {
let path = sub_args.next_argument::<String>().expect("No path passed");
let res = afc_client let res = afc_client
.get_file_info(path) .get_file_info(path)
.await .await
.expect("Failed to get file info"); .expect("Failed to get file info");
println!("{res:#?}"); println!("{res:#?}");
} else if matches.subcommand_matches("device_info").is_some() { }
"device_info" => {
let res = afc_client let res = afc_client
.get_device_info() .get_device_info()
.await .await
.expect("Failed to get file info"); .expect("Failed to get file info");
println!("{res:#?}"); println!("{res:#?}");
} else { }
eprintln!("Invalid usage, pass -h for help"); _ => unreachable!(),
} }
} }

View File

@@ -1,96 +1,65 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command}; use idevice::{IdeviceService, amfi::AmfiClient, provider::IdeviceProvider};
use idevice::{IdeviceService, amfi::AmfiClient}; use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Mess with devleoper mode")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "show",
JkCommand::new().help("Shows the developer mode option in settings"),
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"),
) )
.arg( .with_subcommand("enable", JkCommand::new().help("Enables developer mode"))
Arg::new("pairing_file") .with_subcommand(
.long("pairing-file") "accept",
.value_name("PATH") JkCommand::new().help("Shows the accept dialogue for developer mode"),
.help("Path to the pairing file"),
) )
.arg( .with_subcommand(
Arg::new("udid") "status",
.value_name("UDID") JkCommand::new().help("Gets the developer mode status"),
.help("UDID of the device (overrides host/pairing file)")
.index(1),
) )
.arg( .with_subcommand("trust", JkCommand::new().help("Trusts an app signer"))
Arg::new("about") .with_argument(JkArgument::new().with_help("UUID").required(true))
.long("about") .subcommand_required(true)
.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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut amfi_client = AmfiClient::connect(&*provider) let mut amfi_client = AmfiClient::connect(&*provider)
.await .await
.expect("Failed to connect to amfi"); .expect("Failed to connect to amfi");
if matches.subcommand_matches("show").is_some() { 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 amfi_client
.reveal_developer_mode_option_in_ui() .reveal_developer_mode_option_in_ui()
.await .await
.expect("Failed to show"); .expect("Failed to show");
} else if matches.subcommand_matches("enable").is_some() { }
"enable" => {
amfi_client amfi_client
.enable_developer_mode() .enable_developer_mode()
.await .await
.expect("Failed to show"); .expect("Failed to show");
} else if matches.subcommand_matches("accept").is_some() { }
"accept" => {
amfi_client amfi_client
.accept_developer_mode() .accept_developer_mode()
.await .await
.expect("Failed to show"); .expect("Failed to show");
} else if matches.subcommand_matches("status").is_some() { }
"status" => {
let status = amfi_client let status = amfi_client
.get_developer_mode_status() .get_developer_mode_status()
.await .await
.expect("Failed to get status"); .expect("Failed to get status");
println!("Enabled: {status}"); println!("Enabled: {status}");
} else if let Some(matches) = matches.subcommand_matches("state") { }
let uuid: &String = match matches.get_one("uuid") { "trust" => {
let uuid: String = match sub_args.next_argument() {
Some(u) => u, Some(u) => u,
None => { None => {
eprintln!("No UUID passed. Invalid usage, pass -h for help"); eprintln!("No UUID passed. Invalid usage, pass -h for help");
@@ -102,8 +71,7 @@ async fn main() {
.await .await
.expect("Failed to get state"); .expect("Failed to get state");
println!("Enabled: {status}"); println!("Enabled: {status}");
} else {
eprintln!("Invalid usage, pass -h for help");
} }
return; _ => unreachable!(),
}
} }

View File

@@ -1,111 +1,72 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, RsdService, IdeviceService, RsdService,
core_device::{AppServiceClient, OpenStdioSocketClient}, core_device::{AppServiceClient, OpenStdioSocketClient},
core_device_proxy::CoreDeviceProxy, core_device_proxy::CoreDeviceProxy,
provider::IdeviceProvider,
rsd::RsdHandshake, rsd::RsdHandshake,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with the RemoteXPC app service on the device")
async fn main() { .with_subcommand("list", JkCommand::new().help("List apps on the device"))
tracing_subscriber::fmt::init(); .with_subcommand(
"launch",
let matches = Command::new("remotexpc") JkCommand::new()
.about("Get services from RemoteXPC") .help("Launch an app on the device")
.arg( .with_argument(
Arg::new("host") JkArgument::new()
.long("host") .with_help("Bundle ID to launch")
.value_name("HOST") .required(true),
.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"),
), ),
) )
.subcommand(Command::new("processes").about("List the processes running")) .with_subcommand(
.subcommand( "processes",
Command::new("uninstall").about("Uninstall an app").arg( JkCommand::new().help("List the processes running"),
Arg::new("bundle_id") )
.required(true) .with_subcommand(
.help("The bundle ID to uninstall"), "uninstall",
JkCommand::new().help("Uninstall an app").with_argument(
JkArgument::new()
.with_help("Bundle ID to uninstall")
.required(true),
), ),
) )
.subcommand( .with_subcommand(
Command::new("signal") "signal",
.about("Send a signal to an app") JkCommand::new()
.arg(Arg::new("pid").required(true).help("PID to send to")) .help("Uninstall an app")
.arg(Arg::new("signal").required(true).help("Signal to send")), .with_argument(JkArgument::new().with_help("PID to signal").required(true))
.with_argument(JkArgument::new().with_help("Signal to send").required(true)),
) )
.subcommand( .with_subcommand(
Command::new("icon") "icon",
.about("Send a signal to an app") JkCommand::new()
.arg( .help("Fetch an icon for an app")
Arg::new("bundle_id") .with_argument(
.required(true) JkArgument::new()
.help("The bundle ID to fetch"), .with_help("Bundle ID for the app")
.required(true),
) )
.arg( .with_argument(
Arg::new("path") JkArgument::new()
.required(true) .with_help("Path to save it to")
.help("The path to save the icon to"), .required(true),
) )
.arg(Arg::new("hw").required(false).help("The height and width")) .with_argument(
.arg(Arg::new("scale").required(false).help("The scale")), JkArgument::new()
.with_help("Height and width")
.required(true),
) )
.get_matches(); .with_argument(JkArgument::new().with_help("Scale").required(true)),
)
.subcommand_required(true)
}
if matches.get_flag("about") { pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
println!("debug_proxy - connect to the debug proxy and run commands");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider =
match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");
@@ -123,14 +84,19 @@ async fn main() {
.await .await
.expect("no connect"); .expect("no connect");
if matches.subcommand_matches("list").is_some() { let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand");
let mut sub_args = sub_args.clone();
match sub_name.as_str() {
"list" => {
let apps = asc let apps = asc
.list_apps(true, true, true, true, true) .list_apps(true, true, true, true, true)
.await .await
.expect("Failed to get apps"); .expect("Failed to get apps");
println!("{apps:#?}"); println!("{apps:#?}");
} else if let Some(matches) = matches.subcommand_matches("launch") { }
let bundle_id: &String = match matches.get_one("bundle_id") { "launch" => {
let bundle_id: String = match sub_args.next_argument() {
Some(b) => b, Some(b) => b,
None => { None => {
eprintln!("No bundle ID passed"); eprintln!("No bundle ID passed");
@@ -163,7 +129,6 @@ async fn main() {
eprintln!("Error copying from remote to local: {}", e); eprintln!("Error copying from remote to local: {}", e);
} }
println!("\nRemote connection closed."); println!("\nRemote connection closed.");
return;
} }
// Task 2: Copy data from local stdin to the remote process // Task 2: Copy data from local stdin to the remote process
res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => { res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => {
@@ -171,14 +136,15 @@ async fn main() {
eprintln!("Error copying from local to remote: {}", e); 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() { }
"processes" => {
let p = asc.list_processes().await.expect("no processes?"); let p = asc.list_processes().await.expect("no processes?");
println!("{p:#?}"); println!("{p:#?}");
} else if let Some(matches) = matches.subcommand_matches("uninstall") { }
let bundle_id: &String = match matches.get_one("bundle_id") { "uninstall" => {
let bundle_id: String = match sub_args.next_argument() {
Some(b) => b, Some(b) => b,
None => { None => {
eprintln!("No bundle ID passed"); eprintln!("No bundle ID passed");
@@ -187,16 +153,17 @@ async fn main() {
}; };
asc.uninstall_app(bundle_id).await.expect("no launch") 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::<String>("pid") { "signal" => {
Some(b) => b.parse().expect("failed to parse PID as u32"), let pid: u32 = match sub_args.next_argument() {
Some(b) => b,
None => { None => {
eprintln!("No bundle PID passed"); eprintln!("No bundle PID passed");
return; return;
} }
}; };
let signal: u32 = match matches.get_one::<String>("signal") { let signal: u32 = match sub_args.next_argument() {
Some(b) => b.parse().expect("failed to parse signal as u32"), Some(b) => b,
None => { None => {
eprintln!("No bundle signal passed"); eprintln!("No bundle signal passed");
return; return;
@@ -205,29 +172,24 @@ async fn main() {
let res = asc.send_signal(pid, signal).await.expect("no signal"); let res = asc.send_signal(pid, signal).await.expect("no signal");
println!("{res:#?}"); println!("{res:#?}");
} else if let Some(matches) = matches.subcommand_matches("icon") { }
let bundle_id: &String = match matches.get_one("bundle_id") { "icon" => {
let bundle_id: String = match sub_args.next_argument() {
Some(b) => b, Some(b) => b,
None => { None => {
eprintln!("No bundle ID passed"); eprintln!("No bundle ID passed");
return; return;
} }
}; };
let save_path: &String = match matches.get_one("path") { let save_path: String = match sub_args.next_argument() {
Some(b) => b, Some(b) => b,
None => { None => {
eprintln!("No bundle ID passed"); eprintln!("No bundle ID passed");
return; return;
} }
}; };
let hw: f32 = match matches.get_one::<String>("hw") { let hw: f32 = sub_args.next_argument().unwrap_or(1.0);
Some(b) => b.parse().expect("failed to parse PID as f32"), let scale: f32 = sub_args.next_argument().unwrap_or(1.0);
None => 1.0,
};
let scale: f32 = match matches.get_one::<String>("scale") {
Some(b) => b.parse().expect("failed to parse signal as f32"),
None => 1.0,
};
let res = asc let res = asc
.fetch_app_icon(bundle_id, hw, hw, scale, true) .fetch_app_icon(bundle_id, hw, hw, scale, true)
@@ -237,7 +199,7 @@ async fn main() {
tokio::fs::write(save_path, res.data) tokio::fs::write(save_path, res.data)
.await .await
.expect("failed to save"); .expect("failed to save");
} else { }
eprintln!("Invalid usage, pass -h for help"); _ => unreachable!(),
} }
} }

View File

@@ -1,71 +1,20 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use futures_util::StreamExt; 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 tokio::io::AsyncWrite;
use crate::pcap::{write_pcap_header, write_pcap_record}; use crate::pcap::{write_pcap_header, write_pcap_record};
mod common; pub fn register() -> JkCommand {
mod pcap; JkCommand::new()
.help("Writes Bluetooth pcap data")
.with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)"))
}
#[tokio::main] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() { let out: Option<String> = arguments.clone().next_argument();
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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let out = matches.get_one::<String>("out").map(String::to_owned);
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let logger_client = BtPacketLoggerClient::connect(&*provider) let logger_client = BtPacketLoggerClient::connect(&*provider)
.await .await

View File

@@ -1,83 +1,60 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command, arg};
use idevice::{ use idevice::{
IdeviceService, RsdService, companion_proxy::CompanionProxy, 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}; use plist_macro::{pretty_print_dictionary, pretty_print_plist};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Apple Watch proxy")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "list",
JkCommand::new().help("List the companions on the device"),
let matches = Command::new("companion_proxy") )
.about("Apple Watch things") .with_subcommand("listen", JkCommand::new().help("Listen for devices"))
.arg( .with_subcommand(
Arg::new("host") "get",
.long("host") JkCommand::new()
.value_name("HOST") .help("Gets a value from an AW")
.help("IP address of the device"), .with_argument(
) JkArgument::new()
.arg( .with_help("The AW UDID to get from")
Arg::new("pairing_file") .required(true),
.long("pairing-file") )
.value_name("PATH") .with_argument(
.help("Path to the pairing file"), JkArgument::new()
) .with_help("The value to get")
.arg( .required(true),
Arg::new("udid") ),
.value_name("UDID") )
.help("UDID of the device (overrides host/pairing file)") .with_subcommand(
.index(1), "start",
) JkCommand::new()
.arg( .help("Starts a service on the Apple Watch")
Arg::new("about") .with_argument(
.long("about") JkArgument::new()
.help("Show about information") .with_help("The port to listen on")
.action(clap::ArgAction::SetTrue), .required(true),
) )
.subcommand(Command::new("list").about("List the companions on the device")) .with_argument(JkArgument::new().with_help("The service name")),
.subcommand(Command::new("listen").about("Listen for devices")) )
.subcommand( .with_subcommand(
Command::new("get") "stop",
.about("Gets a value") JkCommand::new()
.arg(arg!(-d --device_udid <STRING> "the device udid to get from").required(true)) .help("Stops a service on the Apple Watch")
.arg(arg!(-v --value <STRING> "the value to get").required(true)), .with_argument(
) JkArgument::new()
.subcommand( .with_help("The port to stop")
Command::new("start") .required(true),
.about("Starts a service") ),
.arg(arg!(-p --port <PORT> "the port").required(true)) )
.arg(arg!(-n --name <STRING> "the optional service name").required(false)), .subcommand_required(true)
) }
.subcommand(
Command::new("stop")
.about("Starts a service")
.arg(arg!(-p --port <PORT> "the port").required(true)),
)
.get_matches();
if matches.get_flag("about") {
println!("companion_proxy");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core_device_proxy"); .expect("no core_device_proxy");
@@ -97,17 +74,23 @@ async fn main() {
// .await // .await
// .expect("Failed to connect to companion proxy"); // .expect("Failed to connect to companion proxy");
if matches.subcommand_matches("list").is_some() { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
let mut sub_args = sub_args.clone();
match sub_name.as_str() {
"list" => {
proxy.get_device_registry().await.expect("Failed to show"); proxy.get_device_registry().await.expect("Failed to show");
} else if matches.subcommand_matches("listen").is_some() { }
"listen" => {
let mut stream = proxy.listen_for_devices().await.expect("Failed to show"); let mut stream = proxy.listen_for_devices().await.expect("Failed to show");
while let Ok(v) = stream.next().await { while let Ok(v) = stream.next().await {
println!("{}", pretty_print_dictionary(&v)); println!("{}", pretty_print_dictionary(&v));
} }
} else if let Some(matches) = matches.subcommand_matches("get") { }
let key = matches.get_one::<String>("value").expect("no value passed"); "get" => {
let udid = matches let key: String = sub_args.next_argument::<String>().expect("no value passed");
.get_one::<String>("device_udid") let udid = sub_args
.next_argument::<String>()
.expect("no AW udid passed"); .expect("no AW udid passed");
match proxy.get_value(udid, key).await { match proxy.get_value(udid, key).await {
@@ -118,15 +101,26 @@ async fn main() {
eprintln!("Error getting value: {e}"); eprintln!("Error getting value: {e}");
} }
} }
} else if let Some(matches) = matches.subcommand_matches("start") { }
let port: u16 = matches "start" => {
.get_one::<String>("port") let port: u16 = sub_args
.next_argument::<String>()
.expect("no port passed") .expect("no port passed")
.parse() .parse()
.expect("not a number"); .expect("not a number");
let name = matches.get_one::<String>("name").map(|x| x.as_str()); let name = sub_args.next_argument::<String>();
match proxy.start_forwarding_service_port(port, name, None).await { match proxy
.start_forwarding_service_port(
port,
match &name {
Some(n) => Some(n.as_str()),
None => None,
},
None,
)
.await
{
Ok(value) => { Ok(value) => {
println!("started on port {value}"); println!("started on port {value}");
} }
@@ -134,9 +128,10 @@ async fn main() {
eprintln!("Error starting: {e}"); eprintln!("Error starting: {e}");
} }
} }
} else if let Some(matches) = matches.subcommand_matches("stop") { }
let port: u16 = matches "stop" => {
.get_one::<String>("port") let port: u16 = sub_args
.next_argument::<String>()
.expect("no port passed") .expect("no port passed")
.parse() .parse()
.expect("not a number"); .expect("not a number");
@@ -144,8 +139,7 @@ async fn main() {
if let Err(e) = proxy.stop_forwarding_service_port(port).await { if let Err(e) = proxy.stop_forwarding_service_port(port).await {
eprintln!("Error starting: {e}"); eprintln!("Error starting: {e}");
} }
} else {
eprintln!("Invalid usage, pass -h for help");
} }
return; _ => unreachable!(),
}
} }

View File

@@ -1,96 +1,79 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, IdeviceService,
crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports}, 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] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "afc-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut crash_client = CrashReportCopyMobileClient::connect(&*provider) let mut crash_client = CrashReportCopyMobileClient::connect(&*provider)
.await .await
.expect("Unable to connect to misagent"); .expect("Unable to connect to misagent");
if let Some(matches) = matches.subcommand_matches("list") { let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub command passed");
let dir_path: Option<&String> = matches.get_one("dir"); let mut sub_args = sub_args.clone();
match sub_name.as_str() {
"list" => {
let dir_path: Option<String> = sub_args.next_argument();
let res = crash_client let res = crash_client
.ls(dir_path.map(|x| x.as_str())) .ls(match &dir_path {
Some(d) => Some(d.as_str()),
None => None,
})
.await .await
.expect("Failed to read dir"); .expect("Failed to read dir");
println!("{res:#?}"); println!("{res:#?}");
} else if matches.subcommand_matches("flush").is_some() { }
"flush" => {
flush_reports(&*provider).await.expect("Failed to flush"); flush_reports(&*provider).await.expect("Failed to flush");
} else if let Some(matches) = matches.subcommand_matches("pull") { }
let path = matches.get_one::<String>("path").expect("No path passed"); "pull" => {
let save = matches.get_one::<String>("save").expect("No path passed"); let path = sub_args.next_argument::<String>().expect("No path passed");
let save = sub_args.next_argument::<String>().expect("No path passed");
let res = crash_client.pull(path).await.expect("Failed to pull log"); let res = crash_client.pull(path).await.expect("Failed to pull log");
tokio::fs::write(save, res) tokio::fs::write(save, res)
.await .await
.expect("Failed to write to file"); .expect("Failed to write to file");
} else { }
eprintln!("Invalid usage, pass -h for help"); _ => unreachable!(),
} }
} }

View File

@@ -2,70 +2,17 @@
use std::io::Write; use std::io::Write;
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, 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] pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
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::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider =
match common::get_provider(udid, host, pairing_file, "debug-proxy-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");

View File

@@ -1,106 +1,71 @@
// Jackson Coxson // Jackson Coxson
// idevice Rust implementation of libimobiledevice's idevicediagnostics // idevice Rust implementation of libimobiledevice's idevicediagnostics
use clap::{Arg, ArgMatches, Command}; use idevice::{
use idevice::{IdeviceService, services::diagnostics_relay::DiagnosticsRelayClient}; IdeviceService, provider::IdeviceProvider, services::diagnostics_relay::DiagnosticsRelayClient,
};
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with the diagnostics interface of a device")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "ioregistry",
JkCommand::new()
let matches = Command::new("idevicediagnostics") .help("Print IORegistry information")
.about("Interact with the diagnostics interface of a device") .with_flag(
.arg( JkFlag::new("plane")
Arg::new("host") .with_help("IORegistry plane to query (e.g., IODeviceTree, IOService)")
.long("host") .with_argument(JkArgument::new().required(true)),
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .with_flag(
Arg::new("pairing_file") JkFlag::new("name")
.long("pairing-file") .with_help("Entry name to filter by")
.value_name("PATH") .with_argument(JkArgument::new().required(true)),
.help("Path to the pairing file"),
) )
.arg( .with_flag(
Arg::new("udid") JkFlag::new("class")
.value_name("UDID") .with_help("Entry class to filter by")
.help("UDID of the device (overrides host/pairing file)") .with_argument(JkArgument::new().required(true)),
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(
Command::new("ioregistry")
.about("Print IORegistry information")
.arg(
Arg::new("plane")
.long("plane")
.value_name("PLANE")
.help("IORegistry plane to query (e.g., IODeviceTree, IOService)"),
)
.arg(
Arg::new("name")
.long("name")
.value_name("NAME")
.help("Entry name to filter by"),
)
.arg(
Arg::new("class")
.long("class")
.value_name("CLASS")
.help("Entry class to filter by"),
), ),
) )
.subcommand( .with_subcommand(
Command::new("mobilegestalt") "mobilegestalt",
.about("Print MobileGestalt information") JkCommand::new()
.arg( .help("Print MobileGestalt information")
Arg::new("keys") .with_argument(
.long("keys") JkArgument::new()
.value_name("KEYS") .with_help("Comma-separated list of keys to query")
.help("Comma-separated list of keys to query") .required(true),
.value_delimiter(',')
.num_args(1..),
), ),
) )
.subcommand(Command::new("gasguage").about("Print gas gauge (battery) information")) .with_subcommand(
.subcommand(Command::new("nand").about("Print NAND flash information")) "gasguage",
.subcommand(Command::new("all").about("Print all available diagnostics information")) JkCommand::new().help("Print gas gauge (battery) information"),
.subcommand(Command::new("wifi").about("Print WiFi diagnostics information")) )
.subcommand(Command::new("goodbye").about("Send Goodbye to diagnostics relay")) .with_subcommand(
.subcommand(Command::new("restart").about("Restart the device")) "nand",
.subcommand(Command::new("shutdown").about("Shutdown the device")) JkCommand::new().help("Print NAND flash information"),
.subcommand(Command::new("sleep").about("Put the device to sleep")) )
.get_matches(); .with_subcommand(
"all",
if matches.get_flag("about") { JkCommand::new().help("Print all available diagnostics information"),
println!( )
"idevicediagnostics - interact with the diagnostics interface of a device. Reimplementation of libimobiledevice's binary." .with_subcommand(
); "wifi",
println!("Copyright (c) 2025 Jackson Coxson"); JkCommand::new().help("Print WiFi diagnostics information"),
return; )
} .with_subcommand(
"goodbye",
let udid = matches.get_one::<String>("udid"); JkCommand::new().help("Send Goodbye to diagnostics relay"),
let host = matches.get_one::<String>("host"); )
let pairing_file = matches.get_one::<String>("pairing_file"); .with_subcommand("restart", JkCommand::new().help("Restart the device"))
.with_subcommand("shutdown", JkCommand::new().help("Shutdown the device"))
let provider = .with_subcommand("sleep", JkCommand::new().help("Put the device to sleep"))
match common::get_provider(udid, host, pairing_file, "idevicediagnostics-jkcoxson").await { .subcommand_required(true)
Ok(p) => p, }
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await { let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await {
Ok(client) => client, Ok(client) => client,
Err(e) => { Err(e) => {
@@ -109,47 +74,52 @@ async fn main() {
} }
}; };
match matches.subcommand() { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
Some(("ioregistry", sub_matches)) => { let mut sub_matches = sub_args.clone();
handle_ioregistry(&mut diagnostics_client, sub_matches).await;
match sub_name.as_str() {
"ioregistry" => {
handle_ioregistry(&mut diagnostics_client, &sub_matches).await;
} }
Some(("mobilegestalt", sub_matches)) => { "mobilegestalt" => {
handle_mobilegestalt(&mut diagnostics_client, sub_matches).await; handle_mobilegestalt(&mut diagnostics_client, &mut sub_matches).await;
} }
Some(("gasguage", _)) => { "gasguage" => {
handle_gasguage(&mut diagnostics_client).await; handle_gasguage(&mut diagnostics_client).await;
} }
Some(("nand", _)) => { "nand" => {
handle_nand(&mut diagnostics_client).await; handle_nand(&mut diagnostics_client).await;
} }
Some(("all", _)) => { "all" => {
handle_all(&mut diagnostics_client).await; handle_all(&mut diagnostics_client).await;
} }
Some(("wifi", _)) => { "wifi" => {
handle_wifi(&mut diagnostics_client).await; handle_wifi(&mut diagnostics_client).await;
} }
Some(("restart", _)) => { "restart" => {
handle_restart(&mut diagnostics_client).await; handle_restart(&mut diagnostics_client).await;
} }
Some(("shutdown", _)) => { "shutdown" => {
handle_shutdown(&mut diagnostics_client).await; handle_shutdown(&mut diagnostics_client).await;
} }
Some(("sleep", _)) => { "sleep" => {
handle_sleep(&mut diagnostics_client).await; handle_sleep(&mut diagnostics_client).await;
} }
Some(("goodbye", _)) => { "goodbye" => {
handle_goodbye(&mut diagnostics_client).await; handle_goodbye(&mut diagnostics_client).await;
} }
_ => { _ => unreachable!(),
eprintln!("No subcommand specified. Use --help for usage information.");
}
} }
} }
async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &CollectedArguments) {
let plane = matches.get_one::<String>("plane").map(|s| s.as_str()); let plane = matches.get_flag::<String>("plane");
let name = matches.get_one::<String>("name").map(|s| s.as_str()); let name = matches.get_flag::<String>("name");
let class = matches.get_one::<String>("class").map(|s| s.as_str()); let class = matches.get_flag::<String>("class");
let plane = plane.as_deref();
let name = name.as_deref();
let class = class.as_deref();
match client.ioregistry(plane, name, class).await { match client.ioregistry(plane, name, class).await {
Ok(Some(data)) => { 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) { async fn handle_mobilegestalt(
let keys = matches client: &mut DiagnosticsRelayClient,
.get_many::<String>("keys") matches: &mut CollectedArguments,
.map(|values| values.map(|s| s.to_string()).collect::<Vec<_>>()); ) {
let keys = matches.next_argument::<String>().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)) => { Ok(Some(data)) => {
println!("{data:#?}"); println!("{data:#?}");
} }

View File

@@ -1,71 +1,18 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use futures_util::StreamExt; use futures_util::StreamExt;
use idevice::{ use idevice::{
IdeviceService, RsdService, core_device::DiagnostisServiceClient, 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; use tokio::io::AsyncWriteExt;
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("Retrieve a sysdiagnose")
}
#[tokio::main] pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
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::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider =
match common::get_provider(udid, host, pairing_file, "diagnosticsservice-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");

View File

@@ -1,10 +1,22 @@
// Jackson Coxson // Jackson Coxson
use idevice::dvt::message::Message; use idevice::{dvt::message::Message, provider::IdeviceProvider};
use jkcli::{CollectedArguments, JkArgument, JkCommand};
#[tokio::main] pub fn register() -> JkCommand {
async fn main() { JkCommand::new()
let file = std::env::args().nth(1).expect("No file passed"); .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<dyn IdeviceProvider>) {
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 mut bytes = tokio::fs::File::open(file).await.unwrap();
let message = Message::from_reader(&mut bytes).await.unwrap(); let message = Message::from_reader(&mut bytes).await.unwrap();

View File

@@ -1,60 +1,14 @@
// Jackson Coxson // Jackson Coxson
// Heartbeat client // Heartbeat client
use clap::{Arg, Command}; use idevice::{IdeviceService, heartbeat::HeartbeatClient, provider::IdeviceProvider};
use idevice::{IdeviceService, heartbeat::HeartbeatClient}; use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("heartbeat a device")
}
#[tokio::main] pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "heartbeat_client-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut heartbeat_client = HeartbeatClient::connect(&*provider) let mut heartbeat_client = HeartbeatClient::connect(&*provider)
.await .await
.expect("Unable to connect to heartbeat"); .expect("Unable to connect to heartbeat");

View File

@@ -1,64 +1,14 @@
// Jackson Coxson // Jackson Coxson
// idevice Rust implementation of libimobiledevice's ideviceinfo // idevice Rust implementation of libimobiledevice's ideviceinfo
use clap::{Arg, Command}; use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider};
use idevice::{IdeviceService, lockdown::LockdownClient}; use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary.")
#[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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut lockdown_client = match LockdownClient::connect(&*provider).await { let mut lockdown_client = match LockdownClient::connect(&*provider).await {
Ok(l) => l, Ok(l) => l,
Err(e) => { Err(e) => {

View File

@@ -1,72 +1,41 @@
// A minimal ideviceinstaller-like CLI to install/upgrade apps // A minimal ideviceinstaller-like CLI to install/upgrade apps
use clap::{Arg, ArgAction, Command}; use idevice::{provider::IdeviceProvider, utils::installation};
use idevice::utils::installation; use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Manage files in the AFC jail of a device")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "install",
JkCommand::new()
let matches = Command::new("ideviceinstaller") .help("Install a local .ipa or directory")
.about("Install/upgrade apps on an iOS device (AFC + InstallationProxy)") .with_argument(
.arg( JkArgument::new()
Arg::new("host") .required(true)
.long("host") .with_help("Path to the .ipa or directory containing the app"),
.value_name("HOST") ),
.help("IP address of the device"),
) )
.arg( .with_subcommand(
Arg::new("pairing_file") "upgrade",
.long("pairing-file") JkCommand::new()
.value_name("PATH") .help("Install a local .ipa or directory")
.help("Path to the pairing file"), .with_argument(
JkArgument::new()
.required(true)
.with_help("Path to the .ipa or directory containing the app"),
),
) )
.arg( .subcommand_required(true)
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") { pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
println!("ideviceinstaller - install/upgrade apps using AFC + InstallationProxy (Rust)"); let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg");
println!("Copyright (c) 2025"); let mut sub_args = sub_args.clone();
return;
}
let udid = matches.get_one::<String>("udid"); match sub_name.as_str() {
let host = matches.get_one::<String>("host"); "install" => {
let pairing_file = matches.get_one::<String>("pairing_file"); let path: String = sub_args.next_argument().expect("required");
let provider = match common::get_provider(udid, host, pairing_file, "ideviceinstaller").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
if let Some(matches) = matches.subcommand_matches("install") {
let path: &String = matches.get_one("path").expect("required");
match installation::install_package_with_callback( match installation::install_package_with_callback(
&*provider, &*provider,
path, path,
@@ -81,8 +50,9 @@ async fn main() {
Ok(()) => println!("install success"), Ok(()) => println!("install success"),
Err(e) => eprintln!("Install failed: {e}"), Err(e) => eprintln!("Install failed: {e}"),
} }
} else if let Some(matches) = matches.subcommand_matches("upgrade") { }
let path: &String = matches.get_one("path").expect("required"); "upgrade" => {
let path: String = sub_args.next_argument().expect("required");
match installation::upgrade_package_with_callback( match installation::upgrade_package_with_callback(
&*provider, &*provider,
path, path,
@@ -97,7 +67,7 @@ async fn main() {
Ok(()) => println!("upgrade success"), Ok(()) => println!("upgrade success"),
Err(e) => eprintln!("Upgrade failed: {e}"), Err(e) => eprintln!("Upgrade failed: {e}"),
} }
} else { }
eprintln!("Invalid usage, pass -h for help"); _ => unreachable!(),
} }
} }

View File

@@ -1,87 +1,39 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, 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; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with the RemoteXPC installation coordination proxy")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "info",
JkCommand::new()
let matches = Command::new("installationcoordination_proxy") .help("Get info about an app on the device")
.about("") .with_argument(
.arg( JkArgument::new()
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")
.required(true) .required(true)
.help("The bundle ID to query"), .with_help("The bundle ID to query"),
), ),
) )
.subcommand( .with_subcommand(
Command::new("uninstall") "uninstall",
.about("Get info about an app on the device") JkCommand::new()
.arg( .help("Uninstalls an app on the device")
Arg::new("bundle_id") .with_argument(
JkArgument::new()
.required(true) .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") { pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
println!("debug_proxy - connect to the debug proxy and run commands");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider =
match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");
@@ -103,8 +55,12 @@ async fn main() {
.await .await
.expect("no connect"); .expect("no connect");
if let Some(matches) = matches.subcommand_matches("info") { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
let bundle_id: &String = match matches.get_one("bundle_id") { let mut sub_args = sub_args.clone();
match sub_name.as_str() {
"info" => {
let bundle_id: String = match sub_args.next_argument() {
Some(b) => b, Some(b) => b,
None => { None => {
eprintln!("No bundle ID passed"); eprintln!("No bundle ID passed");
@@ -112,10 +68,14 @@ async fn main() {
} }
}; };
let res = icp.query_app_path(bundle_id).await.expect("no info"); let res = icp
.query_app_path(bundle_id.as_str())
.await
.expect("no info");
println!("Path: {res}"); println!("Path: {res}");
} else if let Some(matches) = matches.subcommand_matches("uninstall") { }
let bundle_id: &String = match matches.get_one("bundle_id") { "uninstall" => {
let bundle_id: String = match sub_args.next_argument() {
Some(b) => b, Some(b) => b,
None => { None => {
eprintln!("No bundle ID passed"); eprintln!("No bundle ID passed");
@@ -123,10 +83,10 @@ async fn main() {
} }
}; };
icp.uninstall_app(bundle_id) icp.uninstall_app(bundle_id.as_str())
.await .await
.expect("uninstall failed"); .expect("uninstall failed");
} else { }
eprintln!("Invalid usage, pass -h for help"); _ => unreachable!(),
} }
} }

View File

@@ -1,89 +1,65 @@
// Jackson Coxson // Jackson Coxson
// Just lists apps for now // Just lists apps for now
use clap::{Arg, Command}; use idevice::{
use idevice::{IdeviceService, installation_proxy::InstallationProxyClient}; IdeviceService, installation_proxy::InstallationProxyClient, provider::IdeviceProvider,
};
use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Manage files in the AFC jail of a device")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "lookup",
JkCommand::new().help("Gets the apps on the device"),
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( .with_subcommand(
Arg::new("pairing_file") "browse",
.long("pairing-file") JkCommand::new().help("Browses the apps on the device"),
.value_name("PATH")
.help("Path to the pairing file"),
) )
.arg( .with_subcommand(
Arg::new("udid") "check_capabilities",
.value_name("UDID") JkCommand::new().help("Check the capabilities"),
.help("UDID of the device (overrides host/pairing file)")
.index(1),
) )
.arg( .with_subcommand(
Arg::new("about") "install",
.long("about") JkCommand::new()
.help("Show about information") .help("Install an app in the AFC jail")
.action(clap::ArgAction::SetTrue), .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_required(true)
.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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "instproxy-jkcoxson").await
{
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut instproxy_client = InstallationProxyClient::connect(&*provider) let mut instproxy_client = InstallationProxyClient::connect(&*provider)
.await .await
.expect("Unable to connect to instproxy"); .expect("Unable to connect to instproxy");
if matches.subcommand_matches("lookup").is_some() {
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(); let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap();
for app in apps.keys() { for app in apps.keys() {
println!("{app}"); println!("{app}");
} }
} else if matches.subcommand_matches("browse").is_some() { }
"browse" => {
instproxy_client.browse(None).await.expect("browse failed"); instproxy_client.browse(None).await.expect("browse failed");
} else if matches.subcommand_matches("check_capabilities").is_some() { }
"check_capabilities" => {
instproxy_client instproxy_client
.check_capabilities_match(Vec::new(), None) .check_capabilities_match(Vec::new(), None)
.await .await
.expect("check failed"); .expect("check failed");
} else if let Some(matches) = matches.subcommand_matches("install") { }
let path: &String = match matches.get_one("path") { "install" => {
let path: String = match sub_args.next_argument() {
Some(p) => p, Some(p) => p,
None => { None => {
eprintln!("No path passed, pass -h for help"); eprintln!("No path passed, pass -h for help");
@@ -102,7 +78,7 @@ async fn main() {
) )
.await .await
.expect("Failed to install") .expect("Failed to install")
} else { }
eprintln!("Invalid usage, pass -h for help"); _ => unreachable!(),
} }
} }

View File

@@ -1,70 +1,33 @@
// Jackson Coxson // Jackson Coxson
// Just lists apps for now // 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::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
use idevice::dvt::location_simulation::LocationSimulationClient; use idevice::dvt::location_simulation::LocationSimulationClient;
use idevice::services::simulate_location::LocationSimulationService; use idevice::services::simulate_location::LocationSimulationService;
mod common; use jkcli::{CollectedArguments, JkArgument, JkCommand};
#[tokio::main] pub fn register() -> JkCommand {
async fn main() { JkCommand::new()
tracing_subscriber::fmt::init(); .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") pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
.about("Simulate device location") let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub arg passed");
.arg( let mut sub_args = sub_args.clone();
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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "simulate_location-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
let rsd_port = proxy.handshake.server_rsd_port; let rsd_port = proxy.handshake.server_rsd_port;
@@ -86,11 +49,13 @@ async fn main() {
let mut ls_client = LocationSimulationClient::new(&mut ls_client) let mut ls_client = LocationSimulationClient::new(&mut ls_client)
.await .await
.expect("Unable to get channel for location simulation"); .expect("Unable to get channel for location simulation");
if matches.subcommand_matches("clear").is_some() { match sub_name.as_str() {
"clear" => {
ls_client.clear().await.expect("Unable to clear"); ls_client.clear().await.expect("Unable to clear");
println!("Location cleared!"); println!("Location cleared!");
} else if let Some(matches) = matches.subcommand_matches("set") { }
let latitude: &String = match matches.get_one("latitude") { "set" => {
let latitude: String = match sub_args.next_argument() {
Some(l) => l, Some(l) => l,
None => { None => {
eprintln!("No latitude passed! Pass -h for help"); eprintln!("No latitude passed! Pass -h for help");
@@ -98,7 +63,7 @@ async fn main() {
} }
}; };
let latitude: f64 = latitude.parse().expect("Failed to parse as float"); let latitude: f64 = latitude.parse().expect("Failed to parse as float");
let longitude: &String = match matches.get_one("longitude") { let longitude: String = match sub_args.next_argument() {
Some(l) => l, Some(l) => l,
None => { None => {
eprintln!("No longitude passed! Pass -h for help"); eprintln!("No longitude passed! Pass -h for help");
@@ -120,8 +85,8 @@ async fn main() {
.expect("Failed to set location"); .expect("Failed to set location");
tokio::time::sleep(std::time::Duration::from_secs(5)).await; tokio::time::sleep(std::time::Duration::from_secs(5)).await;
} }
} else { }
eprintln!("Invalid usage, pass -h for help"); _ => unreachable!(),
} }
} else { } else {
let mut location_client = match LocationSimulationService::connect(&*provider).await { let mut location_client = match LocationSimulationService::connect(&*provider).await {
@@ -133,11 +98,14 @@ async fn main() {
return; return;
} }
}; };
if matches.subcommand_matches("clear").is_some() {
match sub_name.as_str() {
"clear" => {
location_client.clear().await.expect("Unable to clear"); location_client.clear().await.expect("Unable to clear");
println!("Location cleared!"); println!("Location cleared!");
} else if let Some(matches) = matches.subcommand_matches("set") { }
let latitude: &String = match matches.get_one("latitude") { "set" => {
let latitude: String = match sub_args.next_argument() {
Some(l) => l, Some(l) => l,
None => { None => {
eprintln!("No latitude passed! Pass -h for help"); eprintln!("No latitude passed! Pass -h for help");
@@ -145,7 +113,7 @@ async fn main() {
} }
}; };
let longitude: &String = match matches.get_one("longitude") { let longitude: String = match sub_args.next_argument() {
Some(l) => l, Some(l) => l,
None => { None => {
eprintln!("No longitude passed! Pass -h for help"); eprintln!("No longitude passed! Pass -h for help");
@@ -153,15 +121,13 @@ async fn main() {
} }
}; };
location_client location_client
.set(latitude, longitude) .set(latitude.as_str(), longitude.as_str())
.await .await
.expect("Failed to set location"); .expect("Failed to set location");
println!("Location set!"); println!("Location set!");
} else { }
eprintln!("Invalid usage, pass -h for help"); _ => unreachable!(),
} }
}; };
return;
} }

View File

@@ -1,78 +1,40 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command, arg}; use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider};
use idevice::{IdeviceService, lockdown::LockdownClient}; use jkcli::{CollectedArguments, JkArgument, JkCommand};
use plist::Value; use plist::Value;
use plist_macro::pretty_print_plist; use plist_macro::pretty_print_plist;
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with lockdown")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "get",
JkCommand::new()
let matches = Command::new("lockdown") .help("Gets a value from lockdown")
.about("Start a tunnel") .with_argument(JkArgument::new().with_help("The value to get"))
.arg( .with_argument(JkArgument::new().with_help("The domain to get in")),
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .with_subcommand(
Arg::new("pairing_file") "set",
.long("pairing-file") JkCommand::new()
.value_name("PATH") .help("Gets a value from lockdown")
.help("Path to the pairing file"), .with_argument(
JkArgument::new()
.with_help("The value to set")
.required(true),
) )
.arg( .with_argument(
Arg::new("udid") JkArgument::new()
.value_name("UDID") .with_help("The value key to set")
.help("UDID of the device (overrides host/pairing file)") .required(true),
.index(1),
) )
.arg( .with_argument(JkArgument::new().with_help("The domain to set in")),
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
) )
.subcommand( .subcommand_required(true)
Command::new("get") }
.about("Gets a value")
.arg(arg!(-v --value <STRING> "the value to get").required(false))
.arg(arg!(-d --domain <STRING> "the domain to get in").required(false)),
)
.subcommand(
Command::new("set")
.about("Sets a lockdown value")
.arg(arg!(-k --key <STRING> "the key to set").required(true))
.arg(arg!(-v --value <STRING> "the value to set the key to").required(true))
.arg(arg!(-d --domain <STRING> "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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut lockdown_client = LockdownClient::connect(&*provider) let mut lockdown_client = LockdownClient::connect(&*provider)
.await .await
.expect("Unable to connect to lockdown"); .expect("Unable to connect to lockdown");
@@ -82,12 +44,27 @@ async fn main() {
.await .await
.expect("no session"); .expect("no session");
match matches.subcommand() { let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand");
Some(("get", sub_m)) => { let mut sub_args = sub_args.clone();
let key = sub_m.get_one::<String>("value").map(|x| x.as_str());
let domain = sub_m.get_one::<String>("domain").map(|x| x.as_str());
match lockdown_client.get_value(key, domain).await { match sub_name.as_str() {
"get" => {
let key: Option<String> = sub_args.next_argument();
let domain: Option<String> = 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) => { Ok(value) => {
println!("{}", pretty_print_plist(&value)); println!("{}", pretty_print_plist(&value));
} }
@@ -96,25 +73,28 @@ async fn main() {
} }
} }
} }
"set" => {
Some(("set", sub_m)) => { let value_str: String = sub_args.next_argument().unwrap();
let key = sub_m.get_one::<String>("key").unwrap(); let key: String = sub_args.next_argument().unwrap();
let value_str = sub_m.get_one::<String>("value").unwrap(); let domain: Option<String> = sub_args.next_argument();
let domain = sub_m.get_one::<String>("domain");
let value = Value::String(value_str.clone()); let value = Value::String(value_str.clone());
match lockdown_client 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 .await
{ {
Ok(()) => println!("Successfully set"), Ok(()) => println!("Successfully set"),
Err(e) => eprintln!("Error setting value: {e}"), Err(e) => eprintln!("Error setting value: {e}"),
} }
} }
_ => unreachable!(),
_ => {
eprintln!("No subcommand provided. Try `--help` for usage.");
}
} }
} }

328
tools/src/main.rs Normal file
View File

@@ -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::<String>("udid");
let host = arguments.get_flag::<String>("host");
let pairing_file = arguments.get_flag::<String>("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<String>,
host: Option<String>,
pairing_file: Option<String>,
label: &str,
) -> Result<Box<dyn IdeviceProvider>, String> {
let provider: Box<dyn IdeviceProvider> = if let Some(udid) = udid {
let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") {
let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS");
let socket = tokio::net::TcpStream::connect(socket)
.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)
}

View File

@@ -2,85 +2,53 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Arg, Command, arg, value_parser}; use idevice::{IdeviceService, misagent::MisagentClient, provider::IdeviceProvider};
use idevice::{IdeviceService, misagent::MisagentClient}; use jkcli::{CollectedArguments, JkArgument, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Manage provisioning profiles on the device")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "list",
JkCommand::new()
let matches = Command::new("core_device_proxy_tun") .help("List profiles installed on the device")
.about("Start a tunnel") .with_argument(
.arg( JkArgument::new()
Arg::new("host") .with_help("Path to save profiles from the device")
.long("host") .required(false),
.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 <FOLDER> "the folder to save the profiles to")
.value_parser(value_parser!(PathBuf)),
), ),
) )
.subcommand( .with_subcommand(
Command::new("remove") "remove",
.about("Remove a provisioning profile") JkCommand::new()
.arg(Arg::new("id").required(true).index(1)), .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") { pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
println!( tracing_subscriber::fmt::init();
"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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut misagent_client = MisagentClient::connect(&*provider) let mut misagent_client = MisagentClient::connect(&*provider)
.await .await
.expect("Unable to connect to misagent"); .expect("Unable to connect to misagent");
if let Some(matches) = matches.subcommand_matches("list") { let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed");
let mut sub_args = sub_args.clone();
match sub_name.as_str() {
"list" => {
let images = misagent_client let images = misagent_client
.copy_all() .copy_all()
.await .await
.expect("Unable to get images"); .expect("Unable to get images");
if let Some(path) = matches.get_one::<PathBuf>("save") { if let Some(path) = sub_args.next_argument::<PathBuf>() {
tokio::fs::create_dir_all(path) tokio::fs::create_dir_all(&path)
.await .await
.expect("Unable to create save DIR"); .expect("Unable to create save DIR");
@@ -91,10 +59,14 @@ async fn main() {
.expect("Failed to write image"); .expect("Failed to write image");
} }
} }
} else if let Some(matches) = matches.subcommand_matches("remove") { }
let id = matches.get_one::<String>("id").expect("No ID passed"); "remove" => {
misagent_client.remove(id).await.expect("Failed to remove"); let id = sub_args.next_argument::<String>().expect("No ID passed");
} else { misagent_client
eprintln!("Invalid usage, pass -h for help"); .remove(id.as_str())
.await
.expect("Failed to remove");
}
_ => unreachable!(),
} }
} }

View File

@@ -1,191 +1,135 @@
// Jackson Coxson // Jackson Coxson
// Mobile Backup 2 tool for iOS devices // Mobile Backup 2 tool for iOS devices
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, IdeviceService,
mobilebackup2::{MobileBackup2Client, RestoreOptions}, mobilebackup2::{MobileBackup2Client, RestoreOptions},
provider::IdeviceProvider,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
use plist::Dictionary; use plist::Dictionary;
use std::fs; use std::fs;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::path::Path; use std::path::Path;
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Mobile Backup 2 tool for iOS devices")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "info",
JkCommand::new()
let matches = Command::new("mobilebackup2") .help("Get backup information from a local backup directory")
.about("Mobile Backup 2 tool for iOS devices") .with_argument(
.arg( JkArgument::new()
Arg::new("host") .with_help("Backup DIR to read from")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
)
.arg(
Arg::new("pairing_file")
.long("pairing-file")
.value_name("PATH")
.help("Path to the pairing file"),
)
.arg(
Arg::new("udid")
.value_name("UDID")
.help("UDID of the device (overrides host/pairing file)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(
Command::new("info")
.about("Get backup information from a local backup directory")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(
Arg::new("source")
.long("source")
.value_name("SOURCE")
.help("Source identifier (defaults to current UDID)"),
),
)
.subcommand(
Command::new("list")
.about("List files of the last backup from a local backup directory")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(Arg::new("source").long("source").value_name("SOURCE")),
)
.subcommand(
Command::new("backup")
.about("Start a backup operation")
.arg(
Arg::new("dir")
.long("dir")
.value_name("DIR")
.help("Backup directory on host")
.required(true), .required(true),
) )
.arg( .with_argument(
Arg::new("target") JkArgument::new()
.long("target") .with_help("Source identifier (defaults to current UDID)")
.value_name("TARGET") .required(true),
.help("Target identifier for the backup"),
)
.arg(
Arg::new("source")
.long("source")
.value_name("SOURCE")
.help("Source identifier for the backup"),
), ),
) )
.subcommand( .with_subcommand(
Command::new("restore") "list",
.about("Restore from a local backup directory (DeviceLink)") JkCommand::new()
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) .help("List files of the last backup from a local backup directory")
.arg( .with_argument(
Arg::new("source") JkArgument::new()
.long("source") .with_help("Backup DIR to read from")
.value_name("SOURCE") .required(true),
.help("Source UDID; defaults to current device UDID"),
) )
.arg( .with_argument(
Arg::new("password") JkArgument::new()
.long("password") .with_help("Source identifier (defaults to current UDID)")
.value_name("PWD") .required(true),
.help("Backup password if encrypted"),
)
.arg(
Arg::new("no-reboot")
.long("no-reboot")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("no-copy")
.long("no-copy")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("no-settings")
.long("no-settings")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("system")
.long("system")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("remove")
.long("remove")
.action(clap::ArgAction::SetTrue),
), ),
) )
.subcommand( .with_subcommand(
Command::new("unback") "backup",
.about("Unpack a complete backup to device hierarchy") JkCommand::new()
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) .help("Start a backup operation")
.arg(Arg::new("source").long("source").value_name("SOURCE")) .with_argument(
.arg(Arg::new("password").long("password").value_name("PWD")), JkArgument::new()
) .with_help("Backup directory on host")
.subcommand(
Command::new("extract")
.about("Extract a file from a previous backup")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(Arg::new("source").long("source").value_name("SOURCE"))
.arg(
Arg::new("domain")
.long("domain")
.value_name("DOMAIN")
.required(true), .required(true),
) )
.arg( .with_argument(
Arg::new("path") JkArgument::new()
.long("path") .with_help("Target identifier for the backup")
.value_name("REL_PATH")
.required(true), .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( .with_subcommand(
Command::new("change-password") "restore",
.about("Change backup password") JkCommand::new()
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) .help("Restore from a local backup directory (DeviceLink)")
.arg(Arg::new("old").long("old").value_name("OLD")) .with_argument(JkArgument::new().with_help("DIR").required(true))
.arg(Arg::new("new").long("new").value_name("NEW")), .with_argument(
JkArgument::new()
.with_help("Source UDID; defaults to current device UDID")
.required(true),
) )
.subcommand( .with_argument(
Command::new("erase-device") JkArgument::new()
.about("Erase the device via mobilebackup2") .with_help("Backup password if encrypted")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)), .required(true),
) )
.subcommand(Command::new("freespace").about("Get free space information")) .with_flag(JkFlag::new("no-reboot"))
.subcommand(Command::new("encryption").about("Check backup encryption status")) .with_flag(JkFlag::new("no-copy"))
.get_matches(); .with_flag(JkFlag::new("no-settings"))
.with_flag(JkFlag::new("system"))
if matches.get_flag("about") { .with_flag(JkFlag::new("remove")),
println!("mobilebackup2 - manage device backups using Mobile Backup 2 service"); )
println!("Copyright (c) 2025 Jackson Coxson"); .with_subcommand(
return; "unback",
} JkCommand::new()
.help("Unpack a complete backup to device hierarchy")
let udid = matches.get_one::<String>("udid"); .with_argument(JkArgument::new().with_help("DIR").required(true))
let host = matches.get_one::<String>("host"); .with_argument(JkArgument::new().with_help("Source"))
let pairing_file = matches.get_one::<String>("pairing_file"); .with_argument(JkArgument::new().with_help("Password")),
)
let provider = .with_subcommand(
match common::get_provider(udid, host, pairing_file, "mobilebackup2-jkcoxson").await { "extract",
Ok(p) => p, JkCommand::new()
Err(e) => { .help("Extract a file from a previous backup")
eprintln!("Error creating provider: {e}"); .with_argument(JkArgument::new().with_help("DIR").required(true))
return; .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<dyn IdeviceProvider>) {
let mut backup_client = match MobileBackup2Client::connect(&*provider).await { let mut backup_client = match MobileBackup2Client::connect(&*provider).await {
Ok(client) => client, Ok(client) => client,
Err(e) => { Err(e) => {
@@ -194,11 +138,16 @@ async fn main() {
} }
}; };
match matches.subcommand() { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
Some(("info", sub)) => { let mut sub_args = sub_args.clone();
let dir = sub.get_one::<String>("dir").unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str()); match sub_name.as_str() {
match backup_client.info_from_path(Path::new(dir), source).await { "info" => {
let dir = sub_args.next_argument::<String>().unwrap();
let source = sub_args.next_argument::<String>();
let source = source.as_deref();
match backup_client.info_from_path(Path::new(&dir), source).await {
Ok(dict) => { Ok(dict) => {
println!("Backup Information:"); println!("Backup Information:");
for (k, v) in dict { for (k, v) in dict {
@@ -208,10 +157,12 @@ async fn main() {
Err(e) => eprintln!("Failed to get info: {e}"), Err(e) => eprintln!("Failed to get info: {e}"),
} }
} }
Some(("list", sub)) => { "list" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str()); let source = sub_args.next_argument::<String>();
match backup_client.list_from_path(Path::new(dir), source).await { let source = source.as_deref();
match backup_client.list_from_path(Path::new(&dir), source).await {
Ok(dict) => { Ok(dict) => {
println!("List Response:"); println!("List Response:");
for (k, v) in dict { for (k, v) in dict {
@@ -221,12 +172,12 @@ async fn main() {
Err(e) => eprintln!("Failed to list: {e}"), Err(e) => eprintln!("Failed to list: {e}"),
} }
} }
Some(("backup", sub_matches)) => { "backup" => {
let target = sub_matches.get_one::<String>("target").map(|s| s.as_str()); let target = sub_args.next_argument::<String>();
let source = sub_matches.get_one::<String>("source").map(|s| s.as_str()); let target = target.as_deref();
let dir = sub_matches let source = sub_args.next_argument::<String>();
.get_one::<String>("dir") let source = source.as_deref();
.expect("dir is required"); let dir = sub_args.next_argument::<String>().expect("dir is required");
println!("Starting backup operation..."); println!("Starting backup operation...");
let res = backup_client let res = backup_client
@@ -234,95 +185,112 @@ async fn main() {
.await; .await;
if let Err(e) = res { if let Err(e) = res {
eprintln!("Failed to send backup request: {e}"); 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}"); eprintln!("Backup failed during DL loop: {e}");
} else { } else {
println!("Backup flow finished"); println!("Backup flow finished");
} }
} }
Some(("restore", sub)) => { "restore" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str()); let source = sub_args.next_argument::<String>();
let source = source.as_deref();
let mut ropts = RestoreOptions::new(); let mut ropts = RestoreOptions::new();
if sub.get_flag("no-reboot") { if sub_args.has_flag("no-reboot") {
ropts = ropts.with_reboot(false); ropts = ropts.with_reboot(false);
} }
if sub.get_flag("no-copy") { if sub_args.has_flag("no-copy") {
ropts = ropts.with_copy(false); ropts = ropts.with_copy(false);
} }
if sub.get_flag("no-settings") { if sub_args.has_flag("no-settings") {
ropts = ropts.with_preserve_settings(false); ropts = ropts.with_preserve_settings(false);
} }
if sub.get_flag("system") { if sub_args.has_flag("system") {
ropts = ropts.with_system_files(true); 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); ropts = ropts.with_remove_items_not_restored(true);
} }
if let Some(pw) = sub.get_one::<String>("password") { if let Some(pw) = sub_args.next_argument::<String>() {
ropts = ropts.with_password(pw); ropts = ropts.with_password(pw);
} }
match backup_client match backup_client
.restore_from_path(Path::new(dir), source, Some(ropts)) .restore_from_path(Path::new(&dir), source, Some(ropts))
.await .await
{ {
Ok(_) => println!("Restore flow finished"), Ok(_) => println!("Restore flow finished"),
Err(e) => eprintln!("Restore failed: {e}"), Err(e) => eprintln!("Restore failed: {e}"),
} }
} }
Some(("unback", sub)) => { "unback" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str()); let source = sub_args.next_argument::<String>();
let password = sub.get_one::<String>("password").map(|s| s.as_str()); let source = source.as_deref();
let password = sub_args.next_argument::<String>();
let password = password.as_deref();
match backup_client match backup_client
.unback_from_path(Path::new(dir), password, source) .unback_from_path(Path::new(&dir), password, source)
.await .await
{ {
Ok(_) => println!("Unback finished"), Ok(_) => println!("Unback finished"),
Err(e) => eprintln!("Unback failed: {e}"), Err(e) => eprintln!("Unback failed: {e}"),
} }
} }
Some(("extract", sub)) => { "extract" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str()); let source = sub_args.next_argument::<String>();
let domain = sub.get_one::<String>("domain").unwrap(); let source = source.as_deref();
let rel = sub.get_one::<String>("path").unwrap(); let domain = sub_args.next_argument::<String>().unwrap();
let password = sub.get_one::<String>("password").map(|s| s.as_str()); let rel = sub_args.next_argument::<String>().unwrap();
let password = sub_args.next_argument::<String>();
let password = password.as_deref();
match backup_client 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 .await
{ {
Ok(_) => println!("Extract finished"), Ok(_) => println!("Extract finished"),
Err(e) => eprintln!("Extract failed: {e}"), Err(e) => eprintln!("Extract failed: {e}"),
} }
} }
Some(("change-password", sub)) => { "change-password" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
let old = sub.get_one::<String>("old").map(|s| s.as_str()); let old = sub_args.next_argument::<String>();
let newv = sub.get_one::<String>("new").map(|s| s.as_str()); let old = old.as_deref();
let newv = sub_args.next_argument::<String>();
let newv = newv.as_deref();
match backup_client match backup_client
.change_password_from_path(Path::new(dir), old, newv) .change_password_from_path(Path::new(&dir), old, newv)
.await .await
{ {
Ok(_) => println!("Change password finished"), Ok(_) => println!("Change password finished"),
Err(e) => eprintln!("Change password failed: {e}"), Err(e) => eprintln!("Change password failed: {e}"),
} }
} }
Some(("erase-device", sub)) => { "erase-device" => {
let dir = sub.get_one::<String>("dir").unwrap(); let dir = sub_args.next_argument::<String>().unwrap();
match backup_client.erase_device_from_path(Path::new(dir)).await { match backup_client.erase_device_from_path(Path::new(&dir)).await {
Ok(_) => println!("Erase device command sent"), Ok(_) => println!("Erase device command sent"),
Err(e) => eprintln!("Erase device failed: {e}"), Err(e) => eprintln!("Erase device failed: {e}"),
} }
} }
Some(("freespace", _)) => match backup_client.get_freespace().await { "freespace" => match backup_client.get_freespace().await {
Ok(freespace) => { Ok(freespace) => {
let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0); let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0);
println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)"); println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)");
} }
Err(e) => eprintln!("Failed to get free space: {e}"), 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) => { Ok(is_encrypted) => {
println!( println!(
"Backup encryption: {}", "Backup encryption: {}",

View File

@@ -3,88 +3,58 @@
use std::{io::Write, path::PathBuf}; use std::{io::Write, path::PathBuf};
use clap::{Arg, Command, arg, value_parser}; use idevice::{
use idevice::{IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter}; IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter,
provider::IdeviceProvider,
};
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
use plist_macro::pretty_print_plist; use plist_macro::pretty_print_plist;
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Manage mounts on an iOS device")
async fn main() { .with_subcommand(
tracing_subscriber::fmt::init(); "list",
JkCommand::new().help("Lists the images mounted on the device"),
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( .with_subcommand(
Arg::new("pairing_file") "unmount",
.long("pairing-file") JkCommand::new().help("Unmounts the developer disk image"),
.value_name("PATH")
.help("Path to the pairing file"),
) )
.arg( .with_subcommand(
Arg::new("udid") "mount",
.value_name("UDID") JkCommand::new()
.help("UDID of the device (overrides host/pairing file)") .help("Mounts the developer disk image")
.index(1), .with_flag(
) JkFlag::new("image")
.arg( .with_short("i")
Arg::new("about") .with_argument(JkArgument::new().required(true))
.long("about") .with_help("A path to the image to mount")
.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 <FILE> "the developer disk image to mount")
.value_parser(value_parser!(PathBuf))
.required(true), .required(true),
) )
.arg( .with_flag(
arg!(-b --manifest <FILE> "the build manifest (iOS 17+)") JkFlag::new("manifest")
.value_parser(value_parser!(PathBuf)), .with_short("b")
.with_argument(JkArgument::new())
.with_help("the build manifest (iOS 17+)"),
) )
.arg( .with_flag(
arg!(-t --trustcache <FILE> "the trust cache (iOS 17+)") JkFlag::new("trustcache")
.value_parser(value_parser!(PathBuf)), .with_short("t")
.with_argument(JkArgument::new())
.with_help("the trust cache (iOS 17+)"),
) )
.arg( .with_flag(
arg!(-s --signature <FILE> "the image signature (iOS < 17.0") JkFlag::new("signature")
.value_parser(value_parser!(PathBuf)), .with_short("s")
.with_argument(JkArgument::new())
.with_help("the image signature (iOS < 17.0"),
), ),
) )
.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;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut lockdown_client = LockdownClient::connect(&*provider) let mut lockdown_client = LockdownClient::connect(&*provider)
.await .await
.expect("Unable to connect to lockdown"); .expect("Unable to connect to lockdown");
@@ -117,7 +87,12 @@ async fn main() {
.await .await
.expect("Unable to connect to image mounter"); .expect("Unable to connect to image mounter");
if matches.subcommand_matches("list").is_some() { let (subcommand, sub_args) = arguments
.first_subcommand()
.expect("No subcommand passed! Pass -h for help");
match subcommand.as_str() {
"list" => {
let images = mounter_client let images = mounter_client
.copy_devices() .copy_devices()
.await .await
@@ -125,7 +100,8 @@ async fn main() {
for i in images { for i in images {
println!("{}", pretty_print_plist(&i)); println!("{}", pretty_print_plist(&i));
} }
} else if matches.subcommand_matches("unmount").is_some() { }
"unmount" => {
if product_version < 17 { if product_version < 17 {
mounter_client mounter_client
.unmount_image("/Developer") .unmount_image("/Developer")
@@ -137,8 +113,9 @@ async fn main() {
.await .await
.expect("Failed to unmount"); .expect("Failed to unmount");
} }
} else if let Some(matches) = matches.subcommand_matches("mount") { }
let image: &PathBuf = match matches.get_one("image") { "mount" => {
let image: PathBuf = match sub_args.get_flag("image") {
Some(i) => i, Some(i) => i,
None => { None => {
eprintln!("No image was passed! Pass -h for help"); eprintln!("No image was passed! Pass -h for help");
@@ -147,7 +124,7 @@ async fn main() {
}; };
let image = tokio::fs::read(image).await.expect("Unable to read image"); let image = tokio::fs::read(image).await.expect("Unable to read image");
if product_version < 17 { if product_version < 17 {
let signature: &PathBuf = match matches.get_one("signature") { let signature: PathBuf = match sub_args.get_flag("signature") {
Some(s) => s, Some(s) => s,
None => { None => {
eprintln!("No signature was passed! Pass -h for help"); eprintln!("No signature was passed! Pass -h for help");
@@ -163,7 +140,7 @@ async fn main() {
.await .await
.expect("Unable to mount"); .expect("Unable to mount");
} else { } else {
let manifest: &PathBuf = match matches.get_one("manifest") { let manifest: PathBuf = match sub_args.get_flag("manifest") {
Some(s) => s, Some(s) => s,
None => { None => {
eprintln!("No build manifest was passed! Pass -h for help"); eprintln!("No build manifest was passed! Pass -h for help");
@@ -174,7 +151,7 @@ async fn main() {
.await .await
.expect("Unable to read signature"); .expect("Unable to read signature");
let trust_cache: &PathBuf = match matches.get_one("trustcache") { let trust_cache: PathBuf = match sub_args.get_flag("trustcache") {
Some(s) => s, Some(s) => s,
None => { None => {
eprintln!("No trust cache was passed! Pass -h for help"); eprintln!("No trust cache was passed! Pass -h for help");
@@ -223,8 +200,7 @@ async fn main() {
.await .await
.expect("Unable to mount"); .expect("Unable to mount");
} }
} else {
eprintln!("Invalid usage, pass -h for help");
} }
return; _ => unreachable!(),
}
} }

View File

@@ -1,59 +1,16 @@
// Monitor memory and app notifications // Monitor memory and app notifications
use clap::{Arg, Command}; use idevice::{
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
mod common; rsd::RsdHandshake,
};
use jkcli::{CollectedArguments, JkCommand};
#[tokio::main] pub fn register() -> JkCommand {
async fn main() { JkCommand::new().help("Notification proxy")
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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "notifications-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");
@@ -80,7 +37,6 @@ async fn main() {
.await .await
.expect("Failed to start notifications"); .expect("Failed to start notifications");
// Handle Ctrl+C gracefully
loop { loop {
tokio::select! { tokio::select! {
_ = tokio::signal::ctrl_c() => { _ = tokio::signal::ctrl_c() => {
@@ -88,7 +44,6 @@ async fn main() {
break; break;
} }
// Branch 2: Wait for the next batch of notifications.
result = notification_client.get_notification() => { result = notification_client.get_notification() => {
if let Err(e) = result { if let Err(e) = result {
eprintln!("Failed to get notifications: {}", e); eprintln!("Failed to get notifications: {}", e);

View File

@@ -1,58 +1,13 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command}; use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient, provider::IdeviceProvider};
use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient}; use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("Relay OS logs")
}
#[tokio::main] pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let log_client = OsTraceRelayClient::connect(&*provider) let log_client = OsTraceRelayClient::connect(&*provider)
.await .await
.expect("Unable to connect to misagent"); .expect("Unable to connect to misagent");

View File

@@ -1,46 +1,29 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, IdeviceService,
lockdown::LockdownClient, lockdown::LockdownClient,
provider::IdeviceProvider,
usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection}, usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection},
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand};
#[tokio::main] pub fn register() -> JkCommand {
async fn main() { JkCommand::new()
tracing_subscriber::fmt::init(); .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") pub async fn main(arguments: &CollectedArguments, _provider: Box<dyn IdeviceProvider>) {
.about("Pair with the device") let mut arguments = arguments.clone();
.arg( let udid: Option<String> = arguments.next_argument();
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::<String>("udid");
let mut u = UsbmuxdConnection::default() let mut u = UsbmuxdConnection::default()
.await .await
.expect("Failed to connect to usbmuxd"); .expect("Failed to connect to usbmuxd");
let dev = match udid { let dev = match udid {
Some(udid) => u Some(udid) => u
.get_device(udid) .get_device(udid.as_str())
.await .await
.expect("Failed to get device with specific udid"), .expect("Failed to get device with specific udid"),
None => u None => u

View File

@@ -1,55 +1,20 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, IdeviceService,
pcapd::{PcapFileWriter, PcapdClient}, 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] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() { let out = arguments.clone().next_argument::<String>();
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::<String>("udid");
let out = matches.get_one::<String>("out").map(String::to_owned);
let provider = match common::get_provider(udid, None, None, "pcapd-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut logger_client = PcapdClient::connect(&*provider) let mut logger_client = PcapdClient::connect(&*provider)
.await .await

View File

@@ -1,76 +1,34 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command}; use idevice::{IdeviceService, preboard_service::PreboardServiceClient, provider::IdeviceProvider};
use idevice::{IdeviceService, preboard_service::PreboardServiceClient}; use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with the preboard service")
async fn main() { .with_subcommand("create", JkCommand::new().help("Create a stashbag??"))
tracing_subscriber::fmt::init(); .with_subcommand("commit", JkCommand::new().help("Commit a stashbag??"))
.subcommand_required(true)
let matches = Command::new("preboard") }
.about("Mess with developer mode")
.arg(
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
)
.arg(
Arg::new("pairing_file")
.long("pairing-file")
.value_name("PATH")
.help("Path to the pairing file"),
)
.arg(
Arg::new("udid")
.value_name("UDID")
.help("UDID of the device (overrides host/pairing file)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(Command::new("create").about("Create a stashbag??"))
.subcommand(Command::new("commit").about("Commit a stashbag??"))
.get_matches();
if matches.get_flag("about") {
println!("preboard - no idea what this does");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut pc = PreboardServiceClient::connect(&*provider) let mut pc = PreboardServiceClient::connect(&*provider)
.await .await
.expect("Failed to connect to Preboard"); .expect("Failed to connect to Preboard");
if matches.subcommand_matches("create").is_some() { 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]) pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
.await .await
.expect("Failed to create"); .expect("Failed to create");
} else if matches.subcommand_matches("commit").is_some() { }
"commit" => {
pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
.await .await
.expect("Failed to create"); .expect("Failed to create");
} else {
eprintln!("Invalid usage, pass -h for help");
} }
return; _ => unreachable!(),
}
} }

View File

@@ -1,76 +1,26 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command}; use idevice::provider::IdeviceProvider;
use idevice::services::lockdown::LockdownClient; use idevice::services::lockdown::LockdownClient;
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; 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] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
let matches = Command::new("process_control") let mut arguments = arguments.clone();
.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();
if matches.get_flag("about") { let bundle_id: String = arguments.next_argument().expect("No bundle ID specified");
println!("process_control - launch and manage processes on the device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let bundle_id = matches
.get_one::<String>("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 mut rs_client_opt: Option< let mut rs_client_opt: Option<
idevice::dvt::remote_server::RemoteServerClient<Box<dyn idevice::ReadWrite>>, idevice::dvt::remote_server::RemoteServerClient<Box<dyn idevice::ReadWrite>>,

View File

@@ -1,65 +1,17 @@
// Jackson Coxson // Jackson Coxson
// Print out all the RemoteXPC services // Print out all the RemoteXPC services
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, IdeviceService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
tcp::stream::AdapterStream, rsd::RsdHandshake, tcp::stream::AdapterStream,
}; };
use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("Get services from RemoteXPC")
#[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::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider = match common::get_provider(udid, host, pairing_file, "remotexpc-jkcoxson").await
{
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");

View File

@@ -1,77 +1,41 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command};
use idevice::{ use idevice::{
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
restore_service::RestoreServiceClient, rsd::RsdHandshake, restore_service::RestoreServiceClient, rsd::RsdHandshake,
}; };
use jkcli::{CollectedArguments, JkArgument, JkCommand};
use plist_macro::pretty_print_dictionary; use plist_macro::pretty_print_dictionary;
mod common; pub fn register() -> JkCommand {
JkCommand::new()
#[tokio::main] .help("Interact with the Restore Service service")
async fn main() { .with_subcommand("delay", JkCommand::new().help("Delay recovery image"))
tracing_subscriber::fmt::init(); .with_subcommand("recovery", JkCommand::new().help("Enter recovery mode"))
.with_subcommand("reboot", JkCommand::new().help("Reboots the device"))
let matches = Command::new("restore_service") .with_subcommand(
.about("Interact with the Restore Service service") "preflightinfo",
.arg( JkCommand::new().help("Gets the preflight info"),
Arg::new("host")
.long("host")
.value_name("HOST")
.help("IP address of the device"),
) )
.arg( .with_subcommand("nonces", JkCommand::new().help("Gets the nonces"))
Arg::new("pairing_file") .with_subcommand(
.long("pairing-file") "app_parameters",
.value_name("PATH") JkCommand::new().help("Gets the app parameters"),
.help("Path to the pairing file"),
) )
.arg( .with_subcommand(
Arg::new("udid") "restore_lang",
.value_name("UDID") JkCommand::new()
.help("UDID of the device (overrides host/pairing file)"), .help("Restores the language")
.with_argument(
JkArgument::new()
.required(true)
.with_help("Language to restore"),
),
) )
.arg( .subcommand_required(true)
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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider =
match common::get_provider(udid, host, pairing_file, "restore_service-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let proxy = CoreDeviceProxy::connect(&*provider) let proxy = CoreDeviceProxy::connect(&*provider)
.await .await
.expect("no core proxy"); .expect("no core proxy");
@@ -89,37 +53,46 @@ async fn main() {
.await .await
.expect("Unable to connect to service"); .expect("Unable to connect to service");
if matches.subcommand_matches("recovery").is_some() { let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
let mut sub_args = sub_args.clone();
match sub_name.as_str() {
"recovery" => {
restore_client restore_client
.enter_recovery() .enter_recovery()
.await .await
.expect("command failed"); .expect("command failed");
} else if matches.subcommand_matches("reboot").is_some() { }
"reboot" => {
restore_client.reboot().await.expect("command failed"); restore_client.reboot().await.expect("command failed");
} else if matches.subcommand_matches("preflightinfo").is_some() { }
"preflightinfo" => {
let info = restore_client let info = restore_client
.get_preflightinfo() .get_preflightinfo()
.await .await
.expect("command failed"); .expect("command failed");
pretty_print_dictionary(&info); println!("{}", pretty_print_dictionary(&info));
} else if matches.subcommand_matches("nonces").is_some() { }
"nonces" => {
let nonces = restore_client.get_nonces().await.expect("command failed"); let nonces = restore_client.get_nonces().await.expect("command failed");
pretty_print_dictionary(&nonces); println!("{}", pretty_print_dictionary(&nonces));
} else if matches.subcommand_matches("app_parameters").is_some() { }
"app_parameters" => {
let params = restore_client let params = restore_client
.get_app_parameters() .get_app_parameters()
.await .await
.expect("command failed"); .expect("command failed");
pretty_print_dictionary(&params); println!("{}", pretty_print_dictionary(&params));
} else if let Some(matches) = matches.subcommand_matches("restore_lang") { }
let lang = matches "restore_lang" => {
.get_one::<String>("language") let lang: String = sub_args
.next_argument::<String>()
.expect("No language passed"); .expect("No language passed");
restore_client restore_client
.restore_lang(lang) .restore_lang(lang)
.await .await
.expect("failed to restore lang"); .expect("failed to restore lang");
} else { }
eprintln!("Invalid usage, pass -h for help"); _ => unreachable!(),
} }
} }

View File

@@ -1,69 +1,20 @@
use clap::{Arg, Command}; use idevice::{
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
rsd::RsdHandshake,
};
use jkcli::{CollectedArguments, JkArgument, JkCommand};
use std::fs; use std::fs;
use idevice::screenshotr::ScreenshotService; 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] pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
async fn main() { let output_path = arguments.clone().next_argument::<String>().unwrap();
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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let output_path = matches.get_one::<String>("output").unwrap();
let provider =
match common::get_provider(udid, host, pairing_file, "take_screenshot-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { let res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
println!("Using DVT over CoreDeviceProxy"); println!("Using DVT over CoreDeviceProxy");
@@ -104,7 +55,7 @@ async fn main() {
screenshot_client.take_screenshot().await.unwrap() 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), Ok(_) => println!("Screenshot saved to: {}", output_path),
Err(e) => eprintln!("Failed to write screenshot to file: {}", e), Err(e) => eprintln!("Failed to write screenshot to file: {}", e),
} }

View File

@@ -1,58 +1,13 @@
// Jackson Coxson // Jackson Coxson
use clap::{Arg, Command}; use idevice::{IdeviceService, provider::IdeviceProvider, syslog_relay::SyslogRelayClient};
use idevice::{IdeviceService, syslog_relay::SyslogRelayClient}; use jkcli::{CollectedArguments, JkCommand};
mod common; pub fn register() -> JkCommand {
JkCommand::new().help("Relay system logs")
}
#[tokio::main] pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut log_client = SyslogRelayClient::connect(&*provider) let mut log_client = SyslogRelayClient::connect(&*provider)
.await .await
.expect("Unable to connect to misagent"); .expect("Unable to connect to misagent");