[AltStore] Uses GrandSlam Authentication

Retrieves anisette data from AltServer so we can authenticate with GSA.
This commit is contained in:
Riley Testut
2019-11-18 14:49:17 -08:00
parent 9535595df1
commit 691e08202d
14 changed files with 636 additions and 182 deletions

View File

@@ -27,6 +27,12 @@ typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError)
ALTServerErrorInstallationFailed = 8, ALTServerErrorInstallationFailed = 8,
ALTServerErrorMaximumFreeAppLimitReached = 9, ALTServerErrorMaximumFreeAppLimitReached = 9,
ALTServerErrorUnsupportediOSVersion = 10, ALTServerErrorUnsupportediOSVersion = 10,
ALTServerErrorUnknownRequest = 11,
ALTServerErrorUnknownResponse = 12,
ALTServerErrorInvalidAnisetteData = 13,
ALTServerErrorPluginNotFound = 14
}; };
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN

View File

@@ -61,6 +61,18 @@ NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServ
case ALTServerErrorUnsupportediOSVersion: case ALTServerErrorUnsupportediOSVersion:
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @""); return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
case ALTServerErrorUnknownRequest:
return NSLocalizedString(@"AltServer does not support this request.", @"");
case ALTServerErrorUnknownResponse:
return NSLocalizedString(@"Received an unknown response from AltServer.", @"");
case ALTServerErrorInvalidAnisetteData:
return NSLocalizedString(@"Invalid anisette data.", @"");
case ALTServerErrorPluginNotFound:
return NSLocalizedString(@"Could not connect to Mail plug-in. Please make sure the plug-in is installed and Mail is running, then try again.", @"");
} }
} }

View File

@@ -7,22 +7,201 @@
// //
import Foundation import Foundation
import AltSign
public let ALTServerServiceType = "_altserver._tcp" public let ALTServerServiceType = "_altserver._tcp"
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself // Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {} extension ALTServerError.Code: Codable {}
protocol ServerMessage: Codable protocol ServerMessageProtocol: Codable
{ {
var version: Int { get } var version: Int { get }
var identifier: String { get } var identifier: String { get }
} }
public struct PrepareAppRequest: ServerMessage public enum ServerRequest: Decodable
{
case anisetteData(AnisetteDataRequest)
case prepareApp(PrepareAppRequest)
case beginInstallation(BeginInstallationRequest)
case unknown(identifier: String, version: Int)
var identifier: String {
switch self
{
case .anisetteData(let request): return request.identifier
case .prepareApp(let request): return request.identifier
case .beginInstallation(let request): return request.identifier
case .unknown(let identifier, _): return identifier
}
}
var version: Int {
switch self
{
case .anisetteData(let request): return request.version
case .prepareApp(let request): return request.version
case .beginInstallation(let request): return request.version
case .unknown(_, let version): return version
}
}
private enum CodingKeys: String, CodingKey
{
case identifier
case version
}
public init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(Int.self, forKey: .version)
let identifier = try container.decode(String.self, forKey: .identifier)
switch identifier
{
case "AnisetteDataRequest":
let request = try AnisetteDataRequest(from: decoder)
self = .anisetteData(request)
case "PrepareAppRequest":
let request = try PrepareAppRequest(from: decoder)
self = .prepareApp(request)
case "BeginInstallationRequest":
let request = try BeginInstallationRequest(from: decoder)
self = .beginInstallation(request)
default:
self = .unknown(identifier: identifier, version: version)
}
}
}
public enum ServerResponse: Decodable
{
case anisetteData(AnisetteDataResponse)
case installationProgress(InstallationProgressResponse)
case error(ErrorResponse)
case unknown(identifier: String, version: Int)
var identifier: String {
switch self
{
case .anisetteData(let response): return response.identifier
case .installationProgress(let response): return response.identifier
case .error(let response): return response.identifier
case .unknown(let identifier, _): return identifier
}
}
var version: Int {
switch self
{
case .anisetteData(let response): return response.version
case .installationProgress(let response): return response.version
case .error(let response): return response.version
case .unknown(_, let version): return version
}
}
private enum CodingKeys: String, CodingKey
{
case identifier
case version
}
public init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(Int.self, forKey: .version)
let identifier = try container.decode(String.self, forKey: .identifier)
switch identifier
{
case "AnisetteDataResponse":
let response = try AnisetteDataResponse(from: decoder)
self = .anisetteData(response)
case "InstallationProgressResponse":
let response = try InstallationProgressResponse(from: decoder)
self = .installationProgress(response)
case "ErrorResponse":
let response = try ErrorResponse(from: decoder)
self = .error(response)
default:
self = .unknown(identifier: identifier, version: version)
}
}
}
public struct AnisetteDataRequest: ServerMessageProtocol
{ {
public var version = 1 public var version = 1
public var identifier = "PrepareApp" public var identifier = "AnisetteDataRequest"
public init()
{
}
}
public struct AnisetteDataResponse: ServerMessageProtocol
{
public var version = 1
public var identifier = "AnisetteDataResponse"
public var anisetteData: ALTAnisetteData
private enum CodingKeys: String, CodingKey
{
case identifier
case version
case anisetteData
}
public init(anisetteData: ALTAnisetteData)
{
self.anisetteData = anisetteData
}
public init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(Int.self, forKey: .version)
self.identifier = try container.decode(String.self, forKey: .identifier)
let json = try container.decode([String: String].self, forKey: .anisetteData)
if let anisetteData = ALTAnisetteData(json: json)
{
self.anisetteData = anisetteData
}
else
{
throw DecodingError.dataCorruptedError(forKey: CodingKeys.anisetteData, in: container, debugDescription: "Couuld not parse anisette data from JSON")
}
}
public func encode(to encoder: Encoder) throws
{
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.version, forKey: .version)
try container.encode(self.identifier, forKey: .identifier)
let json = self.anisetteData.json()
try container.encode(json, forKey: .anisetteData)
}
}
public struct PrepareAppRequest: ServerMessageProtocol
{
public var version = 1
public var identifier = "PrepareAppRequest"
public var udid: String public var udid: String
public var contentSize: Int public var contentSize: Int
@@ -34,37 +213,41 @@ public struct PrepareAppRequest: ServerMessage
} }
} }
public struct BeginInstallationRequest: ServerMessage public struct BeginInstallationRequest: ServerMessageProtocol
{ {
public var version = 1 public var version = 1
public var identifier = "BeginInstallation" public var identifier = "BeginInstallationRequest"
public init() public init()
{ {
} }
} }
public struct ServerResponse: ServerMessage public struct ErrorResponse: ServerMessageProtocol
{ {
public var version = 1 public var version = 1
public var identifier = "ServerResponse" public var identifier = "ErrorResponse"
public var error: ALTServerError {
return ALTServerError(self.errorCode)
}
private var errorCode: ALTServerError.Code
public init(error: ALTServerError)
{
self.errorCode = error.code
}
}
public struct InstallationProgressResponse: ServerMessageProtocol
{
public var version = 1
public var identifier = "InstallationProgressResponse"
public var progress: Double public var progress: Double
public var error: ALTServerError? { public init(progress: Double)
get {
guard let code = self.errorCode else { return nil }
return ALTServerError(code)
}
set {
self.errorCode = newValue?.code
}
}
private var errorCode: ALTServerError.Code?
public init(progress: Double, error: ALTServerError?)
{ {
self.progress = progress self.progress = progress
self.error = error
} }
} }

