From 4f0001816455bebf1857be336208382590aa3bbf Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 6 Mar 2020 17:08:35 -0800 Subject: [PATCH] Refreshes apps by installing provisioning profiles when possible Assuming the certificate used to originally sign an app is still valid, we can refresh an app simply by installing new provisioning profiles. However, if the signing certificate is no longer valid, we fall back to the old method of resigning + reinstalling. --- AltStore.xcodeproj/project.pbxproj | 32 +- AltStore/AppDelegate.swift | 11 +- .../RefreshAltStoreViewController.swift | 26 +- .../ALTApplication+AppExtensions.swift | 29 - AltStore/Managing Apps/AppManager.swift | 642 ++++++++++-------- .../AltStore 4.xcdatamodel/contents | 3 +- AltStore/Model/DatabaseManager.swift | 5 +- AltStore/Model/InstalledApp.swift | 21 +- AltStore/Model/InstalledExtension.swift | 9 +- AltStore/My Apps/MyAppsViewController.swift | 121 ++-- AltStore/Operations/AppOperationContext.swift | 58 -- .../Operations/AuthenticationOperation.swift | 105 ++- .../FetchAnisetteDataOperation.swift | 14 +- .../Operations/FetchAppIDsOperation.swift | 26 +- .../FetchProvisioningProfilesOperation.swift | 398 +++++++++++ AltStore/Operations/FindServerOperation.swift | 22 +- AltStore/Operations/InstallAppOperation.swift | 11 +- AltStore/Operations/OperationContexts.swift | 79 +++ AltStore/Operations/OperationGroup.swift | 86 --- .../PrepareDeveloperAccountOperation.swift | 81 --- AltStore/Operations/RefreshAppOperation.swift | 125 ++++ AltStore/Operations/RefreshGroup.swift | 77 +++ AltStore/Operations/ResignAppOperation.swift | 369 +--------- AltStore/Operations/SendAppOperation.swift | 2 +- Dependencies/AltSign | 2 +- 25 files changed, 1272 insertions(+), 1082 deletions(-) delete mode 100644 AltStore/Extensions/ALTApplication+AppExtensions.swift delete mode 100644 AltStore/Operations/AppOperationContext.swift create mode 100644 AltStore/Operations/FetchProvisioningProfilesOperation.swift create mode 100644 AltStore/Operations/OperationContexts.swift delete mode 100644 AltStore/Operations/OperationGroup.swift delete mode 100644 AltStore/Operations/PrepareDeveloperAccountOperation.swift create mode 100644 AltStore/Operations/RefreshAppOperation.swift create mode 100644 AltStore/Operations/RefreshGroup.swift 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