Displays progress when downloading/refreshing apps

Refactors download/refresh steps into separate Operation subclasses
This commit is contained in:
Riley Testut
2019-06-10 15:03:47 -07:00
parent 4f372f959a
commit a932e0759e
19 changed files with 1330 additions and 962 deletions

View File

@@ -62,7 +62,7 @@ extension AppDelegate
private func prepareForBackgroundFetch()
{
// Fetch every 6 hours.
UIApplication.shared.setMinimumBackgroundFetchInterval(60 * 60 * 6)
UIApplication.shared.setMinimumBackgroundFetchInterval(60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
}
@@ -72,8 +72,10 @@ extension AppDelegate
{
ServerManager.shared.startDiscovering()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
AppManager.shared.refreshAllApps(presentingViewController: nil) { (result) in
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
_ = AppManager.shared.refresh(installedApps, presentingViewController: nil) { (result) in
ServerManager.shared.stopDiscovering()
let content = UNMutableNotificationContent()
@@ -87,9 +89,7 @@ extension AppDelegate
guard case let .failure(error) = result else { continue }
throw error
}
print(results)
content.title = "Refreshed Apps!"
content.body = "Successfully refreshed all apps."

View File

@@ -124,7 +124,11 @@ private extension AppDetailViewController
sender.isIndicatingActivity = true
AppManager.shared.install(self.app, presentingViewController: self) { (result) in
let progressView = UIProgressView(progressViewStyle: .bar)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.progress = 0.0
let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
do
{
let installedApp = try result.get()
@@ -138,7 +142,7 @@ private extension AppDetailViewController
toastView.show(in: self.navigationController!.view, duration: 2)
}
}
catch AppManager.AppError.authentication(AuthenticationOperation.Error.cancelled)
catch OperationError.cancelled
{
// Ignore
}
@@ -150,12 +154,28 @@ private extension AppDetailViewController
toastView.show(in: self.navigationController!.view, duration: 2)
}
}
DispatchQueue.main.async {
UIView.animate(withDuration: 0.4, animations: {
progressView.alpha = 0.0
}) { _ in
progressView.removeFromSuperview()
}
sender.isIndicatingActivity = false
self.update()
}
}
progressView.observedProgress = progress
if let navigationBar = self.navigationController?.navigationBar
{
navigationBar.addSubview(progressView)
NSLayoutConstraint.activate([progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor),
progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
}
}
}

View File

