mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 14:36:16 +01:00
actually functional
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
||||
|
||||
use crate::Error;
|
||||
use crate::bundle::Bundle;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
@@ -11,9 +12,11 @@ pub struct Application {
|
||||
}
|
||||
|
||||
impl Application {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
||||
if !path.exists() {
|
||||
panic!("Application path does not exist: {}", path.display());
|
||||
return Err(Error::InvalidBundle(
|
||||
"Application path does not exist".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut bundle_path = path.clone();
|
||||
@@ -23,21 +26,24 @@ impl Application {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let temp_path = temp_dir.join(path.file_name().unwrap());
|
||||
if temp_path.exists() {
|
||||
std::fs::remove_dir_all(&temp_path)
|
||||
.expect("Failed to remove existing temporary files");
|
||||
std::fs::remove_dir_all(&temp_path).map_err(|e| Error::Filesystem(e))?;
|
||||
}
|
||||
std::fs::create_dir_all(&temp_path).expect("Failed to create temporary directory");
|
||||
std::fs::create_dir_all(&temp_path).map_err(|e| Error::Filesystem(e))?;
|
||||
|
||||
let file = File::open(&path).expect("Failed to open application file");
|
||||
let mut archive = ZipArchive::new(file).expect("Failed to read application archive");
|
||||
archive
|
||||
.extract(&temp_path)
|
||||
.expect("Failed to extract application archive");
|
||||
let file = File::open(&path).map_err(|e| Error::Filesystem(e))?;
|
||||
let mut archive = ZipArchive::new(file).map_err(|e| {
|
||||
Error::Generic(format!("Failed to open application archive: {}", e))
|
||||
})?;
|
||||
archive.extract(&temp_path).map_err(|e| {
|
||||
Error::Generic(format!("Failed to extract application archive: {}", e))
|
||||
})?;
|
||||
|
||||
let payload_folder = temp_path.join("Payload");
|
||||
if payload_folder.exists() && payload_folder.is_dir() {
|
||||
let app_dirs: Vec<_> = std::fs::read_dir(&payload_folder)
|
||||
.expect("Failed to read Payload directory")
|
||||
.map_err(|e| {
|
||||
Error::Generic(format!("Failed to read Payload directory: {}", e))
|
||||
})?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
|
||||
.filter(|entry| entry.path().extension().map_or(false, |ext| ext == "app"))
|
||||
@@ -45,18 +51,24 @@ impl Application {
|
||||
if app_dirs.len() == 1 {
|
||||
bundle_path = app_dirs[0].path();
|
||||
} else if app_dirs.is_empty() {
|
||||
panic!("No .app directory found in Payload");
|
||||
return Err(Error::InvalidBundle(
|
||||
"No .app directory found in Payload".to_string(),
|
||||
));
|
||||
} else {
|
||||
panic!("Multiple .app directories found in Payload");
|
||||
return Err(Error::InvalidBundle(
|
||||
"Multiple .app directories found in Payload".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
panic!("No Payload directory found in the application archive");
|
||||
return Err(Error::InvalidBundle(
|
||||
"No Payload directory found in the application archive".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let bundle = Bundle::new(bundle_path).expect("Failed to create application bundle");
|
||||
let bundle = Bundle::new(bundle_path)?;
|
||||
|
||||
Application {
|
||||
Ok(Application {
|
||||
bundle, /*temp_path*/
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ use openssl::{
|
||||
x509::{X509, X509Name, X509ReqBuilder},
|
||||
};
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::{fs, path::PathBuf};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::Error;
|
||||
use crate::developer_session::{DeveloperDeviceType, DeveloperSession, DeveloperTeam};
|
||||
@@ -23,7 +26,7 @@ pub struct CertificateIdentity {
|
||||
|
||||
impl CertificateIdentity {
|
||||
pub async fn new(
|
||||
configuration_path: PathBuf,
|
||||
configuration_path: &Path,
|
||||
dev_session: &DeveloperSession,
|
||||
apple_id: String,
|
||||
) -> Result<Self, Error> {
|
||||
@@ -31,8 +34,7 @@ impl CertificateIdentity {
|
||||
hasher.update(apple_id.as_bytes());
|
||||
let hash_string = hex::encode(hasher.finalize()).to_lowercase();
|
||||
let key_path = configuration_path.join("keys").join(hash_string);
|
||||
fs::create_dir_all(&key_path)
|
||||
.map_err(|e| Error::Filesystem(format!("Failed to create key directory: {}", e)))?;
|
||||
fs::create_dir_all(&key_path).map_err(|e| Error::Filesystem(e))?;
|
||||
|
||||
let key_file = key_path.join("key.pem");
|
||||
let cert_file = key_path.join("cert.pem");
|
||||
@@ -53,8 +55,7 @@ impl CertificateIdentity {
|
||||
let pem_data = key
|
||||
.private_key_to_pem_pkcs8()
|
||||
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?;
|
||||
fs::write(&key_file, pem_data)
|
||||
.map_err(|e| Error::Filesystem(format!("Failed to save key file: {}", e)))?;
|
||||
fs::write(&key_file, pem_data).map_err(|e| Error::Filesystem(e))?;
|
||||
key
|
||||
};
|
||||
|
||||
@@ -74,9 +75,7 @@ impl CertificateIdentity {
|
||||
let cert_pem = cert.to_pem().map_err(|e| {
|
||||
Error::Certificate(format!("Failed to encode certificate to PEM: {}", e))
|
||||
})?;
|
||||
fs::write(&cert_identity.cert_file, cert_pem).map_err(|e| {
|
||||
Error::Filesystem(format!("Failed to save certificate file: {}", e))
|
||||
})?;
|
||||
fs::write(&cert_identity.cert_file, cert_pem).map_err(|e| Error::Filesystem(e))?;
|
||||
|
||||
return Ok(cert_identity);
|
||||
}
|
||||
@@ -199,19 +198,18 @@ impl CertificateIdentity {
|
||||
let cert_pem = certificate.to_pem().map_err(|e| {
|
||||
Error::Certificate(format!("Failed to encode certificate to PEM: {}", e))
|
||||
})?;
|
||||
fs::write(&self.cert_file, cert_pem)
|
||||
.map_err(|e| Error::Filesystem(format!("Failed to save certificate file: {}", e)))?;
|
||||
fs::write(&self.cert_file, cert_pem).map_err(|e| Error::Filesystem(e))?;
|
||||
|
||||
self.certificate = Some(certificate);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_certificate_file_path(&self) -> &PathBuf {
|
||||
pub fn get_certificate_file_path(&self) -> &Path {
|
||||
&self.cert_file
|
||||
}
|
||||
|
||||
pub fn get_private_key_file_path(&self) -> &PathBuf {
|
||||
pub fn get_private_key_file_path(&self) -> &Path {
|
||||
&self.key_file
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ use idevice::{
|
||||
usbmuxd::{UsbmuxdAddr, UsbmuxdConnection},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::{future::Future, path::Path};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct DeviceInfo {
|
||||
@@ -69,34 +70,32 @@ pub async fn list_devices() -> Result<Vec<DeviceInfo>, String> {
|
||||
|
||||
pub async fn install_app(
|
||||
device: &DeviceInfo,
|
||||
app_path: &PathBuf,
|
||||
app_path: &Path,
|
||||
callback: impl Fn(u64) -> (),
|
||||
) -> Result<(), String> {
|
||||
) -> Result<(), Error> {
|
||||
let mut usbmuxd = UsbmuxdConnection::default()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to usbmuxd: {:?}", e))?;
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
let device = usbmuxd
|
||||
.get_device(&device.uuid)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get device: {:?}", e))?;
|
||||
.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)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to AFC: {:?}", e))?;
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
|
||||
let dir = format!(
|
||||
"PublicStaging/{}",
|
||||
app_path.file_name().unwrap().to_string_lossy()
|
||||
);
|
||||
afc_upload_dir(&mut afc_client, app_path, &dir)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to upload directory: {:?}", e))?;
|
||||
afc_upload_dir(&mut afc_client, app_path, &dir).await?;
|
||||
|
||||
let mut instproxy_client = InstallationProxyClient::connect(&provider)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to installation proxy: {:?}", e))?;
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
|
||||
let mut options = plist::Dictionary::new();
|
||||
options.insert("PackageType".to_string(), "Developer".into());
|
||||
@@ -110,25 +109,24 @@ pub async fn install_app(
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to install app: {:?}", e))?;
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn afc_upload_dir<'a>(
|
||||
afc_client: &'a mut AfcClient,
|
||||
path: &'a PathBuf,
|
||||
path: &'a Path,
|
||||
afc_path: &'a str,
|
||||
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
|
||||
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let entries =
|
||||
std::fs::read_dir(path).map_err(|e| format!("Failed to read directory: {}", e))?;
|
||||
let entries = std::fs::read_dir(path).map_err(|e| Error::Filesystem(e))?;
|
||||
afc_client
|
||||
.mk_dir(afc_path)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||||
let entry = entry.map_err(|e| Error::Filesystem(e))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let new_afc_path = format!(
|
||||
@@ -148,13 +146,12 @@ fn afc_upload_dir<'a>(
|
||||
idevice::afc::opcode::AfcFopenMode::WrOnly,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
||||
let bytes =
|
||||
std::fs::read(&path).map_err(|e| format!("Failed to read file: {}", e))?;
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
let bytes = std::fs::read(&path).map_err(|e| Error::Filesystem(e))?;
|
||||
file_handle
|
||||
.write(&bytes)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to write file: {}", e))?;
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
23
src/lib.rs
23
src/lib.rs
@@ -5,14 +5,19 @@ pub mod developer_session;
|
||||
pub mod device;
|
||||
pub mod sideload;
|
||||
|
||||
use std::io::Error as IOError;
|
||||
|
||||
pub use developer_session::{
|
||||
AppId, ApplicationGroup, DeveloperDevice, DeveloperDeviceType, DeveloperSession, DeveloperTeam,
|
||||
DevelopmentCertificate, ListAppIdsResponse, ProvisioningProfile,
|
||||
};
|
||||
pub use icloud_auth::{AnisetteConfiguration, AppleAccount};
|
||||
|
||||
use idevice::IdeviceError;
|
||||
use thiserror::Error as ThisError;
|
||||
use zsign_rust::ZSignError;
|
||||
|
||||
#[derive(Debug, Clone, ThisError)]
|
||||
#[derive(Debug, ThisError)]
|
||||
pub enum Error {
|
||||
#[error("Authentication error {0}: {1}")]
|
||||
Auth(i64, String),
|
||||
@@ -26,23 +31,27 @@ pub enum Error {
|
||||
InvalidBundle(String),
|
||||
#[error("Certificate error: {0}")]
|
||||
Certificate(String),
|
||||
#[error("Failed to use files: {0}")]
|
||||
Filesystem(String),
|
||||
#[error(transparent)]
|
||||
Filesystem(#[from] IOError),
|
||||
#[error(transparent)]
|
||||
IdeviceError(#[from] IdeviceError),
|
||||
#[error(transparent)]
|
||||
ZSignError(#[from] ZSignError),
|
||||
}
|
||||
|
||||
pub trait SideloadLogger {
|
||||
async fn log(&self, message: &str);
|
||||
async fn error(&self, error: &Error);
|
||||
fn log(&self, message: &str);
|
||||
fn error(&self, error: &Error);
|
||||
}
|
||||
|
||||
pub struct DefaultLogger;
|
||||
|
||||
impl SideloadLogger for DefaultLogger {
|
||||
async fn log(&self, message: &str) {
|
||||
fn log(&self, message: &str) {
|
||||
println!("{message}");
|
||||
}
|
||||
|
||||
async fn error(&self, error: &Error) {
|
||||
fn error(&self, error: &Error) {
|
||||
eprintln!("Error: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use zsign_rust::ZSignOptions;
|
||||
|
||||
use crate::application::Application;
|
||||
use crate::{Error, SideloadLogger};
|
||||
use crate::{DeveloperTeam, Error, SideloadLogger};
|
||||
use crate::{
|
||||
certificate::CertificateIdentity,
|
||||
developer_session::{DeveloperDeviceType, DeveloperSession},
|
||||
@@ -16,11 +16,20 @@ fn error_and_return(logger: &impl SideloadLogger, error: Error) -> Result<(), Er
|
||||
Err(error)
|
||||
}
|
||||
|
||||
/// Sideloads 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.
|
||||
pub async fn sideload_app(
|
||||
logger: impl SideloadLogger,
|
||||
dev_session: &DeveloperSession,
|
||||
device: &DeviceInfo,
|
||||
app_path: PathBuf,
|
||||
store_dir: PathBuf,
|
||||
) -> Result<(), Error> {
|
||||
if device.uuid.is_empty() {
|
||||
return error_and_return(&logger, Error::Generic("No device selected".to_string()));
|
||||
@@ -35,10 +44,15 @@ pub async fn sideload_app(
|
||||
|
||||
logger.log("Successfully retrieved team");
|
||||
|
||||
ensure_device_registered(&dev_session, window, &team, &device).await?;
|
||||
ensure_device_registered(&logger, dev_session, &team, device).await?;
|
||||
|
||||
let config_dir = handle.path().app_config_dir().map_err(|e| e.to_string())?;
|
||||
let cert = match CertificateIdentity::new(config_dir, &dev_session, get_apple_email()).await {
|
||||
let cert = match CertificateIdentity::new(
|
||||
&store_dir,
|
||||
&dev_session,
|
||||
dev_session.account.apple_id.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, e);
|
||||
@@ -57,7 +71,7 @@ pub async fn sideload_app(
|
||||
}
|
||||
};
|
||||
|
||||
let mut app = Application::new(app_path);
|
||||
let mut app = Application::new(app_path)?;
|
||||
let is_sidestore = app.bundle.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore";
|
||||
let main_app_bundle_id = match app.bundle.bundle_identifier() {
|
||||
Some(id) => id.to_string(),
|
||||
@@ -287,20 +301,15 @@ pub async fn sideload_app(
|
||||
}
|
||||
};
|
||||
|
||||
let profile_path = handle
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join(format!("{}.mobileprovision", main_app_id_str));
|
||||
let profile_path = store_dir.join(format!("{}.mobileprovision", main_app_id_str));
|
||||
|
||||
if profile_path.exists() {
|
||||
std::fs::remove_file(&profile_path).map_err(|e| e.to_string())?;
|
||||
std::fs::remove_file(&profile_path).map_err(|e| Error::Filesystem(e))?;
|
||||
}
|
||||
|
||||
let mut file =
|
||||
std::fs::File::create(&profile_path).map_err(|e| Error::Filesystem(e.to_string()))?;
|
||||
let mut file = std::fs::File::create(&profile_path).map_err(|e| Error::Filesystem(e))?;
|
||||
file.write_all(&provisioning_profile.encoded_profile)
|
||||
.map_err(|e| Error::Filesystem(e.to_string()))?;
|
||||
.map_err(|e| Error::Filesystem(e))?;
|
||||
|
||||
// Without this, zsign complains it can't find the provision file
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -310,7 +319,7 @@ pub async fn sideload_app(
|
||||
}
|
||||
|
||||
// TODO: Recursive for sub-bundles?
|
||||
app.bundle.write_info().map_err(|e| e.to_string())?;
|
||||
app.bundle.write_info()?;
|
||||
|
||||
match ZSignOptions::new(app.bundle.bundle_dir.to_str().unwrap())
|
||||
.with_cert_file(cert.get_certificate_file_path().to_str().unwrap())
|
||||
@@ -320,7 +329,7 @@ pub async fn sideload_app(
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, &format!("Failed to sign app: {:?}", e));
|
||||
return error_and_return(&logger, Error::ZSignError(e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -329,12 +338,37 @@ pub async fn sideload_app(
|
||||
logger.log("Installing app (Transfer)... 0%");
|
||||
|
||||
let res = install_app(&device, &app.bundle.bundle_dir, |percentage| {
|
||||
logger.log(format!("Installing app... {}%", percentage));
|
||||
logger.log(&format!("Installing app... {}%", percentage));
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
return error_and_return(&logger, &format!("Failed to install app: {:?}", e));
|
||||
return error_and_return(&logger, e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ensure_device_registered(
|
||||
logger: &impl SideloadLogger,
|
||||
dev_session: &DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
device: &DeviceInfo,
|
||||
) -> Result<(), Error> {
|
||||
let devices = dev_session
|
||||
.list_devices(DeveloperDeviceType::Ios, team)
|
||||
.await;
|
||||
if let Err(e) = devices {
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
let devices = devices.unwrap();
|
||||
if !devices.iter().any(|d| d.device_number == device.uuid) {
|
||||
logger.log("Device not found in your account");
|
||||
// TODO: Actually test!
|
||||
dev_session
|
||||
.add_device(DeveloperDeviceType::Ios, team, &device.name, &device.uuid)
|
||||
.await?;
|
||||
logger.log("Successfully added device to your account");
|
||||
}
|
||||
logger.log("Device is a development device");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user