mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-11 07:43:28 +01:00
[AltServer] Installs Developer disk image before installing AltStore
Allows AltServer to programmatically initiate a debug session with AltStore, which can be used to start a background refresh or enable JIT on demand. [AltServer] Renames ALTDevice variable name
This commit is contained in:
286
AltServer/DeveloperDiskManager.swift
Normal file
286
AltServer/DeveloperDiskManager.swift
Normal file
@@ -0,0 +1,286 @@
|
||||
//
|
||||
// DeveloperDiskManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 2/19/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
enum DeveloperDiskError: LocalizedError
|
||||
{
|
||||
case unknownDownloadURL
|
||||
case unsupportedOperatingSystem
|
||||
case downloadedDiskNotFound
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .unknownDownloadURL: return NSLocalizedString("The URL to download the Developer disk image could not be determined.", comment: "")
|
||||
case .unsupportedOperatingSystem: return NSLocalizedString("The device's operating system does not support installing Developer disk images.", comment: "")
|
||||
case .downloadedDiskNotFound: return NSLocalizedString("DeveloperDiskImage.dmg and its signature could not be found in the downloaded archive.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension URL
|
||||
{
|
||||
#if STAGING
|
||||
static let developerDiskDownloadURLs = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altserver/developerdisks.json")!
|
||||
#else
|
||||
static let developerDiskDownloadURLs = URL(string: "https://cdn.altstore.io/file/altstore/altserver/developerdisks.json")!
|
||||
#endif
|
||||
}
|
||||
|
||||
private extension DeveloperDiskManager
|
||||
{
|
||||
struct FetchURLsResponse: Decodable
|
||||
{
|
||||
struct Disks: Decodable
|
||||
{
|
||||
var iOS: [String: DeveloperDiskURL]?
|
||||
var tvOS: [String: DeveloperDiskURL]?
|
||||
}
|
||||
|
||||
var version: Int
|
||||
var disks: Disks
|
||||
}
|
||||
|
||||
enum DeveloperDiskURL: Decodable
|
||||
{
|
||||
case archive(URL)
|
||||
case separate(diskURL: URL, signatureURL: URL)
|
||||
|
||||
private enum CodingKeys: CodingKey
|
||||
{
|
||||
case archive
|
||||
case disk
|
||||
case signature
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
if container.contains(.archive)
|
||||
{
|
||||
let archiveURL = try container.decode(URL.self, forKey: .archive)
|
||||
self = .archive(archiveURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
let diskURL = try container.decode(URL.self, forKey: .disk)
|
||||
let signatureURL = try container.decode(URL.self, forKey: .signature)
|
||||
self = .separate(diskURL: diskURL, signatureURL: signatureURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DeveloperDiskManager
|
||||
{
|
||||
func downloadDeveloperDisk(for device: ALTDevice, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
|
||||
{
|
||||
let osVersion = "\(device.osVersion.majorVersion).\(device.osVersion.minorVersion)"
|
||||
let osKeyPath: KeyPath<FetchURLsResponse.Disks, [String: DeveloperDiskURL]?>
|
||||
|
||||
switch device.type
|
||||
{
|
||||
case .iphone, .ipad: osKeyPath = \FetchURLsResponse.Disks.iOS
|
||||
case .appletv: osKeyPath = \FetchURLsResponse.Disks.tvOS
|
||||
default: return completionHandler(.failure(DeveloperDiskError.unsupportedOperatingSystem))
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
let developerDiskDirectoryURL = FileManager.default.developerDisksDirectory.appendingPathComponent(osVersion)
|
||||
try FileManager.default.createDirectory(at: developerDiskDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let developerDiskURL = developerDiskDirectoryURL.appendingPathComponent("DeveloperDiskImage.dmg")
|
||||
let developerDiskSignatureURL = developerDiskDirectoryURL.appendingPathComponent("DeveloperDiskImage.dmg.signature")
|
||||
|
||||
guard !FileManager.default.fileExists(atPath: developerDiskURL.path) || !FileManager.default.fileExists(atPath: developerDiskSignatureURL.path) else {
|
||||
return completionHandler(.success((developerDiskURL, developerDiskSignatureURL)))
|
||||
}
|
||||
|
||||
func finish(_ result: Result<(URL, URL), Error>)
|
||||
{
|
||||
do
|
||||
{
|
||||
let (diskFileURL, signatureFileURL) = try result.get()
|
||||
|
||||
let developerDiskDirectoryURL = FileManager.default.developerDisksDirectory.appendingPathComponent(osVersion)
|
||||
try FileManager.default.createDirectory(at: developerDiskDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
try FileManager.default.copyItem(at: diskFileURL, to: developerDiskURL)
|
||||
try FileManager.default.copyItem(at: signatureFileURL, to: developerDiskSignatureURL)
|
||||
|
||||
completionHandler(.success((developerDiskURL, developerDiskSignatureURL)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
self.fetchDeveloperDiskURLs { (result) in
|
||||
do
|
||||
{
|
||||
let developerDiskURLs = try result.get()
|
||||
guard let diskURL = developerDiskURLs[keyPath: osKeyPath]?[osVersion] else { throw DeveloperDiskError.unknownDownloadURL }
|
||||
|
||||
switch diskURL
|
||||
{
|
||||
case .archive(let archiveURL): self.downloadDiskArchive(from: archiveURL, completionHandler: finish(_:))
|
||||
case .separate(let diskURL, let signatureURL): self.downloadDisk(from: diskURL, signatureURL: signatureURL, completionHandler: finish(_:))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension DeveloperDiskManager
|
||||
{
|
||||
func fetchDeveloperDiskURLs(completionHandler: @escaping (Result<FetchURLsResponse.Disks, Error>) -> Void)
|
||||
{
|
||||
let dataTask = URLSession.shared.dataTask(with: .developerDiskDownloadURLs) { (data, response, error) in
|
||||
do
|
||||
{
|
||||
guard let data = data else { throw error! }
|
||||
|
||||
let response = try JSONDecoder().decode(FetchURLsResponse.self, from: data)
|
||||
completionHandler(.success(response.disks))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
dataTask.resume()
|
||||
}
|
||||
|
||||
func downloadDiskArchive(from url: URL, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
|
||||
{
|
||||
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
guard let fileURL = fileURL else { throw error! }
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
|
||||
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
defer { try? FileManager.default.removeItem(at: temporaryDirectory) }
|
||||
|
||||
try FileManager.default.unzipArchive(at: fileURL, toDirectory: temporaryDirectory)
|
||||
|
||||
guard let enumerator = FileManager.default.enumerator(at: temporaryDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
|
||||
throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: temporaryDirectory])
|
||||
}
|
||||
|
||||
var tempDiskFileURL: URL?
|
||||
var tempSignatureFileURL: URL?
|
||||
|
||||
for case let fileURL as URL in enumerator
|
||||
{
|
||||
switch fileURL.pathExtension.lowercased()
|
||||
{
|
||||
case "dmg": tempDiskFileURL = fileURL
|
||||
case "signature": tempSignatureFileURL = fileURL
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
guard let diskFileURL = tempDiskFileURL, let signatureFileURL = tempSignatureFileURL else { throw DeveloperDiskError.downloadedDiskNotFound }
|
||||
|
||||
completionHandler(.success((diskFileURL, signatureFileURL)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func downloadDisk(from diskURL: URL, signatureURL: URL, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
|
||||
{
|
||||
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) }
|
||||
catch { return completionHandler(.failure(error)) }
|
||||
|
||||
var diskFileURL: URL?
|
||||
var signatureFileURL: URL?
|
||||
|
||||
var downloadError: Error?
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
dispatchGroup.enter()
|
||||
dispatchGroup.enter()
|
||||
|
||||
let diskDownloadTask = URLSession.shared.downloadTask(with: diskURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
guard let fileURL = fileURL else { throw error! }
|
||||
|
||||
let destinationURL = temporaryDirectory.appendingPathComponent("DeveloperDiskImage.dmg")
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
||||
|
||||
diskFileURL = destinationURL
|
||||
}
|
||||
catch
|
||||
{
|
||||
downloadError = error
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let signatureDownloadTask = URLSession.shared.downloadTask(with: signatureURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
guard let fileURL = fileURL else { throw error! }
|
||||
|
||||
let destinationURL = temporaryDirectory.appendingPathComponent("DeveloperDiskImage.dmg.signature")
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
||||
|
||||
signatureFileURL = destinationURL
|
||||
}
|
||||
catch
|
||||
{
|
||||
downloadError = error
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
diskDownloadTask.resume()
|
||||
signatureDownloadTask.resume()
|
||||
|
||||
dispatchGroup.notify(queue: .global(qos: .userInitiated)) {
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||
}
|
||||
|
||||
guard let diskFileURL = diskFileURL, let signatureFileURL = signatureFileURL else {
|
||||
return completionHandler(.failure(downloadError ?? DeveloperDiskError.downloadedDiskNotFound))
|
||||
}
|
||||
|
||||
completionHandler(.success((diskFileURL, signatureFileURL)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import ObjectiveC
|
||||
|
||||
private let appGroupsLock = NSLock()
|
||||
|
||||
private let developerDiskManager = DeveloperDiskManager()
|
||||
|
||||
enum InstallError: LocalizedError
|
||||
{
|
||||
case cancelled
|
||||
@@ -32,7 +34,7 @@ enum InstallError: LocalizedError
|
||||
|
||||
extension ALTDeviceManager
|
||||
{
|
||||
func installApplication(at url: URL, to device: ALTDevice, appleID: String, password: String, completion: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||
func installApplication(at url: URL, to altDevice: ALTDevice, appleID: String, password: String, completion: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||
{
|
||||
let destinationDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
@@ -60,10 +62,11 @@ extension ALTDeviceManager
|
||||
{
|
||||
let team = try result.get()
|
||||
|
||||
self.register(device, team: team, session: session) { (result) in
|
||||
self.register(altDevice, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let device = try result.get()
|
||||
device.osVersion = altDevice.osVersion
|
||||
|
||||
self.fetchCertificate(for: team, session: session) { (result) in
|
||||
do
|
||||
@@ -75,55 +78,67 @@ extension ALTDeviceManager
|
||||
// Show alert before downloading remote .ipa.
|
||||
self.showInstallationAlert(appName: NSLocalizedString("AltStore", comment: ""), deviceName: device.name)
|
||||
}
|
||||
|
||||
self.downloadApp(from: url) { (result) in
|
||||
do
|
||||
|
||||
self.prepare(device) { (result) in
|
||||
switch result
|
||||
{
|
||||
let fileURL = try result.get()
|
||||
|
||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
|
||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
|
||||
|
||||
if url.isFileURL
|
||||
{
|
||||
// Show alert after "downloading" local .ipa.
|
||||
self.showInstallationAlert(appName: application.name, deviceName: device.name)
|
||||
}
|
||||
|
||||
// Refresh anisette data to prevent session timeouts.
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
case .failure(let error):
|
||||
print("Failed to install DeveloperDiskImage.dmg to \(device).", error)
|
||||
fallthrough // Continue installing app even if we couldn't install Developer disk image.
|
||||
|
||||
case .success:
|
||||
self.downloadApp(from: url) { (result) in
|
||||
do
|
||||
{
|
||||
let anisetteData = try result.get()
|
||||
session.anisetteData = anisetteData
|
||||
let fileURL = try result.get()
|
||||
|
||||
self.prepareAllProvisioningProfiles(for: application, device: device, team: team, session: session) { (result) in
|
||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
|
||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
|
||||
|
||||
if url.isFileURL
|
||||
{
|
||||
// Show alert after "downloading" local .ipa.
|
||||
self.showInstallationAlert(appName: application.name, deviceName: device.name)
|
||||
}
|
||||
|
||||
appName = application.name
|
||||
|
||||
// Refresh anisette data to prevent session timeouts.
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
do
|
||||
{
|
||||
let profiles = try result.get()
|
||||
let anisetteData = try result.get()
|
||||
session.anisetteData = anisetteData
|
||||
|
||||
self.install(application, to: device, team: team, certificate: certificate, profiles: profiles) { (result) in
|
||||
finish(result.map { application }, title: "Failed to Install AltStore")
|
||||
self.prepareAllProvisioningProfiles(for: application, device: device, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let profiles = try result.get()
|
||||
|
||||
self.install(application, to: device, team: team, certificate: certificate, profiles: profiles) { (result) in
|
||||
finish(result.map { application }, title: "Failed to Install AltStore")
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Fetch Provisioning Profiles")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Fetch Provisioning Profiles")
|
||||
finish(.failure(error), title: "Failed to Refresh Anisette Data")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Refresh Anisette Data")
|
||||
finish(.failure(error), title: "Failed to Download AltStore")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Download AltStore")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -158,6 +173,35 @@ extension ALTDeviceManager
|
||||
}
|
||||
}
|
||||
|
||||
extension ALTDeviceManager
|
||||
{
|
||||
func prepare(_ device: ALTDevice, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
ALTDeviceManager.shared.isDeveloperDiskImageMounted(for: device) { (isMounted, error) in
|
||||
switch (isMounted, error)
|
||||
{
|
||||
case (_, let error?): return completionHandler(.failure(error))
|
||||
case (true, _): return completionHandler(.success(()))
|
||||
case (false, _):
|
||||
developerDiskManager.downloadDeveloperDisk(for: device) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success((let diskFileURL, let signatureFileURL)):
|
||||
ALTDeviceManager.shared.installDeveloperDiskImage(at: diskFileURL, signatureURL: signatureFileURL, to: device) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success: completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ALTDeviceManager
|
||||
{
|
||||
func downloadApp(from url: URL, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
@@ -270,13 +314,8 @@ private extension ALTDeviceManager
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
let applicationSupportDirectoryURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
let altserverDirectoryURL = applicationSupportDirectoryURL.appendingPathComponent("com.rileytestut.AltServer")
|
||||
let certificatesDirectoryURL = altserverDirectoryURL.appendingPathComponent("Certificates")
|
||||
|
||||
try FileManager.default.createDirectory(at: certificatesDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let certificateFileURL = certificatesDirectoryURL.appendingPathComponent(team.identifier + ".p12")
|
||||
let certificateFileURL = FileManager.default.certificatesDirectory.appendingPathComponent(team.identifier + ".p12")
|
||||
try FileManager.default.createDirectory(at: FileManager.default.certificatesDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
var isCancelled = false
|
||||
|
||||
|
||||
29
AltServer/Extensions/FileManager+URLs.swift
Normal file
29
AltServer/Extensions/FileManager+URLs.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// FileManager+URLs.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 2/23/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FileManager
|
||||
{
|
||||
var altserverDirectory: URL {
|
||||
let applicationSupportDirectoryURL = self.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
|
||||
let altserverDirectoryURL = applicationSupportDirectoryURL.appendingPathComponent("com.rileytestut.AltServer")
|
||||
return altserverDirectoryURL
|
||||
}
|
||||
|
||||
var certificatesDirectory: URL {
|
||||
let certificatesDirectoryURL = self.altserverDirectory.appendingPathComponent("Certificates")
|
||||
return certificatesDirectoryURL
|
||||
}
|
||||
|
||||
var developerDisksDirectory: URL {
|
||||
let developerDisksDirectoryURL = self.altserverDirectory.appendingPathComponent("DeveloperDiskImages")
|
||||
return developerDisksDirectoryURL
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user