[AltServer] Downloads latest supported AltStore version for device OS version

Asks user to install latest compatible version instead if latest AltStore version does not support their device’s OS version.
This commit is contained in:
Riley Testut
2022-11-21 17:50:42 -06:00
parent 61bcb31709
commit 579885acd6
3 changed files with 192 additions and 19 deletions

View File

@@ -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

View File

@@ -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<ALTApplication, Error>) -> 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<URL, Error>) -> Void)
func downloadApp(from url: URL, for device: ALTDevice, completionHandler: @escaping (Result<URL, Error>) -> 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<URL, Error>) -> 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)

View File

@@ -14,6 +14,8 @@
#import <AltStoreCore/AltStoreCore-Swift.h>
#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.", @"");