Change AltStore naming to SideStore

This commit is contained in:
Jackson Coxson
2022-05-30 22:46:21 -06:00
parent 5ea83ccea1
commit 646000920f
340 changed files with 913 additions and 1084 deletions

View File

@@ -1,693 +0,0 @@
//
// AuthenticationOperation.swift
// AltStore
//
// Created by Riley Testut on 6/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import Network
import AltStoreCore
import AltSign
enum AuthenticationError: LocalizedError
{
case noTeam
case noCertificate
case missingPrivateKey
case missingCertificate
var errorDescription: String? {
switch self {
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
}
}
}
@objc(AuthenticationOperation)
class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
{
let context: AuthenticatedOperationContext
private weak var presentingViewController: UIViewController?
private lazy var navigationController: UINavigationController = {
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
if #available(iOS 13.0, *)
{
navigationController.isModalInPresentation = true
}
return navigationController
}()
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
private var appleIDEmailAddress: String?
private var appleIDPassword: String?
private var shouldShowInstructions = false
private let operationQueue = OperationQueue()
private var submitCodeAction: UIAlertAction?
init(context: AuthenticatedOperationContext, presentingViewController: UIViewController?)
{
self.context = context
self.presentingViewController = presentingViewController
super.init()
self.context.authenticationOperation = self
self.operationQueue.name = "com.altstore.AuthenticationOperation"
self.progress.totalUnitCount = 4
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
// Sign In
self.signIn() { (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 account, let session)):
self.context.session = session
self.progress.completedUnitCount += 1
// Fetch Team
self.fetchTeam(for: account, 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 team):
self.context.team = team
self.progress.completedUnitCount += 1
// Fetch Certificate
self.fetchCertificate(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 certificate):
self.context.certificate = certificate
self.progress.completedUnitCount += 1
// 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:
self.progress.completedUnitCount += 1
// 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)
}
}
}
}
}
}
}
}
}
}
}
}
func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
do
{
let account: Account
let team: Team
if let tempAccount = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context)
{
account = tempAccount
}
else
{
account = Account(altTeam.account, context: context)
}
if let tempTeam = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
{
team = tempTeam
}
else
{
team = Team(altTeam, account: account, context: context)
}
account.update(account: altTeam.account)
if let providedEmailAddress = self.appleIDEmailAddress
{
// Save the user's provided email address instead of the one associated with their account (which may be outdated).
account.appleID = providedEmailAddress
}
team.update(team: altTeam)
try context.save()
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
}
override func finish(_ result: Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>)
{
guard !self.isFinished else { return }
print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.perform {
do
{
let (altTeam, altCertificate, session) = try result.get()
guard
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
account.isActiveAccount = true
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
for account in otherAccounts
{
account.isActiveAccount = false
}
// Team
team.isActiveTeam = true
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
let otherTeams = try context.fetch(otherTeamsFetchRequest)
for team in otherTeams
{
team.isActiveTeam = false
}
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
{
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
}
else
{
UserDefaults.standard.activeAppsLimit = nil
}
// Save
try context.save()
// Update keychain
Keychain.shared.appleIDEmailAddress = self.appleIDEmailAddress ?? altTeam.account.appleID // Prefer the user's provided email address over the one associated with their account (which may be outdated).
Keychain.shared.appleIDPassword = self.appleIDPassword
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)
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
}
}
}
}
catch
{
super.finish(result)
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
}
}
}
}
}
private extension AuthenticationOperation
{
func present(_ viewController: UIViewController) -> Bool
{
guard let presentingViewController = self.presentingViewController else { return false }
self.navigationController.view.tintColor = .white
if self.navigationController.viewControllers.isEmpty
{
guard presentingViewController.presentedViewController == nil else { return false }
self.navigationController.setViewControllers([viewController], animated: false)
presentingViewController.present(self.navigationController, animated: true, completion: nil)
}
else
{
viewController.navigationItem.leftBarButtonItem = nil
self.navigationController.pushViewController(viewController, animated: true)
}
return true
}
}
private extension AuthenticationOperation
{
func signIn(completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
{
func authenticate()
{
DispatchQueue.main.async {
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
self.authenticate(appleID: appleID, password: password) { (result) in
completionHandler(result)
}
}
authenticationViewController.completionHandler = { (result) in
if let (account, session, password) = result
{
// We presented the Auth UI and the user signed in.
// In this case, we'll assume we should show the instructions again.
self.shouldShowInstructions = true
self.appleIDPassword = password
completionHandler(.success((account, session)))
}
else
{
completionHandler(.failure(OperationError.cancelled))
}
}
if !self.present(authenticationViewController)
{
completionHandler(.failure(OperationError.notAuthenticated))
}
}
}
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
{
self.authenticate(appleID: appleID, password: password) { (result) in
switch result
{
case .success((let account, let session)):
self.appleIDPassword = password
completionHandler(.success((account, session)))
case .failure(ALTAppleAPIError.incorrectCredentials), .failure(ALTAppleAPIError.appSpecificPasswordRequired):
authenticate()
case .failure(let error):
completionHandler(.failure(error))
}
}
}
else
{
authenticate()
}
}
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
{
self.appleIDEmailAddress = appleID
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
fetchAnisetteDataOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let anisetteData):
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
if let presentingViewController = self.presentingViewController
{
verificationHandler = { (completionHandler) in
DispatchQueue.main.async {
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.keyboardType = .numberPad
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
}
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
let textField = alertController.textFields?.first
let code = textField?.text ?? ""
completionHandler(code)
}
submitAction.isEnabled = false
alertController.addAction(submitAction)
self.submitCodeAction = submitAction
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
completionHandler(nil)
})
if self.navigationController.presentingViewController != nil
{
self.navigationController.present(alertController, animated: true, completion: nil)
}
else
{
presentingViewController.present(alertController, animated: true, completion: nil)
}
}
}
}
else
{
// No view controller to present security code alert, so don't provide verificationHandler.
verificationHandler = nil
}
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
verificationHandler: verificationHandler) { (account, session, error) in
if let account = account, let session = session
{
completionHandler(.success((account, session)))
}
else
{
completionHandler(.failure(error ?? OperationError.unknown))
}
}
}
}
self.operationQueue.addOperation(fetchAnisetteDataOperation)
}
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
{
func selectTeam(from teams: [ALTTeam])
{
if let team = teams.first(where: { $0.type == .individual })
{
return completionHandler(.success(team))
}
else if let team = teams.first(where: { $0.type == .free })
{
return completionHandler(.success(team))
}
else if let team = teams.first
{
return completionHandler(.success(team))
}
else
{
return completionHandler(.failure(AuthenticationError.noTeam))
}
}
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
switch Result(teams, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let teams):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier })
{
completionHandler(.success(altTeam))
}
else
{
selectTeam(from: teams)
}
}
}
}
}
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
{
func requestCertificate()
{
let machineName = "AltStore - " + UIDevice.current.name
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
throw AuthenticationError.missingCertificate
}
certificate.privateKey = privateKey
completionHandler(.success(certificate))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func replaceCertificate(from certificates: [ALTCertificate])
{
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success
{
completionHandler(.failure(error))
}
else
{
requestCertificate()
}
}
}
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
if
let data = Keychain.shared.signingCertificate,
let localCertificate = ALTCertificate(p12Data: data, password: nil),
let certificate = certificates.first(where: { $0.serialNumber == localCertificate.serialNumber })
{
// We have a certificate stored in the keychain and it hasn't been revoked.
localCertificate.machineIdentifier = certificate.machineIdentifier
completionHandler(.success(localCertificate))
}
else if
let serialNumber = Keychain.shared.signingCertificateSerialNumber,
let privateKey = Keychain.shared.signingCertificatePrivateKey,
let certificate = certificates.first(where: { $0.serialNumber == serialNumber })
{
// LEGACY
// We have the private key for one of the certificates, so add it to certificate and use it.
certificate.privateKey = privateKey
completionHandler(.success(certificate))
}
else if
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String,
let certificate = certificates.first(where: { $0.serialNumber == serialNumber }),
let machineIdentifier = certificate.machineIdentifier,
FileManager.default.fileExists(atPath: Bundle.main.certificateURL.path),
let data = try? Data(contentsOf: Bundle.main.certificateURL),
let localCertificate = ALTCertificate(p12Data: data, password: machineIdentifier)
{
// We have an embedded certificate that hasn't been revoked.
localCertificate.machineIdentifier = machineIdentifier
completionHandler(.success(localCertificate))
}
else if certificates.isEmpty
{
// No certificates, so request a new one.
requestCertificate()
}
else
{
// We don't have private keys for any of the certificates,
// so we need to revoke one and create a new one.
replaceCertificate(from: certificates)
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
return completionHandler(.failure(OperationError.unknownUDID))
}
ALTAppleAPI.shared.fetchDevices(for: team, types: [.iphone, .ipad], 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, type: .iphone, 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, Error>) -> Void)
{
let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
fetchAppIDsOperation.resultHandler = { (result) in
do
{
let (_, context) = try result.get()
try context.save()
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
self.operationQueue.addOperation(fetchAppIDsOperation)
}
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
{
guard self.shouldShowInstructions else { return completionHandler(false) }
DispatchQueue.main.async {
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
instructionsViewController.showsBottomButton = true
instructionsViewController.completionHandler = {
completionHandler(true)
}
if !self.present(instructionsViewController)
{
completionHandler(false)
}
}
}
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
{
guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) }
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
#if DEBUG
completionHandler(false)
#else
DispatchQueue.main.async {
let context = AuthenticatedOperationContext(context: self.context)
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController
refreshViewController.context = context
refreshViewController.completionHandler = { _ in
completionHandler(true)
}
if !self.present(refreshViewController)
{
completionHandler(false)
}
}
#endif
}
}
extension AuthenticationOperation
{
@objc func textFieldTextDidChange(_ notification: Notification)
{
guard let textField = notification.object as? UITextField else { return }
self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6
}
}

View File

