mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 14:36:16 +01:00
first commit
This commit is contained in:
62
src/application.rs
Normal file
62
src/application.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
||||
|
||||
use crate::bundle::Bundle;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use zip::ZipArchive;
|
||||
|
||||
pub struct Application {
|
||||
pub bundle: Bundle,
|
||||
//pub temp_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
if !path.exists() {
|
||||
panic!("Application path does not exist: {}", path.display());
|
||||
}
|
||||
|
||||
let mut bundle_path = path.clone();
|
||||
//let mut temp_path = PathBuf::new();
|
||||
|
||||
if path.is_file() {
|
||||
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::create_dir_all(&temp_path).expect("Failed to create temporary directory");
|
||||
|
||||
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 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")
|
||||
.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"))
|
||||
.collect();
|
||||
if app_dirs.len() == 1 {
|
||||
bundle_path = app_dirs[0].path();
|
||||
} else if app_dirs.is_empty() {
|
||||
panic!("No .app directory found in Payload");
|
||||
} else {
|
||||
panic!("Multiple .app directories found in Payload");
|
||||
}
|
||||
} else {
|
||||
panic!("No Payload directory found in the application archive");
|
||||
}
|
||||
}
|
||||
let bundle = Bundle::new(bundle_path).expect("Failed to create application bundle");
|
||||
|
||||
Application {
|
||||
bundle, /*temp_path*/
|
||||
}
|
||||
}
|
||||
}
|
||||
180
src/bundle.rs
Normal file
180
src/bundle.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
||||
|
||||
use crate::Error;
|
||||
use plist::{Dictionary, Value};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub struct Bundle {
|
||||
pub app_info: Dictionary,
|
||||
pub bundle_dir: PathBuf,
|
||||
|
||||
app_extensions: Vec<Bundle>,
|
||||
_frameworks: Vec<Bundle>,
|
||||
_libraries: Vec<String>,
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
pub fn new(bundle_dir: PathBuf) -> Result<Self, Error> {
|
||||
let mut bundle_path = bundle_dir;
|
||||
// Remove trailing slash/backslash
|
||||
if let Some(path_str) = bundle_path.to_str() {
|
||||
if path_str.ends_with('/') || path_str.ends_with('\\') {
|
||||
bundle_path = PathBuf::from(&path_str[..path_str.len() - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
let info_plist_path = bundle_path.join("Info.plist");
|
||||
assert_bundle(
|
||||
info_plist_path.exists(),
|
||||
&format!("No Info.plist here: {}", info_plist_path.display()),
|
||||
)?;
|
||||
|
||||
let plist_data = fs::read(&info_plist_path)
|
||||
.map_err(|e| Error::InvalidBundle(format!("Failed to read Info.plist: {}", e)))?;
|
||||
|
||||
let app_info = plist::from_bytes(&plist_data)
|
||||
.map_err(|e| Error::InvalidBundle(format!("Failed to parse Info.plist: {}", e)))?;
|
||||
|
||||
// Load app extensions from PlugIns directory
|
||||
let plug_ins_dir = bundle_path.join("PlugIns");
|
||||
let app_extensions = if plug_ins_dir.exists() {
|
||||
fs::read_dir(&plug_ins_dir)
|
||||
.map_err(|e| {
|
||||
Error::InvalidBundle(format!("Failed to read PlugIns directory: {}", e))
|
||||
})?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
|
||||
&& entry.path().join("Info.plist").exists()
|
||||
})
|
||||
.filter_map(|entry| Bundle::new(entry.path()).ok())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Load frameworks from Frameworks directory
|
||||
let frameworks_dir = bundle_path.join("Frameworks");
|
||||
let frameworks = if frameworks_dir.exists() {
|
||||
fs::read_dir(&frameworks_dir)
|
||||
.map_err(|e| {
|
||||
Error::InvalidBundle(format!("Failed to read Frameworks directory: {}", e))
|
||||
})?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
|
||||
&& entry.path().join("Info.plist").exists()
|
||||
})
|
||||
.filter_map(|entry| Bundle::new(entry.path()).ok())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Find all .dylib files in the bundle directory (recursive)
|
||||
let libraries = find_dylibs(&bundle_path, &bundle_path)?;
|
||||
|
||||
Ok(Bundle {
|
||||
app_info,
|
||||
bundle_dir: bundle_path,
|
||||
app_extensions,
|
||||
_frameworks: frameworks,
|
||||
_libraries: libraries,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_bundle_identifier(&mut self, id: &str) {
|
||||
self.app_info.insert(
|
||||
"CFBundleIdentifier".to_string(),
|
||||
Value::String(id.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn bundle_identifier(&self) -> Option<&str> {
|
||||
self.app_info
|
||||
.get("CFBundleIdentifier")
|
||||
.and_then(|v| v.as_string())
|
||||
}
|
||||
|
||||
pub fn bundle_name(&self) -> Option<&str> {
|
||||
self.app_info
|
||||
.get("CFBundleName")
|
||||
.and_then(|v| v.as_string())
|
||||
}
|
||||
|
||||
pub fn app_extensions(&self) -> &[Bundle] {
|
||||
&self.app_extensions
|
||||
}
|
||||
|
||||
pub fn app_extensions_mut(&mut self) -> &mut [Bundle] {
|
||||
&mut self.app_extensions
|
||||
}
|
||||
|
||||
pub fn write_info(&self) -> Result<(), Error> {
|
||||
let info_plist_path = self.bundle_dir.join("Info.plist");
|
||||
let result = plist::to_file_binary(&info_plist_path, &self.app_info);
|
||||
|
||||
if result.is_err() {
|
||||
return Err(Error::InvalidBundle(format!(
|
||||
"Failed to write Info.plist: {}",
|
||||
result.unwrap_err()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_bundle(condition: bool, msg: &str) -> Result<(), Error> {
|
||||
if !condition {
|
||||
Err(Error::InvalidBundle(msg.to_string()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn find_dylibs(dir: &Path, bundle_root: &Path) -> Result<Vec<String>, Error> {
|
||||
let mut libraries = Vec::new();
|
||||
|
||||
fn collect_dylibs(
|
||||
dir: &Path,
|
||||
bundle_root: &Path,
|
||||
libraries: &mut Vec<String>,
|
||||
) -> Result<(), Error> {
|
||||
let entries = fs::read_dir(dir).map_err(|e| {
|
||||
Error::InvalidBundle(format!("Failed to read directory {}: {}", dir.display(), e))
|
||||
})?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| {
|
||||
Error::InvalidBundle(format!("Failed to read directory entry: {}", e))
|
||||
})?;
|
||||
|
||||
let path = entry.path();
|
||||
let file_type = entry
|
||||
.file_type()
|
||||
.map_err(|e| Error::InvalidBundle(format!("Failed to get file type: {}", e)))?;
|
||||
|
||||
if file_type.is_file() {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name.ends_with(".dylib") {
|
||||
// Get relative path from bundle root
|
||||
if let Ok(relative_path) = path.strip_prefix(bundle_root) {
|
||||
if let Some(relative_str) = relative_path.to_str() {
|
||||
libraries.push(relative_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if file_type.is_dir() {
|
||||
collect_dylibs(&path, bundle_root, libraries)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
collect_dylibs(dir, bundle_root, &mut libraries)?;
|
||||
Ok(libraries)
|
||||
}
|
||||
214
src/certificate.rs
Normal file
214
src/certificate.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
||||
|
||||
use hex;
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
rsa::Rsa,
|
||||
x509::{X509, X509Name, X509ReqBuilder},
|
||||
};
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::Error;
|
||||
use crate::developer_session::{DeveloperDeviceType, DeveloperSession, DeveloperTeam};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CertificateIdentity {
|
||||
pub certificate: Option<X509>,
|
||||
pub private_key: PKey<Private>,
|
||||
pub key_file: PathBuf,
|
||||
pub cert_file: PathBuf,
|
||||
}
|
||||
|
||||
impl CertificateIdentity {
|
||||
pub async fn new(
|
||||
configuration_path: PathBuf,
|
||||
dev_session: &DeveloperSession,
|
||||
apple_id: String,
|
||||
) -> Result<Self, Error> {
|
||||
let mut hasher = Sha1::new();
|
||||
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| format!("Failed to create key directory: {}", e))?;
|
||||
|
||||
let key_file = key_path.join("key.pem");
|
||||
let cert_file = key_path.join("cert.pem");
|
||||
let teams = dev_session
|
||||
.list_teams()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list teams: {:?}", e))?;
|
||||
let team = teams.first().ok_or("No teams found")?;
|
||||
let private_key = if key_file.exists() {
|
||||
let key_data = fs::read_to_string(&key_file)
|
||||
.map_err(|e| format!("Failed to read key file: {}", e))?;
|
||||
PKey::private_key_from_pem(key_data.as_bytes())
|
||||
.map_err(|e| format!("Failed to load private key: {}", e))?
|
||||
} else {
|
||||
let rsa =
|
||||
Rsa::generate(2048).map_err(|e| format!("Failed to generate RSA key: {}", e))?;
|
||||
let key =
|
||||
PKey::from_rsa(rsa).map_err(|e| format!("Failed to create private key: {}", e))?;
|
||||
let pem_data = key
|
||||
.private_key_to_pem_pkcs8()
|
||||
.map_err(|e| format!("Failed to encode private key: {}", e))?;
|
||||
fs::write(&key_file, pem_data)
|
||||
.map_err(|e| format!("Failed to save key file: {}", e))?;
|
||||
key
|
||||
};
|
||||
|
||||
let mut cert_identity = CertificateIdentity {
|
||||
certificate: None,
|
||||
private_key,
|
||||
key_file,
|
||||
cert_file,
|
||||
};
|
||||
|
||||
if let Ok(cert) = cert_identity
|
||||
.find_matching_certificate(dev_session, team)
|
||||
.await
|
||||
{
|
||||
cert_identity.certificate = Some(cert.clone());
|
||||
|
||||
let cert_pem = cert
|
||||
.to_pem()
|
||||
.map_err(|e| format!("Failed to encode certificate to PEM: {}", e))?;
|
||||
fs::write(&cert_identity.cert_file, cert_pem)
|
||||
.map_err(|e| format!("Failed to save certificate file: {}", e))?;
|
||||
|
||||
return Ok(cert_identity);
|
||||
}
|
||||
|
||||
cert_identity
|
||||
.request_new_certificate(dev_session, team)
|
||||
.await?;
|
||||
Ok(cert_identity)
|
||||
}
|
||||
|
||||
async fn find_matching_certificate(
|
||||
&self,
|
||||
dev_session: &DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<X509, Error> {
|
||||
let certificates = dev_session
|
||||
.list_all_development_certs(DeveloperDeviceType::Ios, team)
|
||||
.await
|
||||
.map_err(|e| Error::Certificate(format!("Failed to list certificates: {:?}", e)))?;
|
||||
|
||||
let our_public_key = self
|
||||
.private_key
|
||||
.public_key_to_der()
|
||||
.map_err(|e| Error::Certificate(format!("Failed to get public key: {}", e)))?;
|
||||
|
||||
for cert in certificates
|
||||
.iter()
|
||||
.filter(|c| c.machine_name == "YCode".to_string())
|
||||
{
|
||||
if let Ok(x509_cert) = X509::from_der(&cert.cert_content) {
|
||||
if let Ok(cert_public_key) = x509_cert.public_key() {
|
||||
if let Ok(cert_public_key_der) = cert_public_key.public_key_to_der() {
|
||||
if cert_public_key_der == our_public_key {
|
||||
return Ok(x509_cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Error::Certificate("No matching certificate found".to_string())
|
||||
}
|
||||
|
||||
async fn request_new_certificate(
|
||||
&mut self,
|
||||
dev_session: &DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<(), Error> {
|
||||
let mut req_builder = X509ReqBuilder::new()
|
||||
.map_err(|e| format!("Failed to create request builder: {}", e))?;
|
||||
let mut name_builder =
|
||||
X509Name::builder().map_err(|e| format!("Failed to create name builder: {}", e))?;
|
||||
|
||||
name_builder
|
||||
.append_entry_by_text("C", "US")
|
||||
.map_err(|e| format!("Failed to set country: {}", e))?;
|
||||
name_builder
|
||||
.append_entry_by_text("ST", "STATE")
|
||||
.map_err(|e| format!("Failed to set state: {}", e))?;
|
||||
name_builder
|
||||
.append_entry_by_text("L", "LOCAL")
|
||||
.map_err(|e| format!("Failed to set locality: {}", e))?;
|
||||
name_builder
|
||||
.append_entry_by_text("O", "ORGNIZATION")
|
||||
.map_err(|e| format!("Failed to set organization: {}", e))?;
|
||||
name_builder
|
||||
.append_entry_by_text("CN", "CN")
|
||||
.map_err(|e| format!("Failed to set common name: {}", e))?;
|
||||
|
||||
req_builder
|
||||
.set_subject_name(&name_builder.build())
|
||||
.map_err(|e| format!("Failed to set subject name: {}", e))?;
|
||||
req_builder
|
||||
.set_pubkey(&self.private_key)
|
||||
.map_err(|e| format!("Failed to set public key: {}", e))?;
|
||||
req_builder
|
||||
.sign(&self.private_key, MessageDigest::sha256())
|
||||
.map_err(|e| format!("Failed to sign request: {}", e))?;
|
||||
|
||||
let csr_pem = req_builder
|
||||
.build()
|
||||
.to_pem()
|
||||
.map_err(|e| format!("Failed to encode CSR: {}", e))?;
|
||||
|
||||
let certificate_id = dev_session
|
||||
.submit_development_csr(
|
||||
DeveloperDeviceType::Ios,
|
||||
team,
|
||||
String::from_utf8_lossy(&csr_pem).to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let is_7460 = match &e {
|
||||
Error::DeveloperSession(code, _) => *code == 7460,
|
||||
_ => false,
|
||||
};
|
||||
if is_7460 {
|
||||
Error::Certificate("You have too many certificates!".to_string())
|
||||
} else {
|
||||
Error::Certificate(format!("Failed to submit CSR: {:?}", e))
|
||||
}
|
||||
})?;
|
||||
|
||||
let certificates = dev_session
|
||||
.list_all_development_certs(DeveloperDeviceType::Ios, team)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list certificates: {:?}", e))?;
|
||||
|
||||
let apple_cert = certificates
|
||||
.iter()
|
||||
.find(|cert| cert.certificate_id == certificate_id)
|
||||
.ok_or("Certificate not found after submission")?;
|
||||
|
||||
let certificate = X509::from_der(&apple_cert.cert_content)
|
||||
.map_err(|e| format!("Failed to parse certificate: {}", e))?;
|
||||
|
||||
// Write certificate to disk
|
||||
let cert_pem = certificate
|
||||
.to_pem()
|
||||
.map_err(|e| format!("Failed to encode certificate to PEM: {}", e))?;
|
||||
fs::write(&self.cert_file, cert_pem)
|
||||
.map_err(|e| format!("Failed to save certificate file: {}", e))?;
|
||||
|
||||
self.certificate = Some(certificate);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_certificate_file_path(&self) -> &PathBuf {
|
||||
&self.cert_file
|
||||
}
|
||||
|
||||
pub fn get_private_key_file_path(&self) -> &PathBuf {
|
||||
&self.key_file
|
||||
}
|
||||
}
|
||||
698
src/developer_session.rs
Normal file
698
src/developer_session.rs
Normal file
@@ -0,0 +1,698 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference for the apple private endpoints
|
||||
|
||||
use crate::Error;
|
||||
use icloud_auth::{AppleAccount, Error as ICloudError};
|
||||
use plist::{Date, Dictionary, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct DeveloperSession {
|
||||
pub account: Arc<AppleAccount>,
|
||||
team: Option<DeveloperTeam>,
|
||||
}
|
||||
|
||||
impl DeveloperSession {
|
||||
pub fn new(account: Arc<AppleAccount>) -> Self {
|
||||
DeveloperSession {
|
||||
account,
|
||||
team: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_developer_request(
|
||||
&self,
|
||||
url: &str,
|
||||
body: Option<Dictionary>,
|
||||
) -> Result<Dictionary, Error> {
|
||||
let mut request = Dictionary::new();
|
||||
request.insert(
|
||||
"clientId".to_string(),
|
||||
Value::String("XABBG36SBA".to_string()),
|
||||
);
|
||||
request.insert(
|
||||
"protocolVersion".to_string(),
|
||||
Value::String("QH65B2".to_string()),
|
||||
);
|
||||
request.insert(
|
||||
"requestId".to_string(),
|
||||
Value::String(Uuid::new_v4().to_string().to_uppercase()),
|
||||
);
|
||||
request.insert(
|
||||
"userLocale".to_string(),
|
||||
Value::Array(vec![Value::String("en_US".to_string())]),
|
||||
);
|
||||
if let Some(body) = body {
|
||||
for (key, value) in body {
|
||||
request.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let response = self
|
||||
.account
|
||||
.send_request(url, Some(request))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if let ICloudError::AuthSrpWithMessage(code, message) = e {
|
||||
Error::DeveloperSession(code, format!("Developer request failed: {}", message))
|
||||
} else {
|
||||
Error::Generic
|
||||
}
|
||||
})?;
|
||||
|
||||
let status_code = response
|
||||
.get("resultCode")
|
||||
.and_then(|v| v.as_unsigned_integer())
|
||||
.unwrap_or(0);
|
||||
if status_code != 0 {
|
||||
let description = response
|
||||
.get("userString")
|
||||
.and_then(|v| v.as_string())
|
||||
.or_else(|| response.get("resultString").and_then(|v| v.as_string()))
|
||||
.unwrap_or("(null)");
|
||||
return Err(Error::DeveloperSession(
|
||||
status_code as i64,
|
||||
description.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn list_teams(&self) -> Result<Vec<DeveloperTeam>, Error> {
|
||||
let url = "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA";
|
||||
let response = self.send_developer_request(url, None).await?;
|
||||
|
||||
let teams = response
|
||||
.get("teams")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for team in teams {
|
||||
let dict = team.as_dictionary().ok_or(Error::Parse)?;
|
||||
let name = dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let team_id = dict
|
||||
.get("teamId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
result.push(DeveloperTeam {
|
||||
_name: name,
|
||||
team_id,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_team(&self) -> Result<DeveloperTeam, Error> {
|
||||
if let Some(team) = &self.team {
|
||||
return Ok(team.clone());
|
||||
}
|
||||
let teams = self.list_teams().await?;
|
||||
if teams.is_empty() {
|
||||
return Err(Error::DeveloperSession(
|
||||
-1,
|
||||
"No developer teams found".to_string(),
|
||||
));
|
||||
}
|
||||
// TODO: Handle multiple teams
|
||||
Ok(teams[0].clone())
|
||||
}
|
||||
|
||||
pub fn set_team(&mut self, team: DeveloperTeam) {
|
||||
self.team = Some(team);
|
||||
}
|
||||
|
||||
pub async fn list_devices(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<Vec<DeveloperDevice>, Error> {
|
||||
let url = dev_url(device_type, "listDevices");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let devices = response
|
||||
.get("devices")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for device in devices {
|
||||
let dict = device.as_dictionary().ok_or(Error::Parse)?;
|
||||
let device_id = dict
|
||||
.get("deviceId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let name = dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let device_number = dict
|
||||
.get("deviceNumber")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
result.push(DeveloperDevice {
|
||||
_device_id: device_id,
|
||||
_name: name,
|
||||
device_number,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn add_device(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
device_name: &str,
|
||||
udid: &str,
|
||||
) -> Result<DeveloperDevice, Error> {
|
||||
let url = dev_url(device_type, "addDevice");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert("name".to_string(), Value::String(device_name.to_string()));
|
||||
body.insert("deviceNumber".to_string(), Value::String(udid.to_string()));
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let device_dict = response
|
||||
.get("device")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let device_id = device_dict
|
||||
.get("deviceId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let name = device_dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let device_number = device_dict
|
||||
.get("deviceNumber")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
|
||||
Ok(DeveloperDevice {
|
||||
_device_id: device_id,
|
||||
_name: name,
|
||||
device_number,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list_all_development_certs(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<Vec<DevelopmentCertificate>, Error> {
|
||||
let url = dev_url(device_type, "listAllDevelopmentCerts");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let certs = response
|
||||
.get("certificates")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for cert in certs {
|
||||
let dict = cert.as_dictionary().ok_or(Error::Parse)?;
|
||||
let name = dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let certificate_id = dict
|
||||
.get("certificateId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let serial_number = dict
|
||||
.get("serialNumber")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let machine_name = dict
|
||||
.get("machineName")
|
||||
.and_then(|v| v.as_string())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let cert_content = dict
|
||||
.get("certContent")
|
||||
.and_then(|v| v.as_data())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_vec();
|
||||
|
||||
result.push(DevelopmentCertificate {
|
||||
name: name,
|
||||
certificate_id,
|
||||
serial_number: serial_number,
|
||||
machine_name: machine_name,
|
||||
cert_content,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn revoke_development_cert(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
serial_number: &str,
|
||||
) -> Result<(), Error> {
|
||||
let url = dev_url(device_type, "revokeDevelopmentCert");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert(
|
||||
"serialNumber".to_string(),
|
||||
Value::String(serial_number.to_string()),
|
||||
);
|
||||
|
||||
self.send_developer_request(&url, Some(body)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn submit_development_csr(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
csr_content: String,
|
||||
) -> Result<String, Error> {
|
||||
let url = dev_url(device_type, "submitDevelopmentCSR");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert("csrContent".to_string(), Value::String(csr_content));
|
||||
body.insert(
|
||||
"machineId".to_string(),
|
||||
Value::String(uuid::Uuid::new_v4().to_string().to_uppercase()),
|
||||
);
|
||||
body.insert(
|
||||
"machineName".to_string(),
|
||||
Value::String("YCode".to_string()),
|
||||
);
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
let cert_dict = response
|
||||
.get("certRequest")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
let id = cert_dict
|
||||
.get("certRequestId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub async fn list_app_ids(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<ListAppIdsResponse, Error> {
|
||||
let url = dev_url(device_type, "listAppIds");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let app_ids = response
|
||||
.get("appIds")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for app_id in app_ids {
|
||||
let dict = app_id.as_dictionary().ok_or(Error::Parse)?;
|
||||
let name = dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let app_id_id = dict
|
||||
.get("appIdId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let identifier = dict
|
||||
.get("identifier")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let features = dict
|
||||
.get("features")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
let expiration_date = if dict.contains_key("expirationDate") {
|
||||
Some(
|
||||
dict.get("expirationDate")
|
||||
.and_then(|v| v.as_date())
|
||||
.ok_or(Error::Parse)?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
result.push(AppId {
|
||||
name,
|
||||
app_id_id,
|
||||
identifier,
|
||||
features: features.clone(),
|
||||
expiration_date,
|
||||
});
|
||||
}
|
||||
|
||||
let max_quantity = response
|
||||
.get("maxQuantity")
|
||||
.and_then(|v| v.as_unsigned_integer())
|
||||
.ok_or(Error::Parse)?;
|
||||
let available_quantity = response
|
||||
.get("availableQuantity")
|
||||
.and_then(|v| v.as_unsigned_integer())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
Ok(ListAppIdsResponse {
|
||||
app_ids: result,
|
||||
max_quantity,
|
||||
available_quantity,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn add_app_id(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
name: &str,
|
||||
identifier: &str,
|
||||
) -> Result<(), Error> {
|
||||
let url = dev_url(device_type, "addAppId");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert("name".to_string(), Value::String(name.to_string()));
|
||||
body.insert(
|
||||
"identifier".to_string(),
|
||||
Value::String(identifier.to_string()),
|
||||
);
|
||||
|
||||
self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_app_id(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
app_id: &AppId,
|
||||
features: &Dictionary,
|
||||
) -> Result<Dictionary, Error> {
|
||||
let url = dev_url(device_type, "updateAppId");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert(
|
||||
"appIdId".to_string(),
|
||||
Value::String(app_id.app_id_id.clone()),
|
||||
);
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
|
||||
for (key, value) in features {
|
||||
body.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
let cert_dict = response
|
||||
.get("appId")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
let feats = cert_dict
|
||||
.get("features")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
Ok(feats.clone())
|
||||
}
|
||||
|
||||
pub async fn delete_app_id(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
app_id_id: String,
|
||||
) -> Result<(), Error> {
|
||||
let url = dev_url(device_type, "deleteAppId");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert("appIdId".to_string(), Value::String(app_id_id.clone()));
|
||||
|
||||
self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_application_groups(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<Vec<ApplicationGroup>, Error> {
|
||||
let url = dev_url(device_type, "listApplicationGroups");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let app_groups = response
|
||||
.get("applicationGroupList")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for app_group in app_groups {
|
||||
let dict = app_group.as_dictionary().ok_or(Error::Parse)?;
|
||||
let application_group = dict
|
||||
.get("applicationGroup")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let name = dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let identifier = dict
|
||||
.get("identifier")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
|
||||
result.push(ApplicationGroup {
|
||||
application_group,
|
||||
_name: name,
|
||||
identifier,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn add_application_group(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
group_identifier: &str,
|
||||
name: &str,
|
||||
) -> Result<ApplicationGroup, Error> {
|
||||
let url = dev_url(device_type, "addApplicationGroup");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert("name".to_string(), Value::String(name.to_string()));
|
||||
body.insert(
|
||||
"identifier".to_string(),
|
||||
Value::String(group_identifier.to_string()),
|
||||
);
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
let app_group_dict = response
|
||||
.get("applicationGroup")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
let application_group = app_group_dict
|
||||
.get("applicationGroup")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let name = app_group_dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let identifier = app_group_dict
|
||||
.get("identifier")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
|
||||
Ok(ApplicationGroup {
|
||||
application_group,
|
||||
_name: name,
|
||||
identifier,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn assign_application_group_to_app_id(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
app_id: &AppId,
|
||||
app_group: &ApplicationGroup,
|
||||
) -> Result<(), Error> {
|
||||
let url = dev_url(device_type, "assignApplicationGroupToAppId");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert(
|
||||
"appIdId".to_string(),
|
||||
Value::String(app_id.app_id_id.clone()),
|
||||
);
|
||||
body.insert(
|
||||
"applicationGroups".to_string(),
|
||||
Value::String(app_group.application_group.clone()),
|
||||
);
|
||||
|
||||
self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_team_provisioning_profile(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
app_id: &AppId,
|
||||
) -> Result<ProvisioningProfile, Error> {
|
||||
let url = dev_url(device_type, "downloadTeamProvisioningProfile");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert(
|
||||
"appIdId".to_string(),
|
||||
Value::String(app_id.app_id_id.clone()),
|
||||
);
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let profile = response
|
||||
.get("provisioningProfile")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
let name = profile
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let provisioning_profile_id = profile
|
||||
.get("provisioningProfileId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let encoded_profile = profile
|
||||
.get("encodedProfile")
|
||||
.and_then(|v| v.as_data())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_vec();
|
||||
|
||||
Ok(ProvisioningProfile {
|
||||
_name: name,
|
||||
_provisioning_profile_id: provisioning_profile_id,
|
||||
encoded_profile,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DeveloperDeviceType {
|
||||
Any,
|
||||
Ios,
|
||||
Tvos,
|
||||
Watchos,
|
||||
}
|
||||
|
||||
impl DeveloperDeviceType {
|
||||
pub fn url_segment(&self) -> &'static str {
|
||||
match self {
|
||||
DeveloperDeviceType::Any => "",
|
||||
DeveloperDeviceType::Ios => "ios/",
|
||||
DeveloperDeviceType::Tvos => "tvos/",
|
||||
DeveloperDeviceType::Watchos => "watchos/",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dev_url(device_type: DeveloperDeviceType, endpoint: &str) -> String {
|
||||
format!(
|
||||
"https://developerservices2.apple.com/services/QH65B2/{}{}.action?clientId=XABBG36SBA",
|
||||
device_type.url_segment(),
|
||||
endpoint
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeveloperDevice {
|
||||
pub _device_id: String,
|
||||
pub _name: String,
|
||||
pub device_number: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeveloperTeam {
|
||||
pub _name: String,
|
||||
pub team_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DevelopmentCertificate {
|
||||
pub name: String,
|
||||
pub certificate_id: String,
|
||||
pub serial_number: String,
|
||||
pub machine_name: String,
|
||||
pub cert_content: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppId {
|
||||
pub app_id_id: String,
|
||||
pub identifier: String,
|
||||
pub name: String,
|
||||
pub features: Dictionary,
|
||||
pub expiration_date: Option<Date>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListAppIdsResponse {
|
||||
pub app_ids: Vec<AppId>,
|
||||
pub max_quantity: u64,
|
||||
pub available_quantity: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApplicationGroup {
|
||||
pub application_group: String,
|
||||
pub _name: String,
|
||||
pub identifier: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProvisioningProfile {
|
||||
pub _provisioning_profile_id: String,
|
||||
pub _name: String,
|
||||
pub encoded_profile: Vec<u8>,
|
||||
}
|
||||
179
src/device.rs
Normal file
179
src/device.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use idevice::{
|
||||
IdeviceService,
|
||||
afc::AfcClient,
|
||||
installation_proxy::InstallationProxyClient,
|
||||
lockdown::LockdownClient,
|
||||
usbmuxd::{UsbmuxdAddr, UsbmuxdConnection},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
|
||||
#[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)
|
||||
}
|
||||
|
||||
pub async fn install_app(
|
||||
device: &DeviceInfo,
|
||||
app_path: &PathBuf,
|
||||
callback: impl Fn(u64) -> (),
|
||||
) -> Result<(), String> {
|
||||
let mut usbmuxd = UsbmuxdConnection::default()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to usbmuxd: {:?}", e))?;
|
||||
let device = usbmuxd
|
||||
.get_device(&device.uuid)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get device: {:?}", 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))?;
|
||||
|
||||
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))?;
|
||||
|
||||
let mut instproxy_client = InstallationProxyClient::connect(&provider)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to installation proxy: {:?}", e))?;
|
||||
|
||||
let mut options = plist::Dictionary::new();
|
||||
options.insert("PackageType".to_string(), "Developer".into());
|
||||
instproxy_client
|
||||
.install_with_callback(
|
||||
dir,
|
||||
Some(plist::Value::Dictionary(options)),
|
||||
async |(percentage, _)| {
|
||||
callback(percentage);
|
||||
},
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to install app: {:?}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn afc_upload_dir<'a>(
|
||||
afc_client: &'a mut AfcClient,
|
||||
path: &'a PathBuf,
|
||||
afc_path: &'a str,
|
||||
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let entries =
|
||||
std::fs::read_dir(path).map_err(|e| format!("Failed to read directory: {}", e))?;
|
||||
afc_client
|
||||
.mk_dir(afc_path)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let new_afc_path = format!(
|
||||
"{}/{}",
|
||||
afc_path,
|
||||
path.file_name().unwrap().to_string_lossy()
|
||||
);
|
||||
afc_upload_dir(afc_client, &path, &new_afc_path).await?;
|
||||
} else {
|
||||
let mut file_handle = afc_client
|
||||
.open(
|
||||
format!(
|
||||
"{}/{}",
|
||||
afc_path,
|
||||
path.file_name().unwrap().to_string_lossy()
|
||||
),
|
||||
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))?;
|
||||
file_handle
|
||||
.write(&bytes)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to write file: {}", e))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn refresh_idevice(window: tauri::Window) {
|
||||
match list_devices().await {
|
||||
Ok(devices) => {
|
||||
window
|
||||
.emit("idevices", devices)
|
||||
.expect("Failed to send devices");
|
||||
}
|
||||
Err(e) => {
|
||||
window
|
||||
.emit("idevices", Vec::<DeviceInfo>::new())
|
||||
.expect("Failed to send error");
|
||||
eprintln!("Failed to list devices: {}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
21
src/lib.rs
Normal file
21
src/lib.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
pub mod application;
|
||||
pub mod bundle;
|
||||
pub mod certificate;
|
||||
pub mod developer_session;
|
||||
pub mod device;
|
||||
pub mod sideload;
|
||||
|
||||
pub use developer_session::{
|
||||
AppId, ApplicationGroup, DeveloperDevice, DeveloperDeviceType, DeveloperSession, DeveloperTeam,
|
||||
DevelopmentCertificate, ListAppIdsResponse, ProvisioningProfile,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Auth(i64, String),
|
||||
DeveloperSession(i64, String),
|
||||
Generic,
|
||||
Parse,
|
||||
InvalidBundle(String),
|
||||
Certificate(String),
|
||||
}
|
||||
400
src/sideload.rs
Normal file
400
src/sideload.rs
Normal file
@@ -0,0 +1,400 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
||||
|
||||
use crate::Error;
|
||||
use crate::{
|
||||
device::{DeviceInfo, install_app},
|
||||
sideloader::{
|
||||
certificate::CertificateIdentity, developer_session::DeveloperDeviceType,
|
||||
},
|
||||
};
|
||||
use std::{io::Write, path::PathBuf};
|
||||
|
||||
pub async fn sideload_app(
|
||||
handle: &tauri::AppHandle,
|
||||
window: &tauri::Window,
|
||||
anisette_server: String,
|
||||
device: DeviceInfo,
|
||||
app_path: PathBuf,
|
||||
) -> Result<(), Error> {
|
||||
if device.uuid.is_empty() {
|
||||
return emit_error_and_return(window, "No device selected");
|
||||
}
|
||||
let dev_session = match crate::sideloader::apple::get_developer_session(
|
||||
&handle,
|
||||
window,
|
||||
anisette_server.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(acc) => acc,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!("Failed to login to Apple account: {:?}", e),
|
||||
);
|
||||
}
|
||||
};
|
||||
let team = match dev_session.get_team().await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(window, &format!("Failed to get team: {:?}", e));
|
||||
}
|
||||
};
|
||||
window
|
||||
.emit("build-output", "Successfully retrieved team".to_string())
|
||||
.ok();
|
||||
ensure_device_registered(&dev_session, window, &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 {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(window, &format!("Failed to get certificate: {:?}", e));
|
||||
}
|
||||
};
|
||||
window
|
||||
.emit(
|
||||
"build-output",
|
||||
"Certificate acquired succesfully".to_string(),
|
||||
)
|
||||
.ok();
|
||||
let mut list_app_id_response = match dev_session
|
||||
.list_app_ids(DeveloperDeviceType::Ios, &team)
|
||||
.await
|
||||
{
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(window, &format!("Failed to list app IDs: {:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let mut app = crate::sideloader::application::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(),
|
||||
None => {
|
||||
return emit_error_and_return(window, "No bundle identifier found in IPA");
|
||||
}
|
||||
};
|
||||
let main_app_id_str = format!("{}.{}", main_app_bundle_id, team.team_id);
|
||||
let main_app_name = match app.bundle.bundle_name() {
|
||||
Some(name) => name.to_string(),
|
||||
None => {
|
||||
return emit_error_and_return(window, "No bundle name found in IPA");
|
||||
}
|
||||
};
|
||||
|
||||
let extensions = app.bundle.app_extensions_mut();
|
||||
// for each extension, ensure it has a unique bundle identifier that starts with the main app's bundle identifier
|
||||
for ext in extensions.iter_mut() {
|
||||
if let Some(id) = ext.bundle_identifier() {
|
||||
if !(id.starts_with(&main_app_bundle_id) && id.len() > main_app_bundle_id.len()) {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!(
|
||||
"Extension {} is not part of the main app bundle identifier: {}",
|
||||
ext.bundle_name().unwrap_or("Unknown"),
|
||||
id
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ext.set_bundle_identifier(&format!(
|
||||
"{}{}",
|
||||
main_app_id_str,
|
||||
&id[main_app_bundle_id.len()..]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
app.bundle.set_bundle_identifier(&main_app_id_str);
|
||||
|
||||
let extension_refs: Vec<_> = app.bundle.app_extensions().into_iter().collect();
|
||||
let mut bundles_with_app_id = vec![&app.bundle];
|
||||
bundles_with_app_id.extend(extension_refs);
|
||||
|
||||
let app_ids_to_register = bundles_with_app_id
|
||||
.iter()
|
||||
.filter(|bundle| {
|
||||
let bundle_id = bundle.bundle_identifier().unwrap_or("");
|
||||
!list_app_id_response
|
||||
.app_ids
|
||||
.iter()
|
||||
.any(|app_id| app_id.identifier == bundle_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if app_ids_to_register.len() > list_app_id_response.available_quantity.try_into().unwrap() {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!(
|
||||
"This app requires {} app ids, but you only have {} available",
|
||||
app_ids_to_register.len(),
|
||||
list_app_id_response.available_quantity
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for bundle in app_ids_to_register {
|
||||
let id = bundle.bundle_identifier().unwrap_or("");
|
||||
let name = bundle.bundle_name().unwrap_or("");
|
||||
if let Err(e) = dev_session
|
||||
.add_app_id(DeveloperDeviceType::Ios, &team, &name, &id)
|
||||
.await
|
||||
{
|
||||
return emit_error_and_return(window, &format!("Failed to register app ID: {:?}", e));
|
||||
}
|
||||
}
|
||||
list_app_id_response = match dev_session
|
||||
.list_app_ids(DeveloperDeviceType::Ios, &team)
|
||||
.await
|
||||
{
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(window, &format!("Failed to list app IDs: {:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let mut app_ids: Vec<_> = list_app_id_response
|
||||
.app_ids
|
||||
.into_iter()
|
||||
.filter(|app_id| {
|
||||
bundles_with_app_id
|
||||
.iter()
|
||||
.any(|bundle| app_id.identifier == bundle.bundle_identifier().unwrap_or(""))
|
||||
})
|
||||
.collect();
|
||||
let main_app_id = match app_ids
|
||||
.iter()
|
||||
.find(|app_id| app_id.identifier == main_app_id_str)
|
||||
.cloned()
|
||||
{
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!(
|
||||
"Main app ID {} not found in registered app IDs",
|
||||
main_app_id_str
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window
|
||||
.emit("build-output", "Registered app IDs".to_string())
|
||||
.ok();
|
||||
|
||||
for app_id in app_ids.iter_mut() {
|
||||
let app_group_feature_enabled = app_id
|
||||
.features
|
||||
.get(
|
||||
"APG3427HIY", /* Gotta love apple and their magic strings! */
|
||||
)
|
||||
.and_then(|v| v.as_boolean())
|
||||
.ok_or("App group feature not found in app id")?;
|
||||
if !app_group_feature_enabled {
|
||||
let mut body = plist::Dictionary::new();
|
||||
body.insert("APG3427HIY".to_string(), plist::Value::Boolean(true));
|
||||
let new_features = match dev_session
|
||||
.update_app_id(DeveloperDeviceType::Ios, &team, &app_id, &body)
|
||||
.await
|
||||
{
|
||||
Ok(new_feats) => new_feats,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!("Failed to update app ID features: {:?}", e),
|
||||
);
|
||||
}
|
||||
};
|
||||
app_id.features = new_features;
|
||||
}
|
||||
}
|
||||
|
||||
let group_identifier = format!("group.{}", main_app_id_str);
|
||||
|
||||
if is_sidestore {
|
||||
app.bundle.app_info.insert(
|
||||
"ALTAppGroups".to_string(),
|
||||
plist::Value::Array(vec![plist::Value::String(group_identifier.clone())]),
|
||||
);
|
||||
}
|
||||
|
||||
let app_groups = match dev_session
|
||||
.list_application_groups(DeveloperDeviceType::Ios, &team)
|
||||
.await
|
||||
{
|
||||
Ok(groups) => groups,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(window, &format!("Failed to list app groups: {:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let matching_app_groups = app_groups
|
||||
.iter()
|
||||
.filter(|group| group.identifier == group_identifier.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let app_group = if matching_app_groups.is_empty() {
|
||||
match dev_session
|
||||
.add_application_group(
|
||||
DeveloperDeviceType::Ios,
|
||||
&team,
|
||||
&group_identifier,
|
||||
&main_app_name,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(group) => group,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!("Failed to register app group: {:?}", e),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
matching_app_groups[0].clone()
|
||||
};
|
||||
|
||||
//let mut provisioning_profiles: HashMap<String, ProvisioningProfile> = HashMap::new();
|
||||
for app_id in app_ids {
|
||||
let assign_res = dev_session
|
||||
.assign_application_group_to_app_id(
|
||||
DeveloperDeviceType::Ios,
|
||||
&team,
|
||||
&app_id,
|
||||
&app_group,
|
||||
)
|
||||
.await;
|
||||
if assign_res.is_err() {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!(
|
||||
"Failed to assign app group to app ID: {:?}",
|
||||
assign_res.err()
|
||||
),
|
||||
);
|
||||
}
|
||||
// let provisioning_profile = match account
|
||||
// // This doesn't seem right to me, but it's what Sideloader does... Shouldn't it be downloading the provisioning profile for this app ID, not the main?
|
||||
// .download_team_provisioning_profile(DeveloperDeviceType::Ios, &team, &main_app_id)
|
||||
// .await
|
||||
// {
|
||||
// Ok(pp /* tee hee */) => pp,
|
||||
// Err(e) => {
|
||||
// return emit_error_and_return(
|
||||
// &window,
|
||||
// &format!("Failed to download provisioning profile: {:?}", e),
|
||||
// );
|
||||
// }
|
||||
// };
|
||||
// provisioning_profiles.insert(app_id.identifier.clone(), provisioning_profile);
|
||||
}
|
||||
|
||||
window
|
||||
.emit("build-output", "Registered app groups".to_string())
|
||||
.ok();
|
||||
|
||||
let provisioning_profile = match dev_session
|
||||
.download_team_provisioning_profile(DeveloperDeviceType::Ios, &team, &main_app_id)
|
||||
.await
|
||||
{
|
||||
Ok(pp /* tee hee */) => pp,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!("Failed to download provisioning profile: {:?}", e),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let profile_path = handle
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join(format!("{}.mobileprovision", main_app_id_str));
|
||||
|
||||
if profile_path.exists() {
|
||||
std::fs::remove_file(&profile_path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::create(&profile_path).map_err(|e| e.to_string())?;
|
||||
file.write_all(&provisioning_profile.encoded_profile)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Without this, zsign complains it can't find the provision file
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
file.sync_all().map_err(|e| e.to_string())?;
|
||||
drop(file);
|
||||
}
|
||||
|
||||
// TODO: Recursive for sub-bundles?
|
||||
app.bundle.write_info().map_err(|e| e.to_string())?;
|
||||
|
||||
window
|
||||
.emit("build-output", "Signining app...".to_string())
|
||||
.ok();
|
||||
|
||||
let zsign_command = handle.shell().sidecar("zsign").unwrap().args([
|
||||
"-k",
|
||||
cert.get_private_key_file_path().to_str().unwrap(),
|
||||
"-c",
|
||||
cert.get_certificate_file_path().to_str().unwrap(),
|
||||
"-m",
|
||||
profile_path.to_str().unwrap(),
|
||||
app.bundle.bundle_dir.to_str().unwrap(),
|
||||
]);
|
||||
let (mut rx, mut _child) = zsign_command.spawn().expect("Failed to spawn zsign");
|
||||
|
||||
let mut signing_failed = false;
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line_bytes) | CommandEvent::Stderr(line_bytes) => {
|
||||
let line = String::from_utf8_lossy(&line_bytes);
|
||||
window
|
||||
.emit("build-output", Some(line))
|
||||
.expect("failed to emit event");
|
||||
}
|
||||
CommandEvent::Terminated(result) => {
|
||||
if result.code != Some(0) {
|
||||
window
|
||||
.emit("build-output", "App signing failed!".to_string())
|
||||
.ok();
|
||||
signing_failed = true;
|
||||
break;
|
||||
}
|
||||
window.emit("build-output", "App signed!").ok();
|
||||
|
||||
window
|
||||
.emit(
|
||||
"build-output",
|
||||
"Installing app (Transfer)... 0%".to_string(),
|
||||
)
|
||||
.ok();
|
||||
|
||||
let res = install_app(&device, &app.bundle.bundle_dir, |percentage| {
|
||||
window
|
||||
.emit("build-output", format!("Installing app... {}%", percentage))
|
||||
.expect("failed to emit event");
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
window
|
||||
.emit("build-output", format!("Failed to install app: {:?}", e))
|
||||
.ok();
|
||||
signing_failed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if signing_failed {
|
||||
return Err("Signing or installation failed".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user