View File

@@ -28,11 +28,13 @@ class AnisetteDataManager: NSObject
let requestUUID = UUID().uuidString let requestUUID = UUID().uuidString
self.anisetteDataCompletionHandlers[requestUUID] = completion self.anisetteDataCompletionHandlers[requestUUID] = completion
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { (timer) in let timer = Timer(timeInterval: 1.0, repeats: false) { (timer) in
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.pluginNotFound))) self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.pluginNotFound)))
} }
self.anisetteDataTimers[requestUUID] = timer self.anisetteDataTimers[requestUUID] = timer
RunLoop.main.add(timer, forMode: .default)
DistributedNotificationCenter.default().postNotificationName(Notification.Name("com.rileytestut.AltServer.FetchAnisetteData"), object: nil, userInfo: ["requestUUID": requestUUID], options: .deliverImmediately) DistributedNotificationCenter.default().postNotificationName(Notification.Name("com.rileytestut.AltServer.FetchAnisetteData"), object: nil, userInfo: ["requestUUID": requestUUID], options: .deliverImmediately)
} }
} }

View File

@@ -8,6 +8,7 @@
import Foundation import Foundation
import Network import Network
import AppKit
import AltKit import AltKit
@@ -188,7 +189,6 @@ private extension ConnectionManager
guard !self.connections.contains(where: { $0 === connection }) else { return } guard !self.connections.contains(where: { $0 === connection }) else { return }
self.connections.append(connection) self.connections.append(connection)
connection.stateUpdateHandler = { [weak self] (state) in connection.stateUpdateHandler = { [weak self] (state) in
switch state switch state
{ {
@@ -196,10 +196,7 @@ private extension ConnectionManager
case .ready: case .ready:
print("Connected to client:", connection.endpoint) print("Connected to client:", connection.endpoint)
self?.handleRequest(for: connection)
self?.receiveApp(from: connection) { (result) in
self?.finish(connection: connection, error: result.error)
}
case .waiting: case .waiting:
print("Waiting for connection...") print("Waiting for connection...")
@@ -218,7 +215,55 @@ private extension ConnectionManager
connection.start(queue: self.dispatchQueue) connection.start(queue: self.dispatchQueue)
} }
func receiveApp(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void) func handleRequest(for connection: NWConnection)
{
self.receiveRequest(from: connection) { (result) in
print("Received initial request with result:", result)
switch result
{
case .failure(let error):
let response = ErrorResponse(error: ALTServerError(error))
self.send(response, to: connection, shouldDisconnect: true) { (result) in
print("Sent error response with result:", result)
}
case .success(.anisetteData(let request)):
self.handleAnisetteDataRequest(request, for: connection)
case .success(.prepareApp(let request)):
self.handlePrepareAppRequest(request, for: connection)
case .success:
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
self.send(response, to: connection, shouldDisconnect: true) { (result) in
print("Sent unknown request response with result:", result)
}
}
}
}
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: NWConnection)
{
AnisetteDataManager.shared.requestAnisetteData { (result) in
switch result
{
case .failure(let error):
let errorResponse = ErrorResponse(error: ALTServerError(error))
self.send(errorResponse, to: connection, shouldDisconnect: true) { (result) in
print("Sent anisette data error response with result:", result)
}
case .success(let anisetteData):
let response = AnisetteDataResponse(anisetteData: anisetteData)
self.send(response, to: connection, shouldDisconnect: true) { (result) in
print("Sent anisette data response with result:", result)
}
}
}
}
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: NWConnection)
{ {
var temporaryURL: URL? var temporaryURL: URL?
@@ -230,34 +275,44 @@ private extension ConnectionManager
catch { print("Failed to remove .ipa.", error) } catch { print("Failed to remove .ipa.", error) }
} }
completionHandler(result)
}
self.receive(PrepareAppRequest.self, from: connection) { (result) in
print("Received request with result:", result)
switch result switch result
{ {
case .failure(let error): finish(.failure(error)) case .failure(let error):
case .success(let request): print("Failed to process request from \(connection.endpoint).", error)
let response = ErrorResponse(error: ALTServerError(error))
self.send(response, to: connection, shouldDisconnect: true) { (result) in
print("Sent install app error response to \(connection.endpoint) with result:", result)
}
case .success:
print("Processed request from \(connection.endpoint).")
let response = InstallationProgressResponse(progress: 1.0)
self.send(response, to: connection, shouldDisconnect: true) { (result) in
print("Sent install app response to \(connection.endpoint) with result:", result)
}
}
}
self.receiveApp(for: request, from: connection) { (result) in self.receiveApp(for: request, from: connection) { (result) in
print("Received app with result:", result) print("Received app with result:", result)
switch result switch result
{ {
case .failure(let error): finish(.failure(error)) case .failure(let error): finish(.failure(error))
case .success(let request, let fileURL): case .success(let fileURL):
temporaryURL = fileURL temporaryURL = fileURL
print("Awaiting begin installation request...") print("Awaiting begin installation request...")
self.receive(BeginInstallationRequest.self, from: connection) { (result) in self.receiveRequest(from: connection) { (result) in
print("Received begin installation request with result:", result) print("Received begin installation request with result:", result)
switch result switch result
{ {
case .failure(let error): finish(.failure(error)) case .failure(let error): finish(.failure(error))
case .success: case .success(.beginInstallation):
print("Installing to device \(request.udid)...") print("Installing to device \(request.udid)...")
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in
@@ -268,7 +323,11 @@ private extension ConnectionManager
case .success: finish(.success(())) case .success: finish(.success(()))
} }
} }
}
case .success:
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
self.send(response, to: connection, shouldDisconnect: true) { (result) in
print("Sent unknown request error response to \(connection.endpoint) with result:", result)
} }
} }
} }
@@ -276,30 +335,7 @@ private extension ConnectionManager
} }
} }
func finish(connection: NWConnection, error: ALTServerError?) func receiveApp(for request: PrepareAppRequest, from connection: NWConnection, completionHandler: @escaping (Result<URL, ALTServerError>) -> Void)
{
if let error = error
{
print("Failed to process request from \(connection.endpoint).", error)
}
else
{
print("Processed request from \(connection.endpoint).")
}
let response = ServerResponse(progress: 1.0, error: error)
self.send(response, to: connection) { (result) in
print("Sent response to \(connection.endpoint) with result:", result)
// Add short delay to prevent us from dropping connection too quickly.
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
self.disconnect(connection)
}
}
}
func receiveApp(for request: PrepareAppRequest, from connection: NWConnection, completionHandler: @escaping (Result<(PrepareAppRequest, URL), ALTServerError>) -> Void)
{ {
connection.receive(minimumIncompleteLength: request.contentSize, maximumLength: request.contentSize) { (data, _, _, error) in connection.receive(minimumIncompleteLength: request.contentSize, maximumLength: request.contentSize) { (data, _, _, error) in
do do
@@ -319,7 +355,7 @@ private extension ConnectionManager
print("Wrote app to URL:", temporaryURL) print("Wrote app to URL:", temporaryURL)
completionHandler(.success((request, temporaryURL))) completionHandler(.success(temporaryURL))
} }
catch catch
{ {
@@ -359,7 +395,7 @@ private extension ConnectionManager
isSending = true isSending = true
print("Progress:", progress.fractionCompleted) print("Progress:", progress.fractionCompleted)
let response = ServerResponse(progress: progress.fractionCompleted, error: nil) let response = InstallationProgressResponse(progress: progress.fractionCompleted)
self.send(response, to: connection) { (result) in self.send(response, to: connection) { (result) in
serialQueue.async { serialQueue.async {
@@ -370,8 +406,21 @@ private extension ConnectionManager
}) })
} }
func send<T: Encodable>(_ response: T, to connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void) func send<T: Encodable>(_ response: T, to connection: NWConnection, shouldDisconnect: Bool = false, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{ {
func finish(_ result: Result<Void, ALTServerError>)
{
completionHandler(result)
if shouldDisconnect
{
// Add short delay to prevent us from dropping connection too quickly.
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
self.disconnect(connection)
}
}
}
do do
{ {
let data = try JSONEncoder().encode(response) let data = try JSONEncoder().encode(response)
@@ -388,27 +437,27 @@ private extension ConnectionManager
connection.send(content: data, completion: .contentProcessed { (error) in connection.send(content: data, completion: .contentProcessed { (error) in
if error != nil if error != nil
{ {
completionHandler(.failure(.init(.lostConnection))) finish(.failure(.init(.lostConnection)))
} }
else else
{ {
completionHandler(.success(())) finish(.success(()))
} }
}) })
} }
catch catch
{ {
completionHandler(.failure(.init(.lostConnection))) finish(.failure(.init(.lostConnection)))
} }
}) })
} }
catch catch
{ {
completionHandler(.failure(.init(.invalidResponse))) finish(.failure(.init(.invalidResponse)))
} }
} }
func receive<T: Decodable>(_ responseType: T.Type, from connection: NWConnection, completionHandler: @escaping (Result<T, ALTServerError>) -> Void) func receiveRequest(from connection: NWConnection, completionHandler: @escaping (Result<ServerRequest, ALTServerError>) -> Void)
{ {
let size = MemoryLayout<Int32>.size let size = MemoryLayout<Int32>.size
@@ -426,7 +475,7 @@ private extension ConnectionManager
{ {
let data = try self.process(data: data, error: error, from: connection) let data = try self.process(data: data, error: error, from: connection)
let request = try JSONDecoder().decode(T.self, from: data) let request = try JSONDecoder().decode(ServerRequest.self, from: data)
print("Received installation request:", request) print("Received installation request:", request)

View File

@@ -12,7 +12,8 @@ import AltSign
class AuthenticationViewController: UIViewController class AuthenticationViewController: UIViewController
{ {
var authenticationHandler: (((ALTAccount, String)?) -> Void)? var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
private weak var toastView: ToastView? private weak var toastView: ToastView?
@@ -96,14 +97,16 @@ private extension AuthenticationViewController
self.signInButton.isIndicatingActivity = true self.signInButton.isIndicatingActivity = true
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in self.authenticationHandler?(emailAddress, password) { (result) in
do switch result
{ {
let account = try Result(account, error).get() case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
self.authenticationHandler?((account, password)) // Ignore
DispatchQueue.main.async {
self.signInButton.isIndicatingActivity = false
} }
catch
{ case .failure(let error):
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription) let toastView = ToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription)
toastView.textLabel.textColor = .altPink toastView.textLabel.textColor = .altPink
@@ -113,6 +116,9 @@ private extension AuthenticationViewController
self.signInButton.isIndicatingActivity = false self.signInButton.isIndicatingActivity = false
} }
case .success(let account, let session):
self.completionHandler?((account, session, password))
} }
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -123,7 +129,7 @@ private extension AuthenticationViewController
@IBAction func cancel(_ sender: UIBarButtonItem) @IBAction func cancel(_ sender: UIBarButtonItem)
{ {
self.authenticationHandler?(nil) self.completionHandler?(nil)
} }
} }

View File

@@ -14,6 +14,7 @@ import Roxas
class RefreshAltStoreViewController: UIViewController class RefreshAltStoreViewController: UIViewController
{ {
var signer: ALTSigner! var signer: ALTSigner!
var session: ALTAppleAPISession!
var completionHandler: ((Result<Void, Error>) -> Void)? var completionHandler: ((Result<Void, Error>) -> Void)?
@@ -49,6 +50,7 @@ private extension RefreshAltStoreViewController
let group = OperationGroup() let group = OperationGroup()
group.signer = self.signer // Prevent us from trying to authenticate a second time. group.signer = self.signer // Prevent us from trying to authenticate a second time.
group.session = self.session // ^
group.completionHandler = { (result) in group.completionHandler = { (result) in
if let error = result.error ?? result.value?.values.compactMap({ $0.error }).first if let error = result.error ?? result.value?.values.compactMap({ $0.error }).first
{ {

View File

@@ -80,12 +80,26 @@ extension AppManager
#endif #endif
} }
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTSigner, Error>) -> Void) func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTSigner, ALTAppleAPISession), Error>) -> Void)
{ {
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController) let group = OperationGroup()
let findServerOperation = FindServerOperation(group: group)
findServerOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): group.error = error
case .success(let server): group.server = server
}
}
let authenticationOperation = AuthenticationOperation(group: group, presentingViewController: presentingViewController)
authenticationOperation.addDependency(findServerOperation)
authenticationOperation.resultHandler = { (result) in authenticationOperation.resultHandler = { (result) in
completionHandler(result) completionHandler(result)
} }
self.operationQueue.addOperation(findServerOperation)
self.operationQueue.addOperation(authenticationOperation) self.operationQueue.addOperation(authenticationOperation)
} }
} }
@@ -194,20 +208,29 @@ private extension AppManager
} }
operations.append(findServerOperation) operations.append(findServerOperation)
if group.signer == nil let authenticationOperation: AuthenticationOperation?
if group.signer == nil || group.session == nil
{ {
/* Authenticate */ /* Authenticate */
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController) let operation = AuthenticationOperation(group: group, presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in operation.resultHandler = { (result) in
switch result switch result
{ {
case .failure(let error): group.error = error case .failure(let error): group.error = error
case .success(let signer): group.signer = signer case .success(let signer, let session):
group.signer = signer
group.session = session
} }
} }
operations.append(authenticationOperation) operations.append(operation)
operation.addDependency(findServerOperation)
findServerOperation.addDependency(authenticationOperation) authenticationOperation = operation
}
else
{
authenticationOperation = nil
} }
for app in apps for app in apps
@@ -222,7 +245,7 @@ private extension AppManager
guard let resignedApp = self.process(result, context: context) else { return } guard let resignedApp = self.process(result, context: context) else { return }
context.resignedApp = resignedApp context.resignedApp = resignedApp
} }
resignAppOperation.addDependency(findServerOperation) resignAppOperation.addDependency(authenticationOperation ?? findServerOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20) progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
operations.append(resignAppOperation) operations.append(resignAppOperation)

