Supports installing/refreshings apps w/o computer on jailbroken devices

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.
This commit is contained in:
Riley Testut
2020-06-04 19:53:10 -07:00
parent cb4656722a
commit 496aca642c
6 changed files with 131 additions and 78 deletions

View File

@@ -1225,9 +1225,9 @@ private extension AppManager
switch error.code switch error.code
{ {
case .deviceNotFound, .lostConnection: 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 return error
} }
else else

View File

@@ -10,14 +10,12 @@ import Foundation
import AltKit import AltKit
import Roxas import Roxas
private extension Notification.Name private let ReceivedServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{
static let didReceiveWiredServerConnectionResponse = Notification.Name("io.altstore.didReceiveWiredServerConnectionResponse")
}
private let ReceivedWiredServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{ (center, observer, name, object, userInfo) in { (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) @objc(FindServerOperation)
@@ -26,6 +24,7 @@ class FindServerOperation: ResultOperation<Server>
let context: OperationContext let context: OperationContext
private var isWiredServerConnectionAvailable = false private var isWiredServerConnectionAvailable = false
private var isLocalServerConnectionAvailable = false
init(context: OperationContext = OperationContext()) init(context: OperationContext = OperationContext())
{ {
@@ -49,48 +48,68 @@ class FindServerOperation: ResultOperation<Server>
} }
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
// Prepare observers to receive callback from wired server (if connected). // Prepare observers to receive callback from wired connection or background daemon (if available).
CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedWiredServerConnectionResponse, CFNotificationName.wiredServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately) CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedServerConnectionResponse, CFNotificationName.wiredServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately)
NotificationCenter.default.addObserver(self, selector: #selector(FindServerOperation.didReceiveWiredServerConnectionResponse(_:)), name: .didReceiveWiredServerConnectionResponse, object: nil) CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedServerConnectionResponse, CFNotificationName.localServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately)
// Post notification. // Post notifications.
CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionAvailableRequest, nil, nil, true) CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionAvailableRequest, nil, nil, true)
CFNotificationCenterPostNotification(notificationCenter, .localServerConnectionAvailableRequest, nil, nil, true)
// Wait for either callback or timeout. // Wait for either callback or timeout.
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { 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)) self.finish(.success(server))
} }
else else
{ {
if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred }) // No servers.
{ self.finish(.failure(ConnectionError.serverNotFound))
// 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))
}
} }
} }
} }
}
private extension FindServerOperation override func finish(_ result: Result<Server, Error>)
{
@objc func didReceiveWiredServerConnectionResponse(_ notification: Notification)
{ {
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
}
}
}

View File

@@ -144,7 +144,7 @@ class InstallAppOperation: ResultOperation<InstalledApp>
}) })
} }
let request = BeginInstallationRequest(activeProfiles: activeProfiles) let request = BeginInstallationRequest(activeProfiles: activeProfiles, bundleIdentifier: installedApp.resignedBundleIdentifier)
connection.send(request) { (result) in connection.send(request) { (result) in
switch result switch result
{ {
@@ -173,6 +173,21 @@ class InstallAppOperation: ResultOperation<InstalledApp>
{ {
self.cleanUp() 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) super.finish(result)
} }
} }
@@ -223,12 +238,6 @@ private extension InstallAppOperation
do do
{ {
try FileManager.default.removeItem(at: self.context.temporaryDirectory) 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 catch
{ {

View File

@@ -76,26 +76,41 @@ private extension SendAppOperation
guard let appData = try? Data(contentsOf: fileURL) else { throw OperationError.invalidApp } 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 } 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 connection.send(request) { (result) in
switch result switch result
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
case .success: case .success:
print("Sending app data (\(appData.count) bytes)") if connection.server.connectionType == .local
connection.send(appData, prependSize: false) { (result) in {
switch result // Sent file URL, so don't need to send any more.
{ completionHandler(.success(()))
case .failure(let error): }
print("Failed to send app data (\(appData.count) bytes)") else
completionHandler(.failure(error)) {
print("Sending app data (\(appData.count) bytes)...")
case .success: connection.send(appData, prependSize: false) { (result) in
print("Successfully sent app data (\(appData.count) bytes)") switch result
completionHandler(.success(())) {
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(()))
}
} }
} }
} }

View File

@@ -10,22 +10,6 @@ import Network
import AltKit import AltKit
extension ALTServerError
{
init<E: Error>(_ error: E)
{
switch error
{
case let error as ALTServerError: self = error
case is DecodingError: self = ALTServerError(.invalidResponse)
case is EncodingError: self = ALTServerError(.invalidRequest)
default:
assertionFailure("Caught unknown error type")
self = ALTServerError(.unknown)
}
}
}
enum ConnectionError: LocalizedError enum ConnectionError: LocalizedError
{ {
case serverNotFound case serverNotFound
@@ -42,13 +26,23 @@ enum ConnectionError: LocalizedError
} }
} }
extension Server
{
enum ConnectionType
{
case wireless
case wired
case local
}
}
struct Server: Equatable struct Server: Equatable
{ {
var identifier: String? = nil var identifier: String? = nil
var service: NetService? = nil var service: NetService? = nil
var isPreferred = false var isPreferred = false
var isWiredConnection = false var connectionType: ConnectionType = .wireless
} }
extension Server extension Server

View File

@@ -94,12 +94,18 @@ extension ServerManager
connection.start(queue: self.dispatchQueue) 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() 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) _ = incomingConnectionsSemaphore.wait(timeout: .now() + 10.0)
@@ -114,7 +120,17 @@ extension ServerManager
} }
else if let service = server.service 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) start(connection)
} }
} }