@@ -14,52 +14,10 @@ import AltKit
import Roxas
extension AppManager
{
enum AppError: LocalizedError
{
case unknown
case missingUDID
case noServersFound
case missingPrivateKey
case missingCertificate
case notAuthenticated
case multipleCertificates
case multipleTeams
case download(URLError)
case authentication(Error)
case fetchingSigningResources(Error)
case prepare(Error)
case install(Error)
var errorDescription: String? {
switch self
{
case .unknown: return "An unknown error occured."
case .missingUDID: return "The UDID for this device is unknown."
case .noServersFound: return "An active AltServer could not be found."
case .missingPrivateKey: return "A valid private key must be provided."
case .missingCertificate: return "A valid certificate must be provided."
case .notAuthenticated: return "You must be logged in with your Apple ID to install apps."
case .multipleCertificates: return "You must select a certificate to use to install apps."
case .multipleTeams: return "You must select a team to use to install apps."
case .download(let error): return error.localizedDescription
case .authentication(let error): return error.localizedDescription
case .fetchingSigningResources(let error): return error.localizedDescription
case .prepare(let error): return error.localizedDescription
case .install(let error): return error.localizedDescription
}
}
}
}
class AppManager
{
static let shared = AppManager()
private let session = URLSession(configuration: .default)
private let operationQueue = OperationQueue()
private init()
@@ -107,7 +65,7 @@ extension AppManager
#endif
}
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTTeam, ALTCertificate), Error>) -> Void)
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTSigner, Error>) -> Void)
{
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in
@@ -119,91 +77,60 @@ extension AppManager
extension AppManager
{
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, AppError>) -> Void)
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
let ipaURL = InstalledApp.ipaURL(for: app)
let progress = Progress.discreteProgress(totalUnitCount: 100)
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.InstallApp")
func finish(_ result: Result<InstalledApp, AppError>)
{
completionHandler(result)
RSTEndBackgroundTask(backgroundTaskID)
}
// Download app
self.downloadApp(from: app.downloadURL) { (result) in
let result = result.flatMap { (fileURL) -> Result<Void, URLError> in
// Copy downloaded app to proper location
let result = Result { try FileManager.default.copyItem(at: fileURL, to: ipaURL, shouldReplace: true) }
return result.mapError { _ in URLError(.cannotWriteToFile) }
}
// Authenticate
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): finish(.failure(.download(error)))
case .success:
// Authenticate
self.authenticate(presentingViewController: presentingViewController) { (result) in
switch result
{
case .failure(let error): finish(.failure(.authentication(error)))
case .success(let team, let certificate):
// Fetch provisioning profile
self.prepareProvisioningProfile(for: app, team: team) { (result) in
switch result
{
case .failure(let error): finish(.failure(.fetchingSigningResources(error)))
case .success(let profile):
// Prepare app
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let app = context.object(with: app.objectID) as! App
let installedApp = InstalledApp(app: app,
bundleIdentifier: profile.appID.bundleIdentifier,
expirationDate: profile.expirationDate,
context: context)
let signer = ALTSigner(team: team, certificate: certificate)
self.prepare(installedApp, provisioningProfile: profile, signer: signer) { (result) in
switch result
{
case .failure(let error): finish(.failure(.prepare(error)))
case .success(let resignedURL):
// Send app to server
context.perform {
self.sendAppToServer(fileURL: resignedURL, identifier: installedApp.bundleIdentifier) { (result) in
switch result
{
case .failure(let error): finish(.failure(.install(error)))
case .success:
context.perform {
finish(.success(installedApp))
}
}
}
}
}
case .failure(let error): completionHandler(.failure(error))
case .success(let signer):
// Download
app.managedObjectContext?.perform {
let downloadAppOperation = DownloadAppOperation(app: app)
downloadAppOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let installedApp):
let context = installedApp.managedObjectContext
// Refresh/Install
let (resignProgress, installProgress) = self.refresh(installedApp, signer: signer, presentingViewController: presentingViewController) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success:
context?.perform {
completionHandler(.success(installedApp))
}
}
}
progress.addChild(resignProgress, withPendingUnitCount: 10)
progress.addChild(installProgress, withPendingUnitCount: 45)
}
}
progress.addChild(downloadAppOperation.progress, withPendingUnitCount: 40)
self.operationQueue.addOperation(downloadAppOperation)
}
}
}
progress.addChild(authenticationOperation.progress, withPendingUnitCount: 5)
self.operationQueue.addOperation(authenticationOperation)
return progress
}
func refresh(_ app: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
func refresh(_ app: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
self.refresh([app], presentingViewController: presentingViewController) { (result) in
return self.refresh([app], presentingViewController: presentingViewController) { (result) in
do
{
guard let (_, result) = try result.get().first else { throw AppError.unknown }
guard let (_, result) = try result.get().first else { throw OperationError.unknown }
completionHandler(result)
}
catch
@@ -213,303 +140,101 @@ extension AppManager
}
}
func refreshAllApps(presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], AppError>) -> Void)
@discardableResult func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> Progress
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
do
{
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)]
let installedApps = try context.fetch(fetchRequest)
self.refresh(installedApps, presentingViewController: presentingViewController) { (result) in
context.perform { // keep context alive
completionHandler(result)
}
}
}
catch
{
completionHandler(.failure(.prepare(error)))
}
}
}
private func refresh<T: Collection>(_ installedApps: T, presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], AppError>) -> Void) where T.Element == InstalledApp
{
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.RefreshApps")
let progress = Progress.discreteProgress(totalUnitCount: Int64(installedApps.count))
func finish(_ result: Result<[String: Result<InstalledApp, Error>], AppError>)
{
completionHandler(result)
RSTEndBackgroundTask(backgroundTaskID)
guard let context = installedApps.first?.managedObjectContext else {
completionHandler(.success([:]))
return progress
}
guard !ServerManager.shared.discoveredServers.isEmpty else { return finish(.failure(.noServersFound)) }
// Authenticate
self.authenticate(presentingViewController: presentingViewController) { (result) in
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): finish(.failure(.authentication(error)))
case .success(let team, let certificate):
case .failure(let error): completionHandler(.failure(error))
case .success(let signer):
// Sign
let signer = ALTSigner(team: team, certificate: certificate)
let dispatchGroup = DispatchGroup()
var results = [String: Result<InstalledApp, Error>]()
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
for app in installedApps
{
dispatchGroup.enter()
// Refresh
context.perform {
let dispatchGroup = DispatchGroup()
var results = [String: Result<InstalledApp, Error>]()
app.managedObjectContext?.perform {
let bundleIdentifier = app.bundleIdentifier
for installedApp in installedApps
{
let bundleIdentifier = installedApp.bundleIdentifier
print("Refreshing App:", bundleIdentifier)
self.refresh(app, signer: signer, context: context) { (result) in
dispatchGroup.enter()
let (resignProgress, installProgress) = self.refresh(installedApp, signer: signer, presentingViewController: presentingViewController) { (result) in
print("Refreshed App: \(bundleIdentifier).", result)
results[bundleIdentifier] = result
dispatchGroup.leave()
}
let refreshProgress = Progress(totalUnitCount: 100)
refreshProgress.addChild(resignProgress, withPendingUnitCount: 20)
refreshProgress.addChild(installProgress, withPendingUnitCount: 80)
progress.addChild(refreshProgress, withPendingUnitCount: 1)
}
}
dispatchGroup.notify(queue: .global()) {
context.perform {
finish(.success(results))
}
}
}
}
}
}
private extension AppManager
{
func downloadApp(from url: URL, completionHandler: @escaping (Result<URL, URLError>) -> Void)
{
let downloadTask = self.session.downloadTask(with: url) { (fileURL, response, error) in
do
{
let (fileURL, _) = try Result((fileURL, response), error).get()
completionHandler(.success(fileURL))
}
catch let error as URLError
{
completionHandler(.failure(error))
}
catch
{
completionHandler(.failure(URLError(.unknown)))
}
}
downloadTask.resume()
}
func prepareProvisioningProfile(for app: App, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return completionHandler(.failure(AppError.missingUDID)) }
let device = ALTDevice(name: UIDevice.current.name, identifier: udid)
self.register(device, team: team) { (result) in
do
{
_ = try result.get()
app.managedObjectContext?.perform {
self.register(app, with: team) { (result) in
do
{
let appID = try result.get()
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
do
{
let provisioningProfile = try result.get()
completionHandler(.success(provisioningProfile))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func prepare(_ installedApp: InstalledApp, provisioningProfile: ALTProvisioningProfile, signer: ALTSigner, completionHandler: @escaping (Result<URL, Error>) -> Void)
{
do
{
let refreshedAppDirectory = installedApp.directoryURL.appendingPathComponent("Refreshed", isDirectory: true)
if FileManager.default.fileExists(atPath: refreshedAppDirectory.path)
{
try FileManager.default.removeItem(at: refreshedAppDirectory)
}
try FileManager.default.createDirectory(at: refreshedAppDirectory, withIntermediateDirectories: true, attributes: nil)
let appBundleURL = try FileManager.default.unzipAppBundle(at: installedApp.ipaURL, toDirectory: refreshedAppDirectory)
guard let bundle = Bundle(url: appBundleURL) else { throw ALTError(.missingAppBundle) }
guard var infoDictionary = NSDictionary(contentsOf: bundle.infoPlistURL) as? [String: Any] else { throw ALTError(.missingInfoPlist) }
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
"CFBundleURLName": installedApp.bundleIdentifier,
"CFBundleURLSchemes": [installedApp.openAppURL.scheme!]] as [String : Any]
allURLSchemes.append(altstoreURLScheme)
infoDictionary[Bundle.Info.urlTypes] = allURLSchemes
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
signer.signApp(at: appBundleURL, provisioningProfile: provisioningProfile) { (success, error) in
do
{
try Result(success, error).get()
let resignedURL = try FileManager.default.zipAppBundle(at: appBundleURL)
completionHandler(.success(resignedURL))
}
catch
{
completionHandler(.failure(error))
dispatchGroup.notify(queue: .global()) {
context.perform {
completionHandler(.success(results))
}
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
func sendAppToServer(fileURL: URL, identifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
guard let server = ServerManager.shared.discoveredServers.first else { return completionHandler(.failure(AppError.noServersFound)) }
server.installApp(at: fileURL, identifier: identifier) { (result) in
let result = result.mapError { $0 as Error }
completionHandler(result)
}
self.operationQueue.addOperation(authenticationOperation)
return progress
}
}
private extension AppManager
{
func register(_ device: ALTDevice, team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
func refresh(_ installedApp: InstalledApp, signer: ALTSigner, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> (Progress, Progress)
{
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
if let device = devices.first(where: { $0.identifier == device.identifier })
{
completionHandler(.success(device))
}
else
{
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, team: team) { (device, error) in
completionHandler(Result(device, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func register(_ app: App, with team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
let appName = app.name
let bundleID = "com.\(team.identifier).\(app.identifier)"
let context = installedApp.managedObjectContext
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
do
{
let appIDs = try Result(appIDs, error).get()
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleID })
{
completionHandler(.success(appID))
}
else
{
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
completionHandler(Result(appID, error))
}
}
}
catch
let resignAppOperation = ResignAppOperation(installedApp: installedApp)
let installAppOperation = InstallAppOperation()
// Resign
resignAppOperation.signer = signer
resignAppOperation.resultHandler = { (result) in
switch result
{
case .failure(let error):
installAppOperation.cancel()
completionHandler(.failure(error))
case .success(let resignedURL):
installAppOperation.fileURL = resignedURL
}
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
completionHandler(Result(profile, error))
}
}
func refresh(_ installedApp: InstalledApp, signer: ALTSigner, context: NSManagedObjectContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
{
self.prepareProvisioningProfile(for: installedApp.app, team: signer.team) { (result) in
// Install
installAppOperation.addDependency(resignAppOperation)
installAppOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
installedApp.managedObjectContext?.perform {
self.prepare(installedApp, provisioningProfile: profile, signer: signer) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let resignedURL):
// Send app to server
installedApp.managedObjectContext?.perform {
self.sendAppToServer(fileURL: resignedURL, identifier: installedApp.bundleIdentifier) { (result) in
context.perform {
switch result
{
case .success:
let installedApp = context.object(with: installedApp.objectID) as! InstalledApp
installedApp.expirationDate = profile.expirationDate
completionHandler(.success(installedApp))
case .failure(let error):
completionHandler(.failure(error))
}
}
}
}
}
}
case .success:
context?.perform {
completionHandler(.success(installedApp))
}
}
}
self.operationQueue.addOperations([resignAppOperation, installAppOperation], waitUntilFinished: false)
return (resignAppOperation.progress, installAppOperation.progress)
}
}

View File

@@ -76,12 +76,19 @@
<navigationItem key="navigationItem" title="My Apps" id="dz9-0e-LKa">
<barButtonItem key="rightBarButtonItem" title="Refresh All" id="0Ke-yl-tAg">
<connections>
<action selector="refreshAllApps:" destination="Xf1-LZ-1vU" id="6op-Mf-HSD"/>
<action selector="refreshAllApps:" destination="Xf1-LZ-1vU" id="GOS-gx-qKD"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="progressView" destination="CuF-K7-fn8" id="SPP-VP-a9e"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="nb5-5T-hHT" userLabel="First Responder" sceneMemberID="firstResponder"/>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progressViewStyle="bar" id="CuF-K7-fn8">
<rect key="frame" x="0.0" y="0.0" width="150" height="2.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</progressView>
</objects>
<point key="canvasLocation" x="1518" y="420"/>
</scene>
@@ -629,7 +636,7 @@
<image name="second" width="30" height="30"/>
</resources>
<inferredMetricsTieBreakers>
<segue reference="miz-8X-gFg"/>
<segue reference="8jj-zE-2hk"/>
</inferredMetricsTieBreakers>
<color key="tintColor" name="Purple"/>
</document>

View File

@@ -10,7 +10,7 @@ import Foundation
import CoreData
@objc(InstalledApp)
class InstalledApp: NSManagedObject
class InstalledApp: NSManagedObject, Fetchable
{
/* Properties */
@NSManaged var bundleIdentifier: String
@@ -75,6 +75,13 @@ extension InstalledApp
return ipaURL
}
class func refreshedIPAURL(for app: App) -> URL
{
let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
return ipaURL
}
class func directoryURL(for app: App) -> URL
{
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.identifier)
@@ -92,4 +99,8 @@ extension InstalledApp
var ipaURL: URL {
return InstalledApp.ipaURL(for: self.app)
}
var refreshedIPAURL: URL {
return InstalledApp.refreshedIPAURL(for: self.app)
}
}

View File

@@ -9,6 +9,8 @@
import UIKit
import Roxas
import AltSign
class MyAppsViewController: UITableViewController
{
private var refreshErrors = [String: Error]()
@@ -22,12 +24,23 @@ class MyAppsViewController: UITableViewController
return dateFormatter
}()
@IBOutlet private var progressView: UIProgressView!
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
if let navigationBar = self.navigationController?.navigationBar
{
self.progressView.translatesAutoresizingMaskIntoConstraints = false
navigationBar.addSubview(self.progressView)
NSLayoutConstraint.activate([self.progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor),
self.progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
}
self.update()
}
@@ -106,7 +119,9 @@ private extension MyAppsViewController
{
sender.isIndicatingActivity = true
AppManager.shared.refreshAllApps(presentingViewController: self) { (result) in
let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
let progress = AppManager.shared.refresh(installedApps, presentingViewController: self) { (result) in
DispatchQueue.main.async {
switch result
{
@@ -146,10 +161,49 @@ private extension MyAppsViewController
self.refreshErrors = failures
}
self.progressView.observedProgress = nil
self.progressView.progress = 0.0
sender.isIndicatingActivity = false
self.update()
}
}
self.progressView.observedProgress = progress
}
func refresh(_ installedApp: InstalledApp)
{
let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
do
{
let app = try result.get()
try app.managedObjectContext?.save()
DispatchQueue.main.async {
let toastView = RSTToastView(text: "Refreshed \(installedApp.app.name)!", detailText: nil)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
self.update()
}
}
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: "Failed to refresh \(installedApp.app.name)", detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
}
}
DispatchQueue.main.async {
self.progressView.observedProgress = nil
self.progressView.progress = 0.0
}
}
self.progressView.observedProgress = progress
}
}
@@ -183,7 +237,7 @@ extension MyAppsViewController
toastView.activityIndicatorView.startAnimating()
toastView.show(in: self.navigationController?.view ?? self.view)
AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
do
{
let app = try result.get()
@@ -203,11 +257,16 @@ extension MyAppsViewController
let toastView = RSTToastView(text: "Failed to refresh \(installedApp.app.name)", detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
self.update()
}
}
DispatchQueue.main.async {
self.progressView.observedProgress = nil
self.progressView.progress = 0.0
}
}
self.progressView.observedProgress = progress
}
return [deleteAction, refreshAction]

View File

@@ -11,36 +11,27 @@ import Roxas
import AltSign
extension AuthenticationOperation
enum AuthenticationError: LocalizedError
{
enum Error: LocalizedError
{
case cancelled
case notAuthenticated
case noTeam
case noCertificate
case missingPrivateKey
case missingCertificate
var errorDescription: String? {
switch self {
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
}
case noTeam
case noCertificate
case missingPrivateKey
case missingCertificate
var errorDescription: String? {
switch self {
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
}
}
}
class AuthenticationOperation: RSTOperation
@objc(AuthenticationOperation)
class AuthenticationOperation: ResultOperation<ALTSigner>
{
var resultHandler: ((Result<(ALTTeam, ALTCertificate), Swift.Error>) -> Void)?
private weak var presentingViewController: UIViewController?
private lazy var navigationController = UINavigationController()
@@ -48,106 +39,51 @@ class AuthenticationOperation: RSTOperation
private var appleIDPassword: String?
override var isAsynchronous: Bool {
return true
}
init(presentingViewController: UIViewController?)
{
self.presentingViewController = presentingViewController
super.init()
self.progress.totalUnitCount = 3
}
override func main()
{
super.main()
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.Authenticate")
func finish(_ result: Result<(ALTTeam, ALTCertificate), Swift.Error>)
{
print("Finished authenticating with result:", result)
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
do
{
let (altTeam, altCertificate) = try result.get()
let altAccount = altTeam.account
// Account
let account = Account(altAccount, context: context)
account.isActiveAccount = true
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
for account in otherAccounts
{
account.isActiveAccount = false
}
// Team
let team = Team(altTeam, account: account, context: context)
team.isActiveTeam = true
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
let otherTeams = try context.fetch(otherTeamsFetchRequest)
for team in otherTeams
{
team.isActiveTeam = false
}
// Save
try context.save()
// Update keychain
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
Keychain.shared.appleIDPassword = self.appleIDPassword
Keychain.shared.signingCertificateIdentifier = altCertificate.identifier
Keychain.shared.signingCertificatePrivateKey = altCertificate.privateKey
self.resultHandler?(.success((altTeam, altCertificate)))
}
catch
{
self.resultHandler?(.failure(error))
}
self.finish()
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
}
RSTEndBackgroundTask(backgroundTaskID)
}
}
// Sign In
self.signIn { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): finish(.failure(error))
case .failure(let error): self.finish(.failure(error))
case .success(let account):
self.progress.completedUnitCount += 1
// Fetch Team
self.fetchTeam(for: account) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): finish(.failure(error))
case .failure(let error): self.finish(.failure(error))
case .success(let team):
self.progress.completedUnitCount += 1
// Fetch Certificate
self.fetchCertificate(for: team) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): finish(.failure(error))
case .success(let certificate): finish(.success((team, certificate)))
case .failure(let error): self.finish(.failure(error))
case .success(let certificate):
self.progress.completedUnitCount += 1
let signer = ALTSigner(team: team, certificate: certificate)
self.finish(.success(signer))
}
}
}
@@ -155,6 +91,68 @@ class AuthenticationOperation: RSTOperation
}
}
}
override func finish(_ result: Result<ALTSigner, Error>)
{
guard !self.isFinished else { return }
print("Finished authenticating with result:", result)
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
do
{
let signer = try result.get()
let altAccount = signer.team.account
// Account
let account = Account(altAccount, context: context)
account.isActiveAccount = true
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
for account in otherAccounts
{
account.isActiveAccount = false
}
// Team
let team = Team(signer.team, account: account, context: context)
team.isActiveTeam = true
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
let otherTeams = try context.fetch(otherTeamsFetchRequest)
for team in otherTeams
{
team.isActiveTeam = false
}
// Save
try context.save()
// Update keychain
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
Keychain.shared.appleIDPassword = self.appleIDPassword
Keychain.shared.signingCertificateIdentifier = signer.certificate.identifier
Keychain.shared.signingCertificatePrivateKey = signer.certificate.privateKey
super.finish(.success(signer))
}
catch
{
super.finish(.failure(error))
}
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
}
}
}
}
private extension AuthenticationOperation
@@ -199,13 +197,13 @@ private extension AuthenticationOperation
}
else
{
completionHandler(.failure(Error.cancelled))
completionHandler(.failure(OperationError.cancelled))
}
}
if !self.present(authenticationViewController)
{
completionHandler(.failure(Error.notAuthenticated))
completionHandler(.failure(OperationError.notAuthenticated))
}
}
}
@@ -254,13 +252,13 @@ private extension AuthenticationOperation
}
else
{
completionHandler(.failure(Error.cancelled))
completionHandler(.failure(OperationError.cancelled))
}
}
if !self.present(selectTeamViewController)
{
completionHandler(.failure(Error.noTeam))
completionHandler(.failure(AuthenticationError.noTeam))
}
}
}
@@ -294,7 +292,7 @@ private extension AuthenticationOperation
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw Error.missingPrivateKey }
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
do
@@ -302,7 +300,7 @@ private extension AuthenticationOperation
let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else {
throw Error.missingCertificate
throw AuthenticationError.missingCertificate
}
certificate.privateKey = privateKey
@@ -334,13 +332,13 @@ private extension AuthenticationOperation
}
else
{
completionHandler(.failure(Error.cancelled))
completionHandler(.failure(OperationError.cancelled))
}
}
if !self.present(replaceCertificateViewController)
{
completionHandler(.failure(Error.noCertificate))
completionHandler(.failure(AuthenticationError.noCertificate))
}
}
}

