mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 11:43:24 +01:00
Refreshes apps by installing provisioning profiles when possible
Assuming the certificate used to originally sign an app is still valid, we can refresh an app simply by installing new provisioning profiles. However, if the signing certificate is no longer valid, we fall back to the old method of resigning + reinstalling.
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// Contexts.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import Network
|
||||
|
||||
import AltSign
|
||||
|
||||
class AppOperationContext
|
||||
{
|
||||
lazy var temporaryDirectory: URL = {
|
||||
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) }
|
||||
catch { self.error = error }
|
||||
|
||||
return temporaryDirectory
|
||||
}()
|
||||
|
||||
var bundleIdentifier: String
|
||||
var group: OperationGroup
|
||||
|
||||
var app: ALTApplication?
|
||||
var resignedApp: ALTApplication?
|
||||
|
||||
var installationConnection: ServerConnection?
|
||||
|
||||
var installedApp: InstalledApp? {
|
||||
didSet {
|
||||
self.installedAppContext = self.installedApp?.managedObjectContext
|
||||
}
|
||||
}
|
||||
private var installedAppContext: NSManagedObjectContext?
|
||||
|
||||
var isFinished = false
|
||||
|
||||
var error: Error? {
|
||||
get {
|
||||
return _error ?? self.group.error
|
||||
}
|
||||
set {
|
||||
_error = newValue
|
||||
}
|
||||
}
|
||||
private var _error: Error?
|
||||
|
||||
init(bundleIdentifier: String, group: OperationGroup)
|
||||
{
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
self.group = group
|
||||
}
|
||||
}
|
||||
@@ -32,9 +32,9 @@ enum AuthenticationError: LocalizedError
|
||||
}
|
||||
|
||||
@objc(AuthenticationOperation)
|
||||
class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
|
||||
class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
|
||||
{
|
||||
let group: OperationGroup
|
||||
let context: AuthenticatedOperationContext
|
||||
|
||||
private weak var presentingViewController: UIViewController?
|
||||
|
||||
@@ -56,22 +56,23 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
|
||||
|
||||
private var submitCodeAction: UIAlertAction?
|
||||
|
||||
init(group: OperationGroup, presentingViewController: UIViewController?)
|
||||
init(context: AuthenticatedOperationContext, presentingViewController: UIViewController?)
|
||||
{
|
||||
self.group = group
|
||||
self.context = context
|
||||
self.presentingViewController = presentingViewController
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
self.context.authenticationOperation = self
|
||||
self.operationQueue.name = "com.altstore.AuthenticationOperation"
|
||||
self.progress.totalUnitCount = 3
|
||||
self.progress.totalUnitCount = 4
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.group.error
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
@@ -85,6 +86,7 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let account, let session):
|
||||
self.context.session = session
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Fetch Team
|
||||
@@ -95,6 +97,7 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let team):
|
||||
self.context.team = team
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Fetch Certificate
|
||||
@@ -105,22 +108,33 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let certificate):
|
||||
self.context.certificate = certificate
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Save account/team to disk.
|
||||
self.save(team) { (result) in
|
||||
// Register Device
|
||||
self.registerCurrentDevice(for: team, session: session) { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Must cache App IDs _after_ saving account/team to disk.
|
||||
self.cacheAppIDs(signer: signer, session: session) { (result) in
|
||||
let result = result.map { _ in (signer, session) }
|
||||
self.finish(result)
|
||||
// Save account/team to disk.
|
||||
self.save(team) { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
// Must cache App IDs _after_ saving account/team to disk.
|
||||
self.cacheAppIDs(team: team, session: session) { (result) in
|
||||
let result = result.map { _ in (team, certificate, session) }
|
||||
self.finish(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,21 +187,21 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<(ALTSigner, ALTAppleAPISession), Error>)
|
||||
override func finish(_ result: Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>)
|
||||
{
|
||||
guard !self.isFinished else { return }
|
||||
|
||||
print("Finished authenticating with result:", result)
|
||||
print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.perform {
|
||||
do
|
||||
{
|
||||
let (signer, session) = try result.get()
|
||||
let (altTeam, altCertificate, session) = try result.get()
|
||||
|
||||
guard
|
||||
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), signer.team.account.identifier), in: context),
|
||||
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), signer.team.identifier), in: context)
|
||||
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
|
||||
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
|
||||
else { throw AuthenticationError.noTeam }
|
||||
|
||||
// Account
|
||||
@@ -214,24 +228,19 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
|
||||
team.isActiveTeam = false
|
||||
}
|
||||
|
||||
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.team == nil
|
||||
{
|
||||
// No team assigned to AltStore app yet, so assume this team was used to originally install it.
|
||||
altStoreApp.team = team
|
||||
}
|
||||
|
||||
// Save
|
||||
try context.save()
|
||||
|
||||
// Update keychain
|
||||
Keychain.shared.appleIDEmailAddress = signer.team.account.appleID
|
||||
Keychain.shared.appleIDEmailAddress = altTeam.account.appleID
|
||||
Keychain.shared.appleIDPassword = self.appleIDPassword
|
||||
|
||||
Keychain.shared.signingCertificate = signer.certificate.p12Data()
|
||||
Keychain.shared.signingCertificatePassword = signer.certificate.machineIdentifier
|
||||
Keychain.shared.signingCertificate = altCertificate.p12Data()
|
||||
Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier
|
||||
|
||||
self.showInstructionsIfNecessary() { (didShowInstructions) in
|
||||
|
||||
let signer = ALTSigner(team: altTeam, certificate: altCertificate)
|
||||
// Refresh screen must go last since a successful refresh will cause the app to quit.
|
||||
self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in
|
||||
super.finish(result)
|
||||
@@ -340,7 +349,7 @@ private extension AuthenticationOperation
|
||||
|
||||
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
|
||||
{
|
||||
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(group: self.group)
|
||||
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
|
||||
fetchAnisetteDataOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
@@ -557,13 +566,38 @@ private extension AuthenticationOperation
|
||||
}
|
||||
}
|
||||
|
||||
func cacheAppIDs(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
{
|
||||
let group = OperationGroup()
|
||||
group.signer = signer
|
||||
group.session = session
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
|
||||
return completionHandler(.failure(OperationError.unknownUDID))
|
||||
}
|
||||
|
||||
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
|
||||
ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
|
||||
do
|
||||
{
|
||||
let devices = try Result(devices, error).get()
|
||||
|
||||
if let device = devices.first(where: { $0.identifier == udid })
|
||||
{
|
||||
completionHandler(.success(device))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team, session: session) { (device, error) in
|
||||
completionHandler(Result(device, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
|
||||
fetchAppIDsOperation.resultHandler = { (result) in
|
||||
do
|
||||
{
|
||||
@@ -611,8 +645,7 @@ private extension AuthenticationOperation
|
||||
#else
|
||||
DispatchQueue.main.async {
|
||||
let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController
|
||||
refreshViewController.signer = signer
|
||||
refreshViewController.session = session
|
||||
refreshViewController.context = self.context
|
||||
refreshViewController.completionHandler = { _ in
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
@@ -16,26 +16,24 @@ import Roxas
|
||||
@objc(FetchAnisetteDataOperation)
|
||||
class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
||||
{
|
||||
let group: OperationGroup
|
||||
let context: OperationContext
|
||||
|
||||
init(group: OperationGroup)
|
||||
init(context: OperationContext)
|
||||
{
|
||||
self.group = group
|
||||
|
||||
super.init()
|
||||
self.context = context
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.group.error
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let server = self.group.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
ServerManager.shared.connect(to: server) { (result) in
|
||||
switch result
|
||||
@@ -55,7 +53,7 @@ class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
|
||||
case .success:
|
||||
print("Waiting for anisette data...")
|
||||
connection.receiveResponse() { (result) in
|
||||
print("Receiving anisette data:", result)
|
||||
print("Receiving anisette data:", result.error?.localizedDescription ?? "success")
|
||||
|
||||
switch result
|
||||
{
|
||||
|
||||
@@ -16,13 +16,13 @@ import Roxas
|
||||
@objc(FetchAppIDsOperation)
|
||||
class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
|
||||
{
|
||||
let group: OperationGroup
|
||||
let context: NSManagedObjectContext
|
||||
let context: AuthenticatedOperationContext
|
||||
let managedObjectContext: NSManagedObjectContext
|
||||
|
||||
init(group: OperationGroup, context: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
|
||||
init(context: AuthenticatedOperationContext, managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
|
||||
{
|
||||
self.group = group
|
||||
self.context = context
|
||||
self.managedObjectContext = managedObjectContext
|
||||
|
||||
super.init()
|
||||
}
|
||||
@@ -31,24 +31,24 @@ class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.group.error
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
let team = self.group.signer?.team,
|
||||
let session = self.group.session
|
||||
let team = self.context.team,
|
||||
let session = self.context.session
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
||||
self.context.perform {
|
||||
self.managedObjectContext.perform {
|
||||
do
|
||||
{
|
||||
let fetchedAppIDs = try Result(appIDs, error).get()
|
||||
|
||||
guard let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), team.identifier), in: self.context) else { throw OperationError.notAuthenticated }
|
||||
guard let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), team.identifier), in: self.managedObjectContext) else { throw OperationError.notAuthenticated }
|
||||
|
||||
let fetchedIdentifiers = fetchedAppIDs.map { $0.identifier }
|
||||
|
||||
@@ -57,11 +57,11 @@ class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
|
||||
#keyPath(AppID.team), team,
|
||||
#keyPath(AppID.identifier), fetchedIdentifiers)
|
||||
|
||||
let deletedAppIDs = try self.context.fetch(deletedAppIDsRequest)
|
||||
deletedAppIDs.forEach { self.context.delete($0) }
|
||||
let deletedAppIDs = try self.managedObjectContext.fetch(deletedAppIDsRequest)
|
||||
deletedAppIDs.forEach { self.managedObjectContext.delete($0) }
|
||||
|
||||
let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.context) }
|
||||
self.finish(.success((appIDs, self.context)))
|
||||
let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.managedObjectContext) }
|
||||
self.finish(.success((appIDs, self.managedObjectContext)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
398
AltStore/Operations/FetchProvisioningProfilesOperation.swift
Normal file
398
AltStore/Operations/FetchProvisioningProfilesOperation.swift
Normal file
@@ -0,0 +1,398 @@
|
||||
//
|
||||
// FetchProvisioningProfilesOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 2/27/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(FetchProvisioningProfilesOperation)
|
||||
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
|
||||
init(context: AppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 1
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
let app = self.context.app,
|
||||
let team = self.context.team,
|
||||
let session = self.context.session
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
|
||||
|
||||
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let profile = try result.get()
|
||||
|
||||
var profiles = [app.bundleIdentifier: profile]
|
||||
var error: Error?
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for appExtension in app.appExtensions
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let e): error = e
|
||||
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
if let error = error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.finish(.success(profiles))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func process<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 == %@ AND %K == %@",
|
||||
#keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier,
|
||||
#keyPath(InstalledApp.team.identifier), team.identifier)
|
||||
if let installedApp = InstalledApp.first(satisfying: predicate, in: context)
|
||||
{
|
||||
#if DEBUG
|
||||
|
||||
if app.bundleIdentifier == StoreApp.altstoreAppID || app.bundleIdentifier == StoreApp.alternativeAltStoreAppID
|
||||
{
|
||||
// Use legacy bundle ID format for AltStore.
|
||||
preferredBundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
|
||||
}
|
||||
else
|
||||
{
|
||||
preferredBundleID = installedApp.resignedBundleIdentifier
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// This app is already installed, so use the same resigned bundle identifier as before.
|
||||
// This way, if we change the identifier format (again), AltStore will continue to use
|
||||
// the old bundle identifier to prevent it from installing as a new app.
|
||||
preferredBundleID = installedApp.resignedBundleIdentifier
|
||||
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
// This app isn't already installed, so create the resigned bundle identifier ourselves.
|
||||
// Or, if the app _is_ installed but with a different team, we need to create a new
|
||||
// bundle identifier anyway to prevent collisions with the previous team.
|
||||
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
|
||||
let updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
|
||||
|
||||
if app.bundleIdentifier == StoreApp.altstoreAppID || app.bundleIdentifier == StoreApp.alternativeAltStoreAppID
|
||||
{
|
||||
// Use legacy bundle ID format for AltStore.
|
||||
preferredBundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
|
||||
}
|
||||
else
|
||||
{
|
||||
preferredBundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
|
||||
}
|
||||
}
|
||||
|
||||
let preferredName: String
|
||||
|
||||
if let parentApp = parentApp
|
||||
{
|
||||
preferredName = parentApp.name + " " + app.name
|
||||
}
|
||||
else
|
||||
{
|
||||
preferredName = app.name
|
||||
}
|
||||
|
||||
// Register
|
||||
self.registerAppID(for: app, name: preferredName, bundleIdentifier: preferredBundleID, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Update features
|
||||
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Update app groups
|
||||
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Fetch Provisioning Profile
|
||||
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<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 == bundleIdentifier })
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
else
|
||||
{
|
||||
let requiredAppIDs = 1 + application.appExtensions.count
|
||||
let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count)
|
||||
|
||||
let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 })
|
||||
|
||||
if team.type == .free
|
||||
{
|
||||
if requiredAppIDs > availableAppIDs
|
||||
{
|
||||
if let expirationDate = sortedExpirationDates.first
|
||||
{
|
||||
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ALTAppleAPIError(.maximumAppIDLimitReached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
let appID = try Result(appID, error).get()
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
catch ALTAppleAPIError.maximumAppIDLimitReached
|
||||
{
|
||||
if let expirationDate = sortedExpirationDates.first
|
||||
{
|
||||
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ALTAppleAPIError(.maximumAppIDLimitReached)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
||||
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
|
||||
return (feature, value)
|
||||
}
|
||||
|
||||
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
|
||||
|
||||
if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
|
||||
{
|
||||
features[.appGroups] = true
|
||||
}
|
||||
|
||||
var updateFeatures = false
|
||||
|
||||
// Determine whether the required features are already enabled for the AppID.
|
||||
for (feature, value) in features
|
||||
{
|
||||
if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue)
|
||||
{
|
||||
// AppID already has this feature enabled and the values are the same.
|
||||
continue
|
||||
}
|
||||
else
|
||||
{
|
||||
// AppID either doesn't have this feature enabled or the value has changed,
|
||||
// so we need to update it to reflect new values.
|
||||
updateFeatures = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if updateFeatures
|
||||
{
|
||||
let appID = appID.copy() as! ALTAppID
|
||||
appID.features = features
|
||||
|
||||
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
// TODO: Handle apps belonging to more than one app group.
|
||||
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
|
||||
return completionHandler(.success(appID))
|
||||
}
|
||||
|
||||
func finish(_ result: Result<ALTAppGroup, Error>)
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let group):
|
||||
// Assign App Group
|
||||
// TODO: Determine whether app already belongs to app group.
|
||||
|
||||
ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
|
||||
let result = result.map { _ in appID }
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
|
||||
|
||||
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
|
||||
switch Result(groups, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let groups):
|
||||
|
||||
if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
|
||||
{
|
||||
finish(.success(group))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
|
||||
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
|
||||
|
||||
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
|
||||
finish(Result(group, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
|
||||
switch Result(profile, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let profile):
|
||||
|
||||
// Delete existing profile
|
||||
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success:
|
||||
|
||||
// Fetch new provisiong profile
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,27 +23,31 @@ private let ReceivedWiredServerConnectionResponse: @convention(c) (CFNotificatio
|
||||
@objc(FindServerOperation)
|
||||
class FindServerOperation: ResultOperation<Server>
|
||||
{
|
||||
let group: OperationGroup
|
||||
let context: OperationContext
|
||||
|
||||
private var isWiredServerConnectionAvailable = false
|
||||
|
||||
init(group: OperationGroup)
|
||||
{
|
||||
self.group = group
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
init(context: OperationContext = OperationContext())
|
||||
{
|
||||
self.context = context
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.group.error
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
if let server = self.context.server
|
||||
{
|
||||
self.finish(.success(server))
|
||||
return
|
||||
}
|
||||
|
||||
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
|
||||
// Prepare observers to receive callback from wired server (if connected).
|
||||
|
||||
@@ -16,11 +16,11 @@ import Roxas
|
||||
@objc(InstallAppOperation)
|
||||
class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
private var didCleanUp = false
|
||||
|
||||
init(context: AppOperationContext)
|
||||
init(context: InstallAppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
@@ -40,6 +40,7 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
}
|
||||
|
||||
guard
|
||||
let certificate = self.context.certificate,
|
||||
let resignedApp = self.context.resignedApp,
|
||||
let connection = self.context.installationConnection
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
@@ -57,10 +58,10 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
}
|
||||
else
|
||||
{
|
||||
installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, context: backgroundContext)
|
||||
installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, certificateSerialNumber: certificate.serialNumber, context: backgroundContext)
|
||||
}
|
||||
|
||||
installedApp.update(resignedApp: resignedApp)
|
||||
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber)
|
||||
|
||||
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
|
||||
{
|
||||
@@ -108,7 +109,7 @@ class InstallAppOperation: ResultOperation<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()
|
||||
|
||||
self.context.group.beginInstallationHandler?(installedApp)
|
||||
self.context.beginInstallationHandler?(installedApp)
|
||||
|
||||
let request = BeginInstallationRequest()
|
||||
connection.send(request) { (result) in
|
||||
|
||||
79
AltStore/Operations/OperationContexts.swift
Normal file
79
AltStore/Operations/OperationContexts.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Contexts.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import Network
|
||||
|
||||
import AltSign
|
||||
|
||||
class OperationContext
|
||||
{
|
||||
var server: Server?
|
||||
var error: Error?
|
||||
}
|
||||
|
||||
class AuthenticatedOperationContext: OperationContext
|
||||
{
|
||||
var session: ALTAppleAPISession?
|
||||
|
||||
var team: ALTTeam?
|
||||
var certificate: ALTCertificate?
|
||||
|
||||
weak var authenticationOperation: AuthenticationOperation?
|
||||
}
|
||||
|
||||
@dynamicMemberLookup
|
||||
class AppOperationContext
|
||||
{
|
||||
let bundleIdentifier: String
|
||||
private let authenticatedContext: AuthenticatedOperationContext
|
||||
|
||||
var app: ALTApplication?
|
||||
var provisioningProfiles: [String: ALTProvisioningProfile]?
|
||||
|
||||
var isFinished = false
|
||||
|
||||
var error: Error? {
|
||||
get {
|
||||
return _error ?? self.authenticatedContext.error
|
||||
}
|
||||
set {
|
||||
_error = newValue
|
||||
}
|
||||
}
|
||||
private var _error: Error?
|
||||
|
||||
init(bundleIdentifier: String, authenticatedContext: AuthenticatedOperationContext)
|
||||
{
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
self.authenticatedContext = authenticatedContext
|
||||
}
|
||||
|
||||
subscript<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 beginInstallationHandler: ((InstalledApp) -> Void)?
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
//
|
||||
// OperationGroup.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
class OperationGroup
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 0)
|
||||
|
||||
var completionHandler: ((Result<[String: Result<InstalledApp, Error>], Error>) -> Void)?
|
||||
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
||||
|
||||
var session: ALTAppleAPISession?
|
||||
|
||||
var server: Server?
|
||||
var signer: ALTSigner?
|
||||
|
||||
var error: Error?
|
||||
|
||||
var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
private var progressByBundleIdentifier = [String: Progress]()
|
||||
|
||||
private let operationQueue = OperationQueue()
|
||||
private let installOperationQueue = OperationQueue()
|
||||
|
||||
init()
|
||||
{
|
||||
// Enforce only one installation at a time.
|
||||
self.installOperationQueue.maxConcurrentOperationCount = 1
|
||||
}
|
||||
|
||||
func cancel()
|
||||
{
|
||||
self.operationQueue.cancelAllOperations()
|
||||
self.installOperationQueue.cancelAllOperations()
|
||||
}
|
||||
|
||||
func addOperations(_ operations: [Operation])
|
||||
{
|
||||
for operation in operations
|
||||
{
|
||||
if let installOperation = operation as? InstallAppOperation
|
||||
{
|
||||
if let previousOperation = self.installOperationQueue.operations.last
|
||||
{
|
||||
// Ensures they execute in the order they're added, since isReady is still false at this point.
|
||||
installOperation.addDependency(previousOperation)
|
||||
}
|
||||
|
||||
self.installOperationQueue.addOperation(installOperation)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.operationQueue.addOperation(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func set(_ progress: Progress, for app: AppProtocol)
|
||||
{
|
||||
self.progressByBundleIdentifier[app.bundleIdentifier] = progress
|
||||
|
||||
self.progress.totalUnitCount += 1
|
||||
self.progress.addChild(progress, withPendingUnitCount: 1)
|
||||
}
|
||||
|
||||
func progress(for app: AppProtocol) -> Progress?
|
||||
{
|
||||
return self.progress(forAppWithBundleIdentifier: app.bundleIdentifier)
|
||||
}
|
||||
|
||||
func progress(forAppWithBundleIdentifier bundleIdentifier: String) -> Progress?
|
||||
{
|
||||
let progress = self.progressByBundleIdentifier[bundleIdentifier]
|
||||
return progress
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
//
|
||||
// PrepareDeveloperAccountOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 1/7/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(PrepareDeveloperAccountOperation)
|
||||
class PrepareDeveloperAccountOperation: ResultOperation<Void>
|
||||
{
|
||||
let group: OperationGroup
|
||||
|
||||
init(group: OperationGroup)
|
||||
{
|
||||
self.group = group
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 2
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.group.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
let signer = self.group.signer,
|
||||
let session = self.group.session
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
// Register Device
|
||||
self.registerCurrentDevice(for: signer.team, session: session) { (result) in
|
||||
let result = result.map { _ in () }
|
||||
self.finish(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PrepareDeveloperAccountOperation
|
||||
{
|
||||
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<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, session: session) { (devices, error) in
|
||||
do
|
||||
{
|
||||
let devices = try Result(devices, error).get()
|
||||
|
||||
if let device = devices.first(where: { $0.identifier == udid })
|
||||
{
|
||||
completionHandler(.success(device))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team, session: session) { (device, error) in
|
||||
completionHandler(Result(device, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
AltStore/Operations/RefreshAppOperation.swift
Normal file
125
AltStore/Operations/RefreshAppOperation.swift
Normal file
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// RefreshAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 2/27/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
@objc(RefreshAppOperation)
|
||||
class RefreshAppOperation: ResultOperation<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 app = self.context.app,
|
||||
let team = self.context.team,
|
||||
let profiles = self.context.provisioningProfiles
|
||||
else { throw OperationError.invalidParameters }
|
||||
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
|
||||
|
||||
ServerManager.shared.connect(to: server) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let connection):
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
print("Sending refresh app request...")
|
||||
|
||||
var activeProfiles: Set<String>?
|
||||
|
||||
if team.type == .free
|
||||
{
|
||||
let activeApps = InstalledApp.all(in: context)
|
||||
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
|
||||
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
||||
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
||||
})
|
||||
}
|
||||
|
||||
let request = InstallProvisioningProfilesRequest(udid: udid, provisioningProfiles: Set(profiles.values), activeProfiles: activeProfiles)
|
||||
connection.send(request) { (result) in
|
||||
print("Sent refresh app request!")
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
print("Waiting for refresh app response...")
|
||||
connection.receiveResponse() { (result) in
|
||||
print("Receiving refresh app response:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(.error(let response)): self.finish(.failure(response.error))
|
||||
|
||||
case .success(.installProvisioningProfiles):
|
||||
self.managedObjectContext.perform {
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
|
||||
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
|
||||
return self.finish(.failure(OperationError.invalidApp))
|
||||
}
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
if let provisioningProfile = profiles[app.bundleIdentifier]
|
||||
{
|
||||
installedApp.update(provisioningProfile: provisioningProfile)
|
||||
}
|
||||
|
||||
for installedExtension in installedApp.appExtensions
|
||||
{
|
||||
guard let provisioningProfile = profiles[installedExtension.bundleIdentifier] else { continue }
|
||||
installedExtension.update(provisioningProfile: provisioningProfile)
|
||||
}
|
||||
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
|
||||
case .success: self.finish(.failure(ALTServerError(.unknownRequest)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
77
AltStore/Operations/RefreshGroup.swift
Normal file
77
AltStore/Operations/RefreshGroup.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// RefreshGroup.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
class RefreshGroup: NSObject
|
||||
{
|
||||
let context: AuthenticatedOperationContext
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 0)
|
||||
|
||||
var completionHandler: (([String: Result<InstalledApp, Error>]) -> Void)?
|
||||
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
||||
|
||||
private(set) var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
private var isFinished = false
|
||||
|
||||
private let dispatchGroup = DispatchGroup()
|
||||
private var operations: [Foundation.Operation] = []
|
||||
|
||||
init(context: AuthenticatedOperationContext = AuthenticatedOperationContext())
|
||||
{
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func add(_ operations: [Foundation.Operation])
|
||||
{
|
||||
for operation in operations
|
||||
{
|
||||
self.dispatchGroup.enter()
|
||||
|
||||
operation.completionBlock = { [weak self] in
|
||||
self?.dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
if self.operations.isEmpty && !operations.isEmpty
|
||||
{
|
||||
self.dispatchGroup.notify(queue: .global()) {
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
self.operations.append(contentsOf: operations)
|
||||
}
|
||||
|
||||
func set(_ result: Result<InstalledApp, Error>, forAppWithBundleIdentifier bundleIdentifier: String)
|
||||
{
|
||||
self.results[bundleIdentifier] = result
|
||||
}
|
||||
|
||||
func cancel()
|
||||
{
|
||||
self.operations.forEach { $0.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
private extension RefreshGroup
|
||||
{
|
||||
func finish()
|
||||
{
|
||||
guard !self.isFinished else { return }
|
||||
self.isFinished = true
|
||||
|
||||
self.completionHandler?(self.results)
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ import AltSign
|
||||
@objc(ResignAppOperation)
|
||||
class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
init(context: AppOperationContext)
|
||||
init(context: InstallAppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
@@ -37,46 +37,42 @@ class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
|
||||
guard
|
||||
let app = self.context.app,
|
||||
let signer = self.context.group.signer,
|
||||
let session = self.context.group.session
|
||||
let profiles = self.context.provisioningProfiles,
|
||||
let team = self.context.team,
|
||||
let certificate = self.context.certificate
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
// Prepare Provisioning Profiles
|
||||
self.prepareProvisioningProfiles(app.fileURL, team: signer.team, session: session) { (result) in
|
||||
guard let profiles = self.process(result) else { return }
|
||||
// Prepare app bundle
|
||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
||||
|
||||
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
|
||||
guard let appBundleURL = self.process(result) else { return }
|
||||
|
||||
// Prepare app bundle
|
||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
||||
print("Resigning App:", self.context.bundleIdentifier)
|
||||
|
||||
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
|
||||
guard let appBundleURL = self.process(result) else { return }
|
||||
// Resign app bundle
|
||||
let resignProgress = self.resignAppBundle(at: appBundleURL, team: team, certificate: certificate, profiles: Array(profiles.values)) { (result) in
|
||||
guard let resignedURL = self.process(result) else { return }
|
||||
|
||||
print("Resigning App:", self.context.bundleIdentifier)
|
||||
|
||||
// Resign app bundle
|
||||
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in
|
||||
guard let resignedURL = self.process(result) else { return }
|
||||
// Finish
|
||||
do
|
||||
{
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
// Finish
|
||||
do
|
||||
{
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
self.finish(.success(resignedApplication))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
self.finish(.success(resignedApplication))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
|
||||
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
|
||||
func process<T>(_ result: Result<T, Error>) -> T?
|
||||
@@ -100,310 +96,6 @@ class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
|
||||
private extension ResignAppOperation
|
||||
{
|
||||
func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
|
||||
{
|
||||
guard let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
|
||||
|
||||
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let profile):
|
||||
var profiles = [app.bundleIdentifier: profile]
|
||||
var error: Error?
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for appExtension in app.appExtensions
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let e): error = e
|
||||
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
if let error = error
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(profiles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<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 == %@ AND %K == %@",
|
||||
#keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier,
|
||||
#keyPath(InstalledApp.team.identifier), team.identifier)
|
||||
if let installedApp = InstalledApp.first(satisfying: predicate, in: context)
|
||||
{
|
||||
// This app is already installed, so use the same resigned bundle identifier as before.
|
||||
// This way, if we change the identifier format (again), AltStore will continue to use
|
||||
// the old bundle identifier to prevent it from installing as a new app.
|
||||
preferredBundleID = installedApp.resignedBundleIdentifier
|
||||
}
|
||||
else
|
||||
{
|
||||
// This app isn't already installed, so create the resigned bundle identifier ourselves.
|
||||
// Or, if the app _is_ installed but with a different team, we need to create a new
|
||||
// bundle identifier anyway to prevent collisions with the previous team.
|
||||
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
|
||||
let updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
|
||||
|
||||
preferredBundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
|
||||
}
|
||||
|
||||
let preferredName: String
|
||||
|
||||
if let parentApp = parentApp
|
||||
{
|
||||
preferredName = "\(parentApp.name) - \(app.name)"
|
||||
}
|
||||
else
|
||||
{
|
||||
preferredName = app.name
|
||||
}
|
||||
|
||||
// Register
|
||||
self.registerAppID(for: app, name: preferredName, bundleIdentifier: preferredBundleID, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Update features
|
||||
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Update app groups
|
||||
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Fetch Provisioning Profile
|
||||
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<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 == bundleIdentifier })
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
else
|
||||
{
|
||||
let requiredAppIDs = 1 + application.appExtensions.count
|
||||
let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count)
|
||||
|
||||
let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 })
|
||||
|
||||
if team.type == .free
|
||||
{
|
||||
if requiredAppIDs > availableAppIDs
|
||||
{
|
||||
if let expirationDate = sortedExpirationDates.first
|
||||
{
|
||||
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ALTAppleAPIError(.maximumAppIDLimitReached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
let appID = try Result(appID, error).get()
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
catch ALTAppleAPIError.maximumAppIDLimitReached
|
||||
{
|
||||
if let expirationDate = sortedExpirationDates.first
|
||||
{
|
||||
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ALTAppleAPIError(.maximumAppIDLimitReached)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
||||
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
|
||||
return (feature, value)
|
||||
}
|
||||
|
||||
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
|
||||
|
||||
if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
|
||||
{
|
||||
features[.appGroups] = true
|
||||
}
|
||||
|
||||
var updateFeatures = false
|
||||
|
||||
// Determine whether the required features are already enabled for the AppID.
|
||||
for (feature, value) in features
|
||||
{
|
||||
if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue)
|
||||
{
|
||||
// AppID already has this feature enabled and the values are the same.
|
||||
continue
|
||||
}
|
||||
else
|
||||
{
|
||||
// AppID either doesn't have this feature enabled or the value has changed,
|
||||
// so we need to update it to reflect new values.
|
||||
updateFeatures = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if updateFeatures
|
||||
{
|
||||
let appID = appID.copy() as! ALTAppID
|
||||
appID.features = features
|
||||
|
||||
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
// TODO: Handle apps belonging to more than one app group.
|
||||
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
|
||||
return completionHandler(.success(appID))
|
||||
}
|
||||
|
||||
func finish(_ result: Result<ALTAppGroup, Error>)
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let group):
|
||||
// Assign App Group
|
||||
// TODO: Determine whether app already belongs to app group.
|
||||
|
||||
ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
|
||||
let result = result.map { _ in appID }
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
|
||||
|
||||
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
|
||||
switch Result(groups, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let groups):
|
||||
|
||||
if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
|
||||
{
|
||||
finish(.success(group))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
|
||||
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
|
||||
|
||||
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
|
||||
finish(Result(group, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
|
||||
switch Result(profile, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let profile):
|
||||
|
||||
// Delete existing profile
|
||||
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success:
|
||||
|
||||
// Fetch new provisiong profile
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
@@ -511,8 +203,9 @@ private extension ResignAppOperation
|
||||
return progress
|
||||
}
|
||||
|
||||
func resignAppBundle(at fileURL: URL, signer: ALTSigner, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> 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
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ class SendAppOperation: ResultOperation<ServerConnection>
|
||||
return
|
||||
}
|
||||
|
||||
guard let app = self.context.app, let server = self.context.group.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
guard let app = self.context.app, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
|
||||
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
|
||||
Reference in New Issue
Block a user