@@ -1,271 +0,0 @@
//
// BackgroundRefreshAppsOperation.swift
// AltStore
//
// Created by Riley Testut on 7/6/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
import CoreData
import AltStoreCore
enum RefreshError: LocalizedError
{
case noInstalledApps
var errorDescription: String? {
switch self
{
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
}
}
}
private extension CFNotificationName
{
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
static let appIsRunning = CFNotificationName("com.altstore.AppState.Running" as CFString)
static func requestAppState(for appID: String) -> CFNotificationName
{
let name = String(CFNotificationName.requestAppState.rawValue) + "." + appID
return CFNotificationName(name as CFString)
}
static func appIsRunning(for appID: String) -> CFNotificationName
{
let name = String(CFNotificationName.appIsRunning.rawValue) + "." + appID
return CFNotificationName(name as CFString)
}
}
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{ (center, observer, name, object, userInfo) in
guard let name = name, let observer = observer else { return }
let operation = unsafeBitCast(observer, to: BackgroundRefreshAppsOperation.self)
operation.receivedApplicationState(notification: name)
}
@objc(BackgroundRefreshAppsOperation)
class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
{
let installedApps: [InstalledApp]
private let managedObjectContext: NSManagedObjectContext
var presentsFinishedNotification: Bool = true
private let refreshIdentifier: String = UUID().uuidString
private var runningApplications: Set<String> = []
init(installedApps: [InstalledApp])
{
self.installedApps = installedApps
self.managedObjectContext = installedApps.compactMap({ $0.managedObjectContext }).first ?? DatabaseManager.shared.persistentContainer.newBackgroundContext()
super.init()
}
override func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
{
super.finish(result)
self.scheduleFinishedRefreshingNotification(for: result, delay: 0)
self.managedObjectContext.perform {
self.stopListeningForRunningApps()
}
DispatchQueue.main.async {
if UIApplication.shared.applicationState == .background
{
ServerManager.shared.stopDiscovering()
}
}
}
override func main()
{
super.main()
guard !self.installedApps.isEmpty else {
self.finish(.failure(RefreshError.noInstalledApps))
return
}
if !ServerManager.shared.isDiscovering
{
ServerManager.shared.startDiscovering()
}
self.managedObjectContext.perform {
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
self.startListeningForRunningApps()
// Wait for 3 seconds (2 now, 1 later in FindServerOperation) to:
// a) give us time to discover AltServers
// b) give other processes a chance to respond to requestAppState notification
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.managedObjectContext.perform {
let filteredApps = self.installedApps.filter { !self.runningApplications.contains($0.bundleIdentifier) }
print("Filtered Apps to Refresh:", filteredApps.map { $0.bundleIdentifier })
let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil)
group.beginInstallationHandler = { (installedApp) in
guard installedApp.bundleIdentifier == StoreApp.altstoreAppID else { return }
// We're starting to install AltStore, which means the app is about to quit.
// So, we schedule a "refresh successful" local notification to be displayed after a delay,
// but if the app is still running, we cancel the notification.
// Then, we schedule another notification and repeat the process.
// 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.context.error
{
self.scheduleFinishedRefreshingNotification(for: .failure(error))
}
else
{
var results = group.results
results[installedApp.bundleIdentifier] = .success(installedApp)
self.scheduleFinishedRefreshingNotification(for: .success(results))
}
}
group.completionHandler = { (results) in
self.finish(.success(results))
}
}
}
}
}
}
private extension BackgroundRefreshAppsOperation
{
func startListeningForRunningApps()
{
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
for installedApp in self.installedApps
{
let appIsRunningNotification = CFNotificationName.appIsRunning(for: installedApp.bundleIdentifier)
CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedApplicationState, appIsRunningNotification.rawValue, nil, .deliverImmediately)
let requestAppStateNotification = CFNotificationName.requestAppState(for: installedApp.bundleIdentifier)
CFNotificationCenterPostNotification(notificationCenter, requestAppStateNotification, nil, nil, true)
}
}
func stopListeningForRunningApps()
{
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
for installedApp in self.installedApps
{
let appIsRunningNotification = CFNotificationName.appIsRunning(for: installedApp.bundleIdentifier)
CFNotificationCenterRemoveObserver(notificationCenter, observer, appIsRunningNotification, nil)
}
}
func receivedApplicationState(notification: CFNotificationName)
{
let baseName = String(CFNotificationName.appIsRunning.rawValue)
let appID = String(notification.rawValue).replacingOccurrences(of: baseName + ".", with: "")
self.runningApplications.insert(appID)
}
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, delay: TimeInterval = 5)
{
func scheduleFinishedRefreshingNotification()
{
self.cancelFinishedRefreshingNotification()
let content = UNMutableNotificationContent()
var shouldPresentAlert = true
do
{
let results = try result.get()
shouldPresentAlert = !results.isEmpty
for (_, result) in results
{
guard case let .failure(error) = result else { continue }
throw error
}
content.title = NSLocalizedString("Refreshed Apps", comment: "")
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
}
catch ConnectionError.serverNotFound
{
shouldPresentAlert = false
}
catch RefreshError.noInstalledApps
{
shouldPresentAlert = false
}
catch
{
print("Failed to refresh apps in background.", error)
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
content.body = error.localizedDescription
shouldPresentAlert = true
}
if shouldPresentAlert
{
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
let request = UNNotificationRequest(identifier: self.refreshIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
if delay > 0
{
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
UNUserNotificationCenter.current().getPendingNotificationRequests() { (requests) in
// If app is still running at this point, we schedule another notification with same identifier.
// This prevents the currently scheduled notification from displaying, and starts another countdown timer.
// First though, make sure there _is_ still a pending request, otherwise it's been cancelled
// and we should stop polling.
guard requests.contains(where: { $0.identifier == self.refreshIdentifier }) else { return }
scheduleFinishedRefreshingNotification()
}
}
}
}
}
if self.presentsFinishedNotification
{
scheduleFinishedRefreshingNotification()
}
// Perform synchronously to ensure app doesn't quit before we've finishing saving to disk.
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
_ = RefreshAttempt(identifier: self.refreshIdentifier, result: result, context: context)
do { try context.save() }
catch { print("Failed to save refresh attempt.", error) }
}
}
func cancelFinishedRefreshingNotification()
{
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [self.refreshIdentifier])
}
}

View File

@@ -1,183 +0,0 @@
//
// BackupAppOperation.swift
// AltStore
//
// Created by Riley Testut on 5/12/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import AltSign
extension BackupAppOperation
{
enum Action: String
{
case backup
case restore
}
}
@objc(BackupAppOperation)
class BackupAppOperation: ResultOperation<Void>
{
let action: Action
let context: InstallAppOperationContext
private var appName: String?
private var timeoutTimer: Timer?
init(action: Action, context: InstallAppOperationContext)
{
self.action = action
self.context = context
super.init()
}
override func main()
{
super.main()
do
{
if let error = self.context.error
{
throw error
}
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
context.perform {
do
{
let appName = installedApp.name
self.appName = appName
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
let altstoreOpenURL = altstoreApp.openAppURL
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
returnURLComponents?.host = "appBackupResponse"
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
var openURLComponents = URLComponents()
openURLComponents.scheme = installedApp.openAppURL.scheme
openURLComponents.host = self.action.rawValue
openURLComponents.queryItems = [URLQueryItem(name: "returnURL", value: returnURL.absoluteString)]
guard let openURL = openURLComponents.url else { throw OperationError.openAppFailed(name: appName) }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let currentTime = CFAbsoluteTimeGetCurrent()
UIApplication.shared.open(openURL, options: [:]) { (success) in
let elapsedTime = CFAbsoluteTimeGetCurrent() - currentTime
if success
{
self.registerObservers()
}
else if elapsedTime < 0.5
{
// Failed too quickly for human to respond to alert, possibly still finalizing installation.
// Try again in a couple seconds.
print("Failed too quickly, retrying after a few seconds...")
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
UIApplication.shared.open(openURL, options: [:]) { (success) in
if success
{
self.registerObservers()
}
else
{
self.finish(.failure(OperationError.openAppFailed(name: appName)))
}
}
}
}
else
{
self.finish(.failure(OperationError.openAppFailed(name: appName)))
}
}
}
}
catch
{
self.finish(.failure(error))
}
}
}
catch
{
self.finish(.failure(error))
}
}
override func finish(_ result: Result<Void, Error>)
{
let result = result.mapError { (error) -> Error in
let appName = self.appName ?? self.context.bundleIdentifier
switch (error, self.action)
{
case (let error as NSError, _) where (self.context.error as NSError?) == error: fallthrough
case (OperationError.cancelled, _):
return error
case (let error as NSError, .backup):
let localizedFailure = String(format: NSLocalizedString("Could not back up “%@”.", comment: ""), appName)
return error.withLocalizedFailure(localizedFailure)
case (let error as NSError, .restore):
let localizedFailure = String(format: NSLocalizedString("Could not restore “%@”.", comment: ""), appName)
return error.withLocalizedFailure(localizedFailure)
}
}
switch result
{
case .success: self.progress.completedUnitCount += 1
case .failure: break
}
super.finish(result)
}
}
private extension BackupAppOperation
{
func registerObservers()
{
var applicationWillReturnObserver: NSObjectProtocol!
applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
guard let self = self, !self.isFinished else { return }
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
// Final delay to ensure we don't prematurely return failure
// in case timer expired while we were in background, but
// are now returning to app with success response.
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
guard let self = self, !self.isFinished else { return }
self.finish(.failure(OperationError.timedOut))
}
}
NotificationCenter.default.removeObserver(applicationWillReturnObserver!)
}
var backupResponseObserver: NSObjectProtocol!
backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
self?.timeoutTimer?.invalidate()
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
self?.finish(result)
NotificationCenter.default.removeObserver(backupResponseObserver!)
}
}
}

View File

@@ -1,89 +0,0 @@
//
// DeactivateAppOperation.swift
// AltStore
//
// Created by Riley Testut on 3/4/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import AltSign
import Roxas
@objc(DeactivateAppOperation)
class DeactivateAppOperation: ResultOperation<InstalledApp>
{
let app: InstalledApp
let context: OperationContext
init(app: InstalledApp, context: OperationContext)
{
self.app = app
self.context = context
super.init()
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) }
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
print("Sending deactivate app request...")
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
let allIdentifiers = [installedApp.resignedBundleIdentifier] + appExtensionProfiles
let request = RemoveProvisioningProfilesRequest(udid: udid, bundleIdentifiers: Set(allIdentifiers))
connection.send(request) { (result) in
print("Sent deactive app request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
print("Waiting for deactivate app response...")
connection.receiveResponse() { (result) in
print("Receiving deactivate 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(.removeProvisioningProfiles):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
self.progress.completedUnitCount += 1
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
installedApp.isActive = false
self.finish(.success(installedApp))
}
case .success: self.finish(.failure(ALTServerError(.unknownResponse)))
}
}
}
}
}
}
}
}
}

View File

@@ -1,325 +0,0 @@
//
// DownloadAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/10/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltStoreCore
import AltSign
private extension DownloadAppOperation
{
struct DependencyError: ALTLocalizedError
{
let dependency: Dependency
let error: Error
var failure: String? {
return String(format: NSLocalizedString("Could not download “%@”.", comment: ""), self.dependency.preferredFilename)
}
var underlyingError: Error? {
return self.error
}
}
}
@objc(DownloadAppOperation)
class DownloadAppOperation: ResultOperation<ALTApplication>
{
let app: AppProtocol
let context: AppOperationContext
private let bundleIdentifier: String
private let sourceURL: URL
private let destinationURL: URL
private let session = URLSession(configuration: .default)
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
{
self.app = app
self.context = context
self.bundleIdentifier = app.bundleIdentifier
self.sourceURL = app.url
self.destinationURL = destinationURL
super.init()
// App = 3, Dependencies = 1
self.progress.totalUnitCount = 4
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
print("Downloading App:", self.bundleIdentifier)
self.downloadApp(from: self.sourceURL) { result in
do
{
let application = try result.get()
if self.context.bundleIdentifier == StoreApp.dolphinAppID, self.context.bundleIdentifier != application.bundleIdentifier
{
if var infoPlist = NSDictionary(contentsOf: application.bundle.infoPlistURL) as? [String: Any]
{
// Manually update the app's bundle identifier to match the one specified in the source.
// This allows people who previously installed the app to still update and refresh normally.
infoPlist[kCFBundleIdentifierKey as String] = StoreApp.dolphinAppID
(infoPlist as NSDictionary).write(to: application.bundle.infoPlistURL, atomically: true)
}
}
self.downloadDependencies(for: application) { result in
do
{
_ = try result.get()
try FileManager.default.copyItem(at: application.fileURL, to: self.destinationURL, shouldReplace: true)
guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp }
self.finish(.success(copiedApplication))
self.progress.completedUnitCount += 1
}
catch
{
self.finish(.failure(error))
}
}
}
catch
{
self.finish(.failure(error))
}
}
}
override func finish(_ result: Result<ALTApplication, Error>)
{
do
{
try FileManager.default.removeItem(at: self.temporaryDirectory)
}
catch
{
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
}
super.finish(result)
}
}
private extension DownloadAppOperation
{
func downloadApp(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
{
func finishOperation(_ result: Result<URL, Error>)
{
do
{
let fileURL = try result.get()
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
let appBundleURL: URL
if isDirectory.boolValue
{
// Directory, so assuming this is .app bundle.
guard Bundle(url: fileURL) != nil else { throw OperationError.invalidApp }
appBundleURL = self.temporaryDirectory.appendingPathComponent(fileURL.lastPathComponent)
try FileManager.default.copyItem(at: fileURL, to: appBundleURL)
}
else
{
// File, so assuming this is a .ipa file.
appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: self.temporaryDirectory)
}
guard let application = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
completionHandler(.success(application))
}
catch
{
completionHandler(.failure(error))
}
}
if self.sourceURL.isFileURL
{
finishOperation(.success(sourceURL))
self.progress.completedUnitCount += 3
}
else
{
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
do
{
let (fileURL, _) = try Result((fileURL, response), error).get()
finishOperation(.success(fileURL))
try? FileManager.default.removeItem(at: fileURL)
}
catch
{
finishOperation(.failure(error))
}
}
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 3)
downloadTask.resume()
}
}
}
private extension DownloadAppOperation
{
struct AltStorePlist: Decodable
{
private enum CodingKeys: String, CodingKey
{
case dependencies = "ALTDependencies"
}
var dependencies: [Dependency]
}
struct Dependency: Decodable
{
var downloadURL: URL
var path: String?
var preferredFilename: String {
let preferredFilename = self.path.map { ($0 as NSString).lastPathComponent } ?? self.downloadURL.lastPathComponent
return preferredFilename
}
init(from decoder: Decoder) throws
{
enum CodingKeys: String, CodingKey
{
case downloadURL
case path
}
let container = try decoder.container(keyedBy: CodingKeys.self)
let urlString = try container.decode(String.self, forKey: .downloadURL)
let path = try container.decodeIfPresent(String.self, forKey: .path)
guard let downloadURL = URL(string: urlString) else {
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "downloadURL is not a valid URL.")
}
self.downloadURL = downloadURL
self.path = path
}
}
func downloadDependencies(for application: ALTApplication, completionHandler: @escaping (Result<Set<URL>, Error>) -> Void)
{
guard FileManager.default.fileExists(atPath: application.bundle.altstorePlistURL.path) else {
return completionHandler(.success([]))
}
do
{
let data = try Data(contentsOf: application.bundle.altstorePlistURL)
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
var dependencyURLs = Set<URL>()
var dependencyError: DependencyError?
let dispatchGroup = DispatchGroup()
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
for dependency in altstorePlist.dependencies
{
dispatchGroup.enter()
self.download(dependency, for: application, progress: progress) { result in
switch result
{
case .failure(let error): dependencyError = error
case .success(let fileURL): dependencyURLs.insert(fileURL)
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(qos: .userInitiated, queue: .global()) {
if let dependencyError = dependencyError
{
completionHandler(.failure(dependencyError))
}
else
{
completionHandler(.success(dependencyURLs))
}
}
}
catch let error as DecodingError
{
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not download dependencies for %@.", comment: ""), application.name))
completionHandler(.failure(nsError))
}
catch
{
completionHandler(.failure(error))
}
}
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, DependencyError>) -> Void)
{
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
do
{
let (fileURL, _) = try Result((fileURL, response), error).get()
defer { try? FileManager.default.removeItem(at: fileURL) }
let path = dependency.path ?? dependency.preferredFilename
let destinationURL = application.fileURL.appendingPathComponent(path)
let directoryURL = destinationURL.deletingLastPathComponent()
if !FileManager.default.fileExists(atPath: directoryURL.path)
{
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
}
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
completionHandler(.success(destinationURL))
}
catch
{
completionHandler(.failure(DependencyError(dependency: dependency, error: error)))
}
}
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
downloadTask.resume()
}
}