View File

@@ -0,0 +1,65 @@
//
// DownloadAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/10/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltSign
@objc(DownloadAppOperation)
class DownloadAppOperation: ResultOperation<InstalledApp>
{
let app: App
private let downloadURL: URL
private let ipaURL: URL
private let session = URLSession(configuration: .default)
init(app: App)
{
self.app = app
self.downloadURL = app.downloadURL
self.ipaURL = InstalledApp.ipaURL(for: app)
super.init()
self.progress.totalUnitCount = 1
}
override func main()
{
super.main()
let downloadTask = self.session.downloadTask(with: self.downloadURL) { (fileURL, response, error) in
do
{
let (fileURL, _) = try Result((fileURL, response), error).get()
try FileManager.default.copyItem(at: fileURL, to: self.ipaURL, shouldReplace: true)
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let app = context.object(with: self.app.objectID) as! App
let installedApp = InstalledApp(app: app,
bundleIdentifier: app.identifier,
expirationDate: Date(),
context: context)
self.finish(.success(installedApp))
}
}
catch let error
{
self.finish(.failure(error))
}
}
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
downloadTask.resume()
}
}

View File

@@ -0,0 +1,286 @@
//
// InstallAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Network
import AltKit
extension ALTServerError
{
init<E: Error>(_ error: E)
{
switch error
{
case let error as ALTServerError: self = error
case is DecodingError: self = ALTServerError(.invalidResponse)
case is EncodingError: self = ALTServerError(.invalidRequest)
default:
assertionFailure("Caught unknown error type")
self = ALTServerError(.unknown)
}
}
}
enum InstallationError: LocalizedError
{
case serverNotFound
case connectionFailed
case connectionDropped
case invalidApp
var errorDescription: String? {
switch self
{
case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "")
case .connectionFailed: return NSLocalizedString("Could not connect to AltServer.", comment: "")
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
}
}
}
@objc(InstallAppOperation)
class InstallAppOperation: ResultOperation<Void>
{
var fileURL: URL?
private let dispatchQueue = DispatchQueue(label: "com.altstore.InstallAppOperation")
private var connection: NWConnection?
override init()
{
super.init()
self.progress.totalUnitCount = 4
}
override func main()
{
super.main()
guard let fileURL = self.fileURL else { return self.finish(.failure(OperationError.appNotFound)) }
// Connect to server.
self.connect { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
self.connection = connection
// Send app to server.
self.sendApp(at: fileURL, via: connection) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
self.progress.completedUnitCount += 1
// Receive response from server.
let progress = self.receiveResponse(from: connection) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success: self.finish(.success(()))
}
}
self.progress.addChild(progress, withPendingUnitCount: 3)
}
}
}
}
}
override func finish(_ result: Result<Void, Error>)
{
super.finish(result)
if let connection = self.connection
{
connection.cancel()
}
}
}
private extension InstallAppOperation
{
func connect(completionHandler: @escaping (Result<NWConnection, Error>) -> Void)
{
guard let server = ServerManager.shared.discoveredServers.first else { return completionHandler(.failure(InstallationError.serverNotFound)) }
let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp)
connection.stateUpdateHandler = { [unowned connection] (state) in
switch state
{
case .failed(let error):
print("Failed to connect to service \(server.service.name).", error)
completionHandler(.failure(InstallationError.connectionFailed))
case .cancelled:
completionHandler(.failure(OperationError.cancelled))
case .ready:
completionHandler(.success(connection))
case .waiting: break
case .setup: break
case .preparing: break
@unknown default: break
}
}
connection.start(queue: self.dispatchQueue)
}
func sendApp(at fileURL: URL, via connection: NWConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
guard let appData = try? Data(contentsOf: fileURL) else { throw InstallationError.invalidApp }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
let request = ServerRequest(udid: udid, contentSize: appData.count)
let requestData: Data
do {
requestData = try JSONEncoder().encode(request)
}
catch {
print("Invalid request.", error)
throw ALTServerError(.invalidRequest)
}
let requestSize = Int32(requestData.count)
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
func process(_ error: Error?) -> Bool
{
if error != nil
{
completionHandler(.failure(InstallationError.connectionDropped))
return false
}
else
{
return true
}
}
// Send request data size.
print("Sending request data size \(requestSize)")
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
guard process(error) else { return }
// Send request.
print("Sending request \(request)")
connection.send(content: requestData, completion: .contentProcessed { (error) in
guard process(error) else { return }
// Send app data.
print("Sending app data (Size: \(appData.count))")
connection.send(content: appData, completion: .contentProcessed { (error) in
print("Sent app data!")
guard process(error) else { return }
completionHandler(.success(()))
})
})
})
}
catch
{
completionHandler(.failure(error))
}
}
func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result<Void, Error>) -> Void) -> Progress
{
func receive(from connection: NWConnection, progress: Progress, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let size = MemoryLayout<Int32>.size
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
print(response)
if let error = response.error
{
completionHandler(.failure(error))
}
else if response.progress == 1.0
{
completionHandler(.success(()))
}
else
{
progress.completedUnitCount = Int64(response.progress * 100)
receive(from: connection, progress: progress, completionHandler: completionHandler)
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
let progress = Progress.discreteProgress(totalUnitCount: 100)
receive(from: connection, progress: progress, completionHandler: completionHandler)
return progress
}
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
}
}

