Improve API

This commit is contained in:
nab138
2025-08-10 17:51:42 -04:00
parent 77894f0663
commit d08ae68e78
10 changed files with 158 additions and 129 deletions

1
Cargo.lock generated
View File

@@ -1366,6 +1366,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
name = "minimal"
version = "0.1.0"
dependencies = [
"idevice",
"isideload",
"tokio",
]

View File

@@ -19,8 +19,10 @@ Then, you can use it like so:
```rs
use std::{env, path::PathBuf, sync::Arc};
use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection};
use isideload::{
AnisetteConfiguration, AppleAccount, DefaultLogger, DeveloperSession, device::list_devices,
AnisetteConfiguration, AppleAccount, DeveloperSession, SideloadConfiguration,
sideload::sideload_app,
};
@@ -36,11 +38,23 @@ async fn main() {
.expect("Please provide the Apple ID to use for installation");
let apple_password = args.get(3).expect("Please provide the Apple ID password");
// You don't have to use the builtin list_devices method if you don't want to use usbmuxd
// You can use idevice to get the device info however you want
// This is just easier
let device = list_devices().await.unwrap().into_iter().next().unwrap();
println!("Target device: {}", device.name);
// You don't have to use usbmuxd, you can use any IdeviceProvider
let usbmuxd = UsbmuxdConnection::default().await;
if usbmuxd.is_err() {
panic!("Failed to connect to usbmuxd: {:?}", usbmuxd.err());
}
let mut usbmuxd = usbmuxd.unwrap();
let devs = usbmuxd.get_devices().await.unwrap();
if devs.is_empty() {
panic!("No devices found");
}
let provider = devs
.iter()
.next()
.unwrap()
.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo");
// Change the anisette url and such here
// Note that right now only remote anisette servers are supported
@@ -63,11 +77,10 @@ async fn main() {
let dev_session = DeveloperSession::new(Arc::new(account));
// This is where certificates, mobileprovision, and anisette data will be stored
let store_dir = std::env::current_dir().unwrap();
// You can change the machine name, store directory (for certs, anisette data, & provision files), and logger
let config = SideloadConfiguration::default().set_machine_name("isideload-demo".to_string());
// DefaultLogger just prints to the stdout/stderr, but you can provide your own implementation
sideload_app(DefaultLogger {}, &dev_session, &device, app_path, store_dir)
sideload_app(&provider, &dev_session, app_path, config)
.await
.unwrap()
}

View File

@@ -6,4 +6,5 @@ publish = false
[dependencies]
isideload = { path = "../../isideload", features = ["vendored-openssl", "vendored-botan"] }
idevice = { version = "0.1.37", features = ["usbmuxd"]}
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

View File

@@ -1,7 +1,8 @@
use std::{env, path::PathBuf, sync::Arc};
use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection};
use isideload::{
AnisetteConfiguration, AppleAccount, DefaultLogger, DeveloperSession, device::list_devices,
AnisetteConfiguration, AppleAccount, DeveloperSession, SideloadConfiguration,
sideload::sideload_app,
};
@@ -17,11 +18,23 @@ async fn main() {
.expect("Please provide the Apple ID to use for installation");
let apple_password = args.get(3).expect("Please provide the Apple ID password");
// You don't have to use the builtin list_devices method if you don't want to use usbmuxd
// You can use idevice to get the device info however you want
// This is just easier
let device = list_devices().await.unwrap().into_iter().next().unwrap();
println!("Target device: {}", device.name);
// You don't have to use usbmuxd, you can use any IdeviceProvider
let usbmuxd = UsbmuxdConnection::default().await;
if usbmuxd.is_err() {
panic!("Failed to connect to usbmuxd: {:?}", usbmuxd.err());
}
let mut usbmuxd = usbmuxd.unwrap();
let devs = usbmuxd.get_devices().await.unwrap();
if devs.is_empty() {
panic!("No devices found");
}
let provider = devs
.iter()
.next()
.unwrap()
.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo");
// Change the anisette url and such here
// Note that right now only remote anisette servers are supported
@@ -44,11 +57,10 @@ async fn main() {
let dev_session = DeveloperSession::new(Arc::new(account));
// This is where certificates, mobileprovision, and anisette data will be stored
let store_dir = std::env::current_dir().unwrap();
// You can change the machine name, store directory (for certs, anisette data, & provision files), and logger
let config = SideloadConfiguration::default().set_machine_name("isideload-demo".to_string());
// DefaultLogger just prints to the stdout/stderr, but you can provide your own implementation
sideload_app(DefaultLogger {}, &dev_session, &device, app_path, store_dir)
sideload_app(&provider, &dev_session, app_path, config)
.await
.unwrap()
}

View File

@@ -11,7 +11,7 @@ keywords = ["ios", "sideload"]
[features]
default = []
vendored-openssl = ["openssl/vendored"]
vendored-openssl = ["openssl/vendored", "zsign-rust/vendored-openssl"]
vendored-botan = ["icloud_auth/vendored-botan"]
[dependencies]
@@ -22,7 +22,7 @@ uuid = { version = "1.17.0", features = ["v4"] }
zip = "4.3"
hex = "0.4"
sha1 = "0.10"
idevice = { version = "0.1.37", features = ["afc", "usbmuxd", "installation_proxy"] }
idevice = { version = "0.1.37", features = ["afc", "installation_proxy"] }
openssl = "0.10"
futures = "0.3"
zsign-rust = "0.1"

View File

@@ -22,6 +22,7 @@ pub struct CertificateIdentity {
pub private_key: PKey<Private>,
pub key_file: PathBuf,
pub cert_file: PathBuf,
pub machine_name: String,
}
impl CertificateIdentity {
@@ -29,6 +30,7 @@ impl CertificateIdentity {
configuration_path: &Path,
dev_session: &DeveloperSession,
apple_id: String,
machine_name: String,
) -> Result<Self, Error> {
let mut hasher = Sha1::new();
hasher.update(apple_id.as_bytes());
@@ -64,6 +66,7 @@ impl CertificateIdentity {
private_key,
key_file,
cert_file,
machine_name,
};
if let Ok(cert) = cert_identity
@@ -103,7 +106,7 @@ impl CertificateIdentity {
for cert in certificates
.iter()
.filter(|c| c.machine_name == "YCode".to_string())
.filter(|c| c.machine_name == self.machine_name)
{
if let Ok(x509_cert) = X509::from_der(&cert.cert_content) {
if let Ok(cert_public_key) = x509_cert.public_key() {
@@ -166,6 +169,7 @@ impl CertificateIdentity {
DeveloperDeviceType::Ios,
team,
String::from_utf8_lossy(&csr_pem).to_string(),
self.machine_name.clone(),
)
.await
.map_err(|e| {

View File

@@ -297,6 +297,7 @@ impl DeveloperSession {
device_type: DeveloperDeviceType,
team: &DeveloperTeam,
csr_content: String,
machine_name: String,
) -> Result<String, Error> {
let url = dev_url(device_type, "submitDevelopmentCSR");
let mut body = Dictionary::new();
@@ -306,10 +307,7 @@ impl DeveloperSession {
"machineId".to_string(),
Value::String(uuid::Uuid::new_v4().to_string().to_uppercase()),
);
body.insert(
"machineName".to_string(),
Value::String("YCode".to_string()),
);
body.insert("machineName".to_string(), Value::String(machine_name));
let response = self.send_developer_request(&url, Some(body)).await?;
let cert_dict = response

View File

@@ -1,89 +1,19 @@
use idevice::{
IdeviceService,
afc::AfcClient,
installation_proxy::InstallationProxyClient,
lockdown::LockdownClient,
usbmuxd::{UsbmuxdAddr, UsbmuxdConnection},
IdeviceService, afc::AfcClient, installation_proxy::InstallationProxyClient,
provider::IdeviceProvider,
};
use serde::{Deserialize, Serialize};
use std::pin::Pin;
use std::{future::Future, path::Path};
use crate::Error;
#[derive(Deserialize, Serialize, Clone)]
pub struct DeviceInfo {
pub name: String,
pub id: u32,
pub uuid: String,
}
pub async fn list_devices() -> Result<Vec<DeviceInfo>, String> {
let usbmuxd = UsbmuxdConnection::default().await;
if usbmuxd.is_err() {
eprintln!("Failed to connect to usbmuxd: {:?}", usbmuxd.err());
return Err("Failed to connect to usbmuxd".to_string());
}
let mut usbmuxd = usbmuxd.unwrap();
let devs = usbmuxd.get_devices().await.unwrap();
if devs.is_empty() {
return Ok(vec![]);
}
let device_info_futures: Vec<_> = devs
.iter()
.map(|d| async move {
let provider = d.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "y-code");
let device_uid = d.device_id;
let mut lockdown_client = match LockdownClient::connect(&provider).await {
Ok(l) => l,
Err(e) => {
eprintln!("Unable to connect to lockdown: {e:?}");
return DeviceInfo {
name: String::from("Unknown Device"),
id: device_uid,
uuid: d.udid.clone(),
};
}
};
let device_name = lockdown_client
.get_value("DeviceName", None)
.await
.expect("Failed to get device name")
.as_string()
.expect("Failed to convert device name to string")
.to_string();
DeviceInfo {
name: device_name,
id: device_uid,
uuid: d.udid.clone(),
}
})
.collect();
Ok(futures::future::join_all(device_info_futures).await)
}
/// Installs an ***already signed*** app onto your device.
pub async fn install_app(
device: &DeviceInfo,
provider: &impl IdeviceProvider,
app_path: &Path,
callback: impl Fn(u64) -> (),
progress_callback: impl Fn(u64) -> (),
) -> Result<(), Error> {
let mut usbmuxd = UsbmuxdConnection::default()
.await
.map_err(|e| Error::IdeviceError(e))?;
let device = usbmuxd
.get_device(&device.uuid)
.await
.map_err(|e| Error::IdeviceError(e))?;
let provider = device.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "y-code");
let mut afc_client = AfcClient::connect(&provider)
let mut afc_client = AfcClient::connect(provider)
.await
.map_err(|e| Error::IdeviceError(e))?;
@@ -93,7 +23,7 @@ pub async fn install_app(
);
afc_upload_dir(&mut afc_client, app_path, &dir).await?;
let mut instproxy_client = InstallationProxyClient::connect(&provider)
let mut instproxy_client = InstallationProxyClient::connect(provider)
.await
.map_err(|e| Error::IdeviceError(e))?;
@@ -104,7 +34,7 @@ pub async fn install_app(
dir,
Some(plist::Value::Dictionary(options)),
async |(percentage, _)| {
callback(percentage);
progress_callback(percentage);
},
(),
)

View File

@@ -39,7 +39,7 @@ pub enum Error {
ZSignError(#[from] ZSignError),
}
pub trait SideloadLogger {
pub trait SideloadLogger: Send + Sync {
fn log(&self, message: &str);
fn error(&self, error: &Error);
}
@@ -55,3 +55,44 @@ impl SideloadLogger for DefaultLogger {
eprintln!("Error: {}", error);
}
}
/// Sideload configuration options.
pub struct SideloadConfiguration {
/// An arbitrary machine name to appear on the certificate (e.x. "YCode")
pub machine_name: String,
/// Logger for reporting progress and errors
pub logger: Box<dyn SideloadLogger>,
/// Directory used to store intermediate artifacts (profiles, certs, etc.). This directory will not be cleared at the end.
pub store_dir: std::path::PathBuf,
}
impl Default for SideloadConfiguration {
fn default() -> Self {
SideloadConfiguration::new()
}
}
impl SideloadConfiguration {
pub fn new() -> Self {
SideloadConfiguration {
machine_name: "isideload".to_string(),
logger: Box::new(DefaultLogger),
store_dir: std::env::current_dir().unwrap(),
}
}
pub fn set_machine_name(mut self, machine_name: String) -> Self {
self.machine_name = machine_name;
self
}
pub fn set_logger(mut self, logger: Box<dyn SideloadLogger>) -> Self {
self.logger = logger;
self
}
pub fn set_store_dir(mut self, store_dir: std::path::PathBuf) -> Self {
self.store_dir = store_dir;
self
}
}

View File

@@ -1,39 +1,64 @@
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
use idevice::IdeviceService;
use idevice::lockdown::LockdownClient;
use idevice::provider::IdeviceProvider;
use zsign_rust::ZSignOptions;
use crate::application::Application;
use crate::{DeveloperTeam, Error, SideloadLogger};
use crate::device::install_app;
use crate::{DeveloperTeam, Error, SideloadConfiguration, SideloadLogger};
use crate::{
certificate::CertificateIdentity,
developer_session::{DeveloperDeviceType, DeveloperSession},
device::{DeviceInfo, install_app},
};
use std::{io::Write, path::PathBuf};
fn error_and_return(logger: &impl SideloadLogger, error: Error) -> Result<(), Error> {
fn error_and_return(logger: &Box<dyn SideloadLogger>, error: Error) -> Result<(), Error> {
logger.error(&error);
Err(error)
}
/// Sideloads an `.ipa` or `.app` onto a device.
/// Signs and installs an `.ipa` or `.app` onto a device.
///
/// # Arguments
/// - `logger` — Reports progress and errors.
/// - `dev_session` Authenticated Apple developer session ([`crate::developer_session::DeveloperSession`]).
/// - `device` — Target device information ([`crate::device::DeviceInfo`]).
/// - `app_path` — Path to the `.ipa` file or `.app` bundle to sign and install
/// - `store_dir` — Directory used to store intermediate artifacts (profiles, certs, etc.). This directory will not be cleared at the end.
/// - `device_provider` - [`idevice::provider::IdeviceProvider`] for the device
/// - `dev_session` - Authenticated Apple developer session ([`crate::developer_session::DeveloperSession`]).
/// - `app_path` - Path to the `.ipa` file or `.app` bundle to sign and install
/// - `config` - Sideload configuration options ([`crate::SideloadConfiguration`])
pub async fn sideload_app(
logger: impl SideloadLogger,
device_provider: &impl IdeviceProvider,
dev_session: &DeveloperSession,
device: &DeviceInfo,
app_path: PathBuf,
store_dir: PathBuf,
config: SideloadConfiguration,
) -> Result<(), Error> {
if device.uuid.is_empty() {
return error_and_return(&logger, Error::Generic("No device selected".to_string()));
}
let logger = config.logger;
let mut lockdown_client = match LockdownClient::connect(device_provider).await {
Ok(l) => l,
Err(e) => {
return error_and_return(&logger, Error::IdeviceError(e));
}
};
let device_name = lockdown_client
.get_value("DeviceName", None)
.await
.map_err(|e| Error::IdeviceError(e))?
.as_string()
.ok_or(Error::Generic(
"Failed to convert DeviceName to string".to_string(),
))?
.to_string();
let device_uuid = lockdown_client
.get_value("UniqueDeviceID", None)
.await
.map_err(|e| Error::IdeviceError(e))?
.as_string()
.ok_or(Error::Generic(
"Failed to convert UniqueDeviceID to string".to_string(),
))?
.to_string();
let team = match dev_session.get_team().await {
Ok(t) => t,
@@ -44,12 +69,13 @@ pub async fn sideload_app(
logger.log("Successfully retrieved team");
ensure_device_registered(&logger, dev_session, &team, device).await?;
ensure_device_registered(&logger, dev_session, &team, &device_uuid, &device_name).await?;
let cert = match CertificateIdentity::new(
&store_dir,
&config.store_dir,
&dev_session,
dev_session.account.apple_id.clone(),
config.machine_name,
)
.await
{
@@ -301,7 +327,9 @@ pub async fn sideload_app(
}
};
let profile_path = store_dir.join(format!("{}.mobileprovision", main_app_id_str));
let profile_path = config
.store_dir
.join(format!("{}.mobileprovision", main_app_id_str));
if profile_path.exists() {
std::fs::remove_file(&profile_path).map_err(|e| Error::Filesystem(e))?;
@@ -337,7 +365,7 @@ pub async fn sideload_app(
logger.log("Installing app (Transfer)... 0%");
let res = install_app(&device, &app.bundle.bundle_dir, |percentage| {
let res = install_app(device_provider, &app.bundle.bundle_dir, |percentage| {
logger.log(&format!("Installing app... {}%", percentage));
})
.await;
@@ -349,10 +377,11 @@ pub async fn sideload_app(
}
pub async fn ensure_device_registered(
logger: &impl SideloadLogger,
logger: &Box<dyn SideloadLogger>,
dev_session: &DeveloperSession,
team: &DeveloperTeam,
device: &DeviceInfo,
uuid: &str,
name: &str,
) -> Result<(), Error> {
let devices = dev_session
.list_devices(DeveloperDeviceType::Ios, team)
@@ -361,11 +390,11 @@ pub async fn ensure_device_registered(
return error_and_return(logger, e);
}
let devices = devices.unwrap();
if !devices.iter().any(|d| d.device_number == device.uuid) {
if !devices.iter().any(|d| d.device_number == uuid) {
logger.log("Device not found in your account");
// TODO: Actually test!
dev_session
.add_device(DeveloperDeviceType::Ios, team, &device.name, &device.uuid)
.add_device(DeveloperDeviceType::Ios, team, name, uuid)
.await?;
logger.log("Successfully added device to your account");
}