diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 5dfd2871..a435da3a 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */; }; BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; }; BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; }; + BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */; }; + BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */; }; BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; }; BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648C22E79AC800E9056B /* ALTAppPermission.m */; }; BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */; }; @@ -127,9 +129,9 @@ BF718BD823C93DB700A89F2D /* AltKit.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD723C93DB700A89F2D /* AltKit.m */; }; BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */; }; BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; }; - BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* AppOperationContext.swift */; }; + BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* OperationContexts.swift */; }; BF770E5622BC3C03002A40FE /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5522BC3C02002A40FE /* Server.swift */; }; - BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.swift */; }; + BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */; }; BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */; }; BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; }; BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */; }; @@ -146,7 +148,6 @@ BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172823C56042001B5953 /* ServerConnection.swift */; }; BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; }; BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172C23C5823E001B5953 /* InstalledExtension.swift */; }; - BFA8172F23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */; }; BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; }; BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; }; BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; }; @@ -227,7 +228,6 @@ BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; }; BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; }; BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; }; - BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */; }; BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE944023F22AA100CDA07D /* AppIDComponents.swift */; }; BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; }; BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; }; @@ -332,6 +332,8 @@ BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = ""; }; BF29012E2318F6B100D88A45 /* AppBannerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AppBannerView.xib; sourceTree = ""; }; BF2901302318F7A800D88A45 /* AppBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBannerView.swift; sourceTree = ""; }; + BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchProvisioningProfilesOperation.swift; sourceTree = ""; }; + BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAppOperation.swift; sourceTree = ""; }; BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = ""; }; BF3D648B22E79AC800E9056B /* ALTAppPermission.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = ""; }; BF3D648C22E79AC800E9056B /* ALTAppPermission.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = ""; }; @@ -443,9 +445,9 @@ BF718BD723C93DB700A89F2D /* AltKit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AltKit.m; sourceTree = ""; }; BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardingNavigationController.swift; sourceTree = ""; }; BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = ""; }; - BF770E5322BC044E002A40FE /* AppOperationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOperationContext.swift; sourceTree = ""; }; + BF770E5322BC044E002A40FE /* OperationContexts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationContexts.swift; sourceTree = ""; }; BF770E5522BC3C02002A40FE /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = ""; }; - BF770E5722BC3D0F002A40FE /* OperationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationGroup.swift; sourceTree = ""; }; + BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshGroup.swift; sourceTree = ""; }; BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = ""; }; BF770E6822BD57DD002A40FE /* Silence.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Silence.m4a; sourceTree = ""; }; BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 3.xcdatamodel"; sourceTree = ""; }; @@ -464,7 +466,6 @@ BFA8172823C56042001B5953 /* ServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnection.swift; sourceTree = ""; }; BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnisetteDataOperation.swift; sourceTree = ""; }; BFA8172C23C5823E001B5953 /* InstalledExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledExtension.swift; sourceTree = ""; }; - BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrepareDeveloperAccountOperation.swift; sourceTree = ""; }; BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = ""; }; BFB1169C22932DB100BB457C /* apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apps.json; sourceTree = ""; }; @@ -552,7 +553,6 @@ BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = ""; }; BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = ""; }; - BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTApplication+AppExtensions.swift"; sourceTree = ""; }; BFEE944023F22AA100CDA07D /* AppIDComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDComponents.swift; sourceTree = ""; }; BFF0B68D23219520007A79E1 /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = ""; }; BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonComponents.swift; sourceTree = ""; }; @@ -1098,7 +1098,6 @@ BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */, BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */, BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */, - BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */, ); path = Extensions; sourceTree = ""; @@ -1161,13 +1160,14 @@ children = ( BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */, BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */, - BF770E5722BC3D0F002A40FE /* OperationGroup.swift */, - BF770E5322BC044E002A40FE /* AppOperationContext.swift */, + BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */, + BF770E5322BC044E002A40FE /* OperationContexts.swift */, BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */, BFB364592325985F00CD0EB1 /* FindServerOperation.swift */, - BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */, BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */, BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */, + BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */, + BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */, BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */, BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */, BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */, @@ -1681,6 +1681,7 @@ BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */, BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */, BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */, + BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */, BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */, BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */, BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */, @@ -1689,7 +1690,7 @@ BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */, BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */, BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */, - BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */, + BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */, BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */, BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, @@ -1697,7 +1698,6 @@ BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */, BFE6326822A858F300F30809 /* Account.swift in Sources */, BFE6326622A857C200F30809 /* Team.swift in Sources */, - BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */, BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */, @@ -1721,10 +1721,9 @@ BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */, BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */, BF7C627423DBB78C00515A2D /* InstalledAppPolicy.swift in Sources */, - BFA8172F23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift in Sources */, BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */, BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */, - BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */, + BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */, BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */, BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */, BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */, @@ -1751,6 +1750,7 @@ BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */, BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */, BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */, + BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */, BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */, BF770E5622BC3C03002A40FE /* Server.swift in Sources */, BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */, diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index 65dc2c40..3184eefb 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -64,6 +64,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? private var runningApplications: Set? + private var backgroundRefreshContext: NSManagedObjectContext? // Keep context alive until finished refreshing. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -209,6 +210,8 @@ extension AppDelegate } taskCompletionHandler() + + self.backgroundRefreshContext = nil } if let error = taskResult.error @@ -339,7 +342,6 @@ private extension AppDelegate dispatchGroup.enter() DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in - let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context) guard !installedApps.isEmpty else { serversResult = .success(()) @@ -351,6 +353,7 @@ private extension AppDelegate } self.runningApplications = [] + self.backgroundRefreshContext = context let identifiers = installedApps.compactMap { $0.bundleIdentifier } print("Apps to refresh:", identifiers) @@ -398,7 +401,7 @@ private extension AppDelegate // Also since AltServer has already received the app, it can finish installing even if we're no longer running in background. - if let error = group.error + if let error = group.context.error { self.scheduleFinishedRefreshingNotification(for: .failure(error), identifier: identifier) } @@ -410,8 +413,8 @@ private extension AppDelegate self.scheduleFinishedRefreshingNotification(for: .success(results), identifier: identifier) } } - group.completionHandler = { (result) in - completionHandler(result) + group.completionHandler = { (results) in + completionHandler(.success(results)) } } } diff --git a/AltStore/Authentication/RefreshAltStoreViewController.swift b/AltStore/Authentication/RefreshAltStoreViewController.swift index 74c33570..6c17f427 100644 --- a/AltStore/Authentication/RefreshAltStoreViewController.swift +++ b/AltStore/Authentication/RefreshAltStoreViewController.swift @@ -13,8 +13,7 @@ import Roxas class RefreshAltStoreViewController: UIViewController { - var signer: ALTSigner! - var session: ALTAppleAPISession! + var context: AuthenticatedOperationContext! var completionHandler: ((Result) -> Void)? @@ -42,18 +41,18 @@ private extension RefreshAltStoreViewController { sender.isIndicatingActivity = true - if let progress = AppManager.shared.refreshProgress(for: altStore) ?? AppManager.shared.installationProgress(for: altStore) + if let progress = AppManager.shared.installationProgress(for: altStore) { - // Cancel pending AltStore refresh so we can start a new one. + // Cancel pending AltStore installation so we can start a new one. progress.cancel() } - - let group = OperationGroup() - group.signer = self.signer // Prevent us from trying to authenticate a second time. - group.session = self.session // ^ - group.completionHandler = { (result) in - if let error = result.error ?? result.value?.values.compactMap({ $0.error }).first + + // Install, _not_ refresh, to ensure we are installing with a non-revoked certificate. + let progress = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in + switch result { + case .success: self.completionHandler?(.success(())) + case .failure(let error): DispatchQueue.main.async { sender.progress = nil sender.isIndicatingActivity = false @@ -69,14 +68,9 @@ private extension RefreshAltStoreViewController self.present(alertController, animated: true, completion: nil) } } - else - { - self.completionHandler?(.success(())) - } } - _ = AppManager.shared.refresh([altStore], presentingViewController: self, group: group) - sender.progress = group.progress + sender.progress = progress } refresh() diff --git a/AltStore/Extensions/ALTApplication+AppExtensions.swift b/AltStore/Extensions/ALTApplication+AppExtensions.swift deleted file mode 100644 index 3ed8f0d2..00000000 --- a/AltStore/Extensions/ALTApplication+AppExtensions.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// ALTApplication+AppExtensions.swift -// AltStore -// -// Created by Riley Testut on 2/10/20. -// Copyright © 2020 Riley Testut. All rights reserved. -// - -import AltSign - -extension ALTApplication -{ - var appExtensions: Set { - guard let bundle = Bundle(url: self.fileURL) else { return [] } - - var appExtensions: Set = [] - - if let directory = bundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) - { - for case let fileURL as URL in enumerator where fileURL.pathExtension.lowercased() == "appex" - { - guard let appExtension = ALTApplication(fileURL: fileURL) else { continue } - appExtensions.insert(appExtension) - } - } - - return appExtensions - } -} diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 1f384bf3..c6ac1229 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -30,7 +30,7 @@ class AppManager static let shared = AppManager() private let operationQueue = OperationQueue() - private let processingQueue = DispatchQueue(label: "com.altstore.AppManager.processingQueue") + private let serialOperationQueue = OperationQueue() private var installationProgress = [String: Progress]() private var refreshProgress = [String: Progress]() @@ -38,6 +38,9 @@ class AppManager private init() { self.operationQueue.name = "com.altstore.AppManager.operationQueue" + + self.serialOperationQueue.name = "com.altstore.AppManager.serialOperationQueue" + self.serialOperationQueue.maxConcurrentOperationCount = 1 } } @@ -100,36 +103,47 @@ extension AppManager } @discardableResult - func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTSigner, ALTAppleAPISession), Error>) -> Void) -> OperationGroup + func findServer(context: OperationContext = OperationContext(), completionHandler: @escaping (Result) -> Void) -> FindServerOperation { - let group = OperationGroup() - - let findServerOperation = FindServerOperation(group: group) + let findServerOperation = FindServerOperation(context: context) findServerOperation.resultHandler = { (result) in switch result { - case .failure(let error): group.error = error - case .success(let server): group.server = server + case .failure(let error): context.error = error + case .success(let server): context.server = server } } - self.operationQueue.addOperation(findServerOperation) - let authenticationOperation = AuthenticationOperation(group: group, presentingViewController: presentingViewController) + self.run([findServerOperation]) + + return findServerOperation + } + + @discardableResult + func authenticate(presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>) -> Void) -> AuthenticationOperation + { + if let operation = context.authenticationOperation + { + return operation + } + + let findServerOperation = self.findServer(context: context) { _ in } + + let authenticationOperation = AuthenticationOperation(context: context, presentingViewController: presentingViewController) authenticationOperation.resultHandler = { (result) in switch result { - case .failure(let error): group.error = error - case .success(let signer, let session): - group.signer = signer - group.session = session + case .failure(let error): context.error = error + case .success: break } completionHandler(result) } authenticationOperation.addDependency(findServerOperation) - self.operationQueue.addOperation(authenticationOperation) - return group + self.run([authenticationOperation]) + + return authenticationOperation } } @@ -154,46 +168,30 @@ extension AppManager NotificationCenter.default.post(name: AppManager.didFetchSourceNotification, object: self) } } - self.operationQueue.addOperation(fetchSourceOperation) + self.run([fetchSourceOperation]) } } func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void) { - var group: OperationGroup! - group = self.authenticate(presentingViewController: nil) { (result) in - switch result - { - case .failure(let error): - completionHandler(.failure(error)) - - case .success: - let fetchAppIDsOperation = FetchAppIDsOperation(group: group) - fetchAppIDsOperation.resultHandler = completionHandler - self.operationQueue.addOperation(fetchAppIDsOperation) - } + let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in + print("Authenticated for fetching App IDs with result:", result) } + + let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context) + fetchAppIDsOperation.resultHandler = completionHandler + fetchAppIDsOperation.addDependency(authenticationOperation) + self.run([fetchAppIDsOperation]) } -} - -extension AppManager -{ - func install(_ app: AppProtocol, presentingViewController: UIViewController, completionHandler: @escaping (Result) -> Void) -> Progress + + @discardableResult + func install(_ app: T, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result) -> Void) -> Progress { - if let progress = self.installationProgress(for: app) - { - return progress - } - - let bundleIdentifier = app.bundleIdentifier - - let group = self.install([app], forceDownload: true, presentingViewController: presentingViewController) - group.completionHandler = { (result) in + let group = RefreshGroup(context: context) + group.completionHandler = { (results) in do { - self.installationProgress[bundleIdentifier] = nil - - guard let (_, result) = try result.get().first else { throw OperationError.unknown } + guard let result = results.values.first else { throw OperationError.unknown } completionHandler(result) } catch @@ -202,24 +200,19 @@ extension AppManager } } - self.installationProgress[bundleIdentifier] = group.progress + let operation = AppOperation.install(app) + self.perform([operation], presentingViewController: presentingViewController, group: group) return group.progress } - func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup + @discardableResult + func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: RefreshGroup? = nil) -> RefreshGroup { - let apps = installedApps.filter { self.refreshProgress(for: $0) == nil || self.refreshProgress(for: $0)?.isCancelled == true } - - let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group) + let group = group ?? RefreshGroup() - for app in apps - { - guard let progress = group.progress(for: app) else { continue } - self.refreshProgress[app.bundleIdentifier] = progress - } - - return group + let operations = installedApps.map { AppOperation.refresh($0) } + return self.perform(operations, presentingViewController: presentingViewController, group: group) } func installationProgress(for app: AppProtocol) -> Progress? @@ -237,264 +230,202 @@ extension AppManager private extension AppManager { - func install(_ apps: [AppProtocol], forceDownload: Bool, presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup + enum AppOperation { - // Authenticate -> Download (if necessary) -> Resign -> Send -> Install. - let group = group ?? OperationGroup() - var operations = [Operation]() + case install(AppProtocol) + case refresh(AppProtocol) - /* Find Server */ - let findServerOperation = FindServerOperation(group: group) - findServerOperation.resultHandler = { (result) in - switch result + var app: AppProtocol { + switch self { - case .failure(let error): group.error = error - case .success(let server): group.server = server + case .install(let app), .refresh(let app): return app } } - operations.append(findServerOperation) - let authenticationOperation: AuthenticationOperation? - - if group.signer == nil || group.session == nil - { - /* Authenticate */ - let operation = AuthenticationOperation(group: group, presentingViewController: presentingViewController) - operation.resultHandler = { (result) in - switch result - { - case .failure(let error): group.error = error - case .success(let signer, let session): - group.signer = signer - group.session = session - } - } - operations.append(operation) - operation.addDependency(findServerOperation) + var bundleIdentifier: String { + var bundleIdentifier: String! - authenticationOperation = operation - } - else - { - authenticationOperation = nil - } - - let refreshAnisetteDataOperation = FetchAnisetteDataOperation(group: group) - refreshAnisetteDataOperation.resultHandler = { (result) in - switch result + if let context = (self.app as? NSManagedObject)?.managedObjectContext { - case .failure(let error): group.error = error - case .success(let anisetteData): group.session?.anisetteData = anisetteData - } - } - refreshAnisetteDataOperation.addDependency(authenticationOperation ?? findServerOperation) - operations.append(refreshAnisetteDataOperation) - - /* Prepare Developer Account */ - let prepareDeveloperAccountOperation = PrepareDeveloperAccountOperation(group: group) - prepareDeveloperAccountOperation.resultHandler = { (result) in - switch result - { - case .failure(let error): group.error = error - case .success: break - } - } - prepareDeveloperAccountOperation.addDependency(refreshAnisetteDataOperation) - operations.append(prepareDeveloperAccountOperation) - - for app in apps - { - let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, group: group) - let progress = Progress.discreteProgress(totalUnitCount: 100) - - - /* Resign */ - let resignAppOperation = ResignAppOperation(context: context) - resignAppOperation.resultHandler = { (result) in - guard let resignedApp = self.process(result, context: context) else { return } - context.resignedApp = resignedApp - } - resignAppOperation.addDependency(prepareDeveloperAccountOperation) - progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20) - operations.append(resignAppOperation) - - - /* Download */ - let fileURL = InstalledApp.fileURL(for: app) - - var localApp: ALTApplication? - - let managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() - managedObjectContext.performAndWait { - let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), context.bundleIdentifier) - - if let installedApp = InstalledApp.first(satisfying: predicate, in: managedObjectContext), FileManager.default.fileExists(atPath: fileURL.path), !forceDownload - { - localApp = ALTApplication(fileURL: installedApp.fileURL) - } - } - - if let localApp = localApp - { - // Already installed, don't need to download. - - // If we don't need to download the app, reduce the total unit count by 40. - progress.totalUnitCount -= 40 - - context.app = localApp + context.performAndWait { bundleIdentifier = self.app.bundleIdentifier } } else { - // App is not yet installed (or we're forcing it to download a new version), so download it before resigning it. - - let downloadOperation = DownloadAppOperation(app: app, context: context) - downloadOperation.resultHandler = { (result) in - guard let app = self.process(result, context: context) else { return } - context.app = app - } - progress.addChild(downloadOperation.progress, withPendingUnitCount: 40) - downloadOperation.addDependency(findServerOperation) - resignAppOperation.addDependency(downloadOperation) - operations.append(downloadOperation) + bundleIdentifier = self.app.bundleIdentifier } - /* Send */ - let sendAppOperation = SendAppOperation(context: context) - sendAppOperation.resultHandler = { (result) in - guard let installationConnection = self.process(result, context: context) else { return } - context.installationConnection = installationConnection - } - progress.addChild(sendAppOperation.progress, withPendingUnitCount: 10) - sendAppOperation.addDependency(resignAppOperation) - operations.append(sendAppOperation) - - - let beginInstallationHandler = group.beginInstallationHandler - group.beginInstallationHandler = { (installedApp) in - if installedApp.bundleIdentifier == StoreApp.altstoreAppID - { - self.scheduleExpirationWarningLocalNotification(for: installedApp) - } - - beginInstallationHandler?(installedApp) - } - - /* Install */ - let installOperation = InstallAppOperation(context: context) - installOperation.resultHandler = { (result) in - if let error = result.error - { - context.error = error - } - - if let installedApp = result.value - { - if let app = app as? StoreApp, let storeApp = installedApp.managedObjectContext?.object(with: app.objectID) as? StoreApp - { - installedApp.storeApp = storeApp - } - - context.installedApp = installedApp - } - - self.finishAppOperation(context) // Finish operation no matter what. - } - progress.addChild(installOperation.progress, withPendingUnitCount: 30) - installOperation.addDependency(sendAppOperation) - operations.append(installOperation) - - group.set(progress, for: app) + return bundleIdentifier } + } + + @discardableResult + private func perform(_ operations: [AppOperation], presentingViewController: UIViewController?, group: RefreshGroup) -> RefreshGroup + { + let operations = operations.filter { self.progress(for: $0) == nil || self.progress(for: $0)?.isCancelled == true } - // Refresh anisette data after downloading all apps to prevent session from expiring. - for case let downloadOperation as DownloadAppOperation in operations + for operation in operations { - refreshAnisetteDataOperation.addDependency(downloadOperation) + let progress = Progress.discreteProgress(totalUnitCount: 100) + self.set(progress, for: operation) } - /* Cache App IDs */ - let fetchAppIDsOperation = FetchAppIDsOperation(group: group) - fetchAppIDsOperation.resultHandler = { (result) in - do - { - let (_, context) = try result.get() - try context.save() - } - catch - { - print("Failed to fetch App IDs.", error) + /* Authenticate (if necessary) */ + var authenticationOperation: AuthenticationOperation? + if group.context.session == nil + { + authenticationOperation = self.authenticate(presentingViewController: presentingViewController, context: group.context) { (result) in + switch result + { + case .failure(let error): group.context.error = error + case .success: break + } } } - operations.forEach { fetchAppIDsOperation.addDependency($0) } - operations.append(fetchAppIDsOperation) - group.addOperations(operations) + func performAppOperations() + { + for operation in operations + { + let progress = self.progress(for: operation) + + if let progress = progress + { + group.progress.totalUnitCount += 1 + group.progress.addChild(progress, withPendingUnitCount: 1) + + if group.context.session != nil + { + // Finished authenticating, so increase completed unit count. + progress.completedUnitCount += 20 + } + } + + switch operation + { + case .refresh(let installedApp as InstalledApp) where installedApp.certificateSerialNumber == group.context.certificate?.serialNumber: + // Refreshing apps, but using same certificate as last time, so we can just refresh provisioning profiles. + + let refreshProgress = self._refresh(installedApp, group: group) { (result) in + self.finish(operation, result: result, group: group, progress: progress) + } + progress?.addChild(refreshProgress, withPendingUnitCount: 80) + + case .refresh(let app), .install(let app): + // Either installing for first time, or refreshing with a different signing certificate, + // so we need to resign the app then install it. + + let installProgress = self._install(app, group: group) { (result) in + self.finish(operation, result: result, group: group, progress: progress) + } + progress?.addChild(installProgress, withPendingUnitCount: 80) + } + } + } + + if let authenticationOperation = authenticationOperation + { + let awaitAuthenticationOperation = BlockOperation { + if let managedObjectContext = operations.lazy.compactMap({ ($0.app as? NSManagedObject)?.managedObjectContext }).first + { + managedObjectContext.perform { performAppOperations() } + } + else + { + performAppOperations() + } + } + awaitAuthenticationOperation.addDependency(authenticationOperation) + self.run([awaitAuthenticationOperation], requiresSerialQueue: true) + } + else + { + performAppOperations() + } return group } - @discardableResult func process(_ result: Result, context: AppOperationContext) -> T? - { - do - { - let value = try result.get() - return value - } - catch OperationError.cancelled - { - context.error = OperationError.cancelled - self.finishAppOperation(context) - - return nil - } - catch - { - context.error = error - return nil - } - } - func finishAppOperation(_ context: AppOperationContext) + private func _install(_ app: AppProtocol, group: RefreshGroup, completionHandler: @escaping (Result) -> Void) -> Progress { - self.processingQueue.sync { - guard !context.isFinished else { return } - context.isFinished = true - - if let progress = self.refreshProgress[context.bundleIdentifier], progress == context.group.progress(forAppWithBundleIdentifier: context.bundleIdentifier) + let progress = Progress.discreteProgress(totalUnitCount: 100) + + let context = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context) + context.beginInstallationHandler = group.beginInstallationHandler + + /* Download */ + let downloadOperation = DownloadAppOperation(app: app, context: context) + downloadOperation.resultHandler = { (result) in + switch result { - // Only remove progress if it hasn't been replaced by another one. - self.refreshProgress[context.bundleIdentifier] = nil + case .failure(let error): context.error = error + case .success(let app): context.app = app } - - if let error = context.error + } + progress.addChild(downloadOperation.progress, withPendingUnitCount: 25) + + + /* Refresh Anisette Data */ + let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context) + refreshAnisetteDataOperation.resultHandler = { (result) in + switch result { - switch error + case .failure(let error): context.error = error + case .success(let anisetteData): group.context.session?.anisetteData = anisetteData + } + } + refreshAnisetteDataOperation.addDependency(downloadOperation) + + + /* Fetch Provisioning Profiles */ + let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context) + fetchProvisioningProfilesOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): context.error = error + case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles + } + } + fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation) + progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5) + + + /* Resign */ + let resignAppOperation = ResignAppOperation(context: context) + resignAppOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): context.error = error + case .success(let resignedApp): context.resignedApp = resignedApp + } + } + resignAppOperation.addDependency(fetchProvisioningProfilesOperation) + progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20) + + + /* Send */ + let sendAppOperation = SendAppOperation(context: context) + sendAppOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): context.error = error + case .success(let installationConnection): context.installationConnection = installationConnection + } + } + sendAppOperation.addDependency(resignAppOperation) + progress.addChild(sendAppOperation.progress, withPendingUnitCount: 20) + + + /* Install */ + let installOperation = InstallAppOperation(context: context) + installOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let installedApp): + if let app = app as? StoreApp, let storeApp = installedApp.managedObjectContext?.object(with: app.objectID) as? StoreApp { - case let error as ALTServerError where error.code == .deviceNotFound || error.code == .lostConnection: - if let server = context.group.server, server.isPreferred - { - // Preferred server, so report errors normally. - context.group.results[context.bundleIdentifier] = .failure(error) - } - else - { - // Not preferred server, so ignore these specific errors and throw serverNotFound instead. - context.group.results[context.bundleIdentifier] = .failure(ConnectionError.serverNotFound) - } - - case let error: - context.group.results[context.bundleIdentifier] = .failure(error) - } - - } - else if let installedApp = context.installedApp - { - context.group.results[context.bundleIdentifier] = .success(installedApp) - - // Save after each installation. - installedApp.managedObjectContext?.performAndWait { - do { try installedApp.managedObjectContext?.save() } - catch { print("Error saving installed app.", error) } + installedApp.storeApp = storeApp } if let index = UserDefaults.standard.legacySideloadedApps?.firstIndex(of: installedApp.bundleIdentifier) @@ -502,21 +433,100 @@ private extension AppManager // No longer a legacy sideloaded app, so remove it from cached list. UserDefaults.standard.legacySideloadedApps?.remove(at: index) } - } - - print("Finished operation!", context.bundleIdentifier) - - if context.group.results.count == context.group.progress.totalUnitCount - { - context.group.completionHandler?(.success(context.group.results)) - let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() - backgroundContext.performAndWait { - guard let altstore = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: backgroundContext) else { return } - self.scheduleExpirationWarningLocalNotification(for: altstore) - } + completionHandler(.success(installedApp)) } } + progress.addChild(installOperation.progress, withPendingUnitCount: 30) + installOperation.addDependency(sendAppOperation) + + let operations = [downloadOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation] + group.add(operations) + self.run(operations) + + return progress + } + + private func _refresh(_ app: InstalledApp, group: RefreshGroup, completionHandler: @escaping (Result) -> Void) -> Progress + { + let progress = Progress.discreteProgress(totalUnitCount: 100) + + let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context) + context.app = ALTApplication(fileURL: app.url) + + /* Fetch Provisioning Profiles */ + let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context) + fetchProvisioningProfilesOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): context.error = error + case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles + } + } + progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60) + + /* Refresh */ + let refreshAppOperation = RefreshAppOperation(context: context) + refreshAppOperation.resultHandler = { (result) in + completionHandler(result) + } + progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40) + refreshAppOperation.addDependency(fetchProvisioningProfilesOperation) + + let operations = [fetchProvisioningProfilesOperation, refreshAppOperation] + group.add(operations) + self.run(operations) + + return progress + } + + func finish(_ operation: AppOperation, result: Result, group: RefreshGroup, progress: Progress?) + { + let result = result.mapError { (resultError) -> Error in + guard let error = resultError as? ALTServerError else { return resultError } + + switch error.code + { + case .deviceNotFound, .lostConnection: + if let server = group.context.server, server.isPreferred || server.isWiredConnection + { + // Preferred server (or wired connection), so report errors normally. + return error + } + else + { + // Not preferred server, so ignore these specific errors and throw serverNotFound instead. + return ConnectionError.serverNotFound + } + + default: return error + } + } + + // Must remove before saving installedApp. + if let currentProgress = self.progress(for: operation), currentProgress == progress + { + // Only remove progress if it hasn't been replaced by another one. + self.set(nil, for: operation) + } + + do + { + let installedApp = try result.get() + group.set(.success(installedApp), forAppWithBundleIdentifier: installedApp.bundleIdentifier) + + if installedApp.bundleIdentifier == StoreApp.altstoreAppID + { + self.scheduleExpirationWarningLocalNotification(for: installedApp) + } + + do { try installedApp.managedObjectContext?.save() } + catch { print("Error saving installed app.", error) } + } + catch + { + group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier) + } } func scheduleExpirationWarningLocalNotification(for app: InstalledApp) @@ -539,4 +549,44 @@ private extension AppManager let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) } + + func run(_ operations: [Foundation.Operation], requiresSerialQueue: Bool = false) + { + for operation in operations + { + switch operation + { + case _ where requiresSerialQueue: fallthrough + case is InstallAppOperation, is RefreshAppOperation: + if let previousOperation = self.serialOperationQueue.operations.last + { + // Ensure operations execute in the order they're added, since they may become ready at different points. + operation.addDependency(previousOperation) + } + + self.serialOperationQueue.addOperation(operation) + + default: + self.operationQueue.addOperation(operation) + } + } + } + + func progress(for operation: AppOperation) -> Progress? + { + switch operation + { + case .install: return self.installationProgress[operation.bundleIdentifier] + case .refresh: return self.refreshProgress[operation.bundleIdentifier] + } + } + + func set(_ progress: Progress?, for operation: AppOperation) + { + switch operation + { + case .install: self.installationProgress[operation.bundleIdentifier] = progress + case .refresh: self.refreshProgress[operation.bundleIdentifier] = progress + } + } } diff --git a/AltStore/Model/AltStore.xcdatamodeld/AltStore 4.xcdatamodel/contents b/AltStore/Model/AltStore.xcdatamodeld/AltStore 4.xcdatamodel/contents index 4b75b6dd..0b0dd4f7 100644 --- a/AltStore/Model/AltStore.xcdatamodeld/AltStore 4.xcdatamodel/contents +++ b/AltStore/Model/AltStore.xcdatamodeld/AltStore 4.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -33,6 +33,7 @@ + diff --git a/AltStore/Model/DatabaseManager.swift b/AltStore/Model/DatabaseManager.swift index f5d65222..1efd0b93 100644 --- a/AltStore/Model/DatabaseManager.swift +++ b/AltStore/Model/DatabaseManager.swift @@ -158,6 +158,7 @@ private extension DatabaseManager storeApp.source = altStoreSource } + let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String let installedApp: InstalledApp if let app = storeApp.installedApp @@ -166,7 +167,7 @@ private extension DatabaseManager } else { - installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, context: context) + installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context) installedApp.storeApp = storeApp } @@ -194,7 +195,7 @@ private extension DatabaseManager } // Must go after comparing versions to see if we need to update our cached AltStore app bundle. - installedApp.update(resignedApp: localApp) + installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber) do { diff --git a/AltStore/Model/InstalledApp.swift b/AltStore/Model/InstalledApp.swift index e2b5dd73..ea157f94 100644 --- a/AltStore/Model/InstalledApp.swift +++ b/AltStore/Model/InstalledApp.swift @@ -36,6 +36,8 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol @NSManaged var expirationDate: Date @NSManaged var installedDate: Date + @NSManaged var certificateSerialNumber: String? + /* Relationships */ @NSManaged var storeApp: StoreApp? @NSManaged var team: Team? @@ -50,7 +52,7 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol super.init(entity: entity, insertInto: context) } - init(resignedApp: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext) + init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext) { super.init(entity: InstalledApp.entity(), insertInto: context) @@ -61,22 +63,29 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile. - self.update(resignedApp: resignedApp) + self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber) } - func update(resignedApp: ALTApplication) + func update(resignedApp: ALTApplication, certificateSerialNumber: String?) { self.name = resignedApp.name self.resignedBundleIdentifier = resignedApp.bundleIdentifier self.version = resignedApp.version + + self.certificateSerialNumber = certificateSerialNumber if let provisioningProfile = resignedApp.provisioningProfile { - self.refreshedDate = provisioningProfile.creationDate - self.expirationDate = provisioningProfile.expirationDate + self.update(provisioningProfile: provisioningProfile) } } + + func update(provisioningProfile: ALTProvisioningProfile) + { + self.refreshedDate = provisioningProfile.creationDate + self.expirationDate = provisioningProfile.expirationDate + } } extension InstalledApp @@ -153,7 +162,7 @@ extension InstalledApp if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date { - // Refresh AltStore last since it causes app to quit. + // Refresh AltStore last since it may cause app to quit. installedApps.append(altStoreApp) } diff --git a/AltStore/Model/InstalledExtension.swift b/AltStore/Model/InstalledExtension.swift index f766fa5b..0e9b5393 100644 --- a/AltStore/Model/InstalledExtension.swift +++ b/AltStore/Model/InstalledExtension.swift @@ -55,10 +55,15 @@ class InstalledExtension: NSManagedObject, InstalledAppProtocol if let provisioningProfile = resignedAppExtension.provisioningProfile { - self.refreshedDate = provisioningProfile.creationDate - self.expirationDate = provisioningProfile.expirationDate + self.update(provisioningProfile: provisioningProfile) } } + + func update(provisioningProfile: ALTProvisioningProfile) + { + self.refreshedDate = provisioningProfile.creationDate + self.expirationDate = provisioningProfile.expirationDate + } } extension InstalledExtension diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 696eb7b6..c2e162dc 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -42,7 +42,7 @@ class MyAppsViewController: UICollectionViewController private var isUpdateSectionCollapsed = true private var expandedAppUpdates = Set() private var isRefreshingAllApps = false - private var refreshGroup: OperationGroup? + private var refreshGroup: RefreshGroup? private var sideloadingProgress: Progress? // Cache @@ -313,7 +313,7 @@ private extension MyAppsViewController default: cell.bannerView.button.tintColor = .refreshRed } - if let refreshGroup = self.refreshGroup, let progress = refreshGroup.progress(for: installedApp), progress.fractionCompleted < 1.0 + if let progress = AppManager.shared.refreshProgress(for: installedApp), progress.fractionCompleted < 1.0 { cell.bannerView.button.progress = progress } @@ -398,85 +398,58 @@ private extension MyAppsViewController } } - func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping (Result<[String : Result], Error>) -> Void) + func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping ([String : Result]) -> Void) { - func refresh() - { - let group = AppManager.shared.refresh(installedApps, presentingViewController: self, group: self.refreshGroup) - group.completionHandler = { (result) in - DispatchQueue.main.async { + let group = AppManager.shared.refresh(installedApps, presentingViewController: self, group: self.refreshGroup) + group.completionHandler = { (results) in + DispatchQueue.main.async { + let failures = results.compactMapValues { (result) -> Error? in switch result { - case .failure(let error): - let toastView = ToastView(error: error) - toastView.show(in: self) - - case .success(let results): - let failures = results.compactMapValues { (result) -> Error? in - switch result - { - case .failure(OperationError.cancelled): return nil - case .failure(let error): return error - case .success: return nil - } - } - - guard !failures.isEmpty else { break } - - let toastView: ToastView - - if let failure = failures.first, results.count == 1 - { - toastView = ToastView(error: failure.value) - } - else - { - let localizedText: String - - if failures.count == 1 - { - localizedText = NSLocalizedString("Failed to refresh 1 app.", comment: "") - } - else - { - localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count)) - } - - let detailText = failures.first?.value.localizedDescription - - toastView = ToastView(text: localizedText, detailText: detailText) - toastView.preferredDuration = 2.0 - } - - toastView.show(in: self) + case .failure(OperationError.cancelled): return nil + case .failure(let error): return error + case .success: return nil + } + } + + guard !failures.isEmpty else { return } + + let toastView: ToastView + + if let failure = failures.first, results.count == 1 + { + toastView = ToastView(error: failure.value) + } + else + { + let localizedText: String + + if failures.count == 1 + { + localizedText = NSLocalizedString("Failed to refresh 1 app.", comment: "") + } + else + { + localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count)) } - self.refreshGroup = nil - completionHandler(result) + let detailText = failures.first?.value.localizedDescription + + toastView = ToastView(text: localizedText, detailText: detailText) + toastView.preferredDuration = 2.0 } + + toastView.show(in: self) } - self.refreshGroup = group - - UIView.performWithoutAnimation { - self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue)) - } + self.refreshGroup = nil + completionHandler(results) } - if installedApps.contains(where: { $0.bundleIdentifier == StoreApp.altstoreAppID }) - { - let alertController = UIAlertController(title: NSLocalizedString("Refresh AltStore?", comment: ""), message: NSLocalizedString("AltStore will quit when it is finished refreshing.", comment: ""), preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in - completionHandler(.failure(OperationError.cancelled)) - }) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh", comment: ""), style: .default) { (action) in - refresh() - }) - self.present(alertController, animated: true, completion: nil) - } - else - { - refresh() + self.refreshGroup = group + + UIView.performWithoutAnimation { + self.collectionView.reloadSections([Section.installedApps.rawValue]) } } } @@ -559,16 +532,16 @@ private extension MyAppsViewController return } - self.refresh([installedApp]) { (result) in + self.refresh([installedApp]) { (results) in // If an error occured, reload the section so the progress bar is no longer visible. - if result.error != nil || result.value?.values.contains(where: { $0.error != nil }) == true + if results.values.contains(where: { $0.error != nil }) { DispatchQueue.main.async { self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue)) } } - print("Finished refreshing with result:", result.error?.localizedDescription ?? "success") + print("Finished refreshing with results:", results.map { ($0, $1.error?.localizedDescription ?? "success") }) } } diff --git a/AltStore/Operations/AppOperationContext.swift b/AltStore/Operations/AppOperationContext.swift deleted file mode 100644 index 5087655c..00000000 --- a/AltStore/Operations/AppOperationContext.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Contexts.swift -// AltStore -// -// Created by Riley Testut on 6/20/19. -// Copyright © 2019 Riley Testut. All rights reserved. -// - -import Foundation -import CoreData -import Network - -import AltSign - -class AppOperationContext -{ - lazy var temporaryDirectory: URL = { - let temporaryDirectory = FileManager.default.uniqueTemporaryURL() - - do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) } - catch { self.error = error } - - return temporaryDirectory - }() - - var bundleIdentifier: String - var group: OperationGroup - - var app: ALTApplication? - var resignedApp: ALTApplication? - - var installationConnection: ServerConnection? - - var installedApp: InstalledApp? { - didSet { - self.installedAppContext = self.installedApp?.managedObjectContext - } - } - private var installedAppContext: NSManagedObjectContext? - - var isFinished = false - - var error: Error? { - get { - return _error ?? self.group.error - } - set { - _error = newValue - } - } - private var _error: Error? - - init(bundleIdentifier: String, group: OperationGroup) - { - self.bundleIdentifier = bundleIdentifier - self.group = group - } -} diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift index f5bbb4fa..1ba36aa8 100644 --- a/AltStore/Operations/AuthenticationOperation.swift +++ b/AltStore/Operations/AuthenticationOperation.swift @@ -32,9 +32,9 @@ enum AuthenticationError: LocalizedError } @objc(AuthenticationOperation) -class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> +class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)> { - let group: OperationGroup + let context: AuthenticatedOperationContext private weak var presentingViewController: UIViewController? @@ -56,22 +56,23 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> private var submitCodeAction: UIAlertAction? - init(group: OperationGroup, presentingViewController: UIViewController?) + init(context: AuthenticatedOperationContext, presentingViewController: UIViewController?) { - self.group = group + self.context = context self.presentingViewController = presentingViewController super.init() - + + self.context.authenticationOperation = self self.operationQueue.name = "com.altstore.AuthenticationOperation" - self.progress.totalUnitCount = 3 + self.progress.totalUnitCount = 4 } override func main() { super.main() - if let error = self.group.error + if let error = self.context.error { self.finish(.failure(error)) return @@ -85,6 +86,7 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> { case .failure(let error): self.finish(.failure(error)) case .success(let account, let session): + self.context.session = session self.progress.completedUnitCount += 1 // Fetch Team @@ -95,6 +97,7 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> { case .failure(let error): self.finish(.failure(error)) case .success(let team): + self.context.team = team self.progress.completedUnitCount += 1 // Fetch Certificate @@ -105,22 +108,33 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> { case .failure(let error): self.finish(.failure(error)) case .success(let certificate): + self.context.certificate = certificate self.progress.completedUnitCount += 1 - // Save account/team to disk. - self.save(team) { (result) in + // Register Device + self.registerCurrentDevice(for: team, session: session) { (result) in guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } switch result { case .failure(let error): self.finish(.failure(error)) case .success: - let signer = ALTSigner(team: team, certificate: certificate) + self.progress.completedUnitCount += 1 - // Must cache App IDs _after_ saving account/team to disk. - self.cacheAppIDs(signer: signer, session: session) { (result) in - let result = result.map { _ in (signer, session) } - self.finish(result) + // Save account/team to disk. + self.save(team) { (result) in + guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } + + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success: + // Must cache App IDs _after_ saving account/team to disk. + self.cacheAppIDs(team: team, session: session) { (result) in + let result = result.map { _ in (team, certificate, session) } + self.finish(result) + } + } } } } @@ -173,21 +187,21 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> } } - override func finish(_ result: Result<(ALTSigner, ALTAppleAPISession), Error>) + override func finish(_ result: Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>) { guard !self.isFinished else { return } - print("Finished authenticating with result:", result) + print("Finished authenticating with result:", result.error?.localizedDescription ?? "success") let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() context.perform { do { - let (signer, session) = try result.get() + let (altTeam, altCertificate, session) = try result.get() guard - let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), signer.team.account.identifier), in: context), - let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), signer.team.identifier), in: context) + let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context), + let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context) else { throw AuthenticationError.noTeam } // Account @@ -214,24 +228,19 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> team.isActiveTeam = false } - if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.team == nil - { - // No team assigned to AltStore app yet, so assume this team was used to originally install it. - altStoreApp.team = team - } - // Save try context.save() // Update keychain - Keychain.shared.appleIDEmailAddress = signer.team.account.appleID + Keychain.shared.appleIDEmailAddress = altTeam.account.appleID Keychain.shared.appleIDPassword = self.appleIDPassword - Keychain.shared.signingCertificate = signer.certificate.p12Data() - Keychain.shared.signingCertificatePassword = signer.certificate.machineIdentifier + Keychain.shared.signingCertificate = altCertificate.p12Data() + Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier self.showInstructionsIfNecessary() { (didShowInstructions) in + let signer = ALTSigner(team: altTeam, certificate: altCertificate) // Refresh screen must go last since a successful refresh will cause the app to quit. self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in super.finish(result) @@ -340,7 +349,7 @@ private extension AuthenticationOperation func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void) { - let fetchAnisetteDataOperation = FetchAnisetteDataOperation(group: self.group) + let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context) fetchAnisetteDataOperation.resultHandler = { (result) in switch result { @@ -557,13 +566,38 @@ private extension AuthenticationOperation } } - func cacheAppIDs(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { - let group = OperationGroup() - group.signer = signer - group.session = session + guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { + return completionHandler(.failure(OperationError.unknownUDID)) + } - let fetchAppIDsOperation = FetchAppIDsOperation(group: group) + ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (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, session: session) { (device, error) in + completionHandler(Result(device, error)) + } + } + } + catch + { + completionHandler(.failure(error)) + } + } + } + + func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + { + let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context) fetchAppIDsOperation.resultHandler = { (result) in do { @@ -611,8 +645,7 @@ private extension AuthenticationOperation #else DispatchQueue.main.async { let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController - refreshViewController.signer = signer - refreshViewController.session = session + refreshViewController.context = self.context refreshViewController.completionHandler = { _ in completionHandler(true) } diff --git a/AltStore/Operations/FetchAnisetteDataOperation.swift b/AltStore/Operations/FetchAnisetteDataOperation.swift index a6878fbf..d43411bb 100644 --- a/AltStore/Operations/FetchAnisetteDataOperation.swift +++ b/AltStore/Operations/FetchAnisetteDataOperation.swift @@ -16,26 +16,24 @@ import Roxas @objc(FetchAnisetteDataOperation) class FetchAnisetteDataOperation: ResultOperation { - let group: OperationGroup + let context: OperationContext - init(group: OperationGroup) + init(context: OperationContext) { - self.group = group - - super.init() + self.context = context } override func main() { super.main() - if let error = self.group.error + if let error = self.context.error { self.finish(.failure(error)) return } - guard let server = self.group.server else { return self.finish(.failure(OperationError.invalidParameters)) } + guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) } ServerManager.shared.connect(to: server) { (result) in switch result @@ -55,7 +53,7 @@ class FetchAnisetteDataOperation: ResultOperation case .success: print("Waiting for anisette data...") connection.receiveResponse() { (result) in - print("Receiving anisette data:", result) + print("Receiving anisette data:", result.error?.localizedDescription ?? "success") switch result { diff --git a/AltStore/Operations/FetchAppIDsOperation.swift b/AltStore/Operations/FetchAppIDsOperation.swift index d6c4099d..46f8ccc3 100644 --- a/AltStore/Operations/FetchAppIDsOperation.swift +++ b/AltStore/Operations/FetchAppIDsOperation.swift @@ -16,13 +16,13 @@ import Roxas @objc(FetchAppIDsOperation) class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)> { - let group: OperationGroup - let context: NSManagedObjectContext + let context: AuthenticatedOperationContext + let managedObjectContext: NSManagedObjectContext - init(group: OperationGroup, context: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()) + init(context: AuthenticatedOperationContext, managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()) { - self.group = group self.context = context + self.managedObjectContext = managedObjectContext super.init() } @@ -31,24 +31,24 @@ class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)> { super.main() - if let error = self.group.error + if let error = self.context.error { self.finish(.failure(error)) return } guard - let team = self.group.signer?.team, - let session = self.group.session + let team = self.context.team, + let session = self.context.session else { return self.finish(.failure(OperationError.invalidParameters)) } ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in - self.context.perform { + self.managedObjectContext.perform { do { let fetchedAppIDs = try Result(appIDs, error).get() - guard let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), team.identifier), in: self.context) else { throw OperationError.notAuthenticated } + guard let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), team.identifier), in: self.managedObjectContext) else { throw OperationError.notAuthenticated } let fetchedIdentifiers = fetchedAppIDs.map { $0.identifier } @@ -57,11 +57,11 @@ class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)> #keyPath(AppID.team), team, #keyPath(AppID.identifier), fetchedIdentifiers) - let deletedAppIDs = try self.context.fetch(deletedAppIDsRequest) - deletedAppIDs.forEach { self.context.delete($0) } + let deletedAppIDs = try self.managedObjectContext.fetch(deletedAppIDsRequest) + deletedAppIDs.forEach { self.managedObjectContext.delete($0) } - let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.context) } - self.finish(.success((appIDs, self.context))) + let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.managedObjectContext) } + self.finish(.success((appIDs, self.managedObjectContext))) } catch { diff --git a/AltStore/Operations/FetchProvisioningProfilesOperation.swift b/AltStore/Operations/FetchProvisioningProfilesOperation.swift new file mode 100644 index 00000000..d32e4774 --- /dev/null +++ b/AltStore/Operations/FetchProvisioningProfilesOperation.swift @@ -0,0 +1,398 @@ +// +// FetchProvisioningProfilesOperation.swift +// AltStore +// +// Created by Riley Testut on 2/27/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation +import Roxas + +import AltSign + +@objc(FetchProvisioningProfilesOperation) +class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]> +{ + let context: AppOperationContext + + init(context: AppOperationContext) + { + self.context = context + + super.init() + + self.progress.totalUnitCount = 1 + } + + override func main() + { + super.main() + + if let error = self.context.error + { + self.finish(.failure(error)) + return + } + + guard + let app = self.context.app, + let team = self.context.team, + let session = self.context.session + else { return self.finish(.failure(OperationError.invalidParameters)) } + + self.progress.totalUnitCount = Int64(1 + app.appExtensions.count) + + self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in + do + { + self.progress.completedUnitCount += 1 + + let profile = try result.get() + + var profiles = [app.bundleIdentifier: profile] + var error: Error? + + let dispatchGroup = DispatchGroup() + + for appExtension in app.appExtensions + { + dispatchGroup.enter() + + self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in + switch result + { + case .failure(let e): error = e + case .success(let profile): profiles[appExtension.bundleIdentifier] = profile + } + + dispatchGroup.leave() + + self.progress.completedUnitCount += 1 + } + } + + dispatchGroup.notify(queue: .global()) { + if let error = error + { + self.finish(.failure(error)) + } + else + { + self.finish(.success(profiles)) + } + } + } + catch + { + self.finish(.failure(error)) + } + } + } + + func process(_ result: Result) -> 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 + } + } +} + +extension FetchProvisioningProfilesOperation +{ + func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + { + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + + let preferredBundleID: String + + // Check if we have already installed this app with this team before. + let predicate = NSPredicate(format: "%K == %@ AND %K == %@", + #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier, + #keyPath(InstalledApp.team.identifier), team.identifier) + if let installedApp = InstalledApp.first(satisfying: predicate, in: context) + { + #if DEBUG + + if app.bundleIdentifier == StoreApp.altstoreAppID || app.bundleIdentifier == StoreApp.alternativeAltStoreAppID + { + // Use legacy bundle ID format for AltStore. + preferredBundleID = "com.\(team.identifier).\(app.bundleIdentifier)" + } + else + { + preferredBundleID = installedApp.resignedBundleIdentifier + } + + #else + + // This app is already installed, so use the same resigned bundle identifier as before. + // This way, if we change the identifier format (again), AltStore will continue to use + // the old bundle identifier to prevent it from installing as a new app. + preferredBundleID = installedApp.resignedBundleIdentifier + + #endif + } + else + { + // This app isn't already installed, so create the resigned bundle identifier ourselves. + // Or, if the app _is_ installed but with a different team, we need to create a new + // bundle identifier anyway to prevent collisions with the previous team. + let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier + let updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track. + + if app.bundleIdentifier == StoreApp.altstoreAppID || app.bundleIdentifier == StoreApp.alternativeAltStoreAppID + { + // Use legacy bundle ID format for AltStore. + preferredBundleID = "com.\(team.identifier).\(app.bundleIdentifier)" + } + else + { + preferredBundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID) + } + } + + let preferredName: String + + if let parentApp = parentApp + { + preferredName = parentApp.name + " " + app.name + } + else + { + preferredName = app.name + } + + // Register + self.registerAppID(for: app, name: preferredName, bundleIdentifier: preferredBundleID, team: team, session: session) { (result) in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let appID): + + // Update features + self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let appID): + + // Update app groups + self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let appID): + + // Fetch Provisioning Profile + self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in + completionHandler(result) + } + } + } + } + } + } + } + } + } + + func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + { + ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in + do + { + let appIDs = try Result(appIDs, error).get() + + if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleIdentifier }) + { + completionHandler(.success(appID)) + } + else + { + let requiredAppIDs = 1 + application.appExtensions.count + let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count) + + let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 }) + + if team.type == .free + { + if requiredAppIDs > availableAppIDs + { + if let expirationDate = sortedExpirationDates.first + { + throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate) + } + else + { + throw ALTAppleAPIError(.maximumAppIDLimitReached) + } + } + } + + ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in + do + { + do + { + let appID = try Result(appID, error).get() + completionHandler(.success(appID)) + } + catch ALTAppleAPIError.maximumAppIDLimitReached + { + if let expirationDate = sortedExpirationDates.first + { + throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate) + } + else + { + throw ALTAppleAPIError(.maximumAppIDLimitReached) + } + } + } + catch + { + completionHandler(.failure(error)) + } + } + } + } + catch + { + completionHandler(.failure(error)) + } + } + } + + func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + { + let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in + guard let feature = ALTFeature(entitlement: entitlement) else { return nil } + return (feature, value) + } + + var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 } + + if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty + { + features[.appGroups] = true + } + + var updateFeatures = false + + // Determine whether the required features are already enabled for the AppID. + for (feature, value) in features + { + if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue) + { + // AppID already has this feature enabled and the values are the same. + continue + } + else + { + // AppID either doesn't have this feature enabled or the value has changed, + // so we need to update it to reflect new values. + updateFeatures = true + break + } + } + + if updateFeatures + { + let appID = appID.copy() as! ALTAppID + appID.features = features + + ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in + completionHandler(Result(appID, error)) + } + } + else + { + completionHandler(.success(appID)) + } + } + + func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + { + // TODO: Handle apps belonging to more than one app group. + guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else { + return completionHandler(.success(appID)) + } + + func finish(_ result: Result) + { + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let group): + // Assign App Group + // TODO: Determine whether app already belongs to app group. + + ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in + let result = result.map { _ in appID } + completionHandler(result) + } + } + } + + let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier + + ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in + switch Result(groups, error) + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let groups): + + if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier }) + { + finish(.success(group)) + } + else + { + // Not all characters are allowed in group names, so we replace periods with spaces (like Apple does). + let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ") + + ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in + finish(Result(group, error)) + } + } + } + } + } + + func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + { + ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in + switch Result(profile, error) + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let profile): + + // Delete existing profile + ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in + switch Result(success, error) + { + case .failure(let error): completionHandler(.failure(error)) + case .success: + + // Fetch new provisiong profile + ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in + completionHandler(Result(profile, error)) + } + } + } + } + } + } +} diff --git a/AltStore/Operations/FindServerOperation.swift b/AltStore/Operations/FindServerOperation.swift index 2ba2549e..e30c1b31 100644 --- a/AltStore/Operations/FindServerOperation.swift +++ b/AltStore/Operations/FindServerOperation.swift @@ -23,27 +23,31 @@ private let ReceivedWiredServerConnectionResponse: @convention(c) (CFNotificatio @objc(FindServerOperation) class FindServerOperation: ResultOperation { - let group: OperationGroup + let context: OperationContext private var isWiredServerConnectionAvailable = false - - init(group: OperationGroup) - { - self.group = group - - super.init() - } + init(context: OperationContext = OperationContext()) + { + self.context = context + } + override func main() { super.main() - if let error = self.group.error + if let error = self.context.error { self.finish(.failure(error)) return } + if let server = self.context.server + { + self.finish(.success(server)) + return + } + let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() // Prepare observers to receive callback from wired server (if connected). diff --git a/AltStore/Operations/InstallAppOperation.swift b/AltStore/Operations/InstallAppOperation.swift index ca7fdc9a..394239bf 100644 --- a/AltStore/Operations/InstallAppOperation.swift +++ b/AltStore/Operations/InstallAppOperation.swift @@ -16,11 +16,11 @@ import Roxas @objc(InstallAppOperation) class InstallAppOperation: ResultOperation { - let context: AppOperationContext + let context: InstallAppOperationContext private var didCleanUp = false - init(context: AppOperationContext) + init(context: InstallAppOperationContext) { self.context = context @@ -40,6 +40,7 @@ class InstallAppOperation: ResultOperation } guard + let certificate = self.context.certificate, let resignedApp = self.context.resignedApp, let connection = self.context.installationConnection else { return self.finish(.failure(OperationError.invalidParameters)) } @@ -57,10 +58,10 @@ class InstallAppOperation: ResultOperation } else { - installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, context: backgroundContext) + installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, certificateSerialNumber: certificate.serialNumber, context: backgroundContext) } - installedApp.update(resignedApp: resignedApp) + installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber) if let team = DatabaseManager.shared.activeTeam(in: backgroundContext) { @@ -108,7 +109,7 @@ class InstallAppOperation: ResultOperation // Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to. self.cleanUp() - self.context.group.beginInstallationHandler?(installedApp) + self.context.beginInstallationHandler?(installedApp) let request = BeginInstallationRequest() connection.send(request) { (result) in diff --git a/AltStore/Operations/OperationContexts.swift b/AltStore/Operations/OperationContexts.swift new file mode 100644 index 00000000..ef138bf1 --- /dev/null +++ b/AltStore/Operations/OperationContexts.swift @@ -0,0 +1,79 @@ +// +// Contexts.swift +// AltStore +// +// Created by Riley Testut on 6/20/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation +import CoreData +import Network + +import AltSign + +class OperationContext +{ + var server: Server? + var error: Error? +} + +class AuthenticatedOperationContext: OperationContext +{ + var session: ALTAppleAPISession? + + var team: ALTTeam? + var certificate: ALTCertificate? + + weak var authenticationOperation: AuthenticationOperation? +} + +@dynamicMemberLookup +class AppOperationContext +{ + let bundleIdentifier: String + private let authenticatedContext: AuthenticatedOperationContext + + var app: ALTApplication? + var provisioningProfiles: [String: ALTProvisioningProfile]? + + var isFinished = false + + var error: Error? { + get { + return _error ?? self.authenticatedContext.error + } + set { + _error = newValue + } + } + private var _error: Error? + + init(bundleIdentifier: String, authenticatedContext: AuthenticatedOperationContext) + { + self.bundleIdentifier = bundleIdentifier + self.authenticatedContext = authenticatedContext + } + + subscript(dynamicMember keyPath: WritableKeyPath) -> T + { + return self.authenticatedContext[keyPath: keyPath] + } +} + +class InstallAppOperationContext: AppOperationContext +{ + lazy var temporaryDirectory: URL = { + let temporaryDirectory = FileManager.default.uniqueTemporaryURL() + + do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) } + catch { self.error = error } + + return temporaryDirectory + }() + + var resignedApp: ALTApplication? + var installationConnection: ServerConnection? + + var beginInstallationHandler: ((InstalledApp) -> Void)? +} diff --git a/AltStore/Operations/OperationGroup.swift b/AltStore/Operations/OperationGroup.swift deleted file mode 100644 index 3f9feb09..00000000 --- a/AltStore/Operations/OperationGroup.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// OperationGroup.swift -// AltStore -// -// Created by Riley Testut on 6/20/19. -// Copyright © 2019 Riley Testut. All rights reserved. -// - -import Foundation -import CoreData - -import AltSign - -class OperationGroup -{ - let progress = Progress.discreteProgress(totalUnitCount: 0) - - var completionHandler: ((Result<[String: Result], Error>) -> Void)? - var beginInstallationHandler: ((InstalledApp) -> Void)? - - var session: ALTAppleAPISession? - - var server: Server? - var signer: ALTSigner? - - var error: Error? - - var results = [String: Result]() - - private var progressByBundleIdentifier = [String: Progress]() - - private let operationQueue = OperationQueue() - private let installOperationQueue = OperationQueue() - - init() - { - // Enforce only one installation at a time. - self.installOperationQueue.maxConcurrentOperationCount = 1 - } - - func cancel() - { - self.operationQueue.cancelAllOperations() - self.installOperationQueue.cancelAllOperations() - } - - func addOperations(_ operations: [Operation]) - { - for operation in operations - { - if let installOperation = operation as? InstallAppOperation - { - if let previousOperation = self.installOperationQueue.operations.last - { - // Ensures they execute in the order they're added, since isReady is still false at this point. - installOperation.addDependency(previousOperation) - } - - self.installOperationQueue.addOperation(installOperation) - } - else - { - self.operationQueue.addOperation(operation) - } - } - } - - func set(_ progress: Progress, for app: AppProtocol) - { - self.progressByBundleIdentifier[app.bundleIdentifier] = progress - - self.progress.totalUnitCount += 1 - self.progress.addChild(progress, withPendingUnitCount: 1) - } - - func progress(for app: AppProtocol) -> Progress? - { - return self.progress(forAppWithBundleIdentifier: app.bundleIdentifier) - } - - func progress(forAppWithBundleIdentifier bundleIdentifier: String) -> Progress? - { - let progress = self.progressByBundleIdentifier[bundleIdentifier] - return progress - } -} diff --git a/AltStore/Operations/PrepareDeveloperAccountOperation.swift b/AltStore/Operations/PrepareDeveloperAccountOperation.swift deleted file mode 100644 index cfa21e06..00000000 --- a/AltStore/Operations/PrepareDeveloperAccountOperation.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// PrepareDeveloperAccountOperation.swift -// AltStore -// -// Created by Riley Testut on 1/7/20. -// Copyright © 2020 Riley Testut. All rights reserved. -// - -import Foundation -import Roxas - -import AltSign - -@objc(PrepareDeveloperAccountOperation) -class PrepareDeveloperAccountOperation: ResultOperation -{ - let group: OperationGroup - - init(group: OperationGroup) - { - self.group = group - - super.init() - - self.progress.totalUnitCount = 2 - } - - override func main() - { - super.main() - - if let error = self.group.error - { - self.finish(.failure(error)) - return - } - - guard - let signer = self.group.signer, - let session = self.group.session - else { return self.finish(.failure(OperationError.invalidParameters)) } - - // Register Device - self.registerCurrentDevice(for: signer.team, session: session) { (result) in - let result = result.map { _ in () } - self.finish(result) - } - } -} - -private extension PrepareDeveloperAccountOperation -{ - func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) - { - guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { - return completionHandler(.failure(OperationError.unknownUDID)) - } - - ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (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, session: session) { (device, error) in - completionHandler(Result(device, error)) - } - } - } - catch - { - completionHandler(.failure(error)) - } - } - } -} diff --git a/AltStore/Operations/RefreshAppOperation.swift b/AltStore/Operations/RefreshAppOperation.swift new file mode 100644 index 00000000..a8ba86fc --- /dev/null +++ b/AltStore/Operations/RefreshAppOperation.swift @@ -0,0 +1,125 @@ +// +// RefreshAppOperation.swift +// AltStore +// +// Created by Riley Testut on 2/27/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +import AltSign +import AltKit + +import Roxas + +@objc(RefreshAppOperation) +class RefreshAppOperation: ResultOperation +{ + let context: AppOperationContext + + // Strong reference to managedObjectContext to keep it alive until we're finished. + let managedObjectContext: NSManagedObjectContext + + init(context: AppOperationContext) + { + self.context = context + self.managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() + + super.init() + } + + override func main() + { + super.main() + + do + { + if let error = self.context.error + { + throw error + } + + guard + let server = self.context.server, + let app = self.context.app, + let team = self.context.team, + let profiles = self.context.provisioningProfiles + else { throw OperationError.invalidParameters } + + guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID } + + ServerManager.shared.connect(to: server) { (result) in + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success(let connection): + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + print("Sending refresh app request...") + + var activeProfiles: Set? + + if team.type == .free + { + let activeApps = InstalledApp.all(in: context) + activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in + let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier } + return [installedApp.resignedBundleIdentifier] + appExtensionProfiles + }) + } + + let request = InstallProvisioningProfilesRequest(udid: udid, provisioningProfiles: Set(profiles.values), activeProfiles: activeProfiles) + connection.send(request) { (result) in + print("Sent refresh app request!") + + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success: + print("Waiting for refresh app response...") + connection.receiveResponse() { (result) in + print("Receiving refresh app response:", result) + + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success(.error(let response)): self.finish(.failure(response.error)) + + case .success(.installProvisioningProfiles): + self.managedObjectContext.perform { + let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier) + guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else { + return self.finish(.failure(OperationError.invalidApp)) + } + + self.progress.completedUnitCount += 1 + + if let provisioningProfile = profiles[app.bundleIdentifier] + { + installedApp.update(provisioningProfile: provisioningProfile) + } + + for installedExtension in installedApp.appExtensions + { + guard let provisioningProfile = profiles[installedExtension.bundleIdentifier] else { continue } + installedExtension.update(provisioningProfile: provisioningProfile) + } + + self.finish(.success(installedApp)) + } + + case .success: self.finish(.failure(ALTServerError(.unknownRequest))) + } + } + } + } + } + } + } + } + catch + { + self.finish(.failure(error)) + } + } +} diff --git a/AltStore/Operations/RefreshGroup.swift b/AltStore/Operations/RefreshGroup.swift new file mode 100644 index 00000000..c4f3dd72 --- /dev/null +++ b/AltStore/Operations/RefreshGroup.swift @@ -0,0 +1,77 @@ +// +// RefreshGroup.swift +// AltStore +// +// Created by Riley Testut on 6/20/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation +import CoreData + +import AltSign + +class RefreshGroup: NSObject +{ + let context: AuthenticatedOperationContext + let progress = Progress.discreteProgress(totalUnitCount: 0) + + var completionHandler: (([String: Result]) -> Void)? + var beginInstallationHandler: ((InstalledApp) -> Void)? + + private(set) var results = [String: Result]() + + private var isFinished = false + + private let dispatchGroup = DispatchGroup() + private var operations: [Foundation.Operation] = [] + + init(context: AuthenticatedOperationContext = AuthenticatedOperationContext()) + { + self.context = context + + super.init() + } + + func add(_ operations: [Foundation.Operation]) + { + for operation in operations + { + self.dispatchGroup.enter() + + operation.completionBlock = { [weak self] in + self?.dispatchGroup.leave() + } + } + + if self.operations.isEmpty && !operations.isEmpty + { + self.dispatchGroup.notify(queue: .global()) { + self.finish() + } + } + + self.operations.append(contentsOf: operations) + } + + func set(_ result: Result, forAppWithBundleIdentifier bundleIdentifier: String) + { + self.results[bundleIdentifier] = result + } + + func cancel() + { + self.operations.forEach { $0.cancel() } + } +} + +private extension RefreshGroup +{ + func finish() + { + guard !self.isFinished else { return } + self.isFinished = true + + self.completionHandler?(self.results) + } +} diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift index f1469e3b..b34557c5 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -14,9 +14,9 @@ import AltSign @objc(ResignAppOperation) class ResignAppOperation: ResultOperation { - let context: AppOperationContext + let context: InstallAppOperationContext - init(context: AppOperationContext) + init(context: InstallAppOperationContext) { self.context = context @@ -37,46 +37,42 @@ class ResignAppOperation: ResultOperation guard let app = self.context.app, - let signer = self.context.group.signer, - let session = self.context.group.session + let profiles = self.context.provisioningProfiles, + let team = self.context.team, + let certificate = self.context.certificate else { return self.finish(.failure(OperationError.invalidParameters)) } - // Prepare Provisioning Profiles - self.prepareProvisioningProfiles(app.fileURL, team: signer.team, session: session) { (result) in - guard let profiles = self.process(result) else { return } + // Prepare app bundle + let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2) + self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3) + + let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in + guard let appBundleURL = self.process(result) else { return } - // Prepare app bundle - let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2) - self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3) + print("Resigning App:", self.context.bundleIdentifier) - let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in - guard let appBundleURL = self.process(result) else { return } + // Resign app bundle + let resignProgress = self.resignAppBundle(at: appBundleURL, team: team, certificate: certificate, profiles: Array(profiles.values)) { (result) in + guard let resignedURL = self.process(result) else { return } - print("Resigning App:", self.context.bundleIdentifier) - - // Resign app bundle - let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in - guard let resignedURL = self.process(result) else { return } + // Finish + do + { + let destinationURL = InstalledApp.refreshedIPAURL(for: app) + try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true) - // Finish - do - { - let destinationURL = InstalledApp.refreshedIPAURL(for: app) - try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true) - - // Use appBundleURL since we need an app bundle, not .ipa. - guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp } - self.finish(.success(resignedApplication)) - } - catch - { - self.finish(.failure(error)) - } + // Use appBundleURL since we need an app bundle, not .ipa. + guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp } + self.finish(.success(resignedApplication)) + } + catch + { + self.finish(.failure(error)) } - prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1) } - prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1) + prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1) } + prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1) } func process(_ result: Result) -> T? @@ -100,310 +96,6 @@ class ResignAppOperation: ResultOperation private extension ResignAppOperation { - func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void) - { - guard let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) } - - self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in - switch result - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let profile): - var profiles = [app.bundleIdentifier: profile] - var error: Error? - - let dispatchGroup = DispatchGroup() - - for appExtension in app.appExtensions - { - dispatchGroup.enter() - - self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in - switch result - { - case .failure(let e): error = e - case .success(let profile): profiles[appExtension.bundleIdentifier] = profile - } - - dispatchGroup.leave() - } - } - - dispatchGroup.notify(queue: .global()) { - if let error = error - { - completionHandler(.failure(error)) - } - else - { - completionHandler(.success(profiles)) - } - } - } - } - } - - func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) - { - DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in - - let preferredBundleID: String - - // Check if we have already installed this app with this team before. - let predicate = NSPredicate(format: "%K == %@ AND %K == %@", - #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier, - #keyPath(InstalledApp.team.identifier), team.identifier) - if let installedApp = InstalledApp.first(satisfying: predicate, in: context) - { - // This app is already installed, so use the same resigned bundle identifier as before. - // This way, if we change the identifier format (again), AltStore will continue to use - // the old bundle identifier to prevent it from installing as a new app. - preferredBundleID = installedApp.resignedBundleIdentifier - } - else - { - // This app isn't already installed, so create the resigned bundle identifier ourselves. - // Or, if the app _is_ installed but with a different team, we need to create a new - // bundle identifier anyway to prevent collisions with the previous team. - let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier - let updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track. - - preferredBundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID) - } - - let preferredName: String - - if let parentApp = parentApp - { - preferredName = "\(parentApp.name) - \(app.name)" - } - else - { - preferredName = app.name - } - - // Register - self.registerAppID(for: app, name: preferredName, bundleIdentifier: preferredBundleID, team: team, session: session) { (result) in - switch result - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let appID): - - // Update features - self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in - switch result - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let appID): - - // Update app groups - self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in - switch result - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let appID): - - // Fetch Provisioning Profile - self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in - completionHandler(result) - } - } - } - } - } - } - } - } - } - - func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) - { - ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in - do - { - let appIDs = try Result(appIDs, error).get() - - if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleIdentifier }) - { - completionHandler(.success(appID)) - } - else - { - let requiredAppIDs = 1 + application.appExtensions.count - let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count) - - let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 }) - - if team.type == .free - { - if requiredAppIDs > availableAppIDs - { - if let expirationDate = sortedExpirationDates.first - { - throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate) - } - else - { - throw ALTAppleAPIError(.maximumAppIDLimitReached) - } - } - } - - ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in - do - { - do - { - let appID = try Result(appID, error).get() - completionHandler(.success(appID)) - } - catch ALTAppleAPIError.maximumAppIDLimitReached - { - if let expirationDate = sortedExpirationDates.first - { - throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate) - } - else - { - throw ALTAppleAPIError(.maximumAppIDLimitReached) - } - } - } - catch - { - completionHandler(.failure(error)) - } - } - } - } - catch - { - completionHandler(.failure(error)) - } - } - } - - func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) - { - let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in - guard let feature = ALTFeature(entitlement: entitlement) else { return nil } - return (feature, value) - } - - var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 } - - if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty - { - features[.appGroups] = true - } - - var updateFeatures = false - - // Determine whether the required features are already enabled for the AppID. - for (feature, value) in features - { - if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue) - { - // AppID already has this feature enabled and the values are the same. - continue - } - else - { - // AppID either doesn't have this feature enabled or the value has changed, - // so we need to update it to reflect new values. - updateFeatures = true - break - } - } - - if updateFeatures - { - let appID = appID.copy() as! ALTAppID - appID.features = features - - ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in - completionHandler(Result(appID, error)) - } - } - else - { - completionHandler(.success(appID)) - } - } - - func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) - { - // TODO: Handle apps belonging to more than one app group. - guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else { - return completionHandler(.success(appID)) - } - - func finish(_ result: Result) - { - switch result - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let group): - // Assign App Group - // TODO: Determine whether app already belongs to app group. - - ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in - let result = result.map { _ in appID } - completionHandler(result) - } - } - } - - let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier - - ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in - switch Result(groups, error) - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let groups): - - if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier }) - { - finish(.success(group)) - } - else - { - // Not all characters are allowed in group names, so we replace periods with spaces (like Apple does). - let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ") - - ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in - finish(Result(group, error)) - } - } - } - } - } - - func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) - { - ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in - switch Result(profile, error) - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let profile): - - // Delete existing profile - ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in - switch Result(success, error) - { - case .failure(let error): completionHandler(.failure(error)) - case .success: - - // Fetch new provisiong profile - ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in - completionHandler(Result(profile, error)) - } - } - } - } - } - } - func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result) -> Void) -> Progress { let progress = Progress.discreteProgress(totalUnitCount: 1) @@ -511,8 +203,9 @@ private extension ResignAppOperation return progress } - func resignAppBundle(at fileURL: URL, signer: ALTSigner, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result) -> Void) -> Progress + func resignAppBundle(at fileURL: URL, team: ALTTeam, certificate: ALTCertificate, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result) -> Void) -> Progress { + let signer = ALTSigner(team: team, certificate: certificate) let progress = signer.signApp(at: fileURL, provisioningProfiles: profiles) { (success, error) in do { diff --git a/AltStore/Operations/SendAppOperation.swift b/AltStore/Operations/SendAppOperation.swift index e3cc1413..75b35ec8 100644 --- a/AltStore/Operations/SendAppOperation.swift +++ b/AltStore/Operations/SendAppOperation.swift @@ -39,7 +39,7 @@ class SendAppOperation: ResultOperation return } - guard let app = self.context.app, let server = self.context.group.server else { return self.finish(.failure(OperationError.invalidParameters)) } + guard let app = self.context.app, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) } // self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa. let fileURL = InstalledApp.refreshedIPAURL(for: app) diff --git a/Dependencies/AltSign b/Dependencies/AltSign index 773a85f6..9dc729f3 160000 --- a/Dependencies/AltSign +++ b/Dependencies/AltSign @@ -1 +1 @@ -Subproject commit 773a85f668c9ea2b27c8f1c76197b9666295677e +Subproject commit 9dc729f3d723b5151b7784e7c9c6f29558b05c82