View File

@@ -1,136 +0,0 @@
//
// EnableJITOperation.swift
// EnableJITOperation
//
// Created by Riley Testut on 9/1/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import UIKit
import Combine
import AltStoreCore
@available(iOS 14, *)
protocol EnableJITContext
{
var server: Server? { get }
var installedApp: InstalledApp? { get }
var error: Error? { get }
}
@available(iOS 14, *)
class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
{
let context: Context
private var cancellable: AnyCancellable?
init(context: Context)
{
self.context = context
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let server = self.context.server, let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) }
installedApp.managedObjectContext?.perform {
guard let bundle = Bundle(url: installedApp.fileURL),
let processName = bundle.executableURL?.lastPathComponent
else { return self.finish(.failure(OperationError.invalidApp)) }
let appName = installedApp.name
let openAppURL = installedApp.openAppURL
ServerManager.shared.connect(to: server) { result in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
print("Sending enable JIT request...")
DispatchQueue.main.async {
// Launch app to make sure it is running in foreground.
UIApplication.shared.open(openAppURL) { success in
guard success else { return self.finish(.failure(OperationError.openAppFailed(name: appName))) }
// Combine immediately finishes if an error is thrown, but we want to wait at least until app enters background.
// As a workaround, we set error type to Never and use Result<Void, Error> as the value type instead.
let result = Future<Result<Void, Error>, Never> { promise in
let request = EnableUnsignedCodeExecutionRequest(udid: udid, processName: processName)
connection.send(request) { result in
print("Sent enable JIT request!")
switch result
{
case .failure(let error): promise(.success(.failure(error)))
case .success:
print("Waiting for enable JIT response...")
connection.receiveResponse() { result in
print("Received enable JIT response:", result)
switch result
{
case .failure(let error): promise(.success(.failure(error)))
case .success(.error(let response)): promise(.success(.failure(response.error)))
case .success(.enableUnsignedCodeExecution): promise(.success(.success(())))
case .success: promise(.success(.failure(ALTServerError(.unknownResponse))))
}
}
}
}
}
//TODO: Handle case where app does not enter background (e.g. iPad multitasking).
self.cancellable = result
.combineLatest(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification, object: nil))
.first()
.receive(on: DispatchQueue.main)
.sink { (result, _) in
let content = UNMutableNotificationContent()
switch result
{
case .failure(let error):
content.title = String(format: NSLocalizedString("Could not enable JIT for %@", comment: ""), appName)
content.body = error.localizedDescription
UIDevice.current.vibrate(pattern: .error)
case .success:
content.title = String(format: NSLocalizedString("Enabled JIT for %@", comment: ""), appName)
content.body = String(format: NSLocalizedString("JIT will remain enabled until you quit the app.", comment: ""))
UIDevice.current.vibrate(pattern: .success)
}
if UIApplication.shared.applicationState == .background
{
// For some reason, notification won't show up reliably unless we provide a trigger (as of iOS 15).
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
let request = UNNotificationRequest(identifier: AppManager.enableJITResultNotificationID, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
}
self.finish(result)
}
}
}
}
}
}
}
}

View File

@@ -1,70 +0,0 @@
//
// FetchAnisetteDataOperation.swift
// AltStore
//
// Created by Riley Testut on 1/7/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import AltSign
import Roxas
@objc(FetchAnisetteDataOperation)
class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
{
let context: OperationContext
init(context: OperationContext)
{
self.context = context
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error):
self.finish(.failure(error))
case .success(let connection):
print("Sending anisette data request...")
let request = AnisetteDataRequest()
connection.send(request) { (result) in
print("Sent anisette data request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
print("Waiting for anisette data...")
connection.receiveResponse() { (result) in
print("Receiving anisette data:", result.error?.localizedDescription ?? "success")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(.error(let response)): self.finish(.failure(response.error))
case .success(.anisetteData(let response)): self.finish(.success(response.anisetteData))
case .success: self.finish(.failure(ALTServerError(.unknownRequest)))
}
}
}
}
}
}
}
}

View File

@@ -1,72 +0,0 @@
//
// FetchAppIDsOperation.swift
// AltStore
//
// Created by Riley Testut on 1/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import AltSign
import Roxas
@objc(FetchAppIDsOperation)
class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
{
let context: AuthenticatedOperationContext
let managedObjectContext: NSManagedObjectContext
init(context: AuthenticatedOperationContext, managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
{
self.context = context
self.managedObjectContext = managedObjectContext
super.init()
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard
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.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.managedObjectContext) else { throw OperationError.notAuthenticated }
let fetchedIdentifiers = fetchedAppIDs.map { $0.identifier }
let deletedAppIDsRequest = AppID.fetchRequest() as NSFetchRequest<AppID>
deletedAppIDsRequest.predicate = NSPredicate(format: "%K == %@ AND NOT (%K IN %@)",
#keyPath(AppID.team), team,
#keyPath(AppID.identifier), fetchedIdentifiers)
let deletedAppIDs = try self.managedObjectContext.fetch(deletedAppIDsRequest)
deletedAppIDs.forEach { self.managedObjectContext.delete($0) }
let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.managedObjectContext) }
self.finish(.success((appIDs, self.managedObjectContext)))
}
catch
{
self.finish(.failure(error))
}
}
}
}
}

View File

@@ -1,493 +0,0 @@
//
// FetchProvisioningProfilesOperation.swift
// AltStore
//
// Created by Riley Testut on 2/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import AltSign
import Roxas
@objc(FetchProvisioningProfilesOperation)
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
{
let context: AppOperationContext
var additionalEntitlements: [ALTEntitlement: Any]?
private let appGroupsLock = NSLock()
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 team = self.context.team,
let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) }
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<T>(_ result: Result<T, Error>) -> 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<ALTProvisioningProfile, Error>) -> 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 == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
if let installedApp = InstalledApp.first(satisfying: predicate, in: context)
{
// Teams match if installedApp.team has same identifier as team,
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
#if DEBUG
if app.isAltStoreApp
{
// Use legacy bundle ID format for AltStore.
preferredBundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
}
else
{
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
}
#else
if teamsMatch
{
// This app is already installed with the same team, 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
{
preferredBundleID = nil
}
#endif
}
else
{
preferredBundleID = nil
}
let bundleID: String
if let preferredBundleID = preferredBundleID
{
bundleID = preferredBundleID
}
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: String
if app.isAltStoreApp
{
// Use legacy bundle ID format for AltStore (and its extensions).
updatedParentBundleID = "com.\(team.identifier).\(parentBundleID)"
}
else
{
updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
}
bundleID = 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: bundleID, 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<ALTAppID, Error>) -> 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.lowercased() == bundleIdentifier.lowercased() })
{
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<ALTAppID, Error>) -> Void)
{
var entitlements = app.entitlements
for (key, value) in additionalEntitlements ?? [:]
{
entitlements[key] = value
}
let requiredFeatures = 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 = entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
{
// App uses app groups, so assign `true` to enable the feature.
features[.appGroups] = true
}
else
{
// App has no app groups, so assign `false` to disable the feature.
features[.appGroups] = false
}
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 if appID.features[feature] == nil, let shouldEnableFeature = value as? Bool, !shouldEnableFeature
{
// AppID doesn't already have this feature enabled, but we want it disabled anyway.
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<ALTAppID, Error>) -> Void)
{
var entitlements = app.entitlements
for (key, value) in additionalEntitlements ?? [:]
{
entitlements[key] = value
}
guard var applicationGroups = entitlements[.appGroups] as? [String], !applicationGroups.isEmpty else {
// Assigning an App ID to an empty app group array fails,
// so just do nothing if there are no app groups.
return completionHandler(.success(appID))
}
if app.isAltStoreApp
{
// Potentially updating app groups for this specific AltStore.
// Find the (unique) AltStore app group, then replace it
// with the correct "base" app group ID.
// Otherwise, we may append a duplicate team identifier to the end.
if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) })
{
applicationGroups[index] = Bundle.baseAltStoreAppGroupID
}
else
{
applicationGroups.append(Bundle.baseAltStoreAppGroupID)
}
}
// Dispatch onto global queue to prevent appGroupsLock deadlock.
DispatchQueue.global().async {
// Ensure we're not concurrently fetching and updating app groups,
// which can lead to race conditions such as adding an app group twice.
self.appGroupsLock.lock()
func finish(_ result: Result<ALTAppID, Error>)
{
self.appGroupsLock.unlock()
completionHandler(result)
}
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
switch Result(groups, error)
{
case .failure(let error): finish(.failure(error))
case .success(let fetchedGroups):
let dispatchGroup = DispatchGroup()
var groups = [ALTAppGroup]()
var errors = [Error]()
for groupIdentifier in applicationGroups
{
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
if let group = fetchedGroups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
{
groups.append(group)
}
else
{
dispatchGroup.enter()
// 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
switch Result(group, error)
{
case .success(let group): groups.append(group)
case .failure(let error): errors.append(error)
}
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .global()) {
if let error = errors.first
{
finish(.failure(error))
}
else
{
ALTAppleAPI.shared.assign(appID, to: Array(groups), team: team, session: session) { (success, error) in
let result = Result(success, error)
finish(result.map { _ in appID })
}
}
}
}
}
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, 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, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
}

View File

