mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Displays progress when downloading/refreshing apps
Refactors download/refresh steps into separate Operation subclasses
This commit is contained in:
375
AltStore/Operations/AuthenticationOperation.swift
Normal file
375
AltStore/Operations/AuthenticationOperation.swift
Normal file
@@ -0,0 +1,375 @@
|
||||
//
|
||||
// AuthenticationOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/5/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
enum AuthenticationError: LocalizedError
|
||||
{
|
||||
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: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(AuthenticationOperation)
|
||||
class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
{
|
||||
private weak var presentingViewController: UIViewController?
|
||||
|
||||
private lazy var navigationController = UINavigationController()
|
||||
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
|
||||
|
||||
private var appleIDPassword: String?
|
||||
|
||||
init(presentingViewController: UIViewController?)
|
||||
{
|
||||
self.presentingViewController = presentingViewController
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 3
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
// Sign In
|
||||
self.signIn { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let 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): 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): self.finish(.failure(error))
|
||||
case .success(let certificate):
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
self.finish(.success(signer))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
func present(_ viewController: UIViewController) -> Bool
|
||||
{
|
||||
guard let presentingViewController = self.presentingViewController else { return false }
|
||||
|
||||
self.navigationController.view.tintColor = .altPurple
|
||||
|
||||
if self.navigationController.viewControllers.isEmpty
|
||||
{
|
||||
guard presentingViewController.presentedViewController == nil else { return false }
|
||||
|
||||
self.navigationController.setViewControllers([viewController], animated: false)
|
||||
presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
viewController.navigationItem.leftBarButtonItem = nil
|
||||
self.navigationController.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private extension AuthenticationOperation
|
||||
{
|
||||
func signIn(completionHandler: @escaping (Result<ALTAccount, Swift.Error>) -> Void)
|
||||
{
|
||||
func authenticate()
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
||||
authenticationViewController.authenticationHandler = { (result) in
|
||||
if let (account, password) = result
|
||||
{
|
||||
self.appleIDPassword = password
|
||||
|
||||
completionHandler(.success(account))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(authenticationViewController)
|
||||
{
|
||||
completionHandler(.failure(OperationError.notAuthenticated))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
|
||||
{
|
||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in
|
||||
do
|
||||
{
|
||||
self.appleIDPassword = password
|
||||
|
||||
let account = try Result(account, error).get()
|
||||
completionHandler(.success(account))
|
||||
}
|
||||
catch ALTAppleAPIError.incorrectCredentials
|
||||
{
|
||||
authenticate()
|
||||
}
|
||||
catch ALTAppleAPIError.appSpecificPasswordRequired
|
||||
{
|
||||
authenticate()
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
authenticate()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
|
||||
{
|
||||
func selectTeam(from teams: [ALTTeam])
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
||||
selectTeamViewController.teams = teams
|
||||
selectTeamViewController.selectionHandler = { (team) in
|
||||
if let team = team
|
||||
{
|
||||
completionHandler(.success(team))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(selectTeamViewController)
|
||||
{
|
||||
completionHandler(.failure(AuthenticationError.noTeam))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
|
||||
switch Result(teams, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let teams):
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier })
|
||||
{
|
||||
completionHandler(.success(altTeam))
|
||||
}
|
||||
else
|
||||
{
|
||||
selectTeam(from: teams)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
|
||||
{
|
||||
func requestCertificate()
|
||||
{
|
||||
let machineName = "AltStore - " + UIDevice.current.name
|
||||
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team) { (certificate, error) in
|
||||
do
|
||||
{
|
||||
let certificate = try Result(certificate, error).get()
|
||||
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else {
|
||||
throw AuthenticationError.missingCertificate
|
||||
}
|
||||
|
||||
certificate.privateKey = privateKey
|
||||
completionHandler(.success(certificate))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceCertificate(from certificates: [ALTCertificate])
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let replaceCertificateViewController = self.storyboard.instantiateViewController(withIdentifier: "replaceCertificateViewController") as! ReplaceCertificateViewController
|
||||
replaceCertificateViewController.team = team
|
||||
replaceCertificateViewController.certificates = certificates
|
||||
replaceCertificateViewController.replacementHandler = { (certificate) in
|
||||
if certificate != nil
|
||||
{
|
||||
requestCertificate()
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(replaceCertificateViewController)
|
||||
{
|
||||
completionHandler(.failure(AuthenticationError.noCertificate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
if
|
||||
let identifier = Keychain.shared.signingCertificateIdentifier,
|
||||
let privateKey = Keychain.shared.signingCertificatePrivateKey,
|
||||
let certificate = certificates.first(where: { $0.identifier == identifier })
|
||||
{
|
||||
certificate.privateKey = privateKey
|
||||
completionHandler(.success(certificate))
|
||||
}
|
||||
else if certificates.isEmpty
|
||||
{
|
||||
requestCertificate()
|
||||
}
|
||||
else
|
||||
{
|
||||
replaceCertificate(from: certificates)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user