diff --git a/idevice/src/installation_proxy.rs b/idevice/src/installation_proxy.rs index b638d62..5e35246 100644 --- a/idevice/src/installation_proxy.rs +++ b/idevice/src/installation_proxy.rs @@ -5,6 +5,9 @@ use std::collections::HashMap; +use log::warn; +use plist::Dictionary; + use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; /// Client for interacting with the iOS installation proxy service @@ -124,5 +127,273 @@ impl InstallationProxyClient { _ => Err(IdeviceError::UnexpectedResponse), } } -} + /// Installs an application package on the device + /// + /// # Arguments + /// * `package_path` - Path to the .ipa package in the AFC jail (device's installation directory) + /// * `options` - Optional installation options as a plist dictionary + /// * `callback` - Optional progress callback that receives (percent_complete, state) + /// * `state` - Optional state to pass to the callback + /// + /// # Returns + /// `Ok(())` on successful installation + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The installation fails + /// - The service returns an error + /// + /// # Note + /// The package_path should be relative to the AFC jail root + pub async fn install( + &mut self, + package_path: impl Into, + options: Option, + callback: Option Fut>, + state: Option, + ) -> Result<(), IdeviceError> + where + Fut: std::future::Future, + S: Clone, + { + let package_path = package_path.into(); + let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); + + let mut command = Dictionary::new(); + command.insert("Command".into(), "Install".into()); + command.insert("ClientOptions".into(), options); + command.insert("PackagePath".into(), package_path.into()); + + self.idevice + .send_plist(plist::Value::Dictionary(command)) + .await?; + + self.watch_completion(callback, state).await + } + + /// Upgrades an existing application on the device + /// + /// # Arguments + /// * `package_path` - Path to the .ipa package in the AFC jail (device's installation directory) + /// * `options` - Optional upgrade options as a plist dictionary + /// * `callback` - Optional progress callback that receives (percent_complete, state) + /// * `state` - Optional state to pass to the callback + /// + /// # Returns + /// `Ok(())` on successful upgrade + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The upgrade fails + /// - The service returns an error + pub async fn upgrade( + &mut self, + package_path: impl Into, + options: Option, + callback: Option Fut>, + state: Option, + ) -> Result<(), IdeviceError> + where + Fut: std::future::Future, + S: Clone, + { + let package_path = package_path.into(); + let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); + + let mut command = Dictionary::new(); + command.insert("Command".into(), "Upgrade".into()); + command.insert("ClientOptions".into(), options); + command.insert("PackagePath".into(), package_path.into()); + + self.idevice + .send_plist(plist::Value::Dictionary(command)) + .await?; + + self.watch_completion(callback, state).await + } + + /// Uninstalls an application from the device + /// + /// # Arguments + /// * `bundle_id` - Bundle identifier of the application to uninstall + /// * `options` - Optional uninstall options as a plist dictionary + /// * `callback` - Optional progress callback that receives (percent_complete, state) + /// * `state` - Optional state to pass to the callback + /// + /// # Returns + /// `Ok(())` on successful uninstallation + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The uninstallation fails + /// - The service returns an error + pub async fn uninstall( + &mut self, + bundle_id: impl Into, + options: Option, + callback: Option Fut>, + state: Option, + ) -> Result<(), IdeviceError> + where + Fut: std::future::Future, + S: Clone, + { + let bundle_id = bundle_id.into(); + let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); + + let mut command = Dictionary::new(); + command.insert("Command".into(), "Uninstall".into()); + command.insert("ApplicationIdentifier".into(), bundle_id.into()); + command.insert("ClientOptions".into(), options); + + self.idevice + .send_plist(plist::Value::Dictionary(command)) + .await?; + + self.watch_completion(callback, state).await + } + + /// Checks if the device capabilities match the required capabilities + /// + /// # Arguments + /// * `capabilities` - List of required capabilities as plist values + /// * `options` - Optional check options as a plist dictionary + /// + /// # Returns + /// `true` if all capabilities are supported, `false` otherwise + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The service returns an error + pub async fn check_capabilities_match( + &mut self, + capabilities: Vec, + options: Option, + ) -> Result { + let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); + + let mut command = Dictionary::new(); + command.insert("Command".into(), "CheckCapabilitiesMatch".into()); + command.insert("ClientOptions".into(), options); + command.insert("Capabilities".into(), capabilities.into()); + + self.idevice + .send_plist(plist::Value::Dictionary(command)) + .await?; + let mut res = self.idevice.read_plist().await?; + + if let Some(caps) = res.remove("LookupResult").and_then(|x| x.as_boolean()) { + Ok(caps) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } + + /// Browses installed applications on the device + /// + /// # Arguments + /// * `options` - Optional browse options as a plist dictionary + /// + /// # Returns + /// A vector of plist values representing application information + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The service returns an error + /// + /// # Note + /// This method streams application information in chunks and collects them into a single vector + pub async fn browse( + &mut self, + options: Option, + ) -> Result, IdeviceError> { + let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); + + let mut command = Dictionary::new(); + command.insert("Command".into(), "Browse".into()); + command.insert("ClientOptions".into(), options); + + self.idevice + .send_plist(plist::Value::Dictionary(command)) + .await?; + + let mut values = Vec::new(); + loop { + let mut res = self.idevice.read_plist().await?; + + if let Some(list) = res.remove("CurrentList").and_then(|x| x.into_array()) { + for v in list.into_iter() { + values.push(v); + } + } else { + warn!("browse didn't contain current list"); + break; + } + + if let Some(status) = res.get("Status").and_then(|x| x.as_string()) { + if status == "Complete" { + break; + } + } + } + Ok(values) + } + + /// Watches for operation completion and handles progress callbacks + /// + /// # Arguments + /// * `callback` - Optional progress callback that receives (percent_complete, state) + /// * `state` - Optional state to pass to the callback + /// + /// # Returns + /// `Ok(())` when the operation completes successfully + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The operation fails + /// - The service returns an error + async fn watch_completion( + &mut self, + callback: Option Fut>, + state: Option, + ) -> Result<(), IdeviceError> + where + Fut: std::future::Future, + S: Clone, + { + loop { + let mut res = self.idevice.read_plist().await?; + + if let Some(e) = res.remove("ErrorDescription").and_then(|x| x.into_string()) { + return Err(IdeviceError::InstallationProxyOperationFailed( + e.to_string(), + )); + } + + if let Some(c) = res + .remove("PercentComplete") + .and_then(|x| x.as_unsigned_integer()) + { + if let Some(callback) = &callback { + if let Some(state) = &state { + callback((c, state.clone())).await; + } + } + } + + if let Some(c) = res.remove("Status").and_then(|x| x.into_string()) { + if c == "Complete" { + break; + } + } + } + Ok(()) + } +} diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 65d964b..58a6159 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -406,6 +406,10 @@ pub enum IdeviceError { #[error("misagent operation failed")] MisagentFailure, + #[cfg(feature = "installation_proxy")] + #[error("installation proxy operation failed")] + InstallationProxyOperationFailed(String), + #[cfg(feature = "afc")] #[error("afc error")] Afc(#[from] afc::errors::AfcError), diff --git a/tools/src/instproxy.rs b/tools/src/instproxy.rs index a8aa390..8c7b7b4 100644 --- a/tools/src/instproxy.rs +++ b/tools/src/instproxy.rs @@ -36,6 +36,9 @@ async fn main() { .help("Show about information") .action(clap::ArgAction::SetTrue), ) + .subcommand(Command::new("lookup").about("Gets the apps on the device")) + .subcommand(Command::new("browse").about("Browses the apps on the device")) + .subcommand(Command::new("check_capabilities").about("Check the capabilities")) .get_matches(); if matches.get_flag("about") { @@ -48,23 +51,34 @@ async fn main() { let host = matches.get_one::("host"); let pairing_file = matches.get_one::("pairing_file"); - let provider = - match common::get_provider(udid, host, pairing_file, "ideviceinfo-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + let provider = match common::get_provider(udid, host, pairing_file, "instproxy-jkcoxson").await + { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; let mut instproxy_client = InstallationProxyClient::connect(&*provider) .await .expect("Unable to connect to instproxy"); - let apps = instproxy_client - .get_apps(Some("User".to_string()), None) - .await - .unwrap(); - for app in apps.keys() { - println!("{app}"); + if matches.subcommand_matches("lookup").is_some() { + let apps = instproxy_client + .get_apps(Some("User".to_string()), None) + .await + .unwrap(); + for app in apps.keys() { + println!("{app}"); + } + } else if matches.subcommand_matches("browse").is_some() { + instproxy_client.browse(None).await.expect("browse failed"); + } else if matches.subcommand_matches("check_capabilities").is_some() { + instproxy_client + .check_capabilities_match(Vec::new(), None) + .await + .expect("check failed"); + } else { + eprintln!("Invalid usage, pass -h for help"); } }