@@ -1,107 +0,0 @@
//
// FetchSourceOperation.swift
// AltStore
//
// Created by Riley Testut on 7/30/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltStoreCore
import Roxas
@objc(FetchSourceOperation)
class FetchSourceOperation: ResultOperation<Source>
{
let sourceURL: URL
let managedObjectContext: NSManagedObjectContext
private let session: URLSession
private lazy var dateFormatter: ISO8601DateFormatter = {
let dateFormatter = ISO8601DateFormatter()
return dateFormatter
}()
init(sourceURL: URL, managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
{
self.sourceURL = sourceURL
self.managedObjectContext = managedObjectContext
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
configuration.urlCache = nil
self.session = URLSession(configuration: configuration)
}
override func main()
{
super.main()
let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in
let childContext = DatabaseManager.shared.persistentContainer.newBackgroundContext(withParent: self.managedObjectContext)
childContext.mergePolicy = NSOverwriteMergePolicy
childContext.perform {
do
{
let (data, _) = try Result((data, response), error).get()
let decoder = AltStoreCore.JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let text = try container.decode(String.self)
// Full ISO8601 Format.
self.dateFormatter.formatOptions = [.withFullDate, .withFullTime, .withTimeZone]
if let date = self.dateFormatter.date(from: text)
{
return date
}
// Just date portion of ISO8601.
self.dateFormatter.formatOptions = [.withFullDate]
if let date = self.dateFormatter.date(from: text)
{
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Date is in invalid format.")
})
decoder.managedObjectContext = childContext
decoder.sourceURL = self.sourceURL
let source = try decoder.decode(Source.self, from: data)
let identifier = source.identifier
try childContext.save()
self.managedObjectContext.perform {
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), identifier), in: self.managedObjectContext)
{
self.finish(.success(source))
}
else
{
self.finish(.failure(OperationError.noSources))
}
}
}
catch
{
self.managedObjectContext.perform {
self.finish(.failure(error))
}
}
}
}
self.progress.addChild(dataTask.progress, withPendingUnitCount: 1)
dataTask.resume()
}
}

View File

@@ -1,65 +0,0 @@
//
// FetchTrustedSourcesOperation.swift
// AltStore
//
// Created by Riley Testut on 4/13/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Foundation
private extension URL
{
#if STAGING
static let trustedSources = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altstore/trustedsources.json")!
#else
static let trustedSources = URL(string: "https://cdn.altstore.io/file/altstore/altstore/trustedsources.json")!
#endif
}
extension FetchTrustedSourcesOperation
{
struct TrustedSource: Decodable
{
var identifier: String
var sourceURL: URL?
}
private struct Response: Decodable
{
var version: Int
var sources: [FetchTrustedSourcesOperation.TrustedSource]
}
}
class FetchTrustedSourcesOperation: ResultOperation<[FetchTrustedSourcesOperation.TrustedSource]>
{
override func main()
{
super.main()
let dataTask = URLSession.shared.dataTask(with: .trustedSources) { (data, response, error) in
do
{
if let response = response as? HTTPURLResponse
{
guard response.statusCode != 404 else {
self.finish(.failure(URLError(.fileDoesNotExist, userInfo: [NSURLErrorKey: URL.trustedSources])))
return
}
}
guard let data = data else { throw error! }
let response = try Foundation.JSONDecoder().decode(Response.self, from: data)
self.finish(.success(response.sources))
}
catch
{
self.finish(.failure(error))
}
}
dataTask.resume()
}
}

View File

@@ -1,131 +0,0 @@
//
// FindServerOperation.swift
// AltStore
//
// Created by Riley Testut on 9/8/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import Roxas
private let ReceivedServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{ (center, observer, name, object, userInfo) in
guard let name = name, let observer = observer else { return }
let operation = unsafeBitCast(observer, to: FindServerOperation.self)
operation.handle(name)
}
@objc(FindServerOperation)
class FindServerOperation: ResultOperation<Server>
{
let context: OperationContext
private var isWiredServerConnectionAvailable = false
private var localServerMachServiceName: String?
init(context: OperationContext = OperationContext())
{
self.context = context
}
override func main()
{
super.main()
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()
let observer = Unmanaged.passUnretained(self).toOpaque()
// Prepare observers to receive callback from wired connection or background daemon (if available).
CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedServerConnectionResponse, CFNotificationName.wiredServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately)
// Post notifications.
CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionAvailableRequest, nil, nil, true)
self.discoverLocalServer()
// Wait for either callback or timeout.
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
if let machServiceName = self.localServerMachServiceName
{
// Prefer background daemon, if it exists and is running.
let server = Server(connectionType: .local, machServiceName: machServiceName)
self.finish(.success(server))
}
else if self.isWiredServerConnectionAvailable
{
let server = Server(connectionType: .wired)
self.finish(.success(server))
}
else if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred })
{
// Preferred server.
self.finish(.success(server))
}
else if let server = ServerManager.shared.discoveredServers.first
{
// Any available server.
self.finish(.success(server))
}
else
{
// No servers.
self.finish(.failure(ConnectionError.serverNotFound))
}
}
}
override func finish(_ result: Result<Server, Error>)
{
super.finish(result)
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
CFNotificationCenterRemoveObserver(notificationCenter, observer, .wiredServerConnectionAvailableResponse, nil)
}
}
fileprivate extension FindServerOperation
{
func discoverLocalServer()
{
for machServiceName in XPCConnection.machServiceNames
{
let xpcConnection = NSXPCConnection.makeConnection(machServiceName: machServiceName)
let connection = XPCConnection(xpcConnection)
connection.connect { (result) in
switch result
{
case .failure(let error): print("Could not connect to AltDaemon XPC service \(machServiceName).", error)
case .success: self.localServerMachServiceName = machServiceName
}
}
}
}
func handle(_ notification: CFNotificationName)
{
switch notification
{
case .wiredServerConnectionAvailableResponse: self.isWiredServerConnectionAvailable = true
default: break
}
}
}

View File

@@ -1,248 +0,0 @@
//
// InstallAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/19/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Network
import AltStoreCore
import AltSign
import Roxas
@objc(InstallAppOperation)
class InstallAppOperation: ResultOperation<InstalledApp>
{
let context: InstallAppOperationContext
private var didCleanUp = false
init(context: InstallAppOperationContext)
{
self.context = context
super.init()
self.progress.totalUnitCount = 100
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard
let certificate = self.context.certificate,
let resignedApp = self.context.resignedApp,
let connection = self.context.installationConnection
else { return self.finish(.failure(OperationError.invalidParameters)) }
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
backgroundContext.perform {
/* App */
let installedApp: InstalledApp
// Fetch + update rather than insert + resolve merge conflicts to prevent potential context-level conflicts.
if let app = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), self.context.bundleIdentifier), in: backgroundContext)
{
installedApp = app
}
else
{
installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, certificateSerialNumber: certificate.serialNumber, context: backgroundContext)
}
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber)
installedApp.needsResign = false
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
{
installedApp.team = team
}
/* App Extensions */
var installedExtensions = Set<InstalledExtension>()
if
let bundle = Bundle(url: resignedApp.fileURL),
let directory = bundle.builtInPlugInsURL,
let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
{
for case let fileURL as URL in enumerator
{
guard let appExtensionBundle = Bundle(url: fileURL) else { continue }
guard let appExtension = ALTApplication(fileURL: appExtensionBundle.bundleURL) else { continue }
let parentBundleID = self.context.bundleIdentifier
let resignedParentBundleID = resignedApp.bundleIdentifier
let resignedBundleID = appExtension.bundleIdentifier
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
let installedExtension: InstalledExtension
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
{
installedExtension = appExtension
}
else
{
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: backgroundContext)
}
installedExtension.update(resignedAppExtension: appExtension)
installedExtensions.insert(installedExtension)
}
}
installedApp.appExtensions = installedExtensions
self.context.beginInstallationHandler?(installedApp)
// 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()
var activeProfiles: Set<String>?
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
{
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
let fetchRequest = InstalledApp.activeAppsFetchRequest()
fetchRequest.includesPendingChanges = false
var activeApps = InstalledApp.fetch(fetchRequest, in: backgroundContext)
if !activeApps.contains(installedApp)
{
let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +)
let availableActiveApps = max(sideloadedAppsLimit - activeAppsCount, 0)
if installedApp.requiredActiveSlots <= availableActiveApps
{
// This app has not been explicitly activated, but there are enough slots available,
// so implicitly activate it.
installedApp.isActive = true
activeApps.append(installedApp)
}
else
{
installedApp.isActive = false
}
}
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
})
}
let request = BeginInstallationRequest(activeProfiles: activeProfiles, bundleIdentifier: installedApp.resignedBundleIdentifier)
connection.send(request) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
self.receive(from: connection) { (result) in
switch result
{
case .success:
backgroundContext.perform {
installedApp.refreshedDate = Date()
self.finish(.success(installedApp))
}
case .failure(let error):
self.finish(.failure(error))
}
}
}
}
}
}
override func finish(_ result: Result<InstalledApp, Error>)
{
self.cleanUp()
// Only remove refreshed IPA when finished.
if let app = self.context.app
{
let fileURL = InstalledApp.refreshedIPAURL(for: app)
do
{
try FileManager.default.removeItem(at: fileURL)
}
catch
{
print("Failed to remove refreshed .ipa:", error)
}
}
super.finish(result)
}
}
private extension InstallAppOperation
{
func receive(from connection: ServerConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
connection.receiveResponse() { (result) in
do
{
let response = try result.get()
print(response)
switch response
{
case .installationProgress(let response):
if response.progress == 1.0
{
self.progress.completedUnitCount = self.progress.totalUnitCount
completionHandler(.success(()))
}
else
{
self.progress.completedUnitCount = Int64(response.progress * 100)
self.receive(from: connection, completionHandler: completionHandler)
}
case .error(let response):
completionHandler(.failure(response.error))
default:
completionHandler(.failure(ALTServerError(.unknownRequest)))
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
func cleanUp()
{
guard !self.didCleanUp else { return }
self.didCleanUp = true
do
{
try FileManager.default.removeItem(at: self.context.temporaryDirectory)
}
catch
{
print("Failed to remove temporary directory.", error)
}
}
}

View File

@@ -1,93 +0,0 @@
//
// Operation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
class ResultOperation<ResultType>: Operation
{
var resultHandler: ((Result<ResultType, Error>) -> Void)?
@available(*, unavailable)
override func finish()
{
super.finish()
}
func finish(_ result: Result<ResultType, Error>)
{
guard !self.isFinished else { return }
if self.isCancelled
{
self.resultHandler?(.failure(OperationError.cancelled))
}
else
{
self.resultHandler?(result)
}
super.finish()
}
}
class Operation: RSTOperation, ProgressReporting
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
private var backgroundTaskID: UIBackgroundTaskIdentifier?
override var isAsynchronous: Bool {
return true
}
override init()
{
super.init()
self.progress.cancellationHandler = { [weak self] in self?.cancel() }
}
override func cancel()
{
super.cancel()
if !self.progress.isCancelled
{
self.progress.cancel()
}
}
override func main()
{
super.main()
let name = "com.altstore." + NSStringFromClass(type(of: self))
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: name) { [weak self] in
guard let backgroundTask = self?.backgroundTaskID else { return }
self?.cancel()
UIApplication.shared.endBackgroundTask(backgroundTask)
self?.backgroundTaskID = .invalid
}
}
override func finish()
{
guard !self.isFinished else { return }
super.finish()
if let backgroundTaskID = self.backgroundTaskID
{
UIApplication.shared.endBackgroundTask(backgroundTaskID)
self.backgroundTaskID = .invalid
}
}
}

View File

@@ -1,119 +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 AltStoreCore
import AltSign
class OperationContext
{
var server: Server?
var error: Error?
var presentingViewController: UIViewController?
let operations: NSHashTable<Foundation.Operation>
init(server: Server? = nil, error: Error? = nil, operations: [Foundation.Operation] = [])
{
self.server = server
self.error = error
self.operations = NSHashTable<Foundation.Operation>.weakObjects()
for operation in operations
{
self.operations.add(operation)
}
}
convenience init(context: OperationContext)
{
self.init(server: context.server, error: context.error, operations: context.operations.allObjects)
}
}
class AuthenticatedOperationContext: OperationContext
{
var session: ALTAppleAPISession?
var team: ALTTeam?
var certificate: ALTCertificate?
weak var authenticationOperation: AuthenticationOperation?
convenience init(context: AuthenticatedOperationContext)
{
self.init(server: context.server, error: context.error, operations: context.operations.allObjects)
self.session = context.session
self.team = context.team
self.certificate = context.certificate
self.authenticationOperation = context.authenticationOperation
}
}
@dynamicMemberLookup
class AppOperationContext
{
let bundleIdentifier: String
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<T>(dynamicMember keyPath: WritableKeyPath<AuthenticatedOperationContext, T>) -> 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 installedApp: InstalledApp? {
didSet {
self.installedAppContext = self.installedApp?.managedObjectContext
}
}
private var installedAppContext: NSManagedObjectContext?
var beginInstallationHandler: ((InstalledApp) -> Void)?
var alternateIconURL: URL?
}

View File

@@ -1,92 +0,0 @@
//
// OperationError.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
enum OperationError: LocalizedError
{
case unknown
case unknownResult
case cancelled
case timedOut
case notAuthenticated
case appNotFound
case unknownUDID
case invalidApp
case invalidParameters
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
case noSources
case openAppFailed(name: String)
case missingAppGroup
var failureReason: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
case .noSources: return NSLocalizedString("There are no AltStore sources.", comment: "")
case .openAppFailed(let name): return String(format: NSLocalizedString("AltStore was denied permission to launch %@.", comment: ""), name)
case .missingAppGroup: return NSLocalizedString("AltStore's shared app group could not be found.", comment: "")
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
}
}
var recoverySuggestion: String? {
switch self
{
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
let message: String
if requiredAppIDs > 1
{
let availableText: String
switch availableAppIDs
{
case 0: availableText = NSLocalizedString("none are available", comment: "")
case 1: availableText = NSLocalizedString("only 1 is available", comment: "")
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
}
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage
}
else
{
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.maximumUnitCount = 1
dateComponentsFormatter.unitsStyle = .full
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
message = baseMessage + " " + remainingTimeMessage
}
return message
default: return nil
}
}
}

