Files
SideStore/SideStoreApp/Sources/SideStoreAppKit/Operations/FetchProvisioningProfilesOperation.swift

405 lines
18 KiB
Swift
Raw Normal View History

//
// FetchProvisioningProfilesOperation.swift
// AltStore
//
// Created by Riley Testut on 2/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
2023-03-01 00:48:36 -05:00
import SideStoreCore
2023-03-01 14:36:52 -05:00
import RoxasUIKit
@objc(FetchProvisioningProfilesOperation)
2023-03-01 00:48:36 -05:00
final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]> {
let context: AppOperationContext
2023-03-01 00:48:36 -05:00
var additionalEntitlements: [ALTEntitlement: Any]?
2023-03-01 00:48:36 -05:00
private let appGroupsLock = NSLock()
2023-03-01 00:48:36 -05:00
init(context: AppOperationContext) {
self.context = context
2023-03-01 00:48:36 -05:00
super.init()
2023-03-01 00:48:36 -05:00
progress.totalUnitCount = 1
}
2023-03-01 00:48:36 -05:00
override func main() {
super.main()
2023-03-01 00:48:36 -05:00
if let error = context.error {
finish(.failure(error))
return
}
2023-03-01 00:48:36 -05:00
guard
2023-03-01 00:48:36 -05:00
let team = context.team,
let session = context.session
else { return finish(.failure(OperationError.invalidParameters)) }
guard let app = context.app else { return finish(.failure(OperationError.appNotFound)) }
progress.totalUnitCount = Int64(1 + app.appExtensions.count)
prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { result in
do {
self.progress.completedUnitCount += 1
2023-03-01 00:48:36 -05:00
let profile = try result.get()
2023-03-01 00:48:36 -05:00
var profiles = [app.bundleIdentifier: profile]
var error: Error?
2023-03-01 00:48:36 -05:00
let dispatchGroup = DispatchGroup()
2023-03-01 00:48:36 -05:00
for appExtension in app.appExtensions {
dispatchGroup.enter()
2023-03-01 00:48:36 -05:00
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { result in
switch result {
case let .failure(e): error = e
case let .success(profile): profiles[appExtension.bundleIdentifier] = profile
}
2023-03-01 00:48:36 -05:00
dispatchGroup.leave()
2023-03-01 00:48:36 -05:00
self.progress.completedUnitCount += 1
}
}
2023-03-01 00:48:36 -05:00
dispatchGroup.notify(queue: .global()) {
2023-03-01 00:48:36 -05:00
if let error = error {
self.finish(.failure(error))
2023-03-01 00:48:36 -05:00
} else {
self.finish(.success(profiles))
}
}
2023-03-01 00:48:36 -05:00
} catch {
self.finish(.failure(error))
}
}
}
2023-03-01 00:48:36 -05:00
func process<T>(_ result: Result<T, Error>) -> T? {
switch result {
case let .failure(error):
finish(.failure(error))
return nil
2023-03-01 00:48:36 -05:00
case let .success(value):
guard !isCancelled else {
finish(.failure(OperationError.cancelled))
return nil
}
2023-03-01 00:48:36 -05:00
return value
}
}
}
2023-03-01 00:48:36 -05:00
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?
2023-03-01 00:48:36 -05:00
// Check if we have already installed this app with this team before.
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
2023-03-01 00:48:36 -05:00
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))
2023-03-01 00:48:36 -05:00
// #if DEBUG
//
// if app.isAltStoreApp
// {
// // Use legacy bundle ID format for AltStore.
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
// }
// else
// {
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
// }
//
// #else
2023-03-01 00:48:36 -05:00
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
2023-03-01 00:48:36 -05:00
} else {
preferredBundleID = nil
}
2023-03-01 00:48:36 -05:00
// #endif
} else {
preferredBundleID = nil
}
2023-03-01 00:48:36 -05:00
let bundleID: String
2023-03-01 00:48:36 -05:00
if let preferredBundleID = preferredBundleID {
bundleID = preferredBundleID
2023-03-01 00:48:36 -05:00
} 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
2023-03-01 00:48:36 -05:00
if app.isAltStoreApp {
// Use legacy bundle ID format for AltStore (and its extensions).
updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
2023-03-01 00:48:36 -05:00
} else {
updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
}
2023-03-01 00:48:36 -05:00
bundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
}
2023-03-01 00:48:36 -05:00
let preferredName: String
2023-03-01 00:48:36 -05:00
if let parentApp = parentApp {
preferredName = parentApp.name + " " + app.name
2023-03-01 00:48:36 -05:00
} else {
preferredName = app.name
}
2023-03-01 00:48:36 -05:00
// Register
2023-03-01 00:48:36 -05:00
self.registerAppID(for: app, name: preferredName, bundleIdentifier: bundleID, team: team, session: session) { result in
switch result {
case let .failure(error): completionHandler(.failure(error))
case let .success(appID):
// Update features
2023-03-01 00:48:36 -05:00
self.updateFeatures(for: appID, app: app, team: team, session: session) { result in
switch result {
case let .failure(error): completionHandler(.failure(error))
case let .success(appID):
// Update app groups
2023-03-01 00:48:36 -05:00
self.updateAppGroups(for: appID, app: app, team: team, session: session) { result in
switch result {
case let .failure(error): completionHandler(.failure(error))
case let .success(appID):
// Fetch Provisioning Profile
2023-03-01 00:48:36 -05:00
self.fetchProvisioningProfile(for: appID, team: team, session: session) { result in
completionHandler(result)
}
}
}
}
}
}
}
}
}
2023-03-01 00:48:36 -05:00
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()
2023-03-01 00:48:36 -05:00
if let appID = appIDs.first(where: { $0.bundleIdentifier.lowercased() == bundleIdentifier.lowercased() }) {
completionHandler(.success(appID))
2023-03-01 00:48:36 -05:00
} else {
let requiredAppIDs = 1 + application.appExtensions.count
let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count)
2023-03-01 00:48:36 -05:00
let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 })
2023-03-01 00:48:36 -05:00
if team.type == .free {
if requiredAppIDs > availableAppIDs {
if let expirationDate = sortedExpirationDates.first {
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
2023-03-01 00:48:36 -05:00
} else {
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
}
2023-03-01 00:48:36 -05:00
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))
2023-03-01 00:48:36 -05:00
} catch ALTAppleAPIError.maximumAppIDLimitReached {
if let expirationDate = sortedExpirationDates.first {
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
2023-03-01 00:48:36 -05:00
} else {
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
2023-03-01 00:48:36 -05:00
} catch {
completionHandler(.failure(error))
}
}
}
2023-03-01 00:48:36 -05:00
} catch {
completionHandler(.failure(error))
}
}
}
2023-03-01 00:48:36 -05:00
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void) {
var entitlements = app.entitlements
2023-03-01 00:48:36 -05:00
for (key, value) in additionalEntitlements ?? [:] {
entitlements[key] = value
}
2023-03-01 00:48:36 -05:00
let requiredFeatures = entitlements.compactMap { entitlement, value -> (ALTFeature, Any)? in
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
return (feature, value)
}
2023-03-01 00:48:36 -05:00
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
2023-03-01 00:48:36 -05:00
if let applicationGroups = entitlements[.appGroups] as? [String], !applicationGroups.isEmpty {
// App uses app groups, so assign `true` to enable the feature.
features[.appGroups] = true
2023-03-01 00:48:36 -05:00
} else {
// App has no app groups, so assign `false` to disable the feature.
features[.appGroups] = false
}
2023-03-01 00:48:36 -05:00
var updateFeatures = false
2023-03-01 00:48:36 -05:00
// Determine whether the required features are already enabled for the AppID.
2023-03-01 00:48:36 -05:00
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
2023-03-01 00:48:36 -05:00
} 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
2023-03-01 00:48:36 -05:00
} 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
}
}
2023-03-01 00:48:36 -05:00
if updateFeatures {
let appID = appID.copy() as! ALTAppID
appID.features = features
2023-03-01 00:48:36 -05:00
ALTAppleAPI.shared.update(appID, team: team, session: session) { appID, error in
completionHandler(Result(appID, error))
}
2023-03-01 00:48:36 -05:00
} else {
completionHandler(.success(appID))
}
}
2023-03-01 00:48:36 -05:00
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void) {
var entitlements = app.entitlements
2023-03-01 00:48:36 -05:00
for (key, value) in additionalEntitlements ?? [:] {
entitlements[key] = value
}
2023-03-01 00:48:36 -05:00
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))
}
2023-03-01 00:48:36 -05:00
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.
2023-03-01 00:48:36 -05:00
if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) }) {
applicationGroups[index] = Bundle.baseAltStoreAppGroupID
2023-03-01 00:48:36 -05:00
} else {
applicationGroups.append(Bundle.baseAltStoreAppGroupID)
}
}
2023-03-01 00:48:36 -05:00
// 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()
2023-03-01 00:48:36 -05:00
func finish(_ result: Result<ALTAppID, Error>) {
self.appGroupsLock.unlock()
completionHandler(result)
}
2023-03-01 00:48:36 -05:00
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { groups, error in
switch Result(groups, error) {
case let .failure(error): finish(.failure(error))
case let .success(fetchedGroups):
let dispatchGroup = DispatchGroup()
2023-03-01 00:48:36 -05:00
var groups = [ALTAppGroup]()
var errors = [Error]()
2023-03-01 00:48:36 -05:00
for groupIdentifier in applicationGroups {
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
2023-03-01 00:48:36 -05:00
if let group = fetchedGroups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier }) {
groups.append(group)
2023-03-01 00:48:36 -05:00
} else {
dispatchGroup.enter()
2023-03-01 00:48:36 -05:00
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
2023-03-01 00:48:36 -05:00
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { group, error in
switch Result(group, error) {
case let .success(group): groups.append(group)
case let .failure(error): errors.append(error)
}
2023-03-01 00:48:36 -05:00
dispatchGroup.leave()
}
}
}
2023-03-01 00:48:36 -05:00
dispatchGroup.notify(queue: .global()) {
2023-03-01 00:48:36 -05:00
if let error = errors.first {
finish(.failure(error))
2023-03-01 00:48:36 -05:00
} 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 })
}
}
}
}
}
}
}
2023-03-01 00:48:36 -05:00
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 let .failure(error): completionHandler(.failure(error))
case let .success(profile):
// Delete existing profile
2023-03-01 00:48:36 -05:00
ALTAppleAPI.shared.delete(profile, for: team, session: session) { success, error in
switch Result(success, error) {
case let .failure(error): completionHandler(.failure(error))
case .success:
2023-03-01 00:48:36 -05:00
// Fetch new provisiong profile
2023-03-01 00:48:36 -05:00
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { profile, error in
completionHandler(Result(profile, error))
}
}
}
}
}
}
}