[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) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
case .failure(~OperationErrorCode.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication): case .failure(OperationError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore // Ignore
break break

View File

@@ -10,29 +10,105 @@ import Cocoa
import UserNotifications import UserNotifications
import ObjectiveC 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() private let developerDiskManager = DeveloperDiskManager()
typealias OperationError = OperationErrorCode.Error private let session: URLSession = {
enum OperationErrorCode: Int, ALTErrorEnum let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
configuration.urlCache = nil
let session = URLSession(configuration: configuration)
return session
}()
extension OperationError
{ {
enum Code: Int, ALTErrorCode
{
typealias Error = OperationError
case cancelled case cancelled
case noTeam case noTeam
case missingPrivateKey case missingPrivateKey
case missingCertificate 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 { var errorFailureReason: String {
switch self switch self.code
{ {
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "") case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .noTeam: return NSLocalizedString("You are not a member of any developer teams.", 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 .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 .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 extension ALTDeviceManager
{ {
func installApplication(at url: URL, to altDevice: ALTDevice, appleID: String, password: String, completion: @escaping (Result<ALTApplication, Error>) -> Void) 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. fallthrough // Continue installing app even if we couldn't install Developer disk image.
case .success: case .success:
self.downloadApp(from: url) { (result) in self.downloadApp(from: url, for: altDevice) { (result) in
do do
{ {
let fileURL = try result.get() let fileURL = try result.get()
@@ -229,13 +305,23 @@ extension ALTDeviceManager
private 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)) } guard !url.isFileURL else { return completionHandler(.success(url)) }
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 let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
do 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() let (fileURL, _) = try Result((fileURL, response), error).get()
completionHandler(.success(fileURL)) completionHandler(.success(fileURL))
@@ -250,6 +336,81 @@ private extension ALTDeviceManager
downloadTask.resume() downloadTask.resume()
} }
}
}
func fetchAltStoreDownloadURL(for device: ALTDevice, completion: @escaping (Result<URL, Error>) -> Void)
{
let dataTask = session.dataTask(with: altstoreSourceURL) { (data, response, error) in
do
{
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))
}
}
}
catch
{
completion(.failure(error))
}
}
dataTask.resume()
}
func authenticate(appleID: String, password: String, anisetteData: ALTAnisetteData, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) 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> #import <AltStoreCore/AltStoreCore-Swift.h>
#endif #endif
@import AltSign;
NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError"; NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError";
NSErrorDomain const AltServerInstallationErrorDomain = @"Apple.InstallationError"; NSErrorDomain const AltServerInstallationErrorDomain = @"Apple.InstallationError";
NSErrorDomain const AltServerConnectionErrorDomain = @"AltServer.ConnectionError"; 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.", @""); return NSLocalizedString(@"You cannot activate more than 3 apps with a non-developer Apple ID.", @"");
case ALTServerErrorUnsupportediOSVersion: case ALTServerErrorUnsupportediOSVersion:
{
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 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: case ALTServerErrorUnknownRequest:
return NSLocalizedString(@"AltServer does not support this request.", @""); return NSLocalizedString(@"AltServer does not support this request.", @"");