View File

@@ -1,19 +0,0 @@
//
// ALTAppPatcher.h
// AltStore
//
// Created by Riley Testut on 10/18/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ALTAppPatcher : NSObject
- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *_Nullable *)error;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,143 +0,0 @@
//
// ALTAppPatcher.m
// AltStore
//
// Created by Riley Testut on 10/18/21.
// Copied with minor modifications from sample code provided by Linus Henze.
//
#import "ALTAppPatcher.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@import Roxas;
#define CPU_SUBTYPE_PAC 0x80000000
#define FAT_MAGIC 0xcafebabe
#define ROUND_TO_PAGE(val) (((val % 0x4000) == 0) ? val : (val + (0x4000 - (val & 0x3FFF))))
typedef struct {
uint32_t magic;
uint32_t cpuType;
uint32_t cpuSubType;
// Incomplete, we don't need anything else
} MachOHeader;
typedef struct {
uint32_t cpuType;
uint32_t cpuSubType;
uint32_t fileOffset;
uint32_t size;
uint32_t alignment;
} FatArch;
typedef struct {
uint32_t magic;
uint32_t archCount;
FatArch archs[0];
} FatHeader;
// Given two MachO files, return a FAT file with the following properties:
// 1. installd will still see the original MachO and validate it's code signature
// 2. The kernel will only see the injected MachO instead
//
// Only arm64e for now
void *injectApp(void *originalApp, size_t originalAppSize, void *appToInject, size_t appToInjectSize, size_t *outputSize) {
*outputSize = 0;
// First validate the App to inject: It must be an arm64e application
if (appToInjectSize < sizeof(MachOHeader)) {
return NULL;
}
MachOHeader *injectedHeader = (MachOHeader*) appToInject;
if (injectedHeader->cpuType != CPU_TYPE_ARM64) {
return NULL;
}
if (injectedHeader->cpuSubType != (CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC)) {
return NULL;
}
// Ok, the App to inject is ok
// Now build a fat header
size_t originalAppSizeRounded = ROUND_TO_PAGE(originalAppSize);
size_t appToInjectSizeRounded = ROUND_TO_PAGE(appToInjectSize);
size_t totalSize = 0x4000 /* Fat Header + Alignment */ + originalAppSizeRounded + appToInjectSizeRounded;
void *fatBuf = malloc(totalSize);
if (fatBuf == NULL) {
return NULL;
}
bzero(fatBuf, totalSize);
FatHeader *fatHeader = (FatHeader*) fatBuf;
fatHeader->magic = htonl(FAT_MAGIC);
fatHeader->archCount = htonl(2);
// Write first arch (original app)
fatHeader->archs[0].cpuType = htonl(CPU_TYPE_ARM64);
fatHeader->archs[0].cpuSubType = htonl(CPU_SUBTYPE_ARM64E); /* Note that this is not a valid cpu subtype */
fatHeader->archs[0].fileOffset = htonl(0x4000);
fatHeader->archs[0].size = htonl(originalAppSize);
fatHeader->archs[0].alignment = htonl(0xE);
// Write second arch (injected app)
fatHeader->archs[1].cpuType = htonl(CPU_TYPE_ARM64);
fatHeader->archs[1].cpuSubType = htonl(CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC);
fatHeader->archs[1].fileOffset = htonl(0x4000 + originalAppSizeRounded);
fatHeader->archs[1].size = htonl(appToInjectSize);
fatHeader->archs[1].alignment = htonl(0xE);
// Ok, now write the MachOs
memcpy(fatBuf + 0x4000, originalApp, originalAppSize);
memcpy(fatBuf + 0x4000 + originalAppSizeRounded, appToInject, appToInjectSize);
// We're done!
*outputSize = totalSize;
return fatBuf;
}
@implementation ALTAppPatcher
- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *__autoreleasing *)error
{
NSMutableData *originalApp = [NSMutableData dataWithContentsOfURL:appFileURL options:0 error:error];
if (originalApp == nil)
{
return NO;
}
NSMutableData *injectedApp = [NSMutableData dataWithContentsOfURL:patchFileURL options:0 error:error];
if (injectedApp == nil)
{
return NO;
}
size_t outputSize = 0;
void *output = injectApp(originalApp.mutableBytes, originalApp.length, injectedApp.mutableBytes, injectedApp.length, &outputSize);
if (output == NULL)
{
if (error)
{
// If injectApp fails, it means the patch app is in the wrong format.
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: patchFileURL}];
}
return NO;
}
NSData *outputData = [NSData dataWithBytesNoCopy:output length:outputSize freeWhenDone:YES];
if (![outputData writeToURL:appFileURL options:NSDataWritingAtomic error:error])
{
return NO;
}
return YES;
}
@end

View File

