mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 11:43:24 +01:00
Supports apps with app groups/additional entitlements
This commit is contained in:
@@ -355,7 +355,7 @@ private extension ViewController
|
|||||||
let zippedURL = try FileManager.default.zipAppBundle(at: appBundleURL)
|
let zippedURL = try FileManager.default.zipAppBundle(at: appBundleURL)
|
||||||
|
|
||||||
let resigner = ALTSigner(team: team, certificate: certificate)
|
let resigner = ALTSigner(team: team, certificate: certificate)
|
||||||
resigner.signApp(at: zippedURL, provisioningProfile: profile) { (success, error) in
|
resigner.signApp(at: zippedURL, provisioningProfiles: [profile]) { (success, error) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
try Result(success, error).get()
|
try Result(success, error).get()
|
||||||
|
|||||||
@@ -60,50 +60,53 @@ class ResignAppOperation: ResultOperation<URL>
|
|||||||
self.registerCurrentDevice(for: signer.team) { (result) in
|
self.registerCurrentDevice(for: signer.team) { (result) in
|
||||||
guard let _ = self.process(result) else { return }
|
guard let _ = self.process(result) else { return }
|
||||||
|
|
||||||
// Register App
|
// Prepare Provisioning Profiles
|
||||||
appContext.perform {
|
appContext.perform {
|
||||||
self.register(installedApp.app, team: signer.team) { (result) in
|
self.prepareProvisioningProfiles(installedApp.fileURL, team: signer.team) { (result) in
|
||||||
guard let appID = self.process(result) else { return }
|
guard let profiles = self.process(result) else { return }
|
||||||
|
|
||||||
// Fetch Provisioning Profile
|
// Prepare app bundle
|
||||||
self.fetchProvisioningProfile(for: appID, team: signer.team) { (result) in
|
appContext.perform {
|
||||||
guard let profile = self.process(result) else { return }
|
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||||
|
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
||||||
|
|
||||||
// Prepare app bundle
|
let prepareAppBundleProgress = self.prepareAppBundle(for: installedApp, profiles: profiles) { (result) in
|
||||||
appContext.perform {
|
guard let appBundleURL = self.process(result) else { return }
|
||||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
|
||||||
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
|
||||||
|
|
||||||
let prepareAppBundleProgress = self.prepareAppBundle(for: installedApp) { (result) in
|
print("Resigning App:", appIdentifier)
|
||||||
guard let appBundleURL = self.process(result) else { return }
|
|
||||||
|
// 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 }
|
||||||
|
|
||||||
print("Resigning App:", appIdentifier)
|
// Finish
|
||||||
|
appContext.perform {
|
||||||
// Resign app bundle
|
do
|
||||||
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profile: profile) { (result) in
|
{
|
||||||
guard let resignedURL = self.process(result) else { return }
|
installedApp.refreshedDate = Date()
|
||||||
|
|
||||||
// Finish
|
if let profile = profiles[installedApp.app.identifier]
|
||||||
appContext.perform {
|
|
||||||
do
|
|
||||||
{
|
{
|
||||||
installedApp.expirationDate = profile.expirationDate
|
installedApp.expirationDate = profile.expirationDate
|
||||||
installedApp.refreshedDate = Date()
|
|
||||||
|
|
||||||
try FileManager.default.copyItem(at: resignedURL, to: installedApp.refreshedIPAURL, shouldReplace: true)
|
|
||||||
|
|
||||||
self.finish(.success(installedApp.refreshedIPAURL))
|
|
||||||
}
|
}
|
||||||
catch
|
else
|
||||||
{
|
{
|
||||||
self.finish(.failure(error))
|
installedApp.expirationDate = installedApp.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try FileManager.default.copyItem(at: resignedURL, to: installedApp.refreshedIPAURL, shouldReplace: true)
|
||||||
|
|
||||||
|
self.finish(.success(installedApp.refreshedIPAURL))
|
||||||
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,10 +175,98 @@ private extension ResignAppOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func register(_ app: App, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
|
||||||
|
{
|
||||||
|
guard let bundle = Bundle(url: fileURL), let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
|
||||||
|
|
||||||
|
let dispatchGroup = DispatchGroup()
|
||||||
|
|
||||||
|
var profiles = [String: ALTProvisioningProfile]()
|
||||||
|
var error: Error?
|
||||||
|
|
||||||
|
dispatchGroup.enter()
|
||||||
|
|
||||||
|
self.prepareProvisioningProfile(for: app, team: team) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let e): error = e
|
||||||
|
case .success(let profile):
|
||||||
|
profiles[app.bundleIdentifier] = profile
|
||||||
|
}
|
||||||
|
dispatchGroup.leave()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let directory = bundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
||||||
|
{
|
||||||
|
for case let fileURL as URL in enumerator where fileURL.pathExtension.lowercased() == "appex"
|
||||||
|
{
|
||||||
|
guard let appExtension = ALTApplication(fileURL: fileURL) else { continue }
|
||||||
|
|
||||||
|
dispatchGroup.enter()
|
||||||
|
|
||||||
|
self.prepareProvisioningProfile(for: appExtension, team: team) { (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, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||||
|
{
|
||||||
|
// Register
|
||||||
|
self.register(app, team: team) { (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) { (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) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): completionHandler(.failure(error))
|
||||||
|
case .success(let appID):
|
||||||
|
|
||||||
|
// Fetch Provisioning Profile
|
||||||
|
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
|
||||||
|
completionHandler(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func register(_ app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||||
{
|
{
|
||||||
let appName = app.name
|
let appName = app.name
|
||||||
let bundleID = "com.\(team.identifier).\(app.identifier)"
|
let bundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
|
||||||
|
|
||||||
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
|
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
|
||||||
do
|
do
|
||||||
@@ -200,6 +291,76 @@ private extension ResignAppOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
||||||
|
guard let feature = ALTFeature(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
|
||||||
|
}
|
||||||
|
|
||||||
|
let appID = appID.copy() as! ALTAppID
|
||||||
|
appID.features = features
|
||||||
|
|
||||||
|
ALTAppleAPI.shared.update(appID, team: team) { (appID, error) in
|
||||||
|
completionHandler(Result(appID, error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, 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) { (success, error) in
|
||||||
|
let result = result.map { _ in appID }
|
||||||
|
completionHandler(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let adjustedGroupIdentifier = "group.\(team.identifier)." + groupIdentifier
|
||||||
|
|
||||||
|
ALTAppleAPI.shared.fetchAppGroups(for: team) { (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) { (group, error) in
|
||||||
|
finish(Result(group, error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||||
{
|
{
|
||||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
|
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
|
||||||
@@ -225,7 +386,7 @@ private extension ResignAppOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareAppBundle(for installedApp: InstalledApp, completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
func prepareAppBundle(for installedApp: InstalledApp, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
||||||
{
|
{
|
||||||
let progress = Progress.discreteProgress(totalUnitCount: 1)
|
let progress = Progress.discreteProgress(totalUnitCount: 1)
|
||||||
|
|
||||||
@@ -235,6 +396,22 @@ private extension ResignAppOperation
|
|||||||
|
|
||||||
let fileURL = installedApp.fileURL
|
let fileURL = installedApp.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.infoDictionary else { throw ALTError(.missingInfoPlist) }
|
||||||
|
|
||||||
|
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
||||||
|
|
||||||
|
for (key, value) in additionalInfoDictionaryValues
|
||||||
|
{
|
||||||
|
infoDictionary[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.global().async {
|
DispatchQueue.global().async {
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -244,9 +421,8 @@ private extension ResignAppOperation
|
|||||||
// Become current so we can observe progress from unzipAppBundle().
|
// Become current so we can observe progress from unzipAppBundle().
|
||||||
progress.becomeCurrent(withPendingUnitCount: 1)
|
progress.becomeCurrent(withPendingUnitCount: 1)
|
||||||
|
|
||||||
guard let bundle = Bundle(url: appBundleURL) else { throw ALTError(.missingAppBundle) }
|
guard let appBundle = Bundle(url: appBundleURL) else { throw ALTError(.missingAppBundle) }
|
||||||
|
guard let infoDictionary = appBundle.infoDictionary else { throw ALTError(.missingInfoPlist) }
|
||||||
guard var infoDictionary = NSDictionary(contentsOf: bundle.infoPlistURL) as? [String: Any] else { throw ALTError(.missingInfoPlist) }
|
|
||||||
|
|
||||||
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
|
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
|
||||||
|
|
||||||
@@ -255,15 +431,25 @@ private extension ResignAppOperation
|
|||||||
"CFBundleURLSchemes": [openURL.scheme!]] as [String : Any]
|
"CFBundleURLSchemes": [openURL.scheme!]] as [String : Any]
|
||||||
allURLSchemes.append(altstoreURLScheme)
|
allURLSchemes.append(altstoreURLScheme)
|
||||||
|
|
||||||
infoDictionary[Bundle.Info.urlTypes] = allURLSchemes
|
var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes]
|
||||||
|
|
||||||
if appIdentifier == App.altstoreAppID
|
if appIdentifier == App.altstoreAppID
|
||||||
{
|
{
|
||||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
|
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
|
||||||
infoDictionary[Bundle.Info.deviceID] = udid
|
additionalValues[Bundle.Info.deviceID] = udid
|
||||||
}
|
}
|
||||||
|
|
||||||
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
// 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))
|
completionHandler(.success(appBundleURL))
|
||||||
}
|
}
|
||||||
@@ -276,9 +462,10 @@ private extension ResignAppOperation
|
|||||||
return progress
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
func resignAppBundle(at fileURL: URL, signer: ALTSigner, profile: ALTProvisioningProfile, completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
func resignAppBundle(at fileURL: URL, signer: ALTSigner, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
||||||
{
|
{
|
||||||
let progress = signer.signApp(at: fileURL, provisioningProfile: profile) { (success, error) in
|
|
||||||
|
let progress = signer.signApp(at: fileURL, provisioningProfiles: profiles) { (success, error) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
try Result(success, error).get()
|
try Result(success, error).get()
|
||||||
|
|||||||
2
Dependencies/AltSign
vendored
2
Dependencies/AltSign
vendored
Submodule Dependencies/AltSign updated: 17cc07d3b3...8da82a59a2
Reference in New Issue
Block a user