From 579885acd6e581078ceaebc42b123b685c306f62 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 21 Nov 2022 17:50:42 -0600 Subject: [PATCH] [AltServer] Downloads latest supported AltStore version for device OS version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asks user to install latest compatible version instead if latest AltStore version does not support their device’s OS version. --- AltServer/AppDelegate.swift | 2 +- .../ALTDeviceManager+Installation.swift | 195 ++++++++++++++++-- Shared/Categories/NSError+ALTServerError.m | 14 +- 3 files changed, 192 insertions(+), 19 deletions(-) diff --git a/AltServer/AppDelegate.swift b/AltServer/AppDelegate.swift index 952cea94..9af8ba7c 100644 --- a/AltServer/AppDelegate.swift +++ b/AltServer/AppDelegate.swift @@ -255,7 +255,7 @@ private extension AppDelegate let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) - case .failure(~OperationErrorCode.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication): + case .failure(OperationError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication): // Ignore break diff --git a/AltServer/Devices/ALTDeviceManager+Installation.swift b/AltServer/Devices/ALTDeviceManager+Installation.swift index 5d1ee0ae..efc56fdf 100644 --- a/AltServer/Devices/ALTDeviceManager+Installation.swift +++ b/AltServer/Devices/ALTDeviceManager+Installation.swift @@ -10,29 +10,105 @@ import Cocoa import UserNotifications import ObjectiveC -private let appGroupsSemaphore = DispatchSemaphore(value: 1) +#if STAGING +let altstoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/apps-staging.json")! +#else +let altstoreSourceURL = URL(string: "https://apps.altstore.io")! +#endif +#if BETA +let altstoreBundleID = "com.rileytestut.AltStore.Beta" +#else +let altstoreBundleID = "com.rileytestut.AltStore" +#endif + +private let appGroupsSemaphore = DispatchSemaphore(value: 1) private let developerDiskManager = DeveloperDiskManager() -typealias OperationError = OperationErrorCode.Error -enum OperationErrorCode: Int, ALTErrorEnum +private let session: URLSession = { + let configuration = URLSessionConfiguration.default + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + configuration.urlCache = nil + + let session = URLSession(configuration: configuration) + return session +}() + +extension OperationError { - case cancelled - case noTeam - case missingPrivateKey - case missingCertificate + enum Code: Int, ALTErrorCode + { + typealias Error = OperationError + + case cancelled + case noTeam + case missingPrivateKey + case missingCertificate + + // Source JSON + case appNotFound + } + + static let cancelled = OperationError(code: .cancelled) + static let noTeam = OperationError(code: .noTeam) + static let missingPrivateKey = OperationError(code: .missingPrivateKey) + static let missingCertificate = OperationError(code: .missingCertificate) + + static func appNotFound(bundleID: String) -> OperationError { OperationError(code: .appNotFound, bundleID: bundleID) } +} + +struct OperationError: ALTLocalizedError +{ + var code: Code + var errorTitle: String? + var errorFailure: String? + + var bundleID: String? var errorFailureReason: String { - switch self + switch self.code { case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "") case .noTeam: return NSLocalizedString("You are not a member of any developer teams.", comment: "") case .missingPrivateKey: return NSLocalizedString("The developer certificate's private key could not be found.", comment: "") case .missingCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "") + case .appNotFound: + let appBundleID = self.bundleID.map { "“\($0)”" } ?? "AltStore" + return String(format: NSLocalizedString("%@ could not be located in the source JSON.", comment: ""), appBundleID) } } } +private extension ALTDeviceManager +{ + struct Source: Decodable + { + struct App: Decodable + { + struct Version: Decodable + { + var version: String + var downloadURL: URL + + var minimumOSVersion: OperatingSystemVersion? { + return self.minOSVersion.map { OperatingSystemVersion(string: $0) } + } + private var minOSVersion: String? + } + + var name: String + var bundleIdentifier: String + + var versions: [Version]? + } + + var name: String + var identifier: String + + var apps: [App] + } +} + extension ALTDeviceManager { func installApplication(at url: URL, to altDevice: ALTDevice, appleID: String, password: String, completion: @escaping (Result) -> Void) @@ -102,7 +178,7 @@ extension ALTDeviceManager fallthrough // Continue installing app even if we couldn't install Developer disk image. case .success: - self.downloadApp(from: url) { (result) in + self.downloadApp(from: url, for: altDevice) { (result) in do { let fileURL = try result.get() @@ -229,26 +305,111 @@ extension ALTDeviceManager private extension ALTDeviceManager { - func downloadApp(from url: URL, completionHandler: @escaping (Result) -> Void) + func downloadApp(from url: URL, for device: ALTDevice, completionHandler: @escaping (Result) -> Void) { guard !url.isFileURL else { return completionHandler(.success(url)) } - let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in + self.fetchAltStoreDownloadURL(for: device) { result in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let url): + let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in + do + { + if let response = response as? HTTPURLResponse + { + guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: url]) } + } + + let (fileURL, _) = try Result((fileURL, response), error).get() + completionHandler(.success(fileURL)) + + do { try FileManager.default.removeItem(at: fileURL) } + catch { print("Failed to remove downloaded .ipa.", error) } + } + catch + { + completionHandler(.failure(error)) + } + } + + downloadTask.resume() + } + } + } + + func fetchAltStoreDownloadURL(for device: ALTDevice, completion: @escaping (Result) -> Void) + { + let dataTask = session.dataTask(with: altstoreSourceURL) { (data, response, error) in + do { - let (fileURL, _) = try Result((fileURL, response), error).get() - completionHandler(.success(fileURL)) + if let response = response as? HTTPURLResponse + { + guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: altstoreSourceURL]) } + } + + let (data, _) = try Result((data, response), error).get() + let source = try Foundation.JSONDecoder().decode(Source.self, from: data) + + let osName = device.type.osName ?? "iOS" + + guard let altstore = source.apps.first(where: { $0.bundleIdentifier == altstoreBundleID }) else { throw OperationError.appNotFound(bundleID: altstoreBundleID) } + guard let latestVersion = altstore.versions?.first else { throw ALTServerError(.unsupportediOSVersion, userInfo: [ALTAppNameErrorKey: "AltStore", + ALTOperatingSystemNameErrorKey: osName, + ALTOperatingSystemVersionErrorKey: "12.2"]) } + + let minOSVersionString = latestVersion.minimumOSVersion?.stringValue ?? "12.2" + + guard let latestSupportedVersion = altstore.versions?.first(where: { appVersion in + if let minOSVersion = appVersion.minimumOSVersion, device.osVersion < minOSVersion + { + return false + } + + return true + }) else { throw ALTServerError(.unsupportediOSVersion, userInfo: [ALTAppNameErrorKey: "AltStore", + ALTOperatingSystemNameErrorKey: osName, + ALTOperatingSystemVersionErrorKey: minOSVersionString]) } + + guard latestSupportedVersion.version != latestVersion.version else { + // The newest version is also the newest compatible version, so return its downloadURL. + return completion(.success(latestVersion.downloadURL)) + } + + DispatchQueue.main.async { + var message = String(format: NSLocalizedString("%@ is running %@ %@, but AltStore requires %@ %@ or later.", comment: ""), device.name, osName, device.osVersion.stringValue, osName, minOSVersionString) + message += "\n\n" + message += NSLocalizedString("Would you like to download the last version compatible with your device instead?", comment: "") + + let alert = NSAlert() + alert.messageText = String(format: NSLocalizedString("Unsupported %@ Version", comment: ""), osName) + alert.informativeText = message + + let buttonTitle = String(format: NSLocalizedString("Download %@ %@", comment: ""), altstore.name, latestSupportedVersion.version) + alert.addButton(withTitle: buttonTitle) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) + + let index = alert.runModal() + if index == .alertFirstButtonReturn + { + completion(.success(latestSupportedVersion.downloadURL)) + } + else + { + completion(.failure(OperationError.cancelled)) + } + } - do { try FileManager.default.removeItem(at: fileURL) } - catch { print("Failed to remove downloaded .ipa.", error) } } catch { - completionHandler(.failure(error)) + completion(.failure(error)) } } - downloadTask.resume() + dataTask.resume() } func authenticate(appleID: String, password: String, anisetteData: ALTAnisetteData, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) diff --git a/Shared/Categories/NSError+ALTServerError.m b/Shared/Categories/NSError+ALTServerError.m index 7712154d..a96c4bdd 100644 --- a/Shared/Categories/NSError+ALTServerError.m +++ b/Shared/Categories/NSError+ALTServerError.m @@ -14,6 +14,8 @@ #import #endif +@import AltSign; + NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError"; NSErrorDomain const AltServerInstallationErrorDomain = @"Apple.InstallationError"; NSErrorDomain const AltServerConnectionErrorDomain = @"AltServer.ConnectionError"; @@ -190,7 +192,17 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste return NSLocalizedString(@"You cannot activate more than 3 apps with a non-developer Apple ID.", @""); case ALTServerErrorUnsupportediOSVersion: - return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @""); + { + NSString *appName = self.userInfo[ALTAppNameErrorKey]; + NSString *osVersion = [self altserver_osVersion]; + + if (appName == nil || osVersion == nil) + { + return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @""); + } + + return [NSString stringWithFormat:NSLocalizedString(@"%@ requires %@ or later.", @""), appName, osVersion]; + } case ALTServerErrorUnknownRequest: return NSLocalizedString(@"AltServer does not support this request.", @"");