@@ -1,124 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19162" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="WBb-E1-bN8">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19144"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="dx2-fp-qDX">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="WBb-E1-bN8" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="cVa-8m-fW6" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<color key="barTintColor" name="SettingsBackground"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="idH-XF-rK8" kind="relationship" relationship="rootViewController" id="hSJ-tL-4nB"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="i7K-pi-SRe" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="137.68115942028987" y="137.94642857142856"/>
</scene>
<!--Patch View Controller-->
<scene sceneID="gJ4-4F-79r">
<objects>
<viewController id="idH-XF-rK8" customClass="PatchViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="4bV-S5-z7S">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RHK-C5-7wu" customClass="RSTPlaceholderView">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="PjB-Dc-9n3">
<rect key="frame" x="20" y="736.5" width="374" height="117.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GQF-6P-Fit">
<rect key="frame" x="0.0" y="0.0" width="374" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" name="Text"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SNn-Ad-ICf" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="28.5" width="374" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
<constraint firstAttribute="height" constant="51" id="s4X-uf-nl9"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
<color key="tintColor" name="SettingsHighlighted"/>
<state key="normal" title="Install Untethered Jailbreak">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="performButtonAction" destination="idH-XF-rK8" eventType="primaryActionTriggered" id="FxO-1Y-IML"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="leJ-09-giz">
<rect key="frame" x="0.0" y="87.5" width="374" height="30"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" title="Install Without Untethering">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="installRegularJailbreak" destination="idH-XF-rK8" eventType="primaryActionTriggered" id="1XB-11-Kdn"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="H6g-xA-DdL"/>
<color key="backgroundColor" name="SettingsBackground"/>
<constraints>
<constraint firstItem="RHK-C5-7wu" firstAttribute="top" secondItem="4bV-S5-z7S" secondAttribute="top" id="CVK-6E-iA6"/>
<constraint firstAttribute="trailingMargin" secondItem="PjB-Dc-9n3" secondAttribute="trailing" id="GOg-JU-LIP"/>
<constraint firstItem="RHK-C5-7wu" firstAttribute="bottom" secondItem="4bV-S5-z7S" secondAttribute="bottom" id="LPh-J8-IVx"/>
<constraint firstItem="PjB-Dc-9n3" firstAttribute="leading" secondItem="4bV-S5-z7S" secondAttribute="leadingMargin" id="Rlg-PC-5ZN"/>
<constraint firstItem="RHK-C5-7wu" firstAttribute="trailing" secondItem="H6g-xA-DdL" secondAttribute="trailing" id="XdZ-36-6yS"/>
<constraint firstAttribute="bottomMargin" secondItem="PjB-Dc-9n3" secondAttribute="bottom" id="hTS-nX-0xv"/>
<constraint firstItem="RHK-C5-7wu" firstAttribute="leading" secondItem="H6g-xA-DdL" secondAttribute="leading" id="lzV-fG-Xv6"/>
</constraints>
</view>
<navigationItem key="navigationItem" largeTitleDisplayMode="always" id="0J1-80-RD8">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="vtw-PQ-Dk1">
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<action selector="cancel" destination="idH-XF-rK8" id="4Wk-dv-RYG"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="cancelBarButtonItem" destination="vtw-PQ-Dk1" id="8Mh-GU-KD5"/>
<outlet property="cancelButton" destination="leJ-09-giz" id="BNh-I3-vXc"/>
<outlet property="pillButton" destination="SNn-Ad-ICf" id="iJg-TC-p8q"/>
<outlet property="placeholderView" destination="RHK-C5-7wu" id="5x0-sg-HAH"/>
<outlet property="taskDescriptionLabel" destination="GQF-6P-Fit" id="C4c-xy-kvU"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8ev-19-qsi" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1001" y="138"/>
</scene>
</scenes>
<resources>
<namedColor name="SettingsBackground">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SettingsHighlighted">
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Text">
<color red="1" green="1" blue="1" alpha="0.75" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@@ -1,242 +0,0 @@
//
// PatchAppOperation.swift
// AltStore
//
// Created by Riley Testut on 10/13/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import UIKit
import Combine
import AppleArchive
import System
import AltStoreCore
import AltSign
import Roxas
@available(iOS 14, *)
protocol PatchAppContext
{
var bundleIdentifier: String { get }
var temporaryDirectory: URL { get }
var resignedApp: ALTApplication? { get }
var error: Error? { get }
}
enum PatchAppError: LocalizedError
{
case unsupportedOperatingSystemVersion(OperatingSystemVersion)
var errorDescription: String? {
switch self
{
case .unsupportedOperatingSystemVersion(let osVersion):
var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)"
if osVersion.patchVersion != 0
{
osVersionString += ".\(osVersion.patchVersion)"
}
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
return errorDescription
}
}
}
private struct OTAUpdate
{
var url: URL
var archivePath: String
}
@available(iOS 14, *)
class PatchAppOperation: ResultOperation<Void>
{
let context: PatchAppContext
var progressHandler: ((Progress, String) -> Void)?
private let appPatcher = ALTAppPatcher()
private lazy var patchDirectory: URL = self.context.temporaryDirectory.appendingPathComponent("Patch", isDirectory: true)
private var cancellable: AnyCancellable?
init(context: PatchAppContext)
{
self.context = context
super.init()
self.progress.totalUnitCount = 100
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
self.progressHandler?(self.progress, NSLocalizedString("Downloading iOS firmware...", comment: ""))
self.cancellable = self.fetchOTAUpdate()
.flatMap { self.downloadArchive(from: $0) }
.flatMap { self.extractSpotlightFromArchive(at: $0) }
.flatMap { self.patch(resignedApp, withBinaryAt: $0) }
.tryMap { try FileManager.default.zipAppBundle(at: $0) }
.tryMap { (fileURL) in
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
}
.receive(on: RunLoop.main)
.sink { completion in
switch completion
{
case .failure(let error): self.finish(.failure(error))
case .finished: self.finish(.success(()))
}
} receiveValue: { _ in }
}
override func cancel()
{
super.cancel()
self.cancellable?.cancel()
self.cancellable = nil
}
}
private let ALTFragmentZipCallback: @convention(c) (UInt32) -> Void = { (percentageComplete) in
guard let progress = Progress.current() else { return }
if percentageComplete == 100 && progress.completedUnitCount == 0
{
// Ignore first percentageComplete, which is always 100.
return
}
progress.completedUnitCount = Int64(percentageComplete)
}
@available(iOS 14, *)
private extension PatchAppOperation
{
func fetchOTAUpdate() -> AnyPublisher<OTAUpdate, Error>
{
Just(()).tryMap {
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
switch (osVersion.majorVersion, osVersion.minorVersion)
{
case (14, 3):
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2020WinterFCS/patches/001-87330/99E29969-F6B6-422A-B946-70DE2E2D73BE/com_apple_MobileAsset_SoftwareUpdate/67f9e42f5e57a20e0a87eaf81b69dd2a61311d3f.zip")!,
archivePath: "AssetData/payloadv2/payload.042")
case (14, 4):
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021WinterFCS/patches/001-98606/43AF99A1-F286-43B1-A101-F9F856EA395A/com_apple_MobileAsset_SoftwareUpdate/c4985c32c344beb7b49c61919b4e39d1fd336c90.zip")!,
archivePath: "AssetData/payloadv2/payload.042")
case (14, 5):
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021SpringFCS/patches/061-84483/AB525139-066E-46F8-8E85-DCE802C03BA8/com_apple_MobileAsset_SoftwareUpdate/788573ae93113881db04269acedeecabbaa643e3.zip")!,
archivePath: "AssetData/payloadv2/payload.043")
default: throw PatchAppError.unsupportedOperatingSystemVersion(osVersion)
}
}
.eraseToAnyPublisher()
}
func downloadArchive(from update: OTAUpdate) -> AnyPublisher<URL, Error>
{
Just(()).tryMap {
#if targetEnvironment(simulator)
throw PatchAppError.unsupportedOperatingSystemVersion(ProcessInfo.processInfo.operatingSystemVersion)
#else
try FileManager.default.createDirectory(at: self.patchDirectory, withIntermediateDirectories: true, attributes: nil)
let archiveURL = self.patchDirectory.appendingPathComponent("ota.archive")
try archiveURL.withUnsafeFileSystemRepresentation { archivePath in
guard let fz = fragmentzip_open((update.url.absoluteString as NSString).utf8String!) else {
throw URLError(.cannotConnectToHost, userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("The connection failed because a connection cannot be made to the host.", comment: ""),
NSURLErrorKey: update.url])
}
defer { fragmentzip_close(fz) }
self.progress.becomeCurrent(withPendingUnitCount: 100)
defer { self.progress.resignCurrent() }
guard fragmentzip_download_file(fz, update.archivePath, archivePath!, ALTFragmentZipCallback) == 0 else {
throw URLError(.networkConnectionLost, userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("The connection failed because the network connection was lost.", comment: ""),
NSURLErrorKey: update.url])
}
}
print("Downloaded OTA archive.")
return archiveURL
#endif
}
.mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not download OTA archive.", comment: "")) }
.eraseToAnyPublisher()
}
func extractSpotlightFromArchive(at archiveURL: URL) -> AnyPublisher<URL, Error>
{
Just(()).tryMap {
#if targetEnvironment(simulator)
throw PatchAppError.unsupportedOperatingSystemVersion(ProcessInfo.processInfo.operatingSystemVersion)
#else
let spotlightPath = "Applications/Spotlight.app/Spotlight"
let spotlightFileURL = self.patchDirectory.appendingPathComponent(spotlightPath)
guard let readFileStream = ArchiveByteStream.fileStream(path: FilePath(archiveURL.path), mode: .readOnly, options: [], permissions: FilePermissions(rawValue: 0o644)),
let decompressStream = ArchiveByteStream.decompressionStream(readingFrom: readFileStream),
let decodeStream = ArchiveStream.decodeStream(readingFrom: decompressStream),
let readStream = ArchiveStream.extractStream(extractingTo: FilePath(self.patchDirectory.path))
else { throw CocoaError(.fileReadCorruptFile, userInfo: [NSURLErrorKey: archiveURL]) }
_ = try ArchiveStream.process(readingFrom: decodeStream, writingTo: readStream) { message, filePath, data in
guard filePath == FilePath(spotlightPath) else { return .skip }
return .ok
}
print("Extracted Spotlight from OTA archive.")
return spotlightFileURL
#endif
}
.mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not extract Spotlight from OTA archive.", comment: "")) }
.eraseToAnyPublisher()
}
func patch(_ app: ALTApplication, withBinaryAt patchFileURL: URL) -> AnyPublisher<URL, Error>
{
Just(()).tryMap {
// executableURL may be nil, so use infoDictionary instead to determine executable name.
// guard let appName = app.bundle.executableURL?.lastPathComponent else { throw OperationError.invalidApp }
guard let appName = app.bundle.infoDictionary?[kCFBundleExecutableKey as String] as? String else { throw OperationError.invalidApp }
let temporaryAppURL = self.patchDirectory.appendingPathComponent("Patched.app", isDirectory: true)
try FileManager.default.copyItem(at: app.fileURL, to: temporaryAppURL)
let appBinaryURL = temporaryAppURL.appendingPathComponent(appName, isDirectory: false)
try self.appPatcher.patchAppBinary(at: appBinaryURL, withBinaryAt: patchFileURL)
print("Patched \(app.name).")
return temporaryAppURL
}
.mapError { ($0 as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), app.name)) }
.eraseToAnyPublisher()
}
}

View File

@@ -1,499 +0,0 @@
//
// PatchViewController.swift
// AltStore
//
// Created by Riley Testut on 10/20/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import UIKit
import Combine
import AltStoreCore
import AltSign
import Roxas
@available(iOS 14.0, *)
extension PatchViewController
{
enum Step
{
case confirm
case install
case openApp
case patchApp
case reboot
case refresh
case finish
}
}
@available(iOS 14.0, *)
class PatchViewController: UIViewController
{
var patchApp: AnyApp?
var installedApp: InstalledApp?
var completionHandler: ((Result<Void, Error>) -> Void)?
private let context = AuthenticatedOperationContext()
private var currentStep: Step = .confirm {
didSet {
DispatchQueue.main.async {
self.update()
}
}
}
private var buttonHandler: (() -> Void)?
private var resignedApp: ALTApplication?
private lazy var temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL()
private var didEnterBackgroundObservation: NSObjectProtocol?
private weak var cancellableProgress: Progress?
@IBOutlet private var placeholderView: RSTPlaceholderView!
@IBOutlet private var taskDescriptionLabel: UILabel!
@IBOutlet private var pillButton: PillButton!
@IBOutlet private var cancelBarButtonItem: UIBarButtonItem!
@IBOutlet private var cancelButton: UIButton!
override func viewDidLoad()
{
super.viewDidLoad()
self.isModalInPresentation = true
self.placeholderView.stackView.spacing = 20
self.placeholderView.textLabel.textColor = .white
self.placeholderView.detailTextLabel.textAlignment = .left
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
self.buttonHandler = { [weak self] in
self?.startProcess()
}
do
{
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
}
catch
{
print("Failed to create temporary directory:", error)
}
self.update()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if self.installedApp != nil
{
self.refreshApp()
}
}
}
@available(iOS 14.0, *)
private extension PatchViewController
{
func update()
{
self.cancelButton.alpha = 0.0
switch self.currentStep
{
case .confirm:
guard let app = self.patchApp else { break }
if UIDevice.current.isUntetheredJailbreakRequired
{
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Requires Untethering", comment: "")
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak is untethered, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but AltStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
}
else
{
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Supports Untethering", comment: "")
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak has an untethered version, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but AltStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
}
self.pillButton.setTitle(NSLocalizedString("Install Untethered Jailbreak", comment: ""), for: .normal)
self.cancelButton.alpha = 1.0
case .install:
guard let app = self.patchApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Installing %@ placeholder…", comment: ""), app.name)
self.placeholderView.detailTextLabel.text = NSLocalizedString("A placeholder app needs to be installed in order to prepare your device for untethering.\n\nThis may take a few moments.", comment: "")
case .openApp:
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
case .patchApp:
guard let app = self.patchApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Patching %@ placeholder…", comment: ""), app.name)
self.placeholderView.detailTextLabel.text = NSLocalizedString("This will take a few moments. Please do not turn off the screen or leave the app until patching is complete.", comment: "")
self.pillButton.setTitle(NSLocalizedString("Patch Placeholder", comment: ""), for: .normal)
case .reboot:
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
case .refresh:
guard let installedApp = self.installedApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish installing %@?", comment: ""), installedApp.name)
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("In order to finish jailbreaking this device, you need to install %@ then follow the instructions in the app.", comment: ""), installedApp.name)
self.pillButton.setTitle(String(format: NSLocalizedString("Install %@", comment: ""), installedApp.name), for: .normal)
case .finish:
guard let installedApp = self.installedApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish in %@", comment: ""), installedApp.name)
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("Follow the instructions in %@ to finish jailbreaking this device.", comment: ""), installedApp.name)
self.pillButton.setTitle(String(format: NSLocalizedString("Open %@", comment: ""), installedApp.name), for: .normal)
}
}
func present(_ error: Error, title: String)
{
DispatchQueue.main.async {
let nsError = error as NSError
let alertController = UIAlertController(title: nsError.localizedFailure ?? title, message: error.localizedDescription, preferredStyle: .alert)
alertController.addAction(.ok)
self.present(alertController, animated: true, completion: nil)
self.setProgress(nil, description: nil)
}
}
func setProgress(_ progress: Progress?, description: String?)
{
DispatchQueue.main.async {
self.pillButton.progress = progress
self.taskDescriptionLabel.text = description ?? " " // Use non-empty string to prevent label resizing itself.
}
}
func finish(with result: Result<Void, Error>)
{
do
{
try FileManager.default.removeItem(at: self.temporaryDirectory)
}
catch
{
print("Failed to remove temporary directory:", error)
}
if let observation = self.didEnterBackgroundObservation
{
NotificationCenter.default.removeObserver(observation)
}
self.completionHandler?(result)
self.completionHandler = nil
}
}
@available(iOS 14.0, *)
private extension PatchViewController
{
@IBAction func performButtonAction()
{
self.buttonHandler?()
}
@IBAction func cancel()
{
self.finish(with: .success(()))
self.cancellableProgress?.cancel()
}
@IBAction func installRegularJailbreak()
{
guard let app = self.patchApp else { return }
let title: String
let message: String
if UIDevice.current.isUntetheredJailbreakRequired
{
title = NSLocalizedString("Untethering Required", comment: "")
message = String(format: NSLocalizedString("%@ can not jailbreak this device unless you untether it first. Are you sure you want to install without untethering?", comment: ""), app.name)
}
else
{
title = NSLocalizedString("Untethering Recommended", comment: "")
message = String(format: NSLocalizedString("Untethering this jailbreak will prevent %@ from expiring, even after 7 days or rebooting the device. Are you sure you want to install without untethering?", comment: ""), app.name)
}
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Install Without Untethering", comment: ""), style: .default) { _ in
self.finish(with: .failure(OperationError.cancelled))
})
alertController.addAction(.cancel)
self.present(alertController, animated: true, completion: nil)
}
}
@available(iOS 14.0, *)
private extension PatchViewController
{
func startProcess()
{
guard let patchApp = self.patchApp else { return }
self.currentStep = .install
if let progress = AppManager.shared.installationProgress(for: patchApp)
{
// Cancel pending jailbreak app installation so we can start a new one.
progress.cancel()
}
let appURL = InstalledApp.fileURL(for: patchApp)
let cachedAppURL = self.temporaryDirectory.appendingPathComponent("Cached.app")
do
{
// Make copy of original app, so we can replace the cached patch app with it later.
try FileManager.default.copyItem(at: appURL, to: cachedAppURL, shouldReplace: true)
}
catch
{
self.present(error, title: NSLocalizedString("Could not back up jailbreak app.", comment: ""))
return
}
var unzippingError: Error?
let refreshGroup = AppManager.shared.install(patchApp, presentingViewController: self, context: self.context) { result in
do
{
_ = try result.get()
if let unzippingError = unzippingError
{
throw unzippingError
}
// Replace cached patch app with original app so we can resume installing it post-reboot.
try FileManager.default.copyItem(at: cachedAppURL, to: appURL, shouldReplace: true)
self.openApp()
}
catch
{
self.present(error, title: String(format: NSLocalizedString("Could not install %@ placeholder.", comment: ""), patchApp.name))
}
}
refreshGroup.beginInstallationHandler = { (installedApp) in
do
{
// Replace patch app name with correct name.
installedApp.name = patchApp.name
let ipaURL = installedApp.refreshedIPAURL
let resignedAppURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: self.temporaryDirectory)
self.resignedApp = ALTApplication(fileURL: resignedAppURL)
}
catch
{
print("Error unzipping app bundle:", error)
unzippingError = error
}
}
self.setProgress(refreshGroup.progress, description: nil)
self.cancellableProgress = refreshGroup.progress
}
func openApp()
{
guard let patchApp = self.patchApp else { return }
self.setProgress(nil, description: nil)
self.currentStep = .openApp
// This observation is willEnterForeground because patching starts immediately upon return.
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { (notification) in
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
self.patchApplication()
}
self.buttonHandler = { [weak self] in
guard let self = self else { return }
#if !targetEnvironment(simulator)
let openURL = InstalledApp.openAppURL(for: patchApp)
UIApplication.shared.open(openURL) { success in
guard !success else { return }
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
}
#endif
}
}
func patchApplication()
{
guard let resignedApp = self.resignedApp else { return }
self.currentStep = .patchApp
self.buttonHandler = { [weak self] in
self?.patchApplication()
}
let patchAppOperation = AppManager.shared.patch(resignedApp: resignedApp, presentingViewController: self, context: self.context) { result in
switch result
{
case .failure(let error): self.present(error, title: String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), resignedApp.name))
case .success: self.rebootDevice()
}
}
patchAppOperation.progressHandler = { (progress, description) in
self.setProgress(progress, description: description)
}
self.cancellableProgress = patchAppOperation.progress
}
func rebootDevice()
{
guard let patchApp = self.patchApp else { return }
self.setProgress(nil, description: nil)
self.currentStep = .reboot
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
var patchedApps = UserDefaults.standard.patchedApps ?? []
if !patchedApps.contains(patchApp.bundleIdentifier)
{
patchedApps.append(patchApp.bundleIdentifier)
UserDefaults.standard.patchedApps = patchedApps
}
self.finish(with: .success(()))
}
self.buttonHandler = { [weak self] in
guard let self = self else { return }
#if !targetEnvironment(simulator)
let openURL = InstalledApp.openAppURL(for: patchApp)
UIApplication.shared.open(openURL) { success in
guard !success else { return }
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
}
#endif
}
}
func refreshApp()
{
guard let installedApp = self.installedApp else { return }
self.currentStep = .refresh
self.buttonHandler = { [weak self] in
guard let self = self else { return }
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
tempApp.needsResign = true
let errorTitle = String(format: NSLocalizedString("Could not install %@.", comment: ""), tempApp.name)
do
{
try context.save()
installedApp.managedObjectContext?.perform {
// Refreshing ensures we don't attempt to patch the app again,
// since that is only checked when installing a new app.
let refreshGroup = AppManager.shared.refresh([installedApp], presentingViewController: self, group: nil)
refreshGroup.completionHandler = { [weak refreshGroup, weak self] (results) in
guard let self = self else { return }
do
{
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown }
_ = try result.get()
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
{
patchedApps.remove(at: index)
UserDefaults.standard.patchedApps = patchedApps
}
self.finish()
}
catch
{
self.present(error, title: errorTitle)
}
}
self.setProgress(refreshGroup.progress, description: String(format: NSLocalizedString("Installing %@...", comment: ""), installedApp.name))
}
}
catch
{
self.present(error, title: errorTitle)
}
}
}
}
func finish()
{
guard let installedApp = self.installedApp else { return }
self.setProgress(nil, description: nil)
self.currentStep = .finish
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
self.finish(with: .success(()))
}
installedApp.managedObjectContext?.perform {
let appName = installedApp.name
let openURL = installedApp.openAppURL
self.buttonHandler = { [weak self] in
guard let self = self else { return }
#if !targetEnvironment(simulator)
UIApplication.shared.open(openURL) { success in
guard !success else { return }
self.present(OperationError.openAppFailed(name: appName), title: String(format: NSLocalizedString("Could not open %@.", comment: ""), appName))
}
#endif
}
}
}
}

