From e0a899ee9a1b2913c946648279bcdc9796f2d973 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Wed, 8 Jan 2020 12:41:02 -0800 Subject: [PATCH] Fixes session expiring when downloading apps on slow connection --- AltStore.xcodeproj/project.pbxproj | 8 + AltStore/Managing Apps/AppManager.swift | 28 ++- AltStore/Operations/AppOperationContext.swift | 2 +- .../Operations/AuthenticationOperation.swift | 167 ++++++------------ .../FetchAnisetteDataOperation.swift | 65 +++++++ AltStore/Operations/InstallAppOperation.swift | 13 +- AltStore/Operations/SendAppOperation.swift | 47 ++--- AltStore/Server/Server.swift | 122 ------------- AltStore/Server/ServerConnection.swift | 146 +++++++++++++++ AltStore/Server/ServerManager.swift | 30 ++++ 10 files changed, 346 insertions(+), 282 deletions(-) create mode 100644 AltStore/Operations/FetchAnisetteDataOperation.swift create mode 100644 AltStore/Server/ServerConnection.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 291bafac..b494ec16 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -137,6 +137,8 @@ BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4A22DD137F008935CF /* NavigationBar.swift */; }; BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* PillButton.swift */; }; BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */; }; + BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172823C56042001B5953 /* ServerConnection.swift */; }; + BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; }; BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; }; BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; }; BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; }; @@ -449,6 +451,8 @@ BF9ABA4C22DD16DE008935CF /* PillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButton.swift; sourceTree = ""; }; BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Hex.swift"; sourceTree = ""; }; BF9B63C5229DD44D002F0A62 /* AltSign.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BFA8172823C56042001B5953 /* ServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnection.swift; sourceTree = ""; }; + BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnisetteDataOperation.swift; sourceTree = ""; }; BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = ""; }; BFB1169C22932DB100BB457C /* apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apps.json; sourceTree = ""; }; @@ -915,6 +919,7 @@ children = ( BFD52BD322A0800A000B7ED1 /* ServerManager.swift */, BF770E5522BC3C02002A40FE /* Server.swift */, + BFA8172823C56042001B5953 /* ServerConnection.swift */, ); path = Server; sourceTree = ""; @@ -1130,6 +1135,7 @@ BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */, BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */, BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */, + BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */, ); path = Operations; sourceTree = ""; @@ -1677,6 +1683,7 @@ BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */, BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */, BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */, + BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */, BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */, BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */, BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */, @@ -1714,6 +1721,7 @@ BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */, BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */, BF770E5622BC3C03002A40FE /* Server.swift in Sources */, + BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */, BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 3a41bcc8..4fbca082 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -96,14 +96,13 @@ extension AppManager case .success(let server): group.server = server } } + self.operationQueue.addOperation(findServerOperation) let authenticationOperation = AuthenticationOperation(group: group, presentingViewController: presentingViewController) - authenticationOperation.addDependency(findServerOperation) authenticationOperation.resultHandler = { (result) in completionHandler(result) } - - self.operationQueue.addOperation(findServerOperation) + authenticationOperation.addDependency(findServerOperation) self.operationQueue.addOperation(authenticationOperation) } } @@ -237,6 +236,17 @@ private extension AppManager authenticationOperation = nil } + let refreshAnisetteDataOperation = FetchAnisetteDataOperation(group: group) + refreshAnisetteDataOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): group.error = error + case .success(let anisetteData): group.session?.anisetteData = anisetteData + } + } + refreshAnisetteDataOperation.addDependency(authenticationOperation ?? findServerOperation) + operations.append(refreshAnisetteDataOperation) + for app in apps { let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, group: group) @@ -249,7 +259,7 @@ private extension AppManager guard let resignedApp = self.process(result, context: context) else { return } context.resignedApp = resignedApp } - resignAppOperation.addDependency(authenticationOperation ?? findServerOperation) + resignAppOperation.addDependency(refreshAnisetteDataOperation) progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20) operations.append(resignAppOperation) @@ -296,8 +306,8 @@ private extension AppManager /* Send */ let sendAppOperation = SendAppOperation(context: context) sendAppOperation.resultHandler = { (result) in - guard let connection = self.process(result, context: context) else { return } - context.connection = connection + guard let installationConnection = self.process(result, context: context) else { return } + context.installationConnection = installationConnection } progress.addChild(sendAppOperation.progress, withPendingUnitCount: 10) sendAppOperation.addDependency(resignAppOperation) @@ -341,6 +351,12 @@ private extension AppManager group.set(progress, for: app) } + // Refresh anisette data after downloading all apps to prevent session from expiring. + for case let downloadOperation as DownloadAppOperation in operations + { + refreshAnisetteDataOperation.addDependency(downloadOperation) + } + group.addOperations(operations) return group diff --git a/AltStore/Operations/AppOperationContext.swift b/AltStore/Operations/AppOperationContext.swift index a673a296..5087655c 100644 --- a/AltStore/Operations/AppOperationContext.swift +++ b/AltStore/Operations/AppOperationContext.swift @@ -29,7 +29,7 @@ class AppOperationContext var app: ALTApplication? var resignedApp: ALTApplication? - var connection: NWConnection? + var installationConnection: ServerConnection? var installedApp: InstalledApp? { didSet { diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift index b6981b81..cc6c6abb 100644 --- a/AltStore/Operations/AuthenticationOperation.swift +++ b/AltStore/Operations/AuthenticationOperation.swift @@ -55,7 +55,7 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> private var signer: ALTSigner? private var session: ALTAppleAPISession? - private let dispatchQueue = DispatchQueue(label: "com.altstore.AuthenticationOperation") + private let operationQueue = OperationQueue() private var submitCodeAction: UIAlertAction? @@ -66,6 +66,7 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> super.init() + self.operationQueue.name = "com.altstore.AuthenticationOperation" self.progress.totalUnitCount = 3 } @@ -221,33 +222,6 @@ private extension AuthenticationOperation private extension AuthenticationOperation { - 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() @@ -307,98 +281,73 @@ private extension AuthenticationOperation 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 + let fetchAnisetteDataOperation = FetchAnisetteDataOperation(group: self.group) + fetchAnisetteDataOperation.resultHandler = { (result) in switch result { case .failure(let error): completionHandler(.failure(error)) - case .success(let connection): + case .success(let anisetteData): + let verificationHandler: ((@escaping (String?) -> Void) -> Void)? - 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 + 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 { - 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))) + 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: anisetteData, + verificationHandler: verificationHandler) { (account, session, error) in + if let account = account, let session = session + { + completionHandler(.success((account, session))) + } + else + { + completionHandler(.failure(error ?? OperationError.unknown)) + } + } } } + + self.operationQueue.addOperation(fetchAnisetteDataOperation) } func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) diff --git a/AltStore/Operations/FetchAnisetteDataOperation.swift b/AltStore/Operations/FetchAnisetteDataOperation.swift new file mode 100644 index 00000000..265c2d3e --- /dev/null +++ b/AltStore/Operations/FetchAnisetteDataOperation.swift @@ -0,0 +1,65 @@ +// +// FetchAnisetteDataOperation.swift +// AltStore +// +// Created by Riley Testut on 1/7/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +import AltSign +import AltKit + +import Roxas + +@objc(FetchAnisetteDataOperation) +class FetchAnisetteDataOperation: ResultOperation +{ + let group: OperationGroup + + init(group: OperationGroup) + { + self.group = group + + super.init() + } + + override func main() + { + super.main() + + if let error = self.group.error + { + self.finish(.failure(error)) + return + } + + guard let server = self.group.server else { return self.finish(.failure(OperationError.invalidParameters)) } + + ServerManager.shared.connect(to: server) { (result) in + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success(let connection): + let request = AnisetteDataRequest() + connection.send(request) { (result) in + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success: + connection.receiveResponse() { (result) in + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success(.error(let response)): self.finish(.failure(response.error)) + case .success(.anisetteData(let response)): self.finish(.success(response.anisetteData)) + case .success: self.finish(.failure(ALTServerError(.unknownRequest))) + } + } + } + } + } + } + } +} diff --git a/AltStore/Operations/InstallAppOperation.swift b/AltStore/Operations/InstallAppOperation.swift index 7704e6b6..d9820578 100644 --- a/AltStore/Operations/InstallAppOperation.swift +++ b/AltStore/Operations/InstallAppOperation.swift @@ -41,8 +41,7 @@ class InstallAppOperation: ResultOperation guard let resignedApp = self.context.resignedApp, - let connection = self.context.connection, - let server = self.context.group.server + let connection = self.context.installationConnection else { return self.finish(.failure(OperationError.invalidParameters)) } let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() @@ -73,13 +72,13 @@ class InstallAppOperation: ResultOperation self.context.group.beginInstallationHandler?(installedApp) let request = BeginInstallationRequest() - server.send(request, via: connection) { (result) in + connection.send(request) { (result) in switch result { case .failure(let error): self.finish(.failure(error)) case .success: - self.receive(from: connection, server: server) { (result) in + self.receive(from: connection) { (result) in switch result { case .success: @@ -107,9 +106,9 @@ class InstallAppOperation: ResultOperation private extension InstallAppOperation { - func receive(from connection: NWConnection, server: Server, completionHandler: @escaping (Result) -> Void) + func receive(from connection: ServerConnection, completionHandler: @escaping (Result) -> Void) { - server.receiveResponse(from: connection) { (result) in + connection.receiveResponse() { (result) in do { let response = try result.get() @@ -126,7 +125,7 @@ private extension InstallAppOperation else { self.progress.completedUnitCount = Int64(response.progress * 100) - self.receive(from: connection, server: server, completionHandler: completionHandler) + self.receive(from: connection, completionHandler: completionHandler) } case .error(let response): diff --git a/AltStore/Operations/SendAppOperation.swift b/AltStore/Operations/SendAppOperation.swift index 558c33ee..e3cc1413 100644 --- a/AltStore/Operations/SendAppOperation.swift +++ b/AltStore/Operations/SendAppOperation.swift @@ -12,13 +12,13 @@ import Network import AltKit @objc(SendAppOperation) -class SendAppOperation: ResultOperation +class SendAppOperation: ResultOperation { let context: AppOperationContext private let dispatchQueue = DispatchQueue(label: "com.altstore.SendAppOperation") - private var connection: NWConnection? + private var serverConnection: ServerConnection? init(context: AppOperationContext) { @@ -45,21 +45,21 @@ class SendAppOperation: ResultOperation let fileURL = InstalledApp.refreshedIPAURL(for: app) // Connect to server. - self.connect(to: server) { (result) in + ServerManager.shared.connect(to: server) { (result) in switch result { case .failure(let error): self.finish(.failure(error)) - case .success(let connection): - self.connection = connection + case .success(let serverConnection): + self.serverConnection = serverConnection // Send app to server. - self.sendApp(at: fileURL, via: connection, server: server) { (result) in + self.sendApp(at: fileURL, via: serverConnection) { (result) in switch result { case .failure(let error): self.finish(.failure(error)) case .success: self.progress.completedUnitCount += 1 - self.finish(.success(connection)) + self.finish(.success(serverConnection)) } } } @@ -69,34 +69,7 @@ class SendAppOperation: ResultOperation private extension SendAppOperation { - 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 sendApp(at fileURL: URL, via connection: NWConnection, server: Server, completionHandler: @escaping (Result) -> Void) + func sendApp(at fileURL: URL, via connection: ServerConnection, completionHandler: @escaping (Result) -> Void) { do { @@ -106,14 +79,14 @@ private extension SendAppOperation let request = PrepareAppRequest(udid: udid, contentSize: appData.count) print("Sending request \(request)") - server.send(request, via: connection) { (result) in + connection.send(request) { (result) in switch result { case .failure(let error): completionHandler(.failure(error)) case .success: print("Sending app data (\(appData.count) bytes)") - server.send(appData, via: connection, prependSize: false) { (result) in + connection.send(appData, prependSize: false) { (result) in switch result { case .failure(let error): completionHandler(.failure(error)) diff --git a/AltStore/Server/Server.swift b/AltStore/Server/Server.swift index 709263eb..3fe7df82 100644 --- a/AltStore/Server/Server.swift +++ b/AltStore/Server/Server.swift @@ -57,126 +57,4 @@ struct Server: Equatable self.identifier = identifier self.service = service } - - func send(_ payload: T, via connection: NWConnection, prependSize: Bool = true, completionHandler: @escaping (Result) -> Void) - { - do - { - let data: Data - - if let payload = payload as? Data - { - data = payload - } - else - { - data = try JSONEncoder().encode(payload) - } - - func process(_ error: Error?) -> Bool - { - if error != nil - { - completionHandler(.failure(ConnectionError.connectionDropped)) - return false - } - else - { - return true - } - } - - if prependSize - { - let requestSize = Int32(data.count) - let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) } - - connection.send(content: requestSizeData, completion: .contentProcessed { (error) in - guard process(error) else { return } - - connection.send(content: data, completion: .contentProcessed { (error) in - guard process(error) else { return } - completionHandler(.success(())) - }) - }) - - } - else - { - connection.send(content: data, completion: .contentProcessed { (error) in - guard process(error) else { return } - completionHandler(.success(())) - }) - } - } - catch - { - print("Invalid request.", error) - completionHandler(.failure(ALTServerError(.invalidRequest))) - } - } - - func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result) -> Void) - { - let size = MemoryLayout.size - - connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in - do - { - let data = try self.process(data: data, error: error, from: connection) - - let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) }) - connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in - do - { - let data = try self.process(data: data, error: error, from: connection) - - let response = try JSONDecoder().decode(ServerResponse.self, from: data) - completionHandler(.success(response)) - } - catch - { - completionHandler(.failure(ALTServerError(error))) - } - } - } - catch - { - completionHandler(.failure(ALTServerError(error))) - } - } - } -} - -private extension Server -{ - func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data - { - do - { - do - { - guard let data = data else { throw error ?? ALTServerError(.unknown) } - return data - } - catch let error as NWError - { - print("Error receiving data from connection \(connection)", error) - - throw ALTServerError(.lostConnection) - } - catch - { - throw error - } - } - catch let error as ALTServerError - { - throw error - } - catch - { - preconditionFailure("A non-ALTServerError should never be thrown from this method.") - } - } } diff --git a/AltStore/Server/ServerConnection.swift b/AltStore/Server/ServerConnection.swift new file mode 100644 index 00000000..76d56274 --- /dev/null +++ b/AltStore/Server/ServerConnection.swift @@ -0,0 +1,146 @@ +// +// ServerConnection.swift +// AltStore +// +// Created by Riley Testut on 1/7/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation +import Network + +import AltKit + +class ServerConnection +{ + var server: Server + var connection: NWConnection + + init(server: Server, connection: NWConnection) + { + self.server = server + self.connection = connection + } + + func send(_ payload: T, prependSize: Bool = true, completionHandler: @escaping (Result) -> Void) + { + do + { + let data: Data + + if let payload = payload as? Data + { + data = payload + } + else + { + data = try JSONEncoder().encode(payload) + } + + func process(_ error: Error?) -> Bool + { + if error != nil + { + completionHandler(.failure(ConnectionError.connectionDropped)) + return false + } + else + { + return true + } + } + + if prependSize + { + let requestSize = Int32(data.count) + let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) } + + self.connection.send(content: requestSizeData, completion: .contentProcessed { (error) in + guard process(error) else { return } + + self.connection.send(content: data, completion: .contentProcessed { (error) in + guard process(error) else { return } + completionHandler(.success(())) + }) + }) + + } + else + { + connection.send(content: data, completion: .contentProcessed { (error) in + guard process(error) else { return } + completionHandler(.success(())) + }) + } + } + catch + { + print("Invalid request.", error) + completionHandler(.failure(ALTServerError(.invalidRequest))) + } + } + + func receiveResponse(completionHandler: @escaping (Result) -> Void) + { + let size = MemoryLayout.size + + self.connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in + do + { + let data = try self.process(data: data, error: error) + + let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) }) + self.connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in + do + { + let data = try self.process(data: data, error: error) + + let response = try JSONDecoder().decode(ServerResponse.self, from: data) + completionHandler(.success(response)) + } + catch + { + completionHandler(.failure(ALTServerError(error))) + } + } + } + catch + { + completionHandler(.failure(ALTServerError(error))) + } + } + } +} + +private extension ServerConnection +{ + func process(data: Data?, error: NWError?) throws -> Data + { + do + { + do + { + guard let data = data else { throw error ?? ALTServerError(.unknown) } + return data + } + catch let error as NWError + { + print("Error receiving data from connection \(connection)", error) + + throw ALTServerError(.lostConnection) + } + catch + { + throw error + } + } + catch let error as ALTServerError + { + throw error + } + catch + { + preconditionFailure("A non-ALTServerError should never be thrown from this method.") + } + } +} diff --git a/AltStore/Server/ServerManager.swift b/AltStore/Server/ServerManager.swift index a11d1550..240c6eb2 100644 --- a/AltStore/Server/ServerManager.swift +++ b/AltStore/Server/ServerManager.swift @@ -22,6 +22,8 @@ class ServerManager: NSObject private var services = Set() + private let dispatchQueue = DispatchQueue(label: "io.altstore.ServerManager") + private override init() { super.init() @@ -50,6 +52,34 @@ extension ServerManager self.services.removeAll() self.serviceBrowser.stop() } + + func connect(to server: Server, completion: @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) + completion(.failure(ConnectionError.connectionFailed)) + + case .cancelled: + completion(.failure(OperationError.cancelled)) + + case .ready: + let connection = ServerConnection(server: server, connection: connection) + completion(.success(connection)) + + case .waiting: break + case .setup: break + case .preparing: break + @unknown default: break + } + } + + connection.start(queue: self.dispatchQueue) + } } private extension ServerManager