View File

@@ -0,0 +1,84 @@
//
// Operation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
class ResultOperation<ResultType>: Operation
{
var resultHandler: ((Result<ResultType, Error>) -> Void)?
@available(*, unavailable)
override func finish()
{
super.finish()
}
func finish(_ result: Result<ResultType, Error>)
{
guard !self.isFinished else { return }
super.finish()
self.resultHandler?(result)
}
}
class Operation: RSTOperation, ProgressReporting
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
private var backgroundTaskID: UIBackgroundTaskIdentifier?
override var isAsynchronous: Bool {
return true
}
override init()
{
super.init()
self.progress.cancellationHandler = { [weak self] in self?.cancel() }
}
override func cancel()
{
super.cancel()
if !self.progress.isCancelled
{
self.progress.cancel()
}
}
override func main()
{
super.main()
let name = "com.altstore." + NSStringFromClass(type(of: self))
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: name) { [weak self] in
guard let backgroundTask = self?.backgroundTaskID else { return }
self?.cancel()
UIApplication.shared.endBackgroundTask(backgroundTask)
self?.backgroundTaskID = .invalid
}
}
override func finish()
{
super.finish()
if let backgroundTaskID = self.backgroundTaskID
{
UIApplication.shared.endBackgroundTask(backgroundTaskID)
self.backgroundTaskID = .invalid
}
}
}