View File

@@ -1,18 +0,0 @@
//
// fragmentzip.h
// AltStore
//
// Created by Riley Testut on 10/25/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
#ifndef fragmentzip_h
#define fragmentzip_h
typedef void fragmentzip_t;
typedef void (*fragmentzip_process_callback_t)(unsigned int progress);
fragmentzip_t *fragmentzip_open(const char *url);
int fragmentzip_download_file(fragmentzip_t *info, const char *remotepath, const char *savepath, fragmentzip_process_callback_t callback);
void fragmentzip_close(fragmentzip_t *info);
#endif /* fragmentzip_h */

View File

@@ -1,120 +0,0 @@
//
// RefreshAppOperation.swift
// AltStore
//
// Created by Riley Testut on 2/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import AltSign
import Roxas
@objc(RefreshAppOperation)
class RefreshAppOperation: ResultOperation<InstalledApp>
{
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 profiles = self.context.provisioningProfiles else { throw OperationError.invalidParameters }
guard let app = self.context.app else { throw OperationError.appNotFound }
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<String>?
if UserDefaults.standard.activeAppsLimit != nil
{
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
let activeApps = InstalledApp.fetchActiveApps(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.appNotFound))
}
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))
}
}
}

View File

@@ -1,92 +0,0 @@
//
// RefreshGroup.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltStoreCore
import AltSign
class RefreshGroup: NSObject
{
let context: AuthenticatedOperationContext
let progress = Progress.discreteProgress(totalUnitCount: 0)
var completionHandler: (([String: Result<InstalledApp, Error>]) -> Void)?
var beginInstallationHandler: ((InstalledApp) -> Void)?
private(set) var results = [String: Result<InstalledApp, Error>]()
// Keep strong references to managed object contexts
// so they don't die out from under us.
private(set) var _contexts = Set<NSManagedObjectContext>()
private var isFinished = false
private let dispatchGroup = DispatchGroup()
private var operations: [Foundation.Operation] = []
init(context: AuthenticatedOperationContext = AuthenticatedOperationContext())
{
self.context = context
super.init()
}
/// Used to keep track of which operations belong to this group.
/// This does _not_ add them to any operation queue.
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()) { [weak self] in
self?.finish()
}
}
self.operations.append(contentsOf: operations)
}
func set(_ result: Result<InstalledApp, Error>, forAppWithBundleIdentifier bundleIdentifier: String)
{
self.results[bundleIdentifier] = result
switch result
{
case .failure: break
case .success(let installedApp):
guard let context = installedApp.managedObjectContext else { break }
self._contexts.insert(context)
}
}
func cancel()
{
self.operations.forEach { $0.cancel() }
}
}
private extension RefreshGroup
{
func finish()
{
guard !self.isFinished else { return }
self.isFinished = true
self.completionHandler?(self.results)
}
}

View File

@@ -1,77 +0,0 @@
//
// RemoveAppBackupOperation.swift
// AltStore
//
// Created by Riley Testut on 5/13/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
@objc(RemoveAppBackupOperation)
class RemoveAppBackupOperation: ResultOperation<Void>
{
let context: InstallAppOperationContext
private let coordinator = NSFileCoordinator()
private let coordinatorQueue = OperationQueue()
init(context: InstallAppOperationContext)
{
self.context = context
super.init()
self.coordinatorQueue.name = "AltStore - RemoveAppBackupOperation Queue"
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
installedApp.managedObjectContext?.perform {
guard let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return self.finish(.failure(OperationError.missingAppGroup)) }
let intent = NSFileAccessIntent.writingIntent(with: backupDirectoryURL, options: [.forDeleting])
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
do
{
if let error = error
{
throw error
}
try FileManager.default.removeItem(at: intent.url)
self.finish(.success(()))
}
catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile
{
#if DEBUG
// When debugging, it's expected that app groups don't match, so ignore.
self.finish(.success(()))
#else
print("Failed to remove app backup directory:", error)
self.finish(.failure(error))
#endif
}
catch
{
print("Failed to remove app backup directory:", error)
self.finish(.failure(error))
}
}
}
}
}

View File

@@ -1,83 +0,0 @@
//
// RemoveAppOperation.swift
// AltStore
//
// Created by Riley Testut on 5/12/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
@objc(RemoveAppOperation)
class RemoveAppOperation: ResultOperation<InstalledApp>
{
let context: InstallAppOperationContext
init(context: InstallAppOperationContext)
{
self.context = context
super.init()
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let server = self.context.server, let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) }
installedApp.managedObjectContext?.perform {
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
print("Sending remove app request...")
let request = RemoveAppRequest(udid: udid, bundleIdentifier: resignedBundleIdentifier)
connection.send(request) { (result) in
print("Sent remove app request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
print("Waiting for remove app response...")
connection.receiveResponse() { (result) in
print("Receiving remove 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(.removeApp):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
self.progress.completedUnitCount += 1
let installedApp = context.object(with: installedApp.objectID) as! InstalledApp
installedApp.isActive = false
self.finish(.success(installedApp))
}
case .success: self.finish(.failure(ALTServerError(.unknownResponse)))
}
}
}
}
}
}
}
}
}

View File

