From 691e08202d65bbefcf218ece7aa57f8599f88169 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 18 Nov 2019 14:49:17 -0800 Subject: [PATCH] [AltStore] Uses GrandSlam Authentication Retrieves anisette data from AltServer so we can authenticate with GSA. --- AltKit/NSError+ALTServerError.h | 6 + AltKit/NSError+ALTServerError.m | 12 + AltKit/ServerProtocol.swift | 223 ++++++++++++++++-- AltServer/AnisetteDataManager.swift | 4 +- AltServer/Connections/ConnectionManager.swift | 175 +++++++++----- .../AuthenticationViewController.swift | 24 +- .../RefreshAltStoreViewController.swift | 2 + AltStore/Managing Apps/AppManager.swift | 43 +++- .../Operations/AuthenticationOperation.swift | 223 +++++++++++++++--- AltStore/Operations/InstallAppOperation.swift | 32 ++- AltStore/Operations/OperationGroup.swift | 2 + AltStore/Operations/ResignAppOperation.swift | 55 ++--- AltStore/Server/Server.swift | 4 +- .../Settings/SettingsViewController.swift | 13 + 14 files changed, 636 insertions(+), 182 deletions(-) diff --git a/AltKit/NSError+ALTServerError.h b/AltKit/NSError+ALTServerError.h index f1d5c9c1..bb9176ae 100644 --- a/AltKit/NSError+ALTServerError.h +++ b/AltKit/NSError+ALTServerError.h @@ -27,6 +27,12 @@ typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError) ALTServerErrorInstallationFailed = 8, ALTServerErrorMaximumFreeAppLimitReached = 9, ALTServerErrorUnsupportediOSVersion = 10, + + ALTServerErrorUnknownRequest = 11, + ALTServerErrorUnknownResponse = 12, + + ALTServerErrorInvalidAnisetteData = 13, + ALTServerErrorPluginNotFound = 14 }; NS_ASSUME_NONNULL_BEGIN diff --git a/AltKit/NSError+ALTServerError.m b/AltKit/NSError+ALTServerError.m index eb365a2d..7688d52f 100644 --- a/AltKit/NSError+ALTServerError.m +++ b/AltKit/NSError+ALTServerError.m @@ -61,6 +61,18 @@ NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServ case ALTServerErrorUnsupportediOSVersion: 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.", @""); } } diff --git a/AltKit/ServerProtocol.swift b/AltKit/ServerProtocol.swift index aa3ffc62..1132b3ce 100644 --- a/AltKit/ServerProtocol.swift +++ b/AltKit/ServerProtocol.swift @@ -7,22 +7,201 @@ // import Foundation +import AltSign public let ALTServerServiceType = "_altserver._tcp" // Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself extension ALTServerError.Code: Codable {} -protocol ServerMessage: Codable +protocol ServerMessageProtocol: Codable { var version: Int { 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 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 contentSize: Int @@ -34,37 +213,41 @@ public struct PrepareAppRequest: ServerMessage } } -public struct BeginInstallationRequest: ServerMessage +public struct BeginInstallationRequest: ServerMessageProtocol { public var version = 1 - public var identifier = "BeginInstallation" + public var identifier = "BeginInstallationRequest" public init() { } } -public struct ServerResponse: ServerMessage +public struct ErrorResponse: ServerMessageProtocol { 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 error: ALTServerError? { - 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?) + public init(progress: Double) { self.progress = progress - self.error = error } } diff --git a/AltServer/AnisetteDataManager.swift b/AltServer/AnisetteDataManager.swift index c754b3bb..8464ddd6 100644 --- a/AltServer/AnisetteDataManager.swift +++ b/AltServer/AnisetteDataManager.swift @@ -28,11 +28,13 @@ class AnisetteDataManager: NSObject let requestUUID = UUID().uuidString 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.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) } } diff --git a/AltServer/Connections/ConnectionManager.swift b/AltServer/Connections/ConnectionManager.swift index 92f4219b..8bafa135 100644 --- a/AltServer/Connections/ConnectionManager.swift +++ b/AltServer/Connections/ConnectionManager.swift @@ -8,6 +8,7 @@ import Foundation import Network +import AppKit import AltKit @@ -188,7 +189,6 @@ private extension ConnectionManager guard !self.connections.contains(where: { $0 === connection }) else { return } self.connections.append(connection) - connection.stateUpdateHandler = { [weak self] (state) in switch state { @@ -196,10 +196,7 @@ private extension ConnectionManager case .ready: print("Connected to client:", connection.endpoint) - - self?.receiveApp(from: connection) { (result) in - self?.finish(connection: connection, error: result.error) - } + self?.handleRequest(for: connection) case .waiting: print("Waiting for connection...") @@ -218,7 +215,55 @@ private extension ConnectionManager connection.start(queue: self.dispatchQueue) } - func receiveApp(from connection: NWConnection, completionHandler: @escaping (Result) -> 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? @@ -230,76 +275,67 @@ private extension ConnectionManager catch { print("Failed to remove .ipa.", error) } } - completionHandler(result) + switch result + { + case .failure(let error): + 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.receive(PrepareAppRequest.self, from: connection) { (result) in - print("Received request with result:", result) + self.receiveApp(for: request, from: connection) { (result) in + print("Received app with result:", result) switch result { case .failure(let error): finish(.failure(error)) - case .success(let request): - self.receiveApp(for: request, from: connection) { (result) in - print("Received app with result:", result) + case .success(let fileURL): + temporaryURL = fileURL + + print("Awaiting begin installation request...") + + self.receiveRequest(from: connection) { (result) in + print("Received begin installation request with result:", result) switch result { case .failure(let error): finish(.failure(error)) - case .success(let request, let fileURL): - temporaryURL = fileURL + case .success(.beginInstallation): + print("Installing to device \(request.udid)...") - print("Awaiting begin installation request...") - - self.receive(BeginInstallationRequest.self, from: connection) { (result) in - print("Received begin installation request with result:", result) - + self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in + print("Installed to device with result:", result) switch result { case .failure(let error): finish(.failure(error)) - case .success: - print("Installing to device \(request.udid)...") - - self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in - print("Installed to device with result:", result) - switch result - { - case .failure(let error): finish(.failure(error)) - 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) + } } } } } } - func finish(connection: NWConnection, error: ALTServerError?) - { - 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) + func receiveApp(for request: PrepareAppRequest, from connection: NWConnection, completionHandler: @escaping (Result) -> Void) { connection.receive(minimumIncompleteLength: request.contentSize, maximumLength: request.contentSize) { (data, _, _, error) in do @@ -319,7 +355,7 @@ private extension ConnectionManager print("Wrote app to URL:", temporaryURL) - completionHandler(.success((request, temporaryURL))) + completionHandler(.success(temporaryURL)) } catch { @@ -359,7 +395,7 @@ private extension ConnectionManager isSending = true 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 serialQueue.async { @@ -370,8 +406,21 @@ private extension ConnectionManager }) } - func send(_ response: T, to connection: NWConnection, completionHandler: @escaping (Result) -> Void) + func send(_ response: T, to connection: NWConnection, shouldDisconnect: Bool = false, completionHandler: @escaping (Result) -> Void) { + func finish(_ result: Result) + { + 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 { let data = try JSONEncoder().encode(response) @@ -388,27 +437,27 @@ private extension ConnectionManager connection.send(content: data, completion: .contentProcessed { (error) in if error != nil { - completionHandler(.failure(.init(.lostConnection))) + finish(.failure(.init(.lostConnection))) } else { - completionHandler(.success(())) + finish(.success(())) } }) } catch { - completionHandler(.failure(.init(.lostConnection))) + finish(.failure(.init(.lostConnection))) } }) } catch { - completionHandler(.failure(.init(.invalidResponse))) + finish(.failure(.init(.invalidResponse))) } } - func receive(_ responseType: T.Type, from connection: NWConnection, completionHandler: @escaping (Result) -> Void) + func receiveRequest(from connection: NWConnection, completionHandler: @escaping (Result) -> Void) { let size = MemoryLayout.size @@ -426,7 +475,7 @@ private extension ConnectionManager { 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) diff --git a/AltStore/Authentication/AuthenticationViewController.swift b/AltStore/Authentication/AuthenticationViewController.swift index ac1db12e..9268964a 100644 --- a/AltStore/Authentication/AuthenticationViewController.swift +++ b/AltStore/Authentication/AuthenticationViewController.swift @@ -12,7 +12,8 @@ import AltSign 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? @@ -96,14 +97,16 @@ private extension AuthenticationViewController self.signInButton.isIndicatingActivity = true - ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in - do - { - let account = try Result(account, error).get() - self.authenticationHandler?((account, password)) - } - catch + self.authenticationHandler?(emailAddress, password) { (result) in + switch result { + case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication): + // Ignore + DispatchQueue.main.async { + self.signInButton.isIndicatingActivity = false + } + + case .failure(let error): DispatchQueue.main.async { let toastView = ToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription) toastView.textLabel.textColor = .altPink @@ -113,6 +116,9 @@ private extension AuthenticationViewController self.signInButton.isIndicatingActivity = false } + + case .success(let account, let session): + self.completionHandler?((account, session, password)) } DispatchQueue.main.async { @@ -123,7 +129,7 @@ private extension AuthenticationViewController @IBAction func cancel(_ sender: UIBarButtonItem) { - self.authenticationHandler?(nil) + self.completionHandler?(nil) } } diff --git a/AltStore/Authentication/RefreshAltStoreViewController.swift b/AltStore/Authentication/RefreshAltStoreViewController.swift index 708a9d9d..74c33570 100644 --- a/AltStore/Authentication/RefreshAltStoreViewController.swift +++ b/AltStore/Authentication/RefreshAltStoreViewController.swift @@ -14,6 +14,7 @@ import Roxas class RefreshAltStoreViewController: UIViewController { var signer: ALTSigner! + var session: ALTAppleAPISession! var completionHandler: ((Result) -> Void)? @@ -49,6 +50,7 @@ private extension RefreshAltStoreViewController let group = OperationGroup() group.signer = self.signer // Prevent us from trying to authenticate a second time. + group.session = self.session // ^ group.completionHandler = { (result) in if let error = result.error ?? result.value?.values.compactMap({ $0.error }).first { diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index a87519c8..b6df3c65 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -80,12 +80,26 @@ extension AppManager #endif } - func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result) -> 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 completionHandler(result) } + + self.operationQueue.addOperation(findServerOperation) self.operationQueue.addOperation(authenticationOperation) } } @@ -194,21 +208,30 @@ private extension AppManager } operations.append(findServerOperation) - if group.signer == nil + let authenticationOperation: AuthenticationOperation? + + if group.signer == nil || group.session == nil { /* Authenticate */ - let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController) - authenticationOperation.resultHandler = { (result) in + let operation = AuthenticationOperation(group: group, presentingViewController: presentingViewController) + operation.resultHandler = { (result) in switch result { 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 { @@ -222,7 +245,7 @@ private extension AppManager guard let resignedApp = self.process(result, context: context) else { return } context.resignedApp = resignedApp } - resignAppOperation.addDependency(findServerOperation) + resignAppOperation.addDependency(authenticationOperation ?? findServerOperation) progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20) operations.append(resignAppOperation) diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift index 932826ad..b6981b81 100644 --- a/AltStore/Operations/AuthenticationOperation.swift +++ b/AltStore/Operations/AuthenticationOperation.swift @@ -8,7 +8,9 @@ import Foundation import Roxas +import Network +import AltKit import AltSign enum AuthenticationError: LocalizedError @@ -30,8 +32,10 @@ enum AuthenticationError: LocalizedError } @objc(AuthenticationOperation) -class AuthenticationOperation: ResultOperation +class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> { + let group: OperationGroup + private weak var presentingViewController: UIViewController? private lazy var navigationController: UINavigationController = { @@ -49,9 +53,15 @@ class AuthenticationOperation: ResultOperation private var shouldShowInstructions = false 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 super.init() @@ -63,18 +73,25 @@ class AuthenticationOperation: ResultOperation { super.main() + if let error = self.group.error + { + self.finish(.failure(error)) + return + } + // Sign In - self.signIn { (result) 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): + case .success(let account, let session): + self.session = session self.progress.completedUnitCount += 1 // 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)) } switch result @@ -84,7 +101,7 @@ class AuthenticationOperation: ResultOperation self.progress.completedUnitCount += 1 // 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)) } switch result @@ -97,8 +114,8 @@ class AuthenticationOperation: ResultOperation self.signer = signer self.showInstructionsIfNecessary() { (didShowInstructions) in - self.finish(.success(signer)) - } + self.finish(.success((signer, session))) + } } } } @@ -107,7 +124,7 @@ class AuthenticationOperation: ResultOperation } } - override func finish(_ result: Result) + override func finish(_ result: Result<(ALTSigner, ALTAppleAPISession), Error>) { guard !self.isFinished else { return } @@ -117,7 +134,7 @@ class AuthenticationOperation: ResultOperation context.performAndWait { do { - let signer = try result.get() + let (signer, session) = try result.get() let altAccount = signer.team.account // Account @@ -158,7 +175,7 @@ class AuthenticationOperation: ResultOperation // Refresh screen must go last since a successful refresh will cause the app to quit. self.showRefreshScreenIfNecessary() { (didShowRefreshAlert) in - super.finish(.success(signer)) + super.finish(.success((signer, session))) DispatchQueue.main.async { self.navigationController.dismiss(animated: true, completion: nil) @@ -204,21 +221,53 @@ private extension AuthenticationOperation private extension AuthenticationOperation { - func signIn(completionHandler: @escaping (Result) -> Void) + func connect(to server: Server, completionHandler: @escaping (Result) -> 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() { DispatchQueue.main.async { let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController - authenticationViewController.authenticationHandler = { (result) in - if let (account, password) = result + authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in + 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. // In this case, we'll assume we should show the instructions again. self.shouldShowInstructions = true self.appleIDPassword = password - completionHandler(.success(account)) + completionHandler(.success((account, session))) } else { @@ -235,24 +284,17 @@ private extension AuthenticationOperation if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword { - ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in - do + self.authenticate(appleID: appleID, password: password) { (result) in + switch result { + case .success(let account, let session): self.appleIDPassword = password + completionHandler(.success((account, session))) - let account = try Result(account, error).get() - completionHandler(.success(account)) - } - catch ALTAppleAPIError.incorrectCredentials - { + case .failure(ALTAppleAPIError.incorrectCredentials), .failure(ALTAppleAPIError.appSpecificPasswordRequired): authenticate() - } - catch ALTAppleAPIError.appSpecificPasswordRequired - { - authenticate() - } - catch - { + + case .failure(let error): completionHandler(.failure(error)) } } @@ -263,7 +305,103 @@ private extension AuthenticationOperation } } - func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result) -> 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) -> Void) { 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) { case .failure(let error): completionHandler(.failure(error)) @@ -304,18 +442,18 @@ private extension AuthenticationOperation } } - func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result) -> Void) + func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { func requestCertificate() { 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 { let certificate = try Result(certificate, error).get() 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 { 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)) } - 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 { 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 { let certificates = try Result(certificates, error).get() @@ -431,7 +569,7 @@ private extension AuthenticationOperation 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) } // 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 { let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController refreshViewController.signer = signer + refreshViewController.session = session refreshViewController.completionHandler = { _ in 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 + } +} diff --git a/AltStore/Operations/InstallAppOperation.swift b/AltStore/Operations/InstallAppOperation.swift index 3054f54a..d47d395b 100644 --- a/AltStore/Operations/InstallAppOperation.swift +++ b/AltStore/Operations/InstallAppOperation.swift @@ -94,25 +94,31 @@ class InstallAppOperation: ResultOperation func receive(from connection: NWConnection, server: Server, completionHandler: @escaping (Result) -> Void) { - server.receive(ServerResponse.self, from: connection) { (result) in + server.receiveResponse(from: connection) { (result) in do { let response = try result.get() print(response) - if let error = response.error + switch response { - completionHandler(.failure(error)) - } - else if response.progress == 1.0 - { - self.progress.completedUnitCount = self.progress.totalUnitCount - completionHandler(.success(())) - } - else - { - self.progress.completedUnitCount = Int64(response.progress * 100) - self.receive(from: connection, server: server, completionHandler: completionHandler) + case .installationProgress(let response): + if response.progress == 1.0 + { + self.progress.completedUnitCount = self.progress.totalUnitCount + completionHandler(.success(())) + } + else + { + self.progress.completedUnitCount = Int64(response.progress * 100) + self.receive(from: connection, server: server, completionHandler: completionHandler) + } + + case .error(let response): + completionHandler(.failure(response.error)) + + default: + completionHandler(.failure(ALTServerError(.unknownRequest))) } } catch diff --git a/AltStore/Operations/OperationGroup.swift b/AltStore/Operations/OperationGroup.swift index a6891823..3f9feb09 100644 --- a/AltStore/Operations/OperationGroup.swift +++ b/AltStore/Operations/OperationGroup.swift @@ -18,6 +18,8 @@ class OperationGroup var completionHandler: ((Result<[String: Result], Error>) -> Void)? var beginInstallationHandler: ((InstalledApp) -> Void)? + var session: ALTAppleAPISession? + var server: Server? var signer: ALTSigner? diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift index 77ecc50e..5671ce97 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -37,15 +37,16 @@ class ResignAppOperation: ResultOperation guard 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)) } // 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 } // 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 } // Prepare app bundle @@ -104,13 +105,13 @@ class ResignAppOperation: ResultOperation private extension ResignAppOperation { - func registerCurrentDevice(for team: ALTTeam, completionHandler: @escaping (Result) -> Void) + func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> 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 + ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in do { let devices = try Result(devices, error).get() @@ -121,7 +122,7 @@ private extension ResignAppOperation } 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)) } } @@ -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)) } @@ -144,7 +145,7 @@ private extension ResignAppOperation dispatchGroup.enter() - self.prepareProvisioningProfile(for: app, team: team) { (result) in + self.prepareProvisioningProfile(for: app, team: team, session: session) { (result) in switch result { case .failure(let e): error = e @@ -162,7 +163,7 @@ private extension ResignAppOperation dispatchGroup.enter() - self.prepareProvisioningProfile(for: appExtension, team: team) { (result) in + self.prepareProvisioningProfile(for: appExtension, team: team, session: session) { (result) in switch result { case .failure(let e): error = e @@ -186,31 +187,31 @@ private extension ResignAppOperation } } - func prepareProvisioningProfile(for app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result) -> Void) + func prepareProvisioningProfile(for app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { // Register - self.register(app, team: team) { (result) in + self.register(app, team: team, session: session) { (result) in switch result { case .failure(let error): completionHandler(.failure(error)) case .success(let appID): // Update features - self.updateFeatures(for: appID, app: app, team: team) { (result) in + self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in switch result { case .failure(let error): completionHandler(.failure(error)) case .success(let appID): // Update app groups - self.updateAppGroups(for: appID, app: app, team: team) { (result) in + self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in switch result { case .failure(let error): completionHandler(.failure(error)) case .success(let appID): // Fetch Provisioning Profile - self.fetchProvisioningProfile(for: appID, team: team) { (result) in + self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in completionHandler(result) } } @@ -221,12 +222,12 @@ private extension ResignAppOperation } } - func register(_ app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result) -> Void) + func register(_ app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { let appName = app.name 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 { let appIDs = try Result(appIDs, error).get() @@ -237,7 +238,7 @@ private extension ResignAppOperation } 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)) } } @@ -249,7 +250,7 @@ private extension ResignAppOperation } } - func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result) -> Void) + func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in guard let feature = ALTFeature(entitlement: entitlement) else { return nil } @@ -266,12 +267,12 @@ private extension ResignAppOperation let appID = appID.copy() as! ALTAppID 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)) } } - func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result) -> Void) + func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { // TODO: Handle apps belonging to more than one app group. guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else { @@ -287,7 +288,7 @@ private extension ResignAppOperation // Assign 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 } completionHandler(result) } @@ -296,7 +297,7 @@ private extension ResignAppOperation 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) { 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). 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)) } } @@ -319,23 +320,23 @@ private extension ResignAppOperation } } - func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result) -> Void) + func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> 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) { case .failure(let error): completionHandler(.failure(error)) case .success(let 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) { case .failure(let error): completionHandler(.failure(error)) case .success: // 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)) } } diff --git a/AltStore/Server/Server.swift b/AltStore/Server/Server.swift index 451306ff..709263eb 100644 --- a/AltStore/Server/Server.swift +++ b/AltStore/Server/Server.swift @@ -116,7 +116,7 @@ struct Server: Equatable } } - func receive(_ type: T.Type, from connection: NWConnection, completionHandler: @escaping (Result) -> Void) + func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result) -> Void) { let size = MemoryLayout.size @@ -131,7 +131,7 @@ struct Server: Equatable { 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)) } catch diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift index 15c28068..3ad86e60 100644 --- a/AltStore/Settings/SettingsViewController.swift +++ b/AltStore/Settings/SettingsViewController.swift @@ -184,6 +184,19 @@ private extension SettingsViewController { AppManager.shared.authenticate(presentingViewController: self) { (result) in 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() } }