From 496aca642cb656b4e9758a3f2631ddf627df5fa5 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 4 Jun 2020 19:53:10 -0700 Subject: [PATCH] Supports installing/refreshings apps w/o computer on jailbroken devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AltStore will use AltDaemon as a local AltServer if it’s installed and running. AltStore remains a regular sandboxed app, but AltDaemon has private entitlements necessary to perform AltServer operations without a computer. --- AltStore/Managing Apps/AppManager.swift | 4 +- AltStore/Operations/FindServerOperation.swift | 89 +++++++++++-------- AltStore/Operations/InstallAppOperation.swift | 23 +++-- AltStore/Operations/SendAppOperation.swift | 41 ++++++--- AltStore/Server/Server.swift | 28 +++--- AltStore/Server/ServerManager.swift | 24 ++++- 6 files changed, 131 insertions(+), 78 deletions(-) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index f710f71a..2600f3d6 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -1225,9 +1225,9 @@ private extension AppManager switch error.code { case .deviceNotFound, .lostConnection: - if let server = group.context.server, server.isPreferred || server.isWiredConnection + if let server = group.context.server, server.isPreferred || server.connectionType != .wireless { - // Preferred server (or wired connection), so report errors normally. + // Preferred server (or not random wireless connection), so report errors normally. return error } else diff --git a/AltStore/Operations/FindServerOperation.swift b/AltStore/Operations/FindServerOperation.swift index e30c1b31..343a4789 100644 --- a/AltStore/Operations/FindServerOperation.swift +++ b/AltStore/Operations/FindServerOperation.swift @@ -10,14 +10,12 @@ import Foundation import AltKit import Roxas -private extension Notification.Name -{ - static let didReceiveWiredServerConnectionResponse = Notification.Name("io.altstore.didReceiveWiredServerConnectionResponse") -} - -private let ReceivedWiredServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = +private let ReceivedServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in - NotificationCenter.default.post(name: .didReceiveWiredServerConnectionResponse, object: nil) + guard let name = name, let observer = observer else { return } + + let operation = unsafeBitCast(observer, to: FindServerOperation.self) + operation.handle(name) } @objc(FindServerOperation) @@ -26,12 +24,13 @@ class FindServerOperation: ResultOperation let context: OperationContext private var isWiredServerConnectionAvailable = false + private var isLocalServerConnectionAvailable = false init(context: OperationContext = OperationContext()) { self.context = context } - + override func main() { super.main() @@ -49,48 +48,68 @@ class FindServerOperation: ResultOperation } let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() + let observer = Unmanaged.passUnretained(self).toOpaque() - // Prepare observers to receive callback from wired server (if connected). - CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedWiredServerConnectionResponse, CFNotificationName.wiredServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately) - NotificationCenter.default.addObserver(self, selector: #selector(FindServerOperation.didReceiveWiredServerConnectionResponse(_:)), name: .didReceiveWiredServerConnectionResponse, object: nil) + // Prepare observers to receive callback from wired connection or background daemon (if available). + CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedServerConnectionResponse, CFNotificationName.wiredServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately) + CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedServerConnectionResponse, CFNotificationName.localServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately) - // Post notification. + // Post notifications. CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionAvailableRequest, nil, nil, true) + CFNotificationCenterPostNotification(notificationCenter, .localServerConnectionAvailableRequest, nil, nil, true) // Wait for either callback or timeout. DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { - if self.isWiredServerConnectionAvailable + if self.isLocalServerConnectionAvailable { - let server = Server(isWiredConnection: true) + // Prefer background daemon, if it exists and is running. + let server = Server(connectionType: .local) + self.finish(.success(server)) + } + else if self.isWiredServerConnectionAvailable + { + let server = Server(connectionType: .wired) + self.finish(.success(server)) + } + else if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred }) + { + // Preferred server. + self.finish(.success(server)) + } + else if let server = ServerManager.shared.discoveredServers.first + { + // Any available server. self.finish(.success(server)) } else { - if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred }) - { - // Preferred server. - self.finish(.success(server)) - } - else if let server = ServerManager.shared.discoveredServers.first - { - // Any available server. - self.finish(.success(server)) - } - else - { - // No servers. - self.finish(.failure(ConnectionError.serverNotFound)) - } + // No servers. + self.finish(.failure(ConnectionError.serverNotFound)) } } } -} - -private extension FindServerOperation -{ - @objc func didReceiveWiredServerConnectionResponse(_ notification: Notification) + + override func finish(_ result: Result) { - self.isWiredServerConnectionAvailable = true + super.finish(result) + + let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() + let observer = Unmanaged.passUnretained(self).toOpaque() + + CFNotificationCenterRemoveObserver(notificationCenter, observer, .wiredServerConnectionAvailableResponse, nil) + CFNotificationCenterRemoveObserver(notificationCenter, observer, .localServerConnectionAvailableResponse, nil) } } +fileprivate extension FindServerOperation +{ + func handle(_ notification: CFNotificationName) + { + switch notification + { + case .wiredServerConnectionAvailableResponse: self.isWiredServerConnectionAvailable = true + case .localServerConnectionAvailableResponse: self.isLocalServerConnectionAvailable = true + default: break + } + } +} diff --git a/AltStore/Operations/InstallAppOperation.swift b/AltStore/Operations/InstallAppOperation.swift index 52fdbabb..b5756de6 100644 --- a/AltStore/Operations/InstallAppOperation.swift +++ b/AltStore/Operations/InstallAppOperation.swift @@ -144,7 +144,7 @@ class InstallAppOperation: ResultOperation }) } - let request = BeginInstallationRequest(activeProfiles: activeProfiles) + let request = BeginInstallationRequest(activeProfiles: activeProfiles, bundleIdentifier: installedApp.resignedBundleIdentifier) connection.send(request) { (result) in switch result { @@ -173,6 +173,21 @@ class InstallAppOperation: ResultOperation { self.cleanUp() + // Only remove refreshed IPA when finished. + if let app = self.context.app + { + let fileURL = InstalledApp.refreshedIPAURL(for: app) + + do + { + try FileManager.default.removeItem(at: fileURL) + } + catch + { + print("Failed to remove refreshed .ipa:", error) + } + } + super.finish(result) } } @@ -223,12 +238,6 @@ private extension InstallAppOperation do { try FileManager.default.removeItem(at: self.context.temporaryDirectory) - - if let app = self.context.app - { - let fileURL = InstalledApp.refreshedIPAURL(for: app) - try FileManager.default.removeItem(at: fileURL) - } } catch { diff --git a/AltStore/Operations/SendAppOperation.swift b/AltStore/Operations/SendAppOperation.swift index 9d8835dc..15c40d1b 100644 --- a/AltStore/Operations/SendAppOperation.swift +++ b/AltStore/Operations/SendAppOperation.swift @@ -76,26 +76,41 @@ private extension SendAppOperation guard let appData = try? Data(contentsOf: fileURL) else { throw OperationError.invalidApp } guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID } - let request = PrepareAppRequest(udid: udid, contentSize: appData.count) + var request = PrepareAppRequest(udid: udid, contentSize: appData.count) + + if connection.server.connectionType == .local + { + // Background daemons have low memory limit (~6MB as of 13.5), + // so send just the file URL rather than the app data itself. + request.fileURL = fileURL + } - print("Sending request \(request)") connection.send(request) { (result) in switch result { case .failure(let error): completionHandler(.failure(error)) case .success: - print("Sending app data (\(appData.count) bytes)") - connection.send(appData, prependSize: false) { (result) in - switch result - { - case .failure(let error): - print("Failed to send app data (\(appData.count) bytes)") - completionHandler(.failure(error)) - - case .success: - print("Successfully sent app data (\(appData.count) bytes)") - completionHandler(.success(())) + if connection.server.connectionType == .local + { + // Sent file URL, so don't need to send any more. + completionHandler(.success(())) + } + else + { + print("Sending app data (\(appData.count) bytes)...") + + connection.send(appData, prependSize: false) { (result) in + switch result + { + case .failure(let error): + print("Failed to send app data (\(appData.count) bytes)") + completionHandler(.failure(error)) + + case .success: + print("Successfully sent app data (\(appData.count) bytes)") + completionHandler(.success(())) + } } } } diff --git a/AltStore/Server/Server.swift b/AltStore/Server/Server.swift index 76af2427..28e5af34 100644 --- a/AltStore/Server/Server.swift +++ b/AltStore/Server/Server.swift @@ -10,22 +10,6 @@ import Network import AltKit -extension ALTServerError -{ - init(_ error: E) - { - switch error - { - case let error as ALTServerError: self = error - case is DecodingError: self = ALTServerError(.invalidResponse) - case is EncodingError: self = ALTServerError(.invalidRequest) - default: - assertionFailure("Caught unknown error type") - self = ALTServerError(.unknown) - } - } -} - enum ConnectionError: LocalizedError { case serverNotFound @@ -42,13 +26,23 @@ enum ConnectionError: LocalizedError } } +extension Server +{ + enum ConnectionType + { + case wireless + case wired + case local + } +} + struct Server: Equatable { var identifier: String? = nil var service: NetService? = nil var isPreferred = false - var isWiredConnection = false + var connectionType: ConnectionType = .wireless } extension Server diff --git a/AltStore/Server/ServerManager.swift b/AltStore/Server/ServerManager.swift index 5e8ccf5b..959272c0 100644 --- a/AltStore/Server/ServerManager.swift +++ b/AltStore/Server/ServerManager.swift @@ -94,12 +94,18 @@ extension ServerManager connection.start(queue: self.dispatchQueue) } - if let incomingConnectionsSemaphore = self.incomingConnectionsSemaphore, server.isWiredConnection + if let incomingConnectionsSemaphore = self.incomingConnectionsSemaphore, server.connectionType != .wireless { - print("Waiting for new wired connection...") + print("Waiting for incoming connection...") let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() - CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionStartRequest, nil, nil, true) + + switch server.connectionType + { + case .wired: CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionStartRequest, nil, nil, true) + case .local: CFNotificationCenterPostNotification(notificationCenter, .localServerConnectionStartRequest, nil, nil, true) + case .wireless: break + } _ = incomingConnectionsSemaphore.wait(timeout: .now() + 10.0) @@ -114,7 +120,17 @@ extension ServerManager } else if let service = server.service { - let connection = NWConnection(to: .service(name: service.name, type: service.type, domain: service.domain, interface: nil), using: .tcp) + print("Connecting to service:", service) + + let parameters = NWParameters.tcp + + if server.connectionType == .local + { + // Prevent AltStore from initiating connections over multiple interfaces simultaneously 🤷‍♂️ + parameters.requiredInterfaceType = .loopback + } + + let connection = NWConnection(to: .service(name: service.name, type: service.type, domain: service.domain, interface: nil), using: parameters) start(connection) } }