mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-11 15:53:30 +01:00
Displays progress when downloading/refreshing apps
Refactors download/refresh steps into separate Operation subclasses
This commit is contained in:
@@ -62,7 +62,7 @@ extension AppDelegate
|
||||
private func prepareForBackgroundFetch()
|
||||
{
|
||||
// Fetch every 6 hours.
|
||||
UIApplication.shared.setMinimumBackgroundFetchInterval(60 * 60 * 6)
|
||||
UIApplication.shared.setMinimumBackgroundFetchInterval(60 * 60)
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
||||
}
|
||||
@@ -72,8 +72,10 @@ extension AppDelegate
|
||||
{
|
||||
ServerManager.shared.startDiscovering()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
AppManager.shared.refreshAllApps(presentingViewController: nil) { (result) in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
|
||||
|
||||
_ = AppManager.shared.refresh(installedApps, presentingViewController: nil) { (result) in
|
||||
ServerManager.shared.stopDiscovering()
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
@@ -87,9 +89,7 @@ extension AppDelegate
|
||||
guard case let .failure(error) = result else { continue }
|
||||
throw error
|
||||
}
|
||||
|
||||
print(results)
|
||||
|
||||
|
||||
content.title = "Refreshed Apps!"
|
||||
content.body = "Successfully refreshed all apps."
|
||||
|
||||
|
||||
@@ -124,7 +124,11 @@ private extension AppDetailViewController
|
||||
|
||||
sender.isIndicatingActivity = true
|
||||
|
||||
AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
||||
let progressView = UIProgressView(progressViewStyle: .bar)
|
||||
progressView.translatesAutoresizingMaskIntoConstraints = false
|
||||
progressView.progress = 0.0
|
||||
|
||||
let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
let installedApp = try result.get()
|
||||
@@ -138,7 +142,7 @@ private extension AppDetailViewController
|
||||
toastView.show(in: self.navigationController!.view, duration: 2)
|
||||
}
|
||||
}
|
||||
catch AppManager.AppError.authentication(AuthenticationOperation.Error.cancelled)
|
||||
catch OperationError.cancelled
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
@@ -150,12 +154,28 @@ private extension AppDetailViewController
|
||||
toastView.show(in: self.navigationController!.view, duration: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIView.animate(withDuration: 0.4, animations: {
|
||||
progressView.alpha = 0.0
|
||||
}) { _ in
|
||||
progressView.removeFromSuperview()
|
||||
}
|
||||
|
||||
sender.isIndicatingActivity = false
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
progressView.observedProgress = progress
|
||||
|
||||
if let navigationBar = self.navigationController?.navigationBar
|
||||
{
|
||||
navigationBar.addSubview(progressView)
|
||||
|
||||
NSLayoutConstraint.activate([progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor),
|
||||
progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,52 +14,10 @@ import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
enum AppError: LocalizedError
|
||||
{
|
||||
case unknown
|
||||
case missingUDID
|
||||
case noServersFound
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
case notAuthenticated
|
||||
|
||||
case multipleCertificates
|
||||
case multipleTeams
|
||||
|
||||
case download(URLError)
|
||||
case authentication(Error)
|
||||
case fetchingSigningResources(Error)
|
||||
case prepare(Error)
|
||||
case install(Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .unknown: return "An unknown error occured."
|
||||
case .missingUDID: return "The UDID for this device is unknown."
|
||||
case .noServersFound: return "An active AltServer could not be found."
|
||||
case .missingPrivateKey: return "A valid private key must be provided."
|
||||
case .missingCertificate: return "A valid certificate must be provided."
|
||||
case .notAuthenticated: return "You must be logged in with your Apple ID to install apps."
|
||||
case .multipleCertificates: return "You must select a certificate to use to install apps."
|
||||
case .multipleTeams: return "You must select a team to use to install apps."
|
||||
case .download(let error): return error.localizedDescription
|
||||
case .authentication(let error): return error.localizedDescription
|
||||
case .fetchingSigningResources(let error): return error.localizedDescription
|
||||
case .prepare(let error): return error.localizedDescription
|
||||
case .install(let error): return error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppManager
|
||||
{
|
||||
static let shared = AppManager()
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
private let operationQueue = OperationQueue()
|
||||
|
||||
private init()
|
||||
@@ -107,7 +65,7 @@ extension AppManager
|
||||
#endif
|
||||
}
|
||||
|
||||
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTTeam, ALTCertificate), Error>) -> Void)
|
||||
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTSigner, Error>) -> Void)
|
||||
{
|
||||
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
||||
authenticationOperation.resultHandler = { (result) in
|
||||
@@ -119,91 +77,60 @@ extension AppManager
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, AppError>) -> Void)
|
||||
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let ipaURL = InstalledApp.ipaURL(for: app)
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.InstallApp")
|
||||
|
||||
func finish(_ result: Result<InstalledApp, AppError>)
|
||||
{
|
||||
completionHandler(result)
|
||||
|
||||
RSTEndBackgroundTask(backgroundTaskID)
|
||||
}
|
||||
|
||||
// Download app
|
||||
self.downloadApp(from: app.downloadURL) { (result) in
|
||||
let result = result.flatMap { (fileURL) -> Result<Void, URLError> in
|
||||
// Copy downloaded app to proper location
|
||||
let result = Result { try FileManager.default.copyItem(at: fileURL, to: ipaURL, shouldReplace: true) }
|
||||
return result.mapError { _ in URLError(.cannotWriteToFile) }
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
||||
authenticationOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.download(error)))
|
||||
case .success:
|
||||
// Authenticate
|
||||
self.authenticate(presentingViewController: presentingViewController) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.authentication(error)))
|
||||
case .success(let team, let certificate):
|
||||
|
||||
// Fetch provisioning profile
|
||||
self.prepareProvisioningProfile(for: app, team: team) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.fetchingSigningResources(error)))
|
||||
case .success(let profile):
|
||||
|
||||
// Prepare app
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let app = context.object(with: app.objectID) as! App
|
||||
|
||||
let installedApp = InstalledApp(app: app,
|
||||
bundleIdentifier: profile.appID.bundleIdentifier,
|
||||
expirationDate: profile.expirationDate,
|
||||
context: context)
|
||||
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
self.prepare(installedApp, provisioningProfile: profile, signer: signer) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.prepare(error)))
|
||||
case .success(let resignedURL):
|
||||
|
||||
// Send app to server
|
||||
context.perform {
|
||||
self.sendAppToServer(fileURL: resignedURL, identifier: installedApp.bundleIdentifier) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.install(error)))
|
||||
case .success:
|
||||
context.perform {
|
||||
finish(.success(installedApp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let signer):
|
||||
|
||||
// Download
|
||||
app.managedObjectContext?.perform {
|
||||
let downloadAppOperation = DownloadAppOperation(app: app)
|
||||
downloadAppOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let installedApp):
|
||||
let context = installedApp.managedObjectContext
|
||||
|
||||
// Refresh/Install
|
||||
let (resignProgress, installProgress) = self.refresh(installedApp, signer: signer, presentingViewController: presentingViewController) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success:
|
||||
context?.perform {
|
||||
completionHandler(.success(installedApp))
|
||||
}
|
||||
}
|
||||
}
|
||||
progress.addChild(resignProgress, withPendingUnitCount: 10)
|
||||
progress.addChild(installProgress, withPendingUnitCount: 45)
|
||||
}
|
||||
}
|
||||
progress.addChild(downloadAppOperation.progress, withPendingUnitCount: 40)
|
||||
self.operationQueue.addOperation(downloadAppOperation)
|
||||
}
|
||||
}
|
||||
}
|
||||
progress.addChild(authenticationOperation.progress, withPendingUnitCount: 5)
|
||||
self.operationQueue.addOperation(authenticationOperation)
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
func refresh(_ app: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
func refresh(_ app: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
self.refresh([app], presentingViewController: presentingViewController) { (result) in
|
||||
return self.refresh([app], presentingViewController: presentingViewController) { (result) in
|
||||
do
|
||||
{
|
||||
guard let (_, result) = try result.get().first else { throw AppError.unknown }
|
||||
guard let (_, result) = try result.get().first else { throw OperationError.unknown }
|
||||
completionHandler(result)
|
||||
}
|
||||
catch
|
||||
@@ -213,303 +140,101 @@ extension AppManager
|
||||
}
|
||||
}
|
||||
|
||||
func refreshAllApps(presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], AppError>) -> Void)
|
||||
@discardableResult func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> Progress
|
||||
{
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
do
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)]
|
||||
|
||||
let installedApps = try context.fetch(fetchRequest)
|
||||
self.refresh(installedApps, presentingViewController: presentingViewController) { (result) in
|
||||
context.perform { // keep context alive
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(.prepare(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refresh<T: Collection>(_ installedApps: T, presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], AppError>) -> Void) where T.Element == InstalledApp
|
||||
{
|
||||
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.RefreshApps")
|
||||
let progress = Progress.discreteProgress(totalUnitCount: Int64(installedApps.count))
|
||||
|
||||
func finish(_ result: Result<[String: Result<InstalledApp, Error>], AppError>)
|
||||
{
|
||||
completionHandler(result)
|
||||
|
||||
RSTEndBackgroundTask(backgroundTaskID)
|
||||
guard let context = installedApps.first?.managedObjectContext else {
|
||||
completionHandler(.success([:]))
|
||||
return progress
|
||||
}
|
||||
|
||||
guard !ServerManager.shared.discoveredServers.isEmpty else { return finish(.failure(.noServersFound)) }
|
||||
|
||||
// Authenticate
|
||||
self.authenticate(presentingViewController: presentingViewController) { (result) in
|
||||
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
||||
authenticationOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.authentication(error)))
|
||||
case .success(let team, let certificate):
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let signer):
|
||||
|
||||
// Sign
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
|
||||
for app in installedApps
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
// Refresh
|
||||
context.perform {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
app.managedObjectContext?.perform {
|
||||
let bundleIdentifier = app.bundleIdentifier
|
||||
for installedApp in installedApps
|
||||
{
|
||||
let bundleIdentifier = installedApp.bundleIdentifier
|
||||
print("Refreshing App:", bundleIdentifier)
|
||||
|
||||
self.refresh(app, signer: signer, context: context) { (result) in
|
||||
dispatchGroup.enter()
|
||||
|
||||
let (resignProgress, installProgress) = self.refresh(installedApp, signer: signer, presentingViewController: presentingViewController) { (result) in
|
||||
print("Refreshed App: \(bundleIdentifier).", result)
|
||||
results[bundleIdentifier] = result
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let refreshProgress = Progress(totalUnitCount: 100)
|
||||
refreshProgress.addChild(resignProgress, withPendingUnitCount: 20)
|
||||
refreshProgress.addChild(installProgress, withPendingUnitCount: 80)
|
||||
|
||||
progress.addChild(refreshProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
context.perform {
|
||||
finish(.success(results))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppManager
|
||||
{
|
||||
func downloadApp(from url: URL, completionHandler: @escaping (Result<URL, URLError>) -> Void)
|
||||
{
|
||||
let downloadTask = self.session.downloadTask(with: url) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
completionHandler(.success(fileURL))
|
||||
}
|
||||
catch let error as URLError
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(URLError(.unknown)))
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func prepareProvisioningProfile(for app: App, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return completionHandler(.failure(AppError.missingUDID)) }
|
||||
|
||||
let device = ALTDevice(name: UIDevice.current.name, identifier: udid)
|
||||
self.register(device, team: team) { (result) in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
|
||||
app.managedObjectContext?.perform {
|
||||
self.register(app, with: team) { (result) in
|
||||
do
|
||||
{
|
||||
let appID = try result.get()
|
||||
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
|
||||
do
|
||||
{
|
||||
let provisioningProfile = try result.get()
|
||||
completionHandler(.success(provisioningProfile))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepare(_ installedApp: InstalledApp, provisioningProfile: ALTProvisioningProfile, signer: ALTSigner, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
let refreshedAppDirectory = installedApp.directoryURL.appendingPathComponent("Refreshed", isDirectory: true)
|
||||
|
||||
if FileManager.default.fileExists(atPath: refreshedAppDirectory.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: refreshedAppDirectory)
|
||||
}
|
||||
try FileManager.default.createDirectory(at: refreshedAppDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let appBundleURL = try FileManager.default.unzipAppBundle(at: installedApp.ipaURL, toDirectory: refreshedAppDirectory)
|
||||
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": installedApp.bundleIdentifier,
|
||||
"CFBundleURLSchemes": [installedApp.openAppURL.scheme!]] as [String : Any]
|
||||
allURLSchemes.append(altstoreURLScheme)
|
||||
|
||||
infoDictionary[Bundle.Info.urlTypes] = allURLSchemes
|
||||
|
||||
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
||||
|
||||
signer.signApp(at: appBundleURL, provisioningProfile: provisioningProfile) { (success, error) in
|
||||
do
|
||||
{
|
||||
try Result(success, error).get()
|
||||
|
||||
let resignedURL = try FileManager.default.zipAppBundle(at: appBundleURL)
|
||||
completionHandler(.success(resignedURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
context.perform {
|
||||
completionHandler(.success(results))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func sendAppToServer(fileURL: URL, identifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
guard let server = ServerManager.shared.discoveredServers.first else { return completionHandler(.failure(AppError.noServersFound)) }
|
||||
|
||||
server.installApp(at: fileURL, identifier: identifier) { (result) in
|
||||
let result = result.mapError { $0 as Error }
|
||||
completionHandler(result)
|
||||
}
|
||||
self.operationQueue.addOperation(authenticationOperation)
|
||||
|
||||
return progress
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppManager
|
||||
{
|
||||
func register(_ device: ALTDevice, team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
func refresh(_ installedApp: InstalledApp, signer: ALTSigner, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> (Progress, Progress)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
|
||||
do
|
||||
{
|
||||
let devices = try Result(devices, error).get()
|
||||
|
||||
if let device = devices.first(where: { $0.identifier == device.identifier })
|
||||
{
|
||||
completionHandler(.success(device))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, team: team) { (device, error) in
|
||||
completionHandler(Result(device, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func register(_ app: App, with team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let appName = app.name
|
||||
let bundleID = "com.\(team.identifier).\(app.identifier)"
|
||||
let context = installedApp.managedObjectContext
|
||||
|
||||
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
|
||||
let resignAppOperation = ResignAppOperation(installedApp: installedApp)
|
||||
let installAppOperation = InstallAppOperation()
|
||||
|
||||
// Resign
|
||||
resignAppOperation.signer = signer
|
||||
resignAppOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
installAppOperation.cancel()
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success(let resignedURL):
|
||||
installAppOperation.fileURL = resignedURL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 refresh(_ installedApp: InstalledApp, signer: ALTSigner, context: NSManagedObjectContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
{
|
||||
self.prepareProvisioningProfile(for: installedApp.app, team: signer.team) { (result) in
|
||||
|
||||
// Install
|
||||
installAppOperation.addDependency(resignAppOperation)
|
||||
installAppOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let profile):
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
self.prepare(installedApp, provisioningProfile: profile, signer: signer) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let resignedURL):
|
||||
|
||||
// Send app to server
|
||||
installedApp.managedObjectContext?.perform {
|
||||
self.sendAppToServer(fileURL: resignedURL, identifier: installedApp.bundleIdentifier) { (result) in
|
||||
context.perform {
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
let installedApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||
installedApp.expirationDate = profile.expirationDate
|
||||
completionHandler(.success(installedApp))
|
||||
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .success:
|
||||
context?.perform {
|
||||
completionHandler(.success(installedApp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.operationQueue.addOperations([resignAppOperation, installAppOperation], waitUntilFinished: false)
|
||||
|
||||
return (resignAppOperation.progress, installAppOperation.progress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,12 +76,19 @@
|
||||
<navigationItem key="navigationItem" title="My Apps" id="dz9-0e-LKa">
|
||||
<barButtonItem key="rightBarButtonItem" title="Refresh All" id="0Ke-yl-tAg">
|
||||
<connections>
|
||||
<action selector="refreshAllApps:" destination="Xf1-LZ-1vU" id="6op-Mf-HSD"/>
|
||||
<action selector="refreshAllApps:" destination="Xf1-LZ-1vU" id="GOS-gx-qKD"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="progressView" destination="CuF-K7-fn8" id="SPP-VP-a9e"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="nb5-5T-hHT" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progressViewStyle="bar" id="CuF-K7-fn8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="150" height="2.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</progressView>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1518" y="420"/>
|
||||
</scene>
|
||||
@@ -629,7 +636,7 @@
|
||||
<image name="second" width="30" height="30"/>
|
||||
</resources>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="miz-8X-gFg"/>
|
||||
<segue reference="8jj-zE-2hk"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<color key="tintColor" name="Purple"/>
|
||||
</document>
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(InstalledApp)
|
||||
class InstalledApp: NSManagedObject
|
||||
class InstalledApp: NSManagedObject, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var bundleIdentifier: String
|
||||
@@ -75,6 +75,13 @@ extension InstalledApp
|
||||
return ipaURL
|
||||
}
|
||||
|
||||
class func refreshedIPAURL(for app: App) -> URL
|
||||
{
|
||||
let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
|
||||
return ipaURL
|
||||
}
|
||||
|
||||
|
||||
class func directoryURL(for app: App) -> URL
|
||||
{
|
||||
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.identifier)
|
||||
@@ -92,4 +99,8 @@ extension InstalledApp
|
||||
var ipaURL: URL {
|
||||
return InstalledApp.ipaURL(for: self.app)
|
||||
}
|
||||
|
||||
var refreshedIPAURL: URL {
|
||||
return InstalledApp.refreshedIPAURL(for: self.app)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import UIKit
|
||||
import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
class MyAppsViewController: UITableViewController
|
||||
{
|
||||
private var refreshErrors = [String: Error]()
|
||||
@@ -22,12 +24,23 @@ class MyAppsViewController: UITableViewController
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
@IBOutlet private var progressView: UIProgressView!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
|
||||
if let navigationBar = self.navigationController?.navigationBar
|
||||
{
|
||||
self.progressView.translatesAutoresizingMaskIntoConstraints = false
|
||||
navigationBar.addSubview(self.progressView)
|
||||
|
||||
NSLayoutConstraint.activate([self.progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor),
|
||||
self.progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
@@ -106,7 +119,9 @@ private extension MyAppsViewController
|
||||
{
|
||||
sender.isIndicatingActivity = true
|
||||
|
||||
AppManager.shared.refreshAllApps(presentingViewController: self) { (result) in
|
||||
let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
|
||||
|
||||
let progress = AppManager.shared.refresh(installedApps, presentingViewController: self) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
@@ -146,10 +161,49 @@ private extension MyAppsViewController
|
||||
self.refreshErrors = failures
|
||||
}
|
||||
|
||||
self.progressView.observedProgress = nil
|
||||
self.progressView.progress = 0.0
|
||||
|
||||
sender.isIndicatingActivity = false
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
self.progressView.observedProgress = progress
|
||||
}
|
||||
|
||||
func refresh(_ installedApp: InstalledApp)
|
||||
{
|
||||
let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
try app.managedObjectContext?.save()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let toastView = RSTToastView(text: "Refreshed \(installedApp.app.name)!", detailText: nil)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let toastView = RSTToastView(text: "Failed to refresh \(installedApp.app.name)", detailText: error.localizedDescription)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.progressView.observedProgress = nil
|
||||
self.progressView.progress = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
self.progressView.observedProgress = progress
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +237,7 @@ extension MyAppsViewController
|
||||
toastView.activityIndicatorView.startAnimating()
|
||||
toastView.show(in: self.navigationController?.view ?? self.view)
|
||||
|
||||
AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
|
||||
let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
@@ -203,11 +257,16 @@ extension MyAppsViewController
|
||||
let toastView = RSTToastView(text: "Failed to refresh \(installedApp.app.name)", detailText: error.localizedDescription)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.progressView.observedProgress = nil
|
||||
self.progressView.progress = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
self.progressView.observedProgress = progress
|
||||
}
|
||||
|
||||
return [deleteAction, refreshAction]
|
||||
|
||||
@@ -11,36 +11,27 @@ import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
extension AuthenticationOperation
|
||||
enum AuthenticationError: LocalizedError
|
||||
{
|
||||
enum Error: LocalizedError
|
||||
{
|
||||
case cancelled
|
||||
|
||||
case notAuthenticated
|
||||
case noTeam
|
||||
case noCertificate
|
||||
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
||||
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
||||
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
||||
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
||||
}
|
||||
case noTeam
|
||||
case noCertificate
|
||||
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
||||
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
||||
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
||||
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationOperation: RSTOperation
|
||||
@objc(AuthenticationOperation)
|
||||
class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
{
|
||||
var resultHandler: ((Result<(ALTTeam, ALTCertificate), Swift.Error>) -> Void)?
|
||||
|
||||
private weak var presentingViewController: UIViewController?
|
||||
|
||||
private lazy var navigationController = UINavigationController()
|
||||
@@ -48,106 +39,51 @@ class AuthenticationOperation: RSTOperation
|
||||
|
||||
private var appleIDPassword: String?
|
||||
|
||||
override var isAsynchronous: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(presentingViewController: UIViewController?)
|
||||
{
|
||||
self.presentingViewController = presentingViewController
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 3
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.Authenticate")
|
||||
|
||||
func finish(_ result: Result<(ALTTeam, ALTCertificate), Swift.Error>)
|
||||
{
|
||||
print("Finished authenticating with result:", result)
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
do
|
||||
{
|
||||
let (altTeam, altCertificate) = try result.get()
|
||||
let altAccount = altTeam.account
|
||||
|
||||
// Account
|
||||
let account = Account(altAccount, context: context)
|
||||
account.isActiveAccount = true
|
||||
|
||||
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
|
||||
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
|
||||
|
||||
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
|
||||
for account in otherAccounts
|
||||
{
|
||||
account.isActiveAccount = false
|
||||
}
|
||||
|
||||
// Team
|
||||
let team = Team(altTeam, account: account, context: context)
|
||||
team.isActiveTeam = true
|
||||
|
||||
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
|
||||
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
|
||||
|
||||
let otherTeams = try context.fetch(otherTeamsFetchRequest)
|
||||
for team in otherTeams
|
||||
{
|
||||
team.isActiveTeam = false
|
||||
}
|
||||
|
||||
// Save
|
||||
try context.save()
|
||||
|
||||
// Update keychain
|
||||
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
|
||||
Keychain.shared.appleIDPassword = self.appleIDPassword
|
||||
|
||||
Keychain.shared.signingCertificateIdentifier = altCertificate.identifier
|
||||
Keychain.shared.signingCertificatePrivateKey = altCertificate.privateKey
|
||||
|
||||
self.resultHandler?(.success((altTeam, altCertificate)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.resultHandler?(.failure(error))
|
||||
}
|
||||
|
||||
self.finish()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
RSTEndBackgroundTask(backgroundTaskID)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign In
|
||||
self.signIn { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let account):
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Fetch Team
|
||||
self.fetchTeam(for: account) { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let team):
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Fetch Certificate
|
||||
self.fetchCertificate(for: team) { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let certificate): finish(.success((team, certificate)))
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let certificate):
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
self.finish(.success(signer))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +91,68 @@ class AuthenticationOperation: RSTOperation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<ALTSigner, Error>)
|
||||
{
|
||||
guard !self.isFinished else { return }
|
||||
|
||||
print("Finished authenticating with result:", result)
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
do
|
||||
{
|
||||
let signer = try result.get()
|
||||
let altAccount = signer.team.account
|
||||
|
||||
// Account
|
||||
let account = Account(altAccount, context: context)
|
||||
account.isActiveAccount = true
|
||||
|
||||
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
|
||||
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
|
||||
|
||||
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
|
||||
for account in otherAccounts
|
||||
{
|
||||
account.isActiveAccount = false
|
||||
}
|
||||
|
||||
// Team
|
||||
let team = Team(signer.team, account: account, context: context)
|
||||
team.isActiveTeam = true
|
||||
|
||||
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
|
||||
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
|
||||
|
||||
let otherTeams = try context.fetch(otherTeamsFetchRequest)
|
||||
for team in otherTeams
|
||||
{
|
||||
team.isActiveTeam = false
|
||||
}
|
||||
|
||||
// Save
|
||||
try context.save()
|
||||
|
||||
// Update keychain
|
||||
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
|
||||
Keychain.shared.appleIDPassword = self.appleIDPassword
|
||||
|
||||
Keychain.shared.signingCertificateIdentifier = signer.certificate.identifier
|
||||
Keychain.shared.signingCertificatePrivateKey = signer.certificate.privateKey
|
||||
|
||||
super.finish(.success(signer))
|
||||
}
|
||||
catch
|
||||
{
|
||||
super.finish(.failure(error))
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AuthenticationOperation
|
||||
@@ -199,13 +197,13 @@ private extension AuthenticationOperation
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(Error.cancelled))
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(authenticationViewController)
|
||||
{
|
||||
completionHandler(.failure(Error.notAuthenticated))
|
||||
completionHandler(.failure(OperationError.notAuthenticated))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,13 +252,13 @@ private extension AuthenticationOperation
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(Error.cancelled))
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(selectTeamViewController)
|
||||
{
|
||||
completionHandler(.failure(Error.noTeam))
|
||||
completionHandler(.failure(AuthenticationError.noTeam))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +292,7 @@ private extension AuthenticationOperation
|
||||
do
|
||||
{
|
||||
let certificate = try Result(certificate, error).get()
|
||||
guard let privateKey = certificate.privateKey else { throw Error.missingPrivateKey }
|
||||
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
do
|
||||
@@ -302,7 +300,7 @@ private extension AuthenticationOperation
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else {
|
||||
throw Error.missingCertificate
|
||||
throw AuthenticationError.missingCertificate
|
||||
}
|
||||
|
||||
certificate.privateKey = privateKey
|
||||
@@ -334,13 +332,13 @@ private extension AuthenticationOperation
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(Error.cancelled))
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(replaceCertificateViewController)
|
||||
{
|
||||
completionHandler(.failure(Error.noCertificate))
|
||||
completionHandler(.failure(AuthenticationError.noCertificate))
|
||||
}
|
||||
}
|
||||
}
|
||||
65
AltStore/Operations/DownloadAppOperation.swift
Normal file
65
AltStore/Operations/DownloadAppOperation.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// DownloadAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/10/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(DownloadAppOperation)
|
||||
class DownloadAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
let app: App
|
||||
private let downloadURL: URL
|
||||
private let ipaURL: URL
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
|
||||
init(app: App)
|
||||
{
|
||||
self.app = app
|
||||
self.downloadURL = app.downloadURL
|
||||
self.ipaURL = InstalledApp.ipaURL(for: app)
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 1
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
let downloadTask = self.session.downloadTask(with: self.downloadURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
|
||||
try FileManager.default.copyItem(at: fileURL, to: self.ipaURL, shouldReplace: true)
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let app = context.object(with: self.app.objectID) as! App
|
||||
|
||||
let installedApp = InstalledApp(app: app,
|
||||
bundleIdentifier: app.identifier,
|
||||
expirationDate: Date(),
|
||||
context: context)
|
||||
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
}
|
||||
catch let error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
286
AltStore/Operations/InstallAppOperation.swift
Normal file
286
AltStore/Operations/InstallAppOperation.swift
Normal file
@@ -0,0 +1,286 @@
|
||||
//
|
||||
// InstallAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
|
||||
extension ALTServerError
|
||||
{
|
||||
init<E: Error>(_ error: E)
|
||||
{
|
||||
switch error
|
||||
{
|
||||
case let error as ALTServerError: self = error
|
||||
case is DecodingError: self = ALTServerError(.invalidResponse)
|
||||
case is EncodingError: self = ALTServerError(.invalidRequest)
|
||||
default:
|
||||
assertionFailure("Caught unknown error type")
|
||||
self = ALTServerError(.unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum InstallationError: LocalizedError
|
||||
{
|
||||
case serverNotFound
|
||||
case connectionFailed
|
||||
case connectionDropped
|
||||
case invalidApp
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "")
|
||||
case .connectionFailed: return NSLocalizedString("Could not connect to AltServer.", comment: "")
|
||||
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
|
||||
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(InstallAppOperation)
|
||||
class InstallAppOperation: ResultOperation<Void>
|
||||
{
|
||||
var fileURL: URL?
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "com.altstore.InstallAppOperation")
|
||||
|
||||
private var connection: NWConnection?
|
||||
|
||||
override init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 4
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
guard let fileURL = self.fileURL else { return self.finish(.failure(OperationError.appNotFound)) }
|
||||
|
||||
// Connect to server.
|
||||
self.connect { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let connection):
|
||||
self.connection = connection
|
||||
|
||||
// Send app to server.
|
||||
self.sendApp(at: fileURL, via: connection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Receive response from server.
|
||||
let progress = self.receiveResponse(from: connection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success: self.finish(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
self.progress.addChild(progress, withPendingUnitCount: 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<Void, Error>)
|
||||
{
|
||||
super.finish(result)
|
||||
|
||||
if let connection = self.connection
|
||||
{
|
||||
connection.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension InstallAppOperation
|
||||
{
|
||||
func connect(completionHandler: @escaping (Result<NWConnection, Error>) -> Void)
|
||||
{
|
||||
guard let server = ServerManager.shared.discoveredServers.first else { return completionHandler(.failure(InstallationError.serverNotFound)) }
|
||||
|
||||
let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp)
|
||||
|
||||
connection.stateUpdateHandler = { [unowned connection] (state) in
|
||||
switch state
|
||||
{
|
||||
case .failed(let error):
|
||||
print("Failed to connect to service \(server.service.name).", error)
|
||||
completionHandler(.failure(InstallationError.connectionFailed))
|
||||
|
||||
case .cancelled:
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
|
||||
case .ready:
|
||||
completionHandler(.success(connection))
|
||||
|
||||
case .waiting: break
|
||||
case .setup: break
|
||||
case .preparing: break
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: self.dispatchQueue)
|
||||
}
|
||||
|
||||
func sendApp(at fileURL: URL, via connection: NWConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let appData = try? Data(contentsOf: fileURL) else { throw InstallationError.invalidApp }
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
|
||||
|
||||
let request = ServerRequest(udid: udid, contentSize: appData.count)
|
||||
let requestData: Data
|
||||
|
||||
do {
|
||||
requestData = try JSONEncoder().encode(request)
|
||||
}
|
||||
catch {
|
||||
print("Invalid request.", error)
|
||||
throw ALTServerError(.invalidRequest)
|
||||
}
|
||||
|
||||
let requestSize = Int32(requestData.count)
|
||||
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
|
||||
|
||||
func process(_ error: Error?) -> Bool
|
||||
{
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(InstallationError.connectionDropped))
|
||||
return false
|
||||
}
|
||||
else
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Send request data size.
|
||||
print("Sending request data size \(requestSize)")
|
||||
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
|
||||
guard process(error) else { return }
|
||||
|
||||
// Send request.
|
||||
print("Sending request \(request)")
|
||||
connection.send(content: requestData, completion: .contentProcessed { (error) in
|
||||
guard process(error) else { return }
|
||||
|
||||
// Send app data.
|
||||
print("Sending app data (Size: \(appData.count))")
|
||||
connection.send(content: appData, completion: .contentProcessed { (error) in
|
||||
print("Sent app data!")
|
||||
|
||||
guard process(error) else { return }
|
||||
completionHandler(.success(()))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result<Void, Error>) -> Void) -> Progress
|
||||
{
|
||||
func receive(from connection: NWConnection, progress: Progress, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let size = MemoryLayout<Int32>.size
|
||||
|
||||
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
|
||||
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
|
||||
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
|
||||
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
|
||||
print(response)
|
||||
|
||||
if let error = response.error
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else if response.progress == 1.0
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
else
|
||||
{
|
||||
progress.completedUnitCount = Int64(response.progress * 100)
|
||||
receive(from: connection, progress: progress, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
receive(from: connection, progress: progress, completionHandler: completionHandler)
|
||||
return progress
|
||||
}
|
||||
|
||||
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
|
||||
{
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let data = data else { throw error ?? ALTServerError(.unknown) }
|
||||
return data
|
||||
}
|
||||
catch let error as NWError
|
||||
{
|
||||
print("Error receiving data from connection \(connection)", error)
|
||||
|
||||
throw ALTServerError(.lostConnection)
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw error
|
||||
}
|
||||
}
|
||||
catch let error as ALTServerError
|
||||
{
|
||||
throw error
|
||||
}
|
||||
catch
|
||||
{
|
||||
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
|
||||
}
|
||||
}
|
||||
}
|
||||
84
AltStore/Operations/Operation.swift
Normal file
84
AltStore/Operations/Operation.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Operation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
class ResultOperation<ResultType>: Operation
|
||||
{
|
||||
var resultHandler: ((Result<ResultType, Error>) -> Void)?
|
||||
|
||||
@available(*, unavailable)
|
||||
override func finish()
|
||||
{
|
||||
super.finish()
|
||||
}
|
||||
|
||||
func finish(_ result: Result<ResultType, Error>)
|
||||
{
|
||||
guard !self.isFinished else { return }
|
||||
|
||||
super.finish()
|
||||
|
||||
self.resultHandler?(result)
|
||||
}
|
||||
}
|
||||
|
||||
class Operation: RSTOperation, ProgressReporting
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
|
||||
private var backgroundTaskID: UIBackgroundTaskIdentifier?
|
||||
|
||||
override var isAsynchronous: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
self.progress.cancellationHandler = { [weak self] in self?.cancel() }
|
||||
}
|
||||
|
||||
override func cancel()
|
||||
{
|
||||
super.cancel()
|
||||
|
||||
if !self.progress.isCancelled
|
||||
{
|
||||
self.progress.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
let name = "com.altstore." + NSStringFromClass(type(of: self))
|
||||
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: name) { [weak self] in
|
||||
guard let backgroundTask = self?.backgroundTaskID else { return }
|
||||
|
||||
self?.cancel()
|
||||
|
||||
UIApplication.shared.endBackgroundTask(backgroundTask)
|
||||
self?.backgroundTaskID = .invalid
|
||||
}
|
||||
}
|
||||
|
||||
override func finish()
|
||||
{
|
||||
super.finish()
|
||||
|
||||
if let backgroundTaskID = self.backgroundTaskID
|
||||
{
|
||||
UIApplication.shared.endBackgroundTask(backgroundTaskID)
|
||||
self.backgroundTaskID = .invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
32
AltStore/Operations/OperationError.swift
Normal file
32
AltStore/Operations/OperationError.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// OperationError.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum OperationError: LocalizedError
|
||||
{
|
||||
case unknown
|
||||
case unknownResult
|
||||
case cancelled
|
||||
|
||||
case notAuthenticated
|
||||
case appNotFound
|
||||
|
||||
case unknownUDID
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
|
||||
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
|
||||
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
244
AltStore/Operations/ResignAppOperation.swift
Normal file
244
AltStore/Operations/ResignAppOperation.swift
Normal file
@@ -0,0 +1,244 @@
|
||||
//
|
||||
// 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>
|
||||
{
|
||||
let installedApp: InstalledApp
|
||||
private var context: NSManagedObjectContext?
|
||||
|
||||
var signer: ALTSigner?
|
||||
|
||||
init(installedApp: InstalledApp)
|
||||
{
|
||||
self.installedApp = installedApp
|
||||
self.context = installedApp.managedObjectContext
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 3
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
guard let context = self.context else { return self.finish(.failure(OperationError.appNotFound)) }
|
||||
guard let signer = self.signer else { return self.finish(.failure(OperationError.notAuthenticated)) }
|
||||
|
||||
context.perform {
|
||||
// Register Device
|
||||
self.registerCurrentDevice(for: signer.team) { (result) in
|
||||
guard let _ = self.process(result) else { return }
|
||||
|
||||
// Register App
|
||||
context.perform {
|
||||
self.register(self.installedApp.app, team: signer.team) { (result) in
|
||||
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
|
||||
context.perform {
|
||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
||||
|
||||
let prepareAppBundleProgress = self.prepareAppBundle(for: self.installedApp) { (result) in
|
||||
guard let appBundleURL = self.process(result) else { return }
|
||||
|
||||
// Resign app bundle
|
||||
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profile: profile) { (result) in
|
||||
guard let resignedURL = self.process(result) else { return }
|
||||
|
||||
// Finish
|
||||
context.perform {
|
||||
do
|
||||
{
|
||||
try FileManager.default.copyItem(at: resignedURL, to: self.installedApp.refreshedIPAURL, shouldReplace: true)
|
||||
|
||||
let refreshedDirectory = resignedURL.deletingLastPathComponent()
|
||||
try? FileManager.default.removeItem(at: refreshedDirectory)
|
||||
|
||||
self.finish(.success(self.installedApp.refreshedIPAURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func process<T>(_ result: Result<T, Error>) -> T?
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
self.finish(.failure(error))
|
||||
return nil
|
||||
|
||||
case .success(let value):
|
||||
guard !self.isCancelled else {
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
return nil
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ResignAppOperation
|
||||
{
|
||||
func 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 refreshedAppDirectory = installedApp.directoryURL.appendingPathComponent("Refreshed", isDirectory: true)
|
||||
let ipaURL = installedApp.ipaURL
|
||||
let bundleIdentifier = installedApp.bundleIdentifier
|
||||
let openURL = installedApp.openAppURL
|
||||
|
||||
DispatchQueue.global().async {
|
||||
do
|
||||
{
|
||||
if FileManager.default.fileExists(atPath: refreshedAppDirectory.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: refreshedAppDirectory)
|
||||
}
|
||||
try FileManager.default.createDirectory(at: refreshedAppDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
// Become current so we can observe progress from unzipAppBundle().
|
||||
progress.becomeCurrent(withPendingUnitCount: 1)
|
||||
|
||||
let appBundleURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: refreshedAppDirectory)
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
//
|
||||
// Server.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
|
||||
extension ALTServerError
|
||||
{
|
||||
init<E: Error>(_ error: E)
|
||||
{
|
||||
switch error
|
||||
{
|
||||
case let error as ALTServerError: self = error
|
||||
case is DecodingError: self = ALTServerError(.invalidResponse)
|
||||
case is EncodingError: self = ALTServerError(.invalidRequest)
|
||||
default:
|
||||
assertionFailure("Caught unknown error type")
|
||||
self = ALTServerError(.unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum InstallError: LocalizedError
|
||||
{
|
||||
case unknown
|
||||
case cancelled
|
||||
case invalidApp
|
||||
case noUDID
|
||||
case server(ALTServerError)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .server(let error): return error.localizedDescription
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Server: Equatable
|
||||
{
|
||||
var service: NetService
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltStore.server", qos: .utility)
|
||||
|
||||
func installApp(at fileURL: URL, identifier: String, completionHandler: @escaping (Result<Void, InstallError>) -> Void)
|
||||
{
|
||||
var isFinished = false
|
||||
|
||||
var serverConnection: NWConnection?
|
||||
|
||||
func finish(error: InstallError?)
|
||||
{
|
||||
// Prevent duplicate callbacks if connection is lost.
|
||||
guard !isFinished else { return }
|
||||
isFinished = true
|
||||
|
||||
if let connection = serverConnection
|
||||
{
|
||||
connection.cancel()
|
||||
}
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Failed to install \(identifier).", error)
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Installed \(identifier)!")
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
self.connect { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(error: error)
|
||||
case .success(let connection):
|
||||
serverConnection = connection
|
||||
|
||||
self.sendApp(at: fileURL, via: connection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(error: error)
|
||||
case .success:
|
||||
|
||||
self.receiveResponse(from: connection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: finish(error: nil)
|
||||
case .failure(let error): finish(error: .server(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Server
|
||||
{
|
||||
func connect(completionHandler: @escaping (Result<NWConnection, InstallError>) -> Void)
|
||||
{
|
||||
let connection = NWConnection(to: .service(name: self.service.name, type: self.service.type, domain: self.service.domain, interface: nil), using: .tcp)
|
||||
|
||||
connection.stateUpdateHandler = { [weak service, unowned connection] (state) in
|
||||
switch state
|
||||
{
|
||||
case .ready: completionHandler(.success(connection))
|
||||
case .cancelled: completionHandler(.failure(.cancelled))
|
||||
|
||||
case .failed(let error):
|
||||
print("Failed to connect to service \(service?.name ?? "").", error)
|
||||
completionHandler(.failure(.server(.init(.connectionFailed))))
|
||||
|
||||
case .waiting: break
|
||||
case .setup: break
|
||||
case .preparing: break
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: self.dispatchQueue)
|
||||
}
|
||||
|
||||
func sendApp(at fileURL: URL, via connection: NWConnection, completionHandler: @escaping (Result<Void, InstallError>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let appData = try? Data(contentsOf: fileURL) else { throw InstallError.invalidApp }
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw InstallError.noUDID }
|
||||
|
||||
let request = ServerRequest(udid: udid, contentSize: appData.count)
|
||||
let requestData = try JSONEncoder().encode(request)
|
||||
|
||||
let requestSize = Int32(requestData.count)
|
||||
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
|
||||
|
||||
// Send request data size.
|
||||
print("Sending request data size \(requestSize)")
|
||||
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.lostConnection))))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Send request.
|
||||
print("Sending request \(request)")
|
||||
connection.send(content: requestData, completion: .contentProcessed { (error) in
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.lostConnection))))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Send app data.
|
||||
print("Sending app data (Size: \(appData.count))")
|
||||
connection.send(content: appData, completion: .contentProcessed { (error) in
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.lostConnection))))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
catch is EncodingError
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.invalidRequest))))
|
||||
}
|
||||
catch let error as InstallError
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
catch
|
||||
{
|
||||
assertionFailure("Unknown error type. \(error)")
|
||||
completionHandler(.failure(.unknown))
|
||||
}
|
||||
}
|
||||
|
||||
func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
{
|
||||
let size = MemoryLayout<Int32>.size
|
||||
|
||||
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
|
||||
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
|
||||
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
|
||||
|
||||
if let error = response.error
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
|
||||
{
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let data = data else { throw error ?? ALTServerError(.unknown) }
|
||||
return data
|
||||
}
|
||||
catch let error as NWError
|
||||
{
|
||||
print("Error receiving data from connection \(connection)", error)
|
||||
|
||||
throw ALTServerError(.lostConnection)
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw error
|
||||
}
|
||||
}
|
||||
catch let error as ALTServerError
|
||||
{
|
||||
throw error
|
||||
}
|
||||
catch
|
||||
{
|
||||
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,11 @@ import Network
|
||||
|
||||
import AltKit
|
||||
|
||||
struct Server: Equatable
|
||||
{
|
||||
var service: NetService
|
||||
}
|
||||
|
||||
class ServerManager: NSObject
|
||||
{
|
||||
static let shared = ServerManager()
|
||||
|
||||
Reference in New Issue
Block a user