@@ -1,258 +0,0 @@
//
// ResignAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltStoreCore
import AltSign
@objc(ResignAppOperation)
class ResignAppOperation: ResultOperation<ALTApplication>
{
let context: InstallAppOperationContext
init(context: InstallAppOperationContext)
{
self.context = context
super.init()
self.progress.totalUnitCount = 3
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard
let app = self.context.app,
let profiles = self.context.provisioningProfiles,
let team = self.context.team,
let certificate = self.context.certificate
else { return self.finish(.failure(OperationError.invalidParameters)) }
// 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 }
print("Resigning App:", self.context.bundleIdentifier)
// 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 }
// 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))
}
}
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
}
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
}
func process<T>(_ result: Result<T, Error>) -> 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
}
}
}
private extension ResignAppOperation
{
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
let bundleIdentifier = app.bundleIdentifier
let openURL = InstalledApp.openAppURL(for: app)
let fileURL = app.fileURL
func prepare(_ bundle: Bundle, additionalInfoDictionaryValues: [String: Any] = [:]) throws
{
guard let identifier = bundle.bundleIdentifier else { throw ALTError(.missingAppBundle) }
guard let profile = profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
infoDictionary[Bundle.Info.altBundleID] = identifier
for (key, value) in additionalInfoDictionaryValues
{
infoDictionary[key] = value
}
if let appGroups = profile.entitlements[.appGroups] as? [String]
{
infoDictionary[Bundle.Info.appGroups] = appGroups
// To keep file providers working, remap the NSExtensionFileProviderDocumentGroup, if there is one.
if var extensionInfo = infoDictionary["NSExtension"] as? [String: Any],
let appGroup = extensionInfo["NSExtensionFileProviderDocumentGroup"] as? String,
let localAppGroup = appGroups.filter({ $0.contains(appGroup) }).min(by: { $0.count < $1.count })
{
extensionInfo["NSExtensionFileProviderDocumentGroup"] = localAppGroup
infoDictionary["NSExtension"] = extensionInfo
}
}
// Add app-specific exported UTI so we can check later if this app (extension) is installed or not.
let installedAppUTI = ["UTTypeConformsTo": [],
"UTTypeDescription": "AltStore Installed App",
"UTTypeIconFiles": [],
"UTTypeIdentifier": InstalledApp.installedAppUTI(forBundleIdentifier: profile.bundleIdentifier),
"UTTypeTagSpecification": [:]] as [String : Any]
var exportedUTIs = infoDictionary[Bundle.Info.exportedUTIs] as? [[String: Any]] ?? []
exportedUTIs.append(installedAppUTI)
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
}
DispatchQueue.global().async {
do
{
let appBundleURL = self.context.temporaryDirectory.appendingPathComponent("App.app")
try FileManager.default.copyItem(at: fileURL, to: appBundleURL)
// Become current so we can observe progress from unzipAppBundle().
progress.becomeCurrent(withPendingUnitCount: 1)
guard let appBundle = Bundle(url: appBundleURL) else { throw ALTError(.missingAppBundle) }
guard let infoDictionary = appBundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
"CFBundleURLName": bundleIdentifier,
"CFBundleURLSchemes": [openURL.scheme!]] as [String : Any]
allURLSchemes.append(altstoreURLScheme)
var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes]
if app.isAltStoreApp
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
additionalValues[Bundle.Info.deviceID] = udid
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
if
let data = Keychain.shared.signingCertificate,
let signingCertificate = ALTCertificate(p12Data: data, password: nil),
let encryptingPassword = Keychain.shared.signingCertificatePassword
{
additionalValues[Bundle.Info.certificateID] = signingCertificate.serialNumber
let encryptedData = signingCertificate.encryptedP12Data(withPassword: encryptingPassword)
try encryptedData?.write(to: appBundle.certificateURL, options: .atomic)
}
else
{
// The embedded certificate + certificate identifier are already in app bundle, no need to update them.
}
}
else if infoDictionary.keys.contains(Bundle.Info.deviceID), let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String
{
// There is an ALTDeviceID entry, so assume the app is using AltKit and replace it with the device's UDID.
additionalValues[Bundle.Info.deviceID] = udid
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
}
let iconScale = Int(UIScreen.main.scale)
if let alternateIconURL = self.context.alternateIconURL,
case let data = try Data(contentsOf: alternateIconURL),
let image = UIImage(data: data),
let icon = image.resizing(toFill: CGSize(width: 60 * iconScale, height: 60 * iconScale)),
let iconData = icon.pngData()
{
let iconName = "AltIcon"
let iconURL = appBundleURL.appendingPathComponent(iconName + "@\(iconScale)x.png")
try iconData.write(to: iconURL, options: .atomic)
let iconDictionary = ["CFBundlePrimaryIcon": ["CFBundleIconFiles": [iconName]]]
additionalValues["CFBundleIcons"] = iconDictionary
}
// Prepare app
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
if let directory = appBundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
{
for case let fileURL as URL in enumerator
{
guard let appExtension = Bundle(url: fileURL) else { throw ALTError(.missingAppBundle) }
try prepare(appExtension)
}
}
completionHandler(.success(appBundleURL))
}
catch
{
completionHandler(.failure(error))
}
}
return progress
}
func resignAppBundle(at fileURL: URL, team: ALTTeam, certificate: ALTCertificate, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let signer = ALTSigner(team: team, certificate: certificate)
let progress = signer.signApp(at: fileURL, provisioningProfiles: profiles) { (success, error) in
do
{
try Result(success, error).get()
let ipaURL = try FileManager.default.zipAppBundle(at: fileURL)
completionHandler(.success(ipaURL))
}
catch
{
completionHandler(.failure(error))
}
}
return progress
}
}

View File

@@ -1,125 +0,0 @@
//
// SendAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Network
import AltStoreCore
@objc(SendAppOperation)
class SendAppOperation: ResultOperation<ServerConnection>
{
let context: InstallAppOperationContext
private let dispatchQueue = DispatchQueue(label: "com.altstore.SendAppOperation")
private var serverConnection: ServerConnection?
init(context: InstallAppOperationContext)
{
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 resignedApp = self.context.resignedApp, 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 app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.url)
let fileURL = InstalledApp.refreshedIPAURL(for: app)
// Connect to server.
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let serverConnection):
self.serverConnection = serverConnection
// Send app to server.
self.sendApp(at: fileURL, via: serverConnection) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
self.progress.completedUnitCount += 1
self.finish(.success(serverConnection))
}
}
}
}
}
}
private extension SendAppOperation
{
func sendApp(at fileURL: URL, via connection: ServerConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
guard let appData = try? Data(contentsOf: fileURL) else { throw OperationError.invalidApp }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
var request = PrepareAppRequest(udid: udid, contentSize: appData.count)
if connection.server.connectionType == .local
{
// Background daemons have low memory limit (~6MB as of 13.5),
// so send just the file URL rather than the app data itself.
request.fileURL = fileURL
}
connection.send(request) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success:
if connection.server.connectionType == .local
{
// Sent file URL, so don't need to send any more.
completionHandler(.success(()))
}
else
{
print("Sending app data (\(appData.count) bytes)...")
connection.send(appData, prependSize: false) { (result) in
switch result
{
case .failure(let error):
print("Failed to send app data (\(appData.count) bytes)")
completionHandler(.failure(error))
case .success:
print("Successfully sent app data (\(appData.count) bytes)")
completionHandler(.success(()))
}
}
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}

View File

@@ -1,107 +0,0 @@
//
// UpdatePatronsOperation.swift
// AltStore
//
// Created by Riley Testut on 4/11/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltStoreCore
private extension URL
{
#if STAGING
static let patreonInfo = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altstore/patreon.json")!
#else
static let patreonInfo = URL(string: "https://cdn.altstore.io/file/altstore/altstore/patreon.json")!
#endif
}
extension UpdatePatronsOperation
{
private struct Response: Decodable
{
var version: Int
var accessToken: String
var refreshID: String
}
}
class UpdatePatronsOperation: ResultOperation<Void>
{
let context: NSManagedObjectContext
init(context: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
{
self.context = context
}
override func main()
{
super.main()
let dataTask = URLSession.shared.dataTask(with: .patreonInfo) { (data, response, error) in
do
{
if let response = response as? HTTPURLResponse
{
guard response.statusCode != 404 else {
self.finish(.failure(URLError(.fileDoesNotExist, userInfo: [NSURLErrorKey: URL.patreonInfo])))
return
}
}
guard let data = data else { throw error! }
let response = try AltStoreCore.JSONDecoder().decode(Response.self, from: data)
Keychain.shared.patreonCreatorAccessToken = response.accessToken
let previousRefreshID = UserDefaults.shared.patronsRefreshID
guard response.refreshID != previousRefreshID else {
self.finish(.success(()))
return
}
PatreonAPI.shared.fetchPatrons { (result) in
self.context.perform {
do
{
let patrons = try result.get()
let managedPatrons = patrons.map { ManagedPatron(patron: $0, context: self.context) }
let patronIDs = Set(managedPatrons.map { $0.identifier })
let nonFriendZonePredicate = NSPredicate(format: "NOT (%K IN %@)", #keyPath(ManagedPatron.identifier), patronIDs)
let nonFriendZonePatrons = ManagedPatron.all(satisfying: nonFriendZonePredicate, in: self.context)
for managedPatron in nonFriendZonePatrons
{
self.context.delete(managedPatron)
}
try self.context.save()
UserDefaults.shared.patronsRefreshID = response.refreshID
self.finish(.success(()))
print("Updated Friend Zone Patrons!")
}
catch
{
self.finish(.failure(error))
}
}
}
}
catch
{
self.finish(.failure(error))
}
}
dataTask.resume()
}
}

View File

@@ -1,174 +0,0 @@
//
// VerifyAppOperation.swift
// AltStore
//
// Created by Riley Testut on 5/2/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
import Roxas
enum VerificationError: ALTLocalizedError
{
case privateEntitlements(ALTApplication, entitlements: [String: Any])
case mismatchedBundleIdentifiers(ALTApplication, sourceBundleID: String)
case iOSVersionNotSupported(ALTApplication)
var app: ALTApplication {
switch self
{
case .privateEntitlements(let app, _): return app
case .mismatchedBundleIdentifiers(let app, _): return app
case .iOSVersionNotSupported(let app): return app
}
}
var failure: String? {
return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name)
}
var failureReason: String? {
switch self
{
case .privateEntitlements(let app, _):
return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name)
case .mismatchedBundleIdentifiers(let app, let sourceBundleID):
return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), app.bundleIdentifier, sourceBundleID)
case .iOSVersionNotSupported(let app):
let name = app.name
var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)"
if app.minimumiOSVersion.patchVersion > 0
{
version += ".\(app.minimumiOSVersion.patchVersion)"
}
let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version)
return localizedDescription
}
}
}
@objc(VerifyAppOperation)
class VerifyAppOperation: ResultOperation<Void>
{
let context: AppOperationContext
var verificationHandler: ((VerificationError) -> Bool)?
init(context: AppOperationContext)
{
self.context = context
super.init()
}
override func main()
{
super.main()
do
{
if let error = self.context.error
{
throw error
}
guard let app = self.context.app else { throw OperationError.invalidParameters }
guard app.bundleIdentifier == self.context.bundleIdentifier else {
throw VerificationError.mismatchedBundleIdentifiers(app, sourceBundleID: self.context.bundleIdentifier)
}
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
throw VerificationError.iOSVersionNotSupported(app)
}
if #available(iOS 13.5, *)
{
// No psychic paper, so we can ignore private entitlements
app.hasPrivateEntitlements = false
}
else
{
// Make sure this goes last, since once user responds to alert we don't do any more app verification.
if let commentStart = app.entitlementsString.range(of: "<!---><!-->"), let commentEnd = app.entitlementsString.range(of: "<!-- -->")
{
// Psychic Paper private entitlements.
let entitlementsStart = app.entitlementsString.index(after: commentStart.upperBound)
let rawEntitlements = String(app.entitlementsString[entitlementsStart ..< commentEnd.lowerBound])
let plistTemplate = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
%@
</dict>
</plist>
"""
let entitlementsPlist = String(format: plistTemplate, rawEntitlements)
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
app.hasPrivateEntitlements = true
let error = VerificationError.privateEntitlements(app, entitlements: entitlements)
self.process(error) { (result) in
self.finish(result.mapError { $0 as Error })
}
return
}
else
{
app.hasPrivateEntitlements = false
}
}
self.finish(.success(()))
}
catch
{
self.finish(.failure(error))
}
}
}
private extension VerifyAppOperation
{
func process(_ error: VerificationError, completion: @escaping (Result<Void, VerificationError>) -> Void)
{
guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
DispatchQueue.main.async {
switch error
{
case .privateEntitlements(_, let entitlements):
let permissions = entitlements.keys.sorted().joined(separator: "\n")
let message = String(format: NSLocalizedString("""
You must allow access to these private permissions before continuing:
%@
Private permissions allow apps to do more than normally allowed by iOS, including potentially accessing sensitive private data. Make sure to only install apps from sources you trust.
""", comment: ""), permissions)
let alertController = UIAlertController(title: error.failureReason ?? error.localizedDescription, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Allow Access", comment: ""), style: .destructive) { (action) in
completion(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Deny Access", comment: ""), style: .default, handler: { (action) in
completion(.failure(error))
}))
presentingViewController.present(alertController, animated: true, completion: nil)
case .mismatchedBundleIdentifiers: return completion(.failure(error))
case .iOSVersionNotSupported: return completion(.failure(error))
}
}
}
}