View File

@@ -8,7 +8,9 @@
import Foundation import Foundation
import Roxas import Roxas
import Network
import AltKit
import AltSign import AltSign
enum AuthenticationError: LocalizedError enum AuthenticationError: LocalizedError
@@ -30,8 +32,10 @@ enum AuthenticationError: LocalizedError
} }
@objc(AuthenticationOperation) @objc(AuthenticationOperation)
class AuthenticationOperation: ResultOperation<ALTSigner> class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
{ {
let group: OperationGroup
private weak var presentingViewController: UIViewController? private weak var presentingViewController: UIViewController?
private lazy var navigationController: UINavigationController = { private lazy var navigationController: UINavigationController = {
@@ -49,9 +53,15 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
private var shouldShowInstructions = false private var shouldShowInstructions = false
private var signer: ALTSigner? private var signer: ALTSigner?
private var session: ALTAppleAPISession?
init(presentingViewController: UIViewController?) private let dispatchQueue = DispatchQueue(label: "com.altstore.AuthenticationOperation")
private var submitCodeAction: UIAlertAction?
init(group: OperationGroup, presentingViewController: UIViewController?)
{ {
self.group = group
self.presentingViewController = presentingViewController self.presentingViewController = presentingViewController
super.init() super.init()
@@ -63,18 +73,25 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
{ {
super.main() super.main()
if let error = self.group.error
{
self.finish(.failure(error))
return
}
// Sign In // Sign In
self.signIn { (result) in self.signIn() { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result switch result
{ {
case .failure(let error): self.finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success(let account): case .success(let account, let session):
self.session = session
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
// Fetch Team // Fetch Team
self.fetchTeam(for: account) { (result) in self.fetchTeam(for: account, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result switch result
@@ -84,7 +101,7 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
// Fetch Certificate // Fetch Certificate
self.fetchCertificate(for: team) { (result) in self.fetchCertificate(for: team, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result switch result
@@ -97,7 +114,7 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
self.signer = signer self.signer = signer
self.showInstructionsIfNecessary() { (didShowInstructions) in self.showInstructionsIfNecessary() { (didShowInstructions) in
self.finish(.success(signer)) self.finish(.success((signer, session)))
} }
} }
} }
@@ -107,7 +124,7 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
} }
} }
override func finish(_ result: Result<ALTSigner, Error>) override func finish(_ result: Result<(ALTSigner, ALTAppleAPISession), Error>)
{ {
guard !self.isFinished else { return } guard !self.isFinished else { return }
@@ -117,7 +134,7 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
context.performAndWait { context.performAndWait {
do do
{ {
let signer = try result.get() let (signer, session) = try result.get()
let altAccount = signer.team.account let altAccount = signer.team.account
// Account // Account
@@ -158,7 +175,7 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
// Refresh screen must go last since a successful refresh will cause the app to quit. // Refresh screen must go last since a successful refresh will cause the app to quit.
self.showRefreshScreenIfNecessary() { (didShowRefreshAlert) in self.showRefreshScreenIfNecessary() { (didShowRefreshAlert) in
super.finish(.success(signer)) super.finish(.success((signer, session)))
DispatchQueue.main.async { DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil) self.navigationController.dismiss(animated: true, completion: nil)
@@ -204,21 +221,53 @@ private extension AuthenticationOperation
private extension AuthenticationOperation private extension AuthenticationOperation
{ {
func signIn(completionHandler: @escaping (Result<ALTAccount, Swift.Error>) -> Void) func connect(to server: Server, completionHandler: @escaping (Result<NWConnection, Error>) -> Void)
{
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(ConnectionError.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 signIn(completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
{ {
func authenticate() func authenticate()
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
authenticationViewController.authenticationHandler = { (result) in authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
if let (account, password) = result self.authenticate(appleID: appleID, password: password) { (result) in
completionHandler(result)
}
}
authenticationViewController.completionHandler = { (result) in
if let (account, session, password) = result
{ {
// We presented the Auth UI and the user signed in. // We presented the Auth UI and the user signed in.
// In this case, we'll assume we should show the instructions again. // In this case, we'll assume we should show the instructions again.
self.shouldShowInstructions = true self.shouldShowInstructions = true
self.appleIDPassword = password self.appleIDPassword = password
completionHandler(.success(account)) completionHandler(.success((account, session)))
} }
else else
{ {
@@ -235,24 +284,17 @@ private extension AuthenticationOperation
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
{ {
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in self.authenticate(appleID: appleID, password: password) { (result) in
do switch result
{ {
case .success(let account, let session):
self.appleIDPassword = password self.appleIDPassword = password
completionHandler(.success((account, session)))
let account = try Result(account, error).get() case .failure(ALTAppleAPIError.incorrectCredentials), .failure(ALTAppleAPIError.appSpecificPasswordRequired):
completionHandler(.success(account))
}
catch ALTAppleAPIError.incorrectCredentials
{
authenticate() authenticate()
}
catch ALTAppleAPIError.appSpecificPasswordRequired case .failure(let error):
{
authenticate()
}
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
@@ -263,7 +305,103 @@ private extension AuthenticationOperation
} }
} }
func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void) func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
{
guard let server = self.group.server else { return completionHandler(.failure(OperationError.invalidParameters)) }
self.connect(to: server) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let connection):
let request = AnisetteDataRequest()
server.send(request, via: connection) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success:
server.receiveResponse(from: connection) { (result) in
switch result
{
case .failure(let error):
completionHandler(.failure(error))
case .success(.error(let response)):
completionHandler(.failure(response.error))
case .success(.anisetteData(let response)):
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
if let presentingViewController = self.presentingViewController
{
verificationHandler = { (completionHandler) in
DispatchQueue.main.async {
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""),
message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.keyboardType = .numberPad
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
}
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
let textField = alertController.textFields?.first
let code = textField?.text ?? ""
completionHandler(code)
}
submitAction.isEnabled = false
alertController.addAction(submitAction)
self.submitCodeAction = submitAction
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
completionHandler(nil)
})
if self.navigationController.presentingViewController != nil
{
self.navigationController.present(alertController, animated: true, completion: nil)
}
else
{
presentingViewController.present(alertController, animated: true, completion: nil)
}
}
}
}
else
{
// No view controller to present security code alert, so don't provide verificationHandler.
verificationHandler = nil
}
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: response.anisetteData,
verificationHandler: verificationHandler) { (account, session, error) in
if let account = account, let session = session
{
completionHandler(.success((account, session)))
}
else
{
completionHandler(.failure(error ?? OperationError.unknown))
}
}
case .success:
completionHandler(.failure(ALTServerError(.unknownRequest)))
}
}
}
}
}
}
}
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
{ {
func selectTeam(from teams: [ALTTeam]) func selectTeam(from teams: [ALTTeam])
{ {
@@ -285,7 +423,7 @@ private extension AuthenticationOperation
} }
} }
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
switch Result(teams, error) switch Result(teams, error)
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
@@ -304,18 +442,18 @@ private extension AuthenticationOperation
} }
} }
func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void) func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
{ {
func requestCertificate() func requestCertificate()
{ {
let machineName = "AltStore - " + UIDevice.current.name let machineName = "AltStore - " + UIDevice.current.name
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team) { (certificate, error) in ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
do do
{ {
let certificate = try Result(certificate, error).get() let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey } guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do do
{ {
let certificates = try Result(certificates, error).get() let certificates = try Result(certificates, error).get()
@@ -344,7 +482,7 @@ private extension AuthenticationOperation
{ {
guard let certificate = certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) } guard let certificate = certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success if let error = error, !success
{ {
completionHandler(.failure(error)) completionHandler(.failure(error))
@@ -356,7 +494,7 @@ private extension AuthenticationOperation
} }
} }
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do do
{ {
let certificates = try Result(certificates, error).get() let certificates = try Result(certificates, error).get()
@@ -431,7 +569,7 @@ private extension AuthenticationOperation
func showRefreshScreenIfNecessary(completionHandler: @escaping (Bool) -> Void) func showRefreshScreenIfNecessary(completionHandler: @escaping (Bool) -> Void)
{ {
guard let signer = self.signer else { return completionHandler(false) } guard let signer = self.signer, let session = self.session else { return completionHandler(false) }
guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) } guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) }
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh. // If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
@@ -440,6 +578,7 @@ private extension AuthenticationOperation
DispatchQueue.main.async { DispatchQueue.main.async {
let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController
refreshViewController.signer = signer refreshViewController.signer = signer
refreshViewController.session = session
refreshViewController.completionHandler = { _ in refreshViewController.completionHandler = { _ in
completionHandler(true) completionHandler(true)
} }
@@ -451,3 +590,13 @@ private extension AuthenticationOperation
} }
} }
} }
extension AuthenticationOperation
{
@objc func textFieldTextDidChange(_ notification: Notification)
{
guard let textField = notification.object as? UITextField else { return }
self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6
}
}

View File

@@ -94,17 +94,16 @@ class InstallAppOperation: ResultOperation<InstalledApp>
func receive(from connection: NWConnection, server: Server, completionHandler: @escaping (Result<Void, Error>) -> Void) func receive(from connection: NWConnection, server: Server, completionHandler: @escaping (Result<Void, Error>) -> Void)
{ {
server.receive(ServerResponse.self, from: connection) { (result) in server.receiveResponse(from: connection) { (result) in
do do
{ {
let response = try result.get() let response = try result.get()
print(response) print(response)
if let error = response.error switch response
{ {
completionHandler(.failure(error)) case .installationProgress(let response):
} if response.progress == 1.0
else if response.progress == 1.0
{ {
self.progress.completedUnitCount = self.progress.totalUnitCount self.progress.completedUnitCount = self.progress.totalUnitCount
completionHandler(.success(())) completionHandler(.success(()))
@@ -114,6 +113,13 @@ class InstallAppOperation: ResultOperation<InstalledApp>
self.progress.completedUnitCount = Int64(response.progress * 100) self.progress.completedUnitCount = Int64(response.progress * 100)
self.receive(from: connection, server: server, completionHandler: completionHandler) self.receive(from: connection, server: server, completionHandler: completionHandler)
} }
case .error(let response):
completionHandler(.failure(response.error))
default:
completionHandler(.failure(ALTServerError(.unknownRequest)))
}
} }
catch catch
{ {

View File

@@ -18,6 +18,8 @@ class OperationGroup
var completionHandler: ((Result<[String: Result<InstalledApp, Error>], Error>) -> Void)? var completionHandler: ((Result<[String: Result<InstalledApp, Error>], Error>) -> Void)?
var beginInstallationHandler: ((InstalledApp) -> Void)? var beginInstallationHandler: ((InstalledApp) -> Void)?
var session: ALTAppleAPISession?
var server: Server? var server: Server?
var signer: ALTSigner? var signer: ALTSigner?

View File

@@ -37,15 +37,16 @@ class ResignAppOperation: ResultOperation<ALTApplication>
guard guard
let app = self.context.app, let app = self.context.app,
let signer = self.context.group.signer let signer = self.context.group.signer,
let session = self.context.group.session
else { return self.finish(.failure(OperationError.invalidParameters)) } else { return self.finish(.failure(OperationError.invalidParameters)) }
// Register Device // Register Device
self.registerCurrentDevice(for: signer.team) { (result) in self.registerCurrentDevice(for: signer.team, session: session) { (result) in
guard let _ = self.process(result) else { return } guard let _ = self.process(result) else { return }
// Prepare Provisioning Profiles // Prepare Provisioning Profiles
self.prepareProvisioningProfiles(app.fileURL, team: signer.team) { (result) in self.prepareProvisioningProfiles(app.fileURL, team: signer.team, session: session) { (result) in
guard let profiles = self.process(result) else { return } guard let profiles = self.process(result) else { return }
// Prepare app bundle // Prepare app bundle
@@ -104,13 +105,13 @@ class ResignAppOperation: ResultOperation<ALTApplication>
private extension ResignAppOperation private extension ResignAppOperation
{ {
func registerCurrentDevice(for team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void) func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{ {
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
return completionHandler(.failure(OperationError.unknownUDID)) return completionHandler(.failure(OperationError.unknownUDID))
} }
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
do do
{ {
let devices = try Result(devices, error).get() let devices = try Result(devices, error).get()
@@ -121,7 +122,7 @@ private extension ResignAppOperation
} }
else else
{ {
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team) { (device, error) in ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team, session: session) { (device, error) in
completionHandler(Result(device, error)) completionHandler(Result(device, error))
} }
} }
@@ -133,7 +134,7 @@ private extension ResignAppOperation
} }
} }
func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void) func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
{ {
guard let bundle = Bundle(url: fileURL), let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) } guard let bundle = Bundle(url: fileURL), let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
@@ -144,7 +145,7 @@ private extension ResignAppOperation
dispatchGroup.enter() dispatchGroup.enter()
self.prepareProvisioningProfile(for: app, team: team) { (result) in self.prepareProvisioningProfile(for: app, team: team, session: session) { (result) in
switch result switch result
{ {
case .failure(let e): error = e case .failure(let e): error = e
@@ -162,7 +163,7 @@ private extension ResignAppOperation
dispatchGroup.enter() dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, team: team) { (result) in self.prepareProvisioningProfile(for: appExtension, team: team, session: session) { (result) in
switch result switch result
{ {
case .failure(let e): error = e case .failure(let e): error = e
@@ -186,31 +187,31 @@ private extension ResignAppOperation
} }
} }
func prepareProvisioningProfile(for app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void) func prepareProvisioningProfile(for app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{ {
// Register // Register
self.register(app, team: team) { (result) in self.register(app, team: team, session: session) { (result) in
switch result switch result
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
case .success(let appID): case .success(let appID):
// Update features // Update features
self.updateFeatures(for: appID, app: app, team: team) { (result) in self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
switch result switch result
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
case .success(let appID): case .success(let appID):
// Update app groups // Update app groups
self.updateAppGroups(for: appID, app: app, team: team) { (result) in self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
switch result switch result
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
case .success(let appID): case .success(let appID):
// Fetch Provisioning Profile // Fetch Provisioning Profile
self.fetchProvisioningProfile(for: appID, team: team) { (result) in self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
completionHandler(result) completionHandler(result)
} }
} }
@@ -221,12 +222,12 @@ private extension ResignAppOperation
} }
} }
func register(_ app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void) func register(_ app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{ {
let appName = app.name let appName = app.name
let bundleID = "com.\(team.identifier).\(app.bundleIdentifier)" let bundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do do
{ {
let appIDs = try Result(appIDs, error).get() let appIDs = try Result(appIDs, error).get()
@@ -237,7 +238,7 @@ private extension ResignAppOperation
} }
else else
{ {
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error)) completionHandler(Result(appID, error))
} }
} }
@@ -249,7 +250,7 @@ private extension ResignAppOperation
} }
} }
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void) func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{ {
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
guard let feature = ALTFeature(entitlement: entitlement) else { return nil } guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
@@ -266,12 +267,12 @@ private extension ResignAppOperation
let appID = appID.copy() as! ALTAppID let appID = appID.copy() as! ALTAppID
appID.features = features appID.features = features
ALTAppleAPI.shared.update(appID, team: team) { (appID, error) in ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error)) completionHandler(Result(appID, error))
} }
} }
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void) func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{ {
// TODO: Handle apps belonging to more than one app group. // TODO: Handle apps belonging to more than one app group.
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else { guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
@@ -287,7 +288,7 @@ private extension ResignAppOperation
// Assign App Group // Assign App Group
// TODO: Determine whether app already belongs to app group. // TODO: Determine whether app already belongs to app group.
ALTAppleAPI.shared.add(appID, to: group, team: team) { (success, error) in ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
let result = result.map { _ in appID } let result = result.map { _ in appID }
completionHandler(result) completionHandler(result)
} }
@@ -296,7 +297,7 @@ private extension ResignAppOperation
let adjustedGroupIdentifier = "group.\(team.identifier)." + groupIdentifier let adjustedGroupIdentifier = "group.\(team.identifier)." + groupIdentifier
ALTAppleAPI.shared.fetchAppGroups(for: team) { (groups, error) in ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
switch Result(groups, error) switch Result(groups, error)
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
@@ -311,7 +312,7 @@ private extension ResignAppOperation
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does). // Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ") let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team) { (group, error) in ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
finish(Result(group, error)) finish(Result(group, error))
} }
} }
@@ -319,23 +320,23 @@ private extension ResignAppOperation
} }
} }
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void) func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{ {
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
switch Result(profile, error) switch Result(profile, error)
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
case .success(let profile): case .success(let profile):
// Delete existing profile // Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team) { (success, error) in ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error) switch Result(success, error)
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
case .success: case .success:
// Fetch new provisiong profile // Fetch new provisiong profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error)) completionHandler(Result(profile, error))
} }
} }

View File

@@ -116,7 +116,7 @@ struct Server: Equatable
} }
} }
func receive<T: Decodable>(_ type: T.Type, from connection: NWConnection, completionHandler: @escaping (Result<T, Error>) -> Void) func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result<ServerResponse, Error>) -> Void)
{ {
let size = MemoryLayout<Int32>.size let size = MemoryLayout<Int32>.size
@@ -131,7 +131,7 @@ struct Server: Equatable
{ {
let data = try self.process(data: data, error: error, from: connection) let data = try self.process(data: data, error: error, from: connection)
let response = try JSONDecoder().decode(T.self, from: data) let response = try JSONDecoder().decode(ServerResponse.self, from: data)
completionHandler(.success(response)) completionHandler(.success(response))
} }
catch catch

View File

@@ -184,6 +184,19 @@ private extension SettingsViewController
{ {
AppManager.shared.authenticate(presentingViewController: self) { (result) in AppManager.shared.authenticate(presentingViewController: self) { (result) in
DispatchQueue.main.async { DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled):
// Ignore
break
case .failure(let error):
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
case .success: break
}
self.update() self.update()
} }
} }