View File

@@ -0,0 +1,32 @@
//
// OperationError.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
enum OperationError: LocalizedError
{
case unknown
case unknownResult
case cancelled
case notAuthenticated
case appNotFound
case unknownUDID
var errorDescription: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
}
}
}

View File

@@ -0,0 +1,244 @@
//
// ResignAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltSign
@objc(ResignAppOperation)
class ResignAppOperation: ResultOperation<URL>
{
let installedApp: InstalledApp
private var context: NSManagedObjectContext?
var signer: ALTSigner?
init(installedApp: InstalledApp)
{
self.installedApp = installedApp
self.context = installedApp.managedObjectContext
super.init()
self.progress.totalUnitCount = 3
}
override func main()
{
super.main()
guard let context = self.context else { return self.finish(.failure(OperationError.appNotFound)) }
guard let signer = self.signer else { return self.finish(.failure(OperationError.notAuthenticated)) }
context.perform {
// Register Device
self.registerCurrentDevice(for: signer.team) { (result) in
guard let _ = self.process(result) else { return }
// Register App
context.perform {
self.register(self.installedApp.app, team: signer.team) { (result) in
guard let appID = self.process(result) else { return }
// Fetch Provisioning Profile
self.fetchProvisioningProfile(for: appID, team: signer.team) { (result) in
guard let profile = self.process(result) else { return }
// Prepare app bundle
context.perform {
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
let prepareAppBundleProgress = self.prepareAppBundle(for: self.installedApp) { (result) in
guard let appBundleURL = self.process(result) else { return }
// Resign app bundle
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profile: profile) { (result) in
guard let resignedURL = self.process(result) else { return }
// Finish
context.perform {
do
{
try FileManager.default.copyItem(at: resignedURL, to: self.installedApp.refreshedIPAURL, shouldReplace: true)
let refreshedDirectory = resignedURL.deletingLastPathComponent()
try? FileManager.default.removeItem(at: refreshedDirectory)
self.finish(.success(self.installedApp.refreshedIPAURL))
}
catch
{
self.finish(.failure(error))
}
}
}
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
}
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
}
}
}
}
}
}
}
func process<T>(_ result: Result<T, Error>) -> T?
{
switch result
{
case .failure(let error):
self.finish(.failure(error))
return nil
case .success(let value):
guard !self.isCancelled else {
self.finish(.failure(OperationError.cancelled))
return nil
}
return value
}
}
}
private extension ResignAppOperation
{
func registerCurrentDevice(for team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
return completionHandler(.failure(OperationError.unknownUDID))
}
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
if let device = devices.first(where: { $0.identifier == udid })
{
completionHandler(.success(device))
}
else
{
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team) { (device, error) in
completionHandler(Result(device, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func register(_ app: App, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
let appName = app.name
let bundleID = "com.\(team.identifier).\(app.identifier)"
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
do
{
let appIDs = try Result(appIDs, error).get()
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleID })
{
completionHandler(.success(appID))
}
else
{
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
completionHandler(Result(appID, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
completionHandler(Result(profile, error))
}
}
func prepareAppBundle(for installedApp: InstalledApp, completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
let refreshedAppDirectory = installedApp.directoryURL.appendingPathComponent("Refreshed", isDirectory: true)
let ipaURL = installedApp.ipaURL
let bundleIdentifier = installedApp.bundleIdentifier
let openURL = installedApp.openAppURL
DispatchQueue.global().async {
do
{
if FileManager.default.fileExists(atPath: refreshedAppDirectory.path)
{
try FileManager.default.removeItem(at: refreshedAppDirectory)
}
try FileManager.default.createDirectory(at: refreshedAppDirectory, withIntermediateDirectories: true, attributes: nil)
// Become current so we can observe progress from unzipAppBundle().
progress.becomeCurrent(withPendingUnitCount: 1)
let appBundleURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: refreshedAppDirectory)
guard let bundle = Bundle(url: appBundleURL) else { throw ALTError(.missingAppBundle) }
guard var infoDictionary = NSDictionary(contentsOf: bundle.infoPlistURL) as? [String: Any] else { throw ALTError(.missingInfoPlist) }
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
"CFBundleURLName": bundleIdentifier,
"CFBundleURLSchemes": [openURL.scheme!]] as [String : Any]
allURLSchemes.append(altstoreURLScheme)
infoDictionary[Bundle.Info.urlTypes] = allURLSchemes
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
completionHandler(.success(appBundleURL))
}
catch
{
completionHandler(.failure(error))
}
}
return progress
}
func resignAppBundle(at fileURL: URL, signer: ALTSigner, profile: ALTProvisioningProfile, completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let progress = signer.signApp(at: fileURL, provisioningProfile: profile) { (success, error) in
do
{
try Result(success, error).get()
let ipaURL = try FileManager.default.zipAppBundle(at: fileURL)
completionHandler(.success(ipaURL))
}
catch
{
completionHandler(.failure(error))
}
}
return progress
}
}

View File

@@ -1,264 +0,0 @@
//
// Server.swift
// AltStore
//
// Created by Riley Testut on 5/30/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Network
import AltKit
extension ALTServerError
{
init<E: Error>(_ error: E)
{
switch error
{
case let error as ALTServerError: self = error
case is DecodingError: self = ALTServerError(.invalidResponse)
case is EncodingError: self = ALTServerError(.invalidRequest)
default:
assertionFailure("Caught unknown error type")
self = ALTServerError(.unknown)
}
}
}
enum InstallError: LocalizedError
{
case unknown
case cancelled
case invalidApp
case noUDID
case server(ALTServerError)
var errorDescription: String? {
switch self
{
case .server(let error): return error.localizedDescription
default: return nil
}
}
}
struct Server: Equatable
{
var service: NetService
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltStore.server", qos: .utility)
func installApp(at fileURL: URL, identifier: String, completionHandler: @escaping (Result<Void, InstallError>) -> Void)
{
var isFinished = false
var serverConnection: NWConnection?
func finish(error: InstallError?)
{
// Prevent duplicate callbacks if connection is lost.
guard !isFinished else { return }
isFinished = true
if let connection = serverConnection
{
connection.cancel()
}
if let error = error
{
print("Failed to install \(identifier).", error)
completionHandler(.failure(error))
}
else
{
print("Installed \(identifier)!")
completionHandler(.success(()))
}
}
self.connect { (result) in
switch result
{
case .failure(let error): finish(error: error)
case .success(let connection):
serverConnection = connection
self.sendApp(at: fileURL, via: connection) { (result) in
switch result
{
case .failure(let error): finish(error: error)
case .success:
self.receiveResponse(from: connection) { (result) in
switch result
{
case .success: finish(error: nil)
case .failure(let error): finish(error: .server(error))
}
}
}
}
}
}
}
}
private extension Server
{
func connect(completionHandler: @escaping (Result<NWConnection, InstallError>) -> Void)
{
let connection = NWConnection(to: .service(name: self.service.name, type: self.service.type, domain: self.service.domain, interface: nil), using: .tcp)
connection.stateUpdateHandler = { [weak service, unowned connection] (state) in
switch state
{
case .ready: completionHandler(.success(connection))
case .cancelled: completionHandler(.failure(.cancelled))
case .failed(let error):
print("Failed to connect to service \(service?.name ?? "").", error)
completionHandler(.failure(.server(.init(.connectionFailed))))
case .waiting: break
case .setup: break
case .preparing: break
@unknown default: break
}
}
connection.start(queue: self.dispatchQueue)
}
func sendApp(at fileURL: URL, via connection: NWConnection, completionHandler: @escaping (Result<Void, InstallError>) -> Void)
{
do
{
guard let appData = try? Data(contentsOf: fileURL) else { throw InstallError.invalidApp }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw InstallError.noUDID }
let request = ServerRequest(udid: udid, contentSize: appData.count)
let requestData = try JSONEncoder().encode(request)
let requestSize = Int32(requestData.count)
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
// Send request data size.
print("Sending request data size \(requestSize)")
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
if error != nil
{
completionHandler(.failure(.server(.init(.lostConnection))))
}
else
{
// Send request.
print("Sending request \(request)")
connection.send(content: requestData, completion: .contentProcessed { (error) in
if error != nil
{
completionHandler(.failure(.server(.init(.lostConnection))))
}
else
{
// Send app data.
print("Sending app data (Size: \(appData.count))")
connection.send(content: appData, completion: .contentProcessed { (error) in
if error != nil
{
completionHandler(.failure(.server(.init(.lostConnection))))
}
else
{
completionHandler(.success(()))
}
})
}
})
}
})
}
catch is EncodingError
{
completionHandler(.failure(.server(.init(.invalidRequest))))
}
catch let error as InstallError
{
completionHandler(.failure(error))
}
catch
{
assertionFailure("Unknown error type. \(error)")
completionHandler(.failure(.unknown))
}
}
func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
let size = MemoryLayout<Int32>.size
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
if let error = response.error
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(()))
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
}
}

View File

@@ -11,6 +11,11 @@ import Network
import AltKit
struct Server: Equatable
{
var service: NetService
}
class ServerManager: NSObject
{
static let shared = ServerManager()