mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 06:26:16 +01:00
Improve API
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1366,6 +1366,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
name = "minimal"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"idevice",
|
||||
"isideload",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
33
README.md
33
README.md
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
(),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user