2019-06-10 15:03:47 -07:00
|
|
|
//
|
|
|
|
|
// ResignAppOperation.swift
|
|
|
|
|
// AltStore
|
|
|
|
|
//
|
|
|
|
|
// Created by Riley Testut on 6/7/19.
|
|
|
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import Roxas
|
|
|
|
|
|
|
|
|
|
import AltSign
|
|
|
|
|
|
|
|
|
|
@objc(ResignAppOperation)
|
|
|
|
|
class ResignAppOperation: ResultOperation<URL>
|
|
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
let context: AppOperationContext
|
2019-06-10 15:03:47 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
private let temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL()
|
2019-06-10 15:03:47 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
init(context: AppOperationContext)
|
2019-06-10 15:03:47 -07:00
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
self.context = context
|
2019-06-10 15:03:47 -07:00
|
|
|
|
|
|
|
|
super.init()
|
|
|
|
|
|
|
|
|
|
self.progress.totalUnitCount = 3
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func main()
|
|
|
|
|
{
|
|
|
|
|
super.main()
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
self.finish(.failure(error))
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-06-10 15:03:47 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
if let error = self.context.error
|
|
|
|
|
{
|
|
|
|
|
self.finish(.failure(error))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard
|
|
|
|
|
let installedApp = self.context.installedApp,
|
|
|
|
|
let appContext = installedApp.managedObjectContext,
|
|
|
|
|
let signer = self.context.group.signer
|
|
|
|
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
|
|
|
|
|
|
|
|
|
appContext.perform {
|
|
|
|
|
let appIdentifier = installedApp.app.identifier
|
|
|
|
|
|
2019-06-10 15:03:47 -07:00
|
|
|
// Register Device
|
|
|
|
|
self.registerCurrentDevice(for: signer.team) { (result) in
|
|
|
|
|
guard let _ = self.process(result) else { return }
|
|
|
|
|
|
|
|
|
|
// Register App
|
2019-06-21 11:20:03 -07:00
|
|
|
appContext.perform {
|
|
|
|
|
self.register(installedApp.app, team: signer.team) { (result) in
|
2019-06-10 15:03:47 -07:00
|
|
|
guard let appID = self.process(result) else { return }
|
|
|
|
|
|
|
|
|
|
// Fetch Provisioning Profile
|
|
|
|
|
self.fetchProvisioningProfile(for: appID, team: signer.team) { (result) in
|
|
|
|
|
guard let profile = self.process(result) else { return }
|
|
|
|
|
|
|
|
|
|
// Prepare app bundle
|
2019-06-21 11:20:03 -07:00
|
|
|
appContext.perform {
|
2019-06-10 15:03:47 -07:00
|
|
|
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
|
|
|
|
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
let prepareAppBundleProgress = self.prepareAppBundle(for: installedApp) { (result) in
|
2019-06-10 15:03:47 -07:00
|
|
|
guard let appBundleURL = self.process(result) else { return }
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
print("Resigning App:", appIdentifier)
|
|
|
|
|
|
2019-06-10 15:03:47 -07:00
|
|
|
// Resign app bundle
|
|
|
|
|
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profile: profile) { (result) in
|
|
|
|
|
guard let resignedURL = self.process(result) else { return }
|
|
|
|
|
|
|
|
|
|
// Finish
|
2019-06-21 11:20:03 -07:00
|
|
|
appContext.perform {
|
2019-06-10 15:03:47 -07:00
|
|
|
do
|
|
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
installedApp.expirationDate = profile.expirationDate
|
|
|
|
|
installedApp.refreshedDate = Date()
|
2019-06-10 15:03:47 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
try FileManager.default.copyItem(at: resignedURL, to: installedApp.refreshedIPAURL, shouldReplace: true)
|
2019-06-10 15:03:47 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
self.finish(.success(installedApp.refreshedIPAURL))
|
2019-06-10 15:03:47 -07:00
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
self.finish(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
|
|
|
|
|
}
|
|
|
|
|
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func process<T>(_ result: Result<T, Error>) -> T?
|
|
|
|
|
{
|
|
|
|
|
switch result
|
|
|
|
|
{
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
self.finish(.failure(error))
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
|
|
case .success(let value):
|
|
|
|
|
guard !self.isCancelled else {
|
|
|
|
|
self.finish(.failure(OperationError.cancelled))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-21 11:20:03 -07:00
|
|
|
|
|
|
|
|
override func finish(_ result: Result<URL, Error>)
|
|
|
|
|
{
|
|
|
|
|
super.finish(result)
|
|
|
|
|
|
|
|
|
|
if FileManager.default.fileExists(atPath: self.temporaryDirectory.path, isDirectory: nil)
|
|
|
|
|
{
|
|
|
|
|
do { try FileManager.default.removeItem(at: self.temporaryDirectory) }
|
|
|
|
|
catch { print("Failed to remove app bundle.", error) }
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-10 15:03:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension ResignAppOperation
|
|
|
|
|
{
|
|
|
|
|
func registerCurrentDevice(for team: ALTTeam, 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) { (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) { (device, error) in
|
|
|
|
|
completionHandler(Result(device, error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
completionHandler(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func register(_ app: App, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
|
|
|
|
{
|
|
|
|
|
let appName = app.name
|
|
|
|
|
let bundleID = "com.\(team.identifier).\(app.identifier)"
|
|
|
|
|
|
|
|
|
|
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
let appIDs = try Result(appIDs, error).get()
|
|
|
|
|
|
|
|
|
|
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleID })
|
|
|
|
|
{
|
|
|
|
|
completionHandler(.success(appID))
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
|
|
|
|
|
completionHandler(Result(appID, error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
completionHandler(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
|
|
|
|
{
|
|
|
|
|
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
|
|
|
|
|
completionHandler(Result(profile, error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func prepareAppBundle(for installedApp: InstalledApp, completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
|
|
|
|
{
|
|
|
|
|
let progress = Progress.discreteProgress(totalUnitCount: 1)
|
|
|
|
|
|
|
|
|
|
let bundleIdentifier = installedApp.bundleIdentifier
|
|
|
|
|
let openURL = installedApp.openAppURL
|
2019-06-18 18:32:49 -07:00
|
|
|
let appIdentifier = installedApp.app.identifier
|
2019-06-10 15:03:47 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
let fileURL = installedApp.fileURL
|
|
|
|
|
|
2019-06-10 15:03:47 -07:00
|
|
|
DispatchQueue.global().async {
|
|
|
|
|
do
|
|
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
let appBundleURL = self.temporaryDirectory.appendingPathComponent("App.app")
|
|
|
|
|
try FileManager.default.copyItem(at: fileURL, to: appBundleURL)
|
2019-06-10 15:03:47 -07:00
|
|
|
|
|
|
|
|
// Become current so we can observe progress from unzipAppBundle().
|
|
|
|
|
progress.becomeCurrent(withPendingUnitCount: 1)
|
|
|
|
|
|
|
|
|
|
guard let bundle = Bundle(url: appBundleURL) else { throw ALTError(.missingAppBundle) }
|
|
|
|
|
|
|
|
|
|
guard var infoDictionary = NSDictionary(contentsOf: bundle.infoPlistURL) as? [String: Any] else { throw ALTError(.missingInfoPlist) }
|
|
|
|
|
|
|
|
|
|
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
|
|
|
|
|
|
|
|
|
|
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
|
|
|
|
|
"CFBundleURLName": bundleIdentifier,
|
|
|
|
|
"CFBundleURLSchemes": [openURL.scheme!]] as [String : Any]
|
|
|
|
|
allURLSchemes.append(altstoreURLScheme)
|
|
|
|
|
|
|
|
|
|
infoDictionary[Bundle.Info.urlTypes] = allURLSchemes
|
|
|
|
|
|
2019-06-18 18:32:49 -07:00
|
|
|
if appIdentifier == App.altstoreAppID
|
|
|
|
|
{
|
|
|
|
|
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
|
|
|
|
|
infoDictionary[Bundle.Info.deviceID] = udid
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-10 15:03:47 -07:00
|
|
|
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
|
|
|
|
|
|
|
|
|
completionHandler(.success(appBundleURL))
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
completionHandler(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return progress
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func resignAppBundle(at fileURL: URL, signer: ALTSigner, profile: ALTProvisioningProfile, completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
|
|
|
|
{
|
|
|
|
|
let progress = signer.signApp(at: fileURL, provisioningProfile: profile) { (success, error) in
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
try Result(success, error).get()
|
|
|
|
|
|
|
|
|
|
let ipaURL = try FileManager.default.zipAppBundle(at: fileURL)
|
|
|
|
|
completionHandler(.success(ipaURL))
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
completionHandler(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return progress
|
|
|
|
|
}
|
|
|
|
|
}
|