From ae981057726a64d396d26fe2e3c6d0e7a89a5501 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 13 Jan 2020 10:17:30 -0800 Subject: [PATCH] [Both] Adds support for installing apps over USB --- AltKit/AltKit.h | 3 + AltKit/AltKit.m | 11 + AltKit/CFNotificationName+AltStore.h | 17 + AltKit/CFNotificationName+AltStore.m | 13 + AltServer/AltServer-Bridging-Header.h | 3 + AltServer/AppDelegate.swift | 2 + .../ALTNotificationConnection+Private.h | 24 ++ .../Connections/ALTNotificationConnection.h | 30 ++ .../Connections/ALTNotificationConnection.m | 93 ++++++ .../Connections/ALTWiredConnection+Private.h | 23 ++ AltServer/Connections/ALTWiredConnection.h | 25 ++ AltServer/Connections/ALTWiredConnection.m | 101 ++++++ AltServer/Connections/ClientConnection.swift | 231 ++++++++++++++ AltServer/Connections/ConnectionManager.swift | 291 ++++++++---------- AltServer/Devices/ALTDeviceManager.h | 13 + AltServer/Devices/ALTDeviceManager.mm | 149 ++++++++- AltStore.xcodeproj/project.pbxproj | 30 ++ AltStore/AltStore-Bridging-Header.h | 3 +- .../FetchAnisetteDataOperation.swift | 10 +- AltStore/Operations/FindServerOperation.swift | 69 ++++- AltStore/Server/Server.swift | 15 +- AltStore/Server/ServerManager.swift | 96 ++++-- Dependencies/AltSign | 2 +- 23 files changed, 1044 insertions(+), 210 deletions(-) create mode 100644 AltKit/AltKit.m create mode 100644 AltKit/CFNotificationName+AltStore.h create mode 100644 AltKit/CFNotificationName+AltStore.m create mode 100644 AltServer/Connections/ALTNotificationConnection+Private.h create mode 100644 AltServer/Connections/ALTNotificationConnection.h create mode 100644 AltServer/Connections/ALTNotificationConnection.m create mode 100644 AltServer/Connections/ALTWiredConnection+Private.h create mode 100644 AltServer/Connections/ALTWiredConnection.h create mode 100644 AltServer/Connections/ALTWiredConnection.m create mode 100644 AltServer/Connections/ClientConnection.swift diff --git a/AltKit/AltKit.h b/AltKit/AltKit.h index 759128e5..7d1b7380 100644 --- a/AltKit/AltKit.h +++ b/AltKit/AltKit.h @@ -7,3 +7,6 @@ // #import "NSError+ALTServerError.h" +#import "CFNotificationName+AltStore.h" + +extern uint16_t ALTDeviceListeningSocket; diff --git a/AltKit/AltKit.m b/AltKit/AltKit.m new file mode 100644 index 00000000..1e97d6b7 --- /dev/null +++ b/AltKit/AltKit.m @@ -0,0 +1,11 @@ +// +// AltKit.m +// AltKit +// +// Created by Riley Testut on 1/10/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +#import + +uint16_t ALTDeviceListeningSocket = 28151; diff --git a/AltKit/CFNotificationName+AltStore.h b/AltKit/CFNotificationName+AltStore.h new file mode 100644 index 00000000..01c2a336 --- /dev/null +++ b/AltKit/CFNotificationName+AltStore.h @@ -0,0 +1,17 @@ +// +// CFNotificationName+AltStore.h +// AltKit +// +// Created by Riley Testut on 1/10/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern CFNotificationName const ALTWiredServerConnectionAvailableRequest NS_SWIFT_NAME(wiredServerConnectionAvailableRequest); +extern CFNotificationName const ALTWiredServerConnectionAvailableResponse NS_SWIFT_NAME(wiredServerConnectionAvailableResponse); +extern CFNotificationName const ALTWiredServerConnectionStartRequest NS_SWIFT_NAME(wiredServerConnectionStartRequest); + +NS_ASSUME_NONNULL_END diff --git a/AltKit/CFNotificationName+AltStore.m b/AltKit/CFNotificationName+AltStore.m new file mode 100644 index 00000000..ac0eb5d1 --- /dev/null +++ b/AltKit/CFNotificationName+AltStore.m @@ -0,0 +1,13 @@ +// +// CFNotificationName+AltStore.m +// AltKit +// +// Created by Riley Testut on 1/10/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +#import "CFNotificationName+AltStore.h" + +CFNotificationName const ALTWiredServerConnectionAvailableRequest = CFSTR("io.altstore.Request.WiredServerConnectionAvailable"); +CFNotificationName const ALTWiredServerConnectionAvailableResponse = CFSTR("io.altstore.Response.WiredServerConnectionAvailable"); +CFNotificationName const ALTWiredServerConnectionStartRequest = CFSTR("io.altstore.Request.WiredServerConnectionStart"); diff --git a/AltServer/AltServer-Bridging-Header.h b/AltServer/AltServer-Bridging-Header.h index f02ceeee..05029051 100644 --- a/AltServer/AltServer-Bridging-Header.h +++ b/AltServer/AltServer-Bridging-Header.h @@ -3,3 +3,6 @@ // #import "ALTDeviceManager.h" +#import "ALTWiredConnection.h" +#import "ALTNotificationConnection.h" +#import "AltKit.h" diff --git a/AltServer/AppDelegate.swift b/AltServer/AppDelegate.swift index 55b686dd..8a7bf41c 100644 --- a/AltServer/AppDelegate.swift +++ b/AltServer/AppDelegate.swift @@ -59,7 +59,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { UserDefaults.standard.registerDefaults() UNUserNotificationCenter.current().delegate = self + ConnectionManager.shared.start() + ALTDeviceManager.shared.start() let item = NSStatusBar.system.statusItem(withLength: -1) guard let button = item.button else { return } diff --git a/AltServer/Connections/ALTNotificationConnection+Private.h b/AltServer/Connections/ALTNotificationConnection+Private.h new file mode 100644 index 00000000..ee484915 --- /dev/null +++ b/AltServer/Connections/ALTNotificationConnection+Private.h @@ -0,0 +1,24 @@ +// +// ALTNotificationConnection+Private.h +// AltServer +// +// Created by Riley Testut on 1/10/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +#import "ALTNotificationConnection.h" + +#include +#include + +NS_ASSUME_NONNULL_BEGIN + +@interface ALTNotificationConnection () + +@property (nonatomic, readonly) np_client_t client; + +- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AltServer/Connections/ALTNotificationConnection.h b/AltServer/Connections/ALTNotificationConnection.h new file mode 100644 index 00000000..50a0f59d --- /dev/null +++ b/AltServer/Connections/ALTNotificationConnection.h @@ -0,0 +1,30 @@ +// +// ALTNotificationConnection.h +// AltServer +// +// Created by Riley Testut on 1/10/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(NotificationConnection) +@interface ALTNotificationConnection : NSObject + +@property (nonatomic, copy, readonly) ALTDevice *device; + +@property (nonatomic, copy, nullable) void (^receivedNotificationHandler)(CFNotificationName notification); + +- (void)startListeningForNotifications:(NSArray *)notifications + completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; + +- (void)sendNotification:(CFNotificationName)notification + completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; + +- (void)disconnect; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AltServer/Connections/ALTNotificationConnection.m b/AltServer/Connections/ALTNotificationConnection.m new file mode 100644 index 00000000..6a3a895d --- /dev/null +++ b/AltServer/Connections/ALTNotificationConnection.m @@ -0,0 +1,93 @@ +// +// ALTNotificationConnection.m +// AltServer +// +// Created by Riley Testut on 1/10/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +#import "ALTNotificationConnection+Private.h" +#import "AltKit.h" + +void ALTDeviceReceivedNotification(const char *notification, void *user_data); + +@implementation ALTNotificationConnection + +- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client +{ + self = [super init]; + if (self) + { + _device = [device copy]; + _client = client; + } + + return self; +} + +- (void)dealloc +{ + [self disconnect]; +} + +- (void)disconnect +{ + np_client_free(self.client); + _client = nil; +} + +- (void)startListeningForNotifications:(NSArray *)notifications completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler +{ + dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + const char **notificationNames = (const char **)malloc((notifications.count + 1) * sizeof(char *)); + for (int i = 0; i < notifications.count; i++) + { + NSString *name = notifications[i]; + notificationNames[i] = name.UTF8String; + } + notificationNames[notifications.count] = NULL; // Must have terminating NULL entry. + + np_error_t result = np_observe_notifications(self.client, notificationNames); + if (result != NP_E_SUCCESS) + { + return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]); + } + + result = np_set_notify_callback(self.client, ALTDeviceReceivedNotification, (__bridge void *)self); + if (result != NP_E_SUCCESS) + { + return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]); + } + + completionHandler(YES, nil); + + free(notificationNames); + }); +} + +- (void)sendNotification:(CFNotificationName)notification completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler +{ + dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + np_error_t result = np_post_notification(self.client, [(__bridge NSString *)notification UTF8String]); + if (result == NP_E_SUCCESS) + { + completionHandler(YES, nil); + } + else + { + completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]); + } + }); +} + +@end + +void ALTDeviceReceivedNotification(const char *notification, void *user_data) +{ + ALTNotificationConnection *connection = (__bridge ALTNotificationConnection *)user_data; + + if (connection.receivedNotificationHandler) + { + connection.receivedNotificationHandler((__bridge CFNotificationName)@(notification)); + } +} diff --git a/AltServer/Connections/ALTWiredConnection+Private.h b/AltServer/Connections/ALTWiredConnection+Private.h new file mode 100644 index 00000000..24dc2c85 --- /dev/null +++ b/AltServer/Connections/ALTWiredConnection+Private.h @@ -0,0 +1,23 @@ +// +// ALTWiredConnection+Private.h +// AltServer +// +// Created by Riley Testut on 1/10/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +#import "ALTWiredConnection.h" + +#include + +NS_ASSUME_NONNULL_BEGIN + +@interface ALTWiredConnection () + +@property (nonatomic, readonly) idevice_connection_t connection; + +- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AltServer/Connections/ALTWiredConnection.h b/AltServer/Connections/ALTWiredConnection.h new file mode 100644 index 00000000..05801f98 --- /dev/null +++ b/AltServer/Connections/ALTWiredConnection.h @@ -0,0 +1,25 @@ +// +// ALTWiredConnection.h +// AltServer +// +// Created by Riley Testut on 1/10/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(WiredConnection) +@interface ALTWiredConnection : NSObject + +@property (nonatomic, copy, readonly) ALTDevice *device; + +- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler; +- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler; + +- (void)disconnect; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AltServer/Connections/ALTWiredConnection.m b/AltServer/Connections/ALTWiredConnection.m new file mode 100644 index 00000000..b7cdd265 --- /dev/null +++ b/AltServer/Connections/ALTWiredConnection.m @@ -0,0 +1,101 @@ +// +// ALTWiredConnection.m +// AltServer +// +// Created by Riley Testut on 1/10/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +#import "ALTWiredConnection+Private.h" +#import "AltKit.h" + +@implementation ALTWiredConnection + +- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection +{ + self = [super init]; + if (self) + { + _device = [device copy]; + _connection = connection; + } + + return self; +} + +- (void)dealloc +{ + [self disconnect]; +} + +- (void)disconnect +{ + idevice_disconnect(self.connection); + _connection = nil; +} + +- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler +{ + void (^finish)(NSError *error) = ^(NSError *error) { + if (error != nil) + { + NSLog(@"Send Error: %@", error); + completionHandler(NO, error); + } + else + { + completionHandler(YES, nil); + } + }; + + dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + NSMutableData *mutableData = [data mutableCopy]; + while (mutableData.length > 0) + { + uint32_t sentBytes = 0; + if (idevice_connection_send(self.connection, (const char *)mutableData.bytes, (int32_t)mutableData.length, &sentBytes) != IDEVICE_E_SUCCESS) + { + return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]); + } + + [mutableData replaceBytesInRange:NSMakeRange(0, sentBytes) withBytes:NULL length:0]; + } + + finish(nil); + }); +} + +- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler +{ + void (^finish)(NSData *data, NSError *error) = ^(NSData *data, NSError *error) { + if (error != nil) + { + NSLog(@"Receive Data Error: %@", error); + } + + completionHandler(data, error); + }; + + dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + char bytes[4096]; + NSMutableData *receivedData = [NSMutableData dataWithCapacity:expectedSize]; + + while (receivedData.length < expectedSize) + { + uint32_t size = MIN(4096, (uint32_t)expectedSize - (uint32_t)receivedData.length); + + uint32_t receivedBytes = 0; + if (idevice_connection_receive(self.connection, bytes, size, &receivedBytes) != IDEVICE_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]); + } + + NSData *data = [NSData dataWithBytesNoCopy:bytes length:receivedBytes freeWhenDone:NO]; + [receivedData appendData:data]; + } + + finish(receivedData, nil); + }); +} + +@end diff --git a/AltServer/Connections/ClientConnection.swift b/AltServer/Connections/ClientConnection.swift new file mode 100644 index 00000000..33b5562c --- /dev/null +++ b/AltServer/Connections/ClientConnection.swift @@ -0,0 +1,231 @@ +// +// ClientConnection.swift +// AltServer +// +// Created by Riley Testut on 1/9/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation +import Network + +import AltKit +import AltSign + +extension ClientConnection +{ + enum Connection + { + case wireless(NWConnection) + case wired(WiredConnection) + } +} + +class ClientConnection +{ + let connection: Connection + + init(connection: Connection) + { + self.connection = connection + } + + func disconnect() + { + switch self.connection + { + case .wireless(let connection): + switch connection.state + { + case .cancelled, .failed: + print("Disconnecting from \(connection.endpoint)...") + + default: + // State update handler might call this method again. + connection.cancel() + } + + case .wired(let connection): + connection.disconnect() + } + } + + func send(_ response: T, 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() + } + } + } + + do + { + let data = try JSONEncoder().encode(response) + let responseSize = withUnsafeBytes(of: Int32(data.count)) { Data($0) } + + self.send(responseSize) { (result) in + switch result + { + case .failure: finish(.failure(.init(.lostConnection))) + case .success: + + self.send(data) { (result) in + switch result + { + case .failure: finish(.failure(.init(.lostConnection))) + case .success: finish(.success(())) + } + } + } + } + } + catch + { + finish(.failure(.init(.invalidResponse))) + } + } + + func receiveRequest(completionHandler: @escaping (Result) -> Void) + { + let size = MemoryLayout.size + + print("Receiving request size") + self.receiveData(expectedBytes: size) { (result) in + do + { + let data = try result.get() + + print("Receiving request...") + let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) }) + self.receiveData(expectedBytes: expectedBytes) { (result) in + do + { + let data = try result.get() + let request = try JSONDecoder().decode(ServerRequest.self, from: data) + + print("Received installation request:", request) + completionHandler(.success(request)) + } + catch + { + completionHandler(.failure(ALTServerError(error))) + } + } + } + catch + { + completionHandler(.failure(ALTServerError(error))) + } + } + } + + func send(_ data: Data, completionHandler: @escaping (Result) -> Void) + { + switch self.connection + { + case .wireless(let connection): + connection.send(content: data, completion: .contentProcessed { (error) in + if let error = error + { + completionHandler(.failure(error)) + } + else + { + completionHandler(.success(())) + } + }) + + case .wired(let connection): + connection.send(data) { (success, error) in + if !success + { + completionHandler(.failure(ALTServerError(.lostConnection))) + } + else + { + completionHandler(.success(())) + } + } + } + } + + func receiveData(expectedBytes: Int, completionHandler: @escaping (Result) -> Void) + { + func finish(data: Data?, error: Error?) + { + do + { + let data = try self.process(data: data, error: error) + completionHandler(.success(data)) + } + catch + { + completionHandler(.failure(ALTServerError(error))) + } + } + + switch self.connection + { + case .wireless(let connection): + connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in + finish(data: data, error: error) + } + + case .wired(let connection): + connection.receiveData(withExpectedSize: expectedBytes) { (data, error) in + finish(data: data, error: error) + } + } + } +} + +extension ClientConnection: CustomStringConvertible +{ + var description: String { + switch self.connection + { + case .wireless(let connection): return "\(connection.endpoint) (Wireless)" + case .wired(let connection): return "\(connection.device.name) (Wired)" + } + } +} + +private extension ClientConnection +{ + func process(data: Data?, error: Error?) 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/AltServer/Connections/ConnectionManager.swift b/AltServer/Connections/ConnectionManager.swift index 8bafa135..94cef17a 100644 --- a/AltServer/Connections/ConnectionManager.swift +++ b/AltServer/Connections/ConnectionManager.swift @@ -54,10 +54,13 @@ class ConnectionManager private lazy var listener = self.makeListener() private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltServer.connections", qos: .utility) - private var connections = [NWConnection]() + private var connections = [ClientConnection]() + private var notificationConnections = [ALTDevice: NotificationConnection]() private init() { + NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidConnect(_:)), name: .deviceManagerDeviceDidConnect, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidDisconnect(_:)), name: .deviceManagerDeviceDidDisconnect, object: nil) } func start() @@ -77,6 +80,16 @@ class ConnectionManager default: break } } + + func disconnect(_ connection: ClientConnection) + { + connection.disconnect() + + if let index = self.connections.firstIndex(where: { $0 === connection }) + { + self.connections.remove(at: index) + } + } } private extension ConnectionManager @@ -127,67 +140,18 @@ private extension ConnectionManager } listener.newConnectionHandler = { [weak self] (connection) in - self?.awaitRequest(from: connection) + self?.prepare(connection) } return listener } - func disconnect(_ connection: NWConnection) + func prepare(_ connection: NWConnection) { - switch connection.state - { - case .cancelled, .failed: - print("Disconnecting from \(connection.endpoint)...") - - if let index = self.connections.firstIndex(where: { $0 === connection }) - { - self.connections.remove(at: index) - } - - default: - // State update handler will call this method again. - connection.cancel() - } - } - - 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.") - } - } -} - -private extension ConnectionManager -{ - func awaitRequest(from connection: NWConnection) - { - guard !self.connections.contains(where: { $0 === connection }) else { return } - self.connections.append(connection) + let clientConnection = ClientConnection(connection: .wireless(connection)) + + guard !self.connections.contains(where: { $0 === clientConnection }) else { return } + self.connections.append(clientConnection) connection.stateUpdateHandler = { [weak self] (state) in switch state @@ -196,17 +160,17 @@ private extension ConnectionManager case .ready: print("Connected to client:", connection.endpoint) - self?.handleRequest(for: connection) + self?.handleRequest(for: clientConnection) case .waiting: print("Waiting for connection...") case .failed(let error): print("Failed to connect to service \(connection.endpoint).", error) - self?.disconnect(connection) + self?.disconnect(clientConnection) case .cancelled: - self?.disconnect(connection) + self?.disconnect(clientConnection) @unknown default: break } @@ -214,17 +178,85 @@ private extension ConnectionManager connection.start(queue: self.dispatchQueue) } - - func handleRequest(for connection: NWConnection) +} + +private extension ConnectionManager +{ + func startNotificationConnection(to device: ALTDevice) { - self.receiveRequest(from: connection) { (result) in + ALTDeviceManager.shared.startNotificationConnection(to: device) { (connection, error) in + guard let connection = connection else { return } + + let notifications: [CFNotificationName] = [.wiredServerConnectionAvailableRequest, .wiredServerConnectionStartRequest] + connection.startListening(forNotifications: notifications.map { String($0.rawValue) }) { (success, error) in + guard success else { return } + + connection.receivedNotificationHandler = { [weak self, weak connection] (notification) in + guard let self = self, let connection = connection else { return } + self.handle(notification, for: connection) + } + + self.notificationConnections[device] = connection + } + } + } + + func stopNotificationConnection(to device: ALTDevice) + { + guard let connection = self.notificationConnections[device] else { return } + connection.disconnect() + + self.notificationConnections[device] = nil + } + + func handle(_ notification: CFNotificationName, for connection: NotificationConnection) + { + switch notification + { + case .wiredServerConnectionAvailableRequest: + connection.sendNotification(.wiredServerConnectionAvailableResponse) { (success, error) in + if let error = error, !success + { + print("Error sending wired server connection response.", error) + } + else + { + print("Sent wired server connection available response!") + } + } + + case .wiredServerConnectionStartRequest: + ALTDeviceManager.shared.startWiredConnection(to: connection.device) { (wiredConnection, error) in + if let wiredConnection = wiredConnection + { + print("Started wired server connection!") + + let clientConnection = ClientConnection(connection: .wired(wiredConnection)) + self.handleRequest(for: clientConnection) + } + else if let error = error + { + print("Error starting wired server connection.", error) + } + } + + default: break + } + } +} + +private extension ConnectionManager +{ + func handleRequest(for connection: ClientConnection) + { + connection.receiveRequest() { (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 + connection.send(response, shouldDisconnect: true) { (result) in print("Sent error response with result:", result) } @@ -236,34 +268,34 @@ private extension ConnectionManager case .success: let response = ErrorResponse(error: ALTServerError(.unknownRequest)) - self.send(response, to: connection, shouldDisconnect: true) { (result) in + connection.send(response, shouldDisconnect: true) { (result) in print("Sent unknown request response with result:", result) } } } } - func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: NWConnection) + func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: ClientConnection) { 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 + connection.send(errorResponse, 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 + connection.send(response, shouldDisconnect: true) { (result) in print("Sent anisette data response with result:", result) } } } } - func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: NWConnection) + func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: ClientConnection) { var temporaryURL: URL? @@ -278,19 +310,19 @@ private extension ConnectionManager switch result { case .failure(let error): - print("Failed to process request from \(connection.endpoint).", error) + print("Failed to process request from \(connection).", 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) + connection.send(response, shouldDisconnect: true) { (result) in + print("Sent install app error response to \(connection) with result:", result) } case .success: - print("Processed request from \(connection.endpoint).") + print("Processed request from \(connection).") 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) + connection.send(response, shouldDisconnect: true) { (result) in + print("Sent install app response to \(connection) with result:", result) } } } @@ -306,7 +338,7 @@ private extension ConnectionManager print("Awaiting begin installation request...") - self.receiveRequest(from: connection) { (result) in + connection.receiveRequest() { (result) in print("Received begin installation request with result:", result) switch result @@ -326,8 +358,8 @@ private extension ConnectionManager 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) + connection.send(response, shouldDisconnect: true) { (result) in + print("Sent unknown request error response to \(connection) with result:", result) } } } @@ -335,17 +367,15 @@ private extension ConnectionManager } } - func receiveApp(for request: PrepareAppRequest, from connection: NWConnection, completionHandler: @escaping (Result) -> Void) + func receiveApp(for request: PrepareAppRequest, from connection: ClientConnection, completionHandler: @escaping (Result) -> Void) { - connection.receive(minimumIncompleteLength: request.contentSize, maximumLength: request.contentSize) { (data, _, _, error) in + connection.receiveData(expectedBytes: request.contentSize) { (result) in do { print("Received app data!") - let data = try self.process(data: data, error: error, from: connection) - - print("Processed app data!") - + let data = try result.get() + guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) } print("Writing app data...") @@ -366,7 +396,7 @@ private extension ConnectionManager } } - func installApp(at fileURL: URL, toDeviceWithUDID udid: String, connection: NWConnection, completionHandler: @escaping (Result) -> Void) + func installApp(at fileURL: URL, toDeviceWithUDID udid: String, connection: ClientConnection, completionHandler: @escaping (Result) -> Void) { let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default) var isSending = false @@ -397,7 +427,7 @@ private extension ConnectionManager print("Progress:", progress.fractionCompleted) let response = InstallationProgressResponse(progress: progress.fractionCompleted) - self.send(response, to: connection) { (result) in + connection.send(response) { (result) in serialQueue.async { isSending = false } @@ -405,92 +435,19 @@ private extension ConnectionManager } }) } +} - func send(_ response: T, to connection: NWConnection, shouldDisconnect: Bool = false, completionHandler: @escaping (Result) -> Void) +private extension ConnectionManager +{ + @objc func deviceDidConnect(_ notification: Notification) { - 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) - let responseSize = withUnsafeBytes(of: Int32(data.count)) { Data($0) } - - connection.send(content: responseSize, completion: .contentProcessed { (error) in - do - { - if let error = error - { - throw error - } - - connection.send(content: data, completion: .contentProcessed { (error) in - if error != nil - { - finish(.failure(.init(.lostConnection))) - } - else - { - finish(.success(())) - } - }) - } - catch - { - finish(.failure(.init(.lostConnection))) - } - }) - } - catch - { - finish(.failure(.init(.invalidResponse))) - } + guard let device = notification.object as? ALTDevice else { return } + self.startNotificationConnection(to: device) } - func receiveRequest(from connection: NWConnection, completionHandler: @escaping (Result) -> Void) + @objc func deviceDidDisconnect(_ notification: Notification) { - let size = MemoryLayout.size - - print("Receiving request size") - connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in - do - { - let data = try self.process(data: data, error: error, from: connection) - - print("Receiving request...") - - 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 request = try JSONDecoder().decode(ServerRequest.self, from: data) - - print("Received installation request:", request) - - completionHandler(.success(request)) - } - catch - { - completionHandler(.failure(ALTServerError(error))) - } - } - } - catch - { - completionHandler(.failure(ALTServerError(error))) - } - } + guard let device = notification.object as? ALTDevice else { return } + self.stopNotificationConnection(to: device) } } diff --git a/AltServer/Devices/ALTDeviceManager.h b/AltServer/Devices/ALTDeviceManager.h index 8447b57d..f70af7a3 100644 --- a/AltServer/Devices/ALTDeviceManager.h +++ b/AltServer/Devices/ALTDeviceManager.h @@ -9,8 +9,14 @@ #import #import +@class ALTWiredConnection; +@class ALTNotificationConnection; + NS_ASSUME_NONNULL_BEGIN +extern NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidConnect); +extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidDisconnect); + @interface ALTDeviceManager : NSObject @property (class, nonatomic, readonly) ALTDeviceManager *sharedManager; @@ -18,8 +24,15 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSArray *connectedDevices; @property (nonatomic, readonly) NSArray *availableDevices; +- (void)start; + +/* App Installation */ - (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; +/* Connections */ +- (void)startWiredConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTWiredConnection *_Nullable connection, NSError *_Nullable error))completionHandler; +- (void)startNotificationConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTNotificationConnection *_Nullable connection, NSError *_Nullable error))completionHandler; + @end NS_ASSUME_NONNULL_END diff --git a/AltServer/Devices/ALTDeviceManager.mm b/AltServer/Devices/ALTDeviceManager.mm index 77e97c8b..204c4c97 100644 --- a/AltServer/Devices/ALTDeviceManager.mm +++ b/AltServer/Devices/ALTDeviceManager.mm @@ -7,7 +7,10 @@ // #import "ALTDeviceManager.h" -#import "NSError+ALTServerError.h" + +#import "AltKit.h" +#import "ALTWiredConnection+Private.h" +#import "ALTNotificationConnection+Private.h" #include #include @@ -17,8 +20,10 @@ #include void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *udid); +void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data); -NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError"; +NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification = @"ALTDeviceManagerDeviceDidConnectNotification"; +NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALTDeviceManagerDeviceDidDisconnectNotification"; @interface ALTDeviceManager () @@ -26,6 +31,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError"; @property (nonatomic, readonly) NSMutableDictionary *installationProgress; @property (nonatomic, readonly) dispatch_queue_t installationQueue; +@property (nonatomic, readonly) NSMutableSet *cachedDevices; + @end @implementation ALTDeviceManager @@ -50,11 +57,20 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError"; _installationProgress = [NSMutableDictionary dictionary]; _installationQueue = dispatch_queue_create("com.rileytestut.AltServer.InstallationQueue", DISPATCH_QUEUE_SERIAL); + + _cachedDevices = [NSMutableSet set]; } return self; } +- (void)start +{ + idevice_event_subscribe(ALTDeviceDidChangeConnectionStatus, nil); +} + +#pragma mark - App Installation - + - (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler { NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:4]; @@ -527,6 +543,89 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError"; return success; } +#pragma mark - Connections - + +- (void)startWiredConnectionToDevice:(ALTDevice *)altDevice completionHandler:(void (^)(ALTWiredConnection * _Nullable, NSError * _Nullable))completionHandler +{ + void (^finish)(ALTWiredConnection *connection, NSError *error) = ^(ALTWiredConnection *connection, NSError *error) { + if (error != nil) + { + NSLog(@"Wired Connection Error: %@", error); + } + + completionHandler(connection, error); + }; + + idevice_t device = NULL; + idevice_connection_t connection = NULL; + + /* Find Device */ + if (idevice_new_ignore_network(&device, altDevice.identifier.UTF8String) != IDEVICE_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]); + } + + /* Connect to Listening Socket */ + if (idevice_connect(device, ALTDeviceListeningSocket, &connection) != IDEVICE_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + idevice_free(device); + + ALTWiredConnection *wiredConnection = [[ALTWiredConnection alloc] initWithDevice:altDevice connection:connection]; + finish(wiredConnection, nil); +} + +- (void)startNotificationConnectionToDevice:(ALTDevice *)altDevice completionHandler:(void (^)(ALTNotificationConnection * _Nullable, NSError * _Nullable))completionHandler +{ + void (^finish)(ALTNotificationConnection *, NSError *) = ^(ALTNotificationConnection *connection, NSError *error) { + if (error != nil) + { + NSLog(@"Notification Connection Error: %@", error); + } + + completionHandler(connection, error); + }; + + idevice_t device = NULL; + lockdownd_client_t lockdownClient = NULL; + lockdownd_service_descriptor_t service = NULL; + + np_client_t client = NULL; + + /* Find Device */ + if (idevice_new_ignore_network(&device, altDevice.identifier.UTF8String) != IDEVICE_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]); + } + + /* Connect to Device */ + if (lockdownd_client_new_with_handshake(device, &lockdownClient, "altserver") != LOCKDOWN_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + /* Connect to Notification Proxy */ + if ((lockdownd_start_service(lockdownClient, "com.apple.mobile.notification_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + /* Connect to Client */ + if (np_client_new(device, service, &client) != NP_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + lockdownd_service_descriptor_free(service); + lockdownd_client_free(lockdownClient); + idevice_free(device); + + ALTNotificationConnection *notificationConnection = [[ALTNotificationConnection alloc] initWithDevice:altDevice client:client]; + completionHandler(notificationConnection, nil); +} + #pragma mark - Getters - - (NSArray *)connectedDevices @@ -683,3 +782,49 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid) NSLog(@"Installation Progress: %@", @(percent)); } } + +void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data) +{ + ALTDevice * (^deviceForUDID)(NSString *, NSArray *) = ^ALTDevice *(NSString *udid, NSArray *devices) { + for (ALTDevice *device in devices) + { + if ([device.identifier isEqualToString:udid]) + { + return device; + } + } + + return nil; + }; + + switch (event->event) + { + case IDEVICE_DEVICE_ADD: + { + ALTDevice *device = deviceForUDID(@(event->udid), ALTDeviceManager.sharedManager.connectedDevices); + [[NSNotificationCenter defaultCenter] postNotificationName:ALTDeviceManagerDeviceDidConnectNotification object:device]; + + if (device) + { + [ALTDeviceManager.sharedManager.cachedDevices addObject:device]; + } + + break; + } + + case IDEVICE_DEVICE_REMOVE: + { + ALTDevice *device = deviceForUDID(@(event->udid), ALTDeviceManager.sharedManager.cachedDevices.allObjects); + [[NSNotificationCenter defaultCenter] postNotificationName:ALTDeviceManagerDeviceDidDisconnectNotification object:device]; + + if (device) + { + [ALTDeviceManager.sharedManager.cachedDevices removeObject:device]; + } + + break; + } + + default: break; + } +} diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index b494ec16..0861f26a 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -121,6 +121,10 @@ BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; }; BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */; }; + BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */; }; + BF718BD123C91BD300A89F2D /* ALTWiredConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD023C91BD300A89F2D /* ALTWiredConnection.m */; }; + BF718BD523C928A300A89F2D /* ALTNotificationConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD423C928A300A89F2D /* ALTNotificationConnection.m */; }; + BF718BD823C93DB700A89F2D /* AltKit.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD723C93DB700A89F2D /* AltKit.m */; }; BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */; }; BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; }; BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* AppOperationContext.swift */; }; @@ -131,6 +135,7 @@ BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C122E659F700049BA1 /* AppContentViewController.swift */; }; BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C322E662D300049BA1 /* AppViewController.swift */; }; BF914C262383703800E713BA /* AltPlugin.mailbundle.zip in Resources */ = {isa = PBXBuildFile; fileRef = BF914C252383703800E713BA /* AltPlugin.mailbundle.zip */; }; + BF9A03C623C7DD0D000D08DB /* ClientConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9A03C523C7DD0D000D08DB /* ClientConnection.swift */; }; BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */; }; BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */; }; BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */; }; @@ -434,6 +439,15 @@ BF5C5FCD237DF69100EDD0C6 /* ALTPluginService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPluginService.h; sourceTree = ""; }; BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPluginService.m; sourceTree = ""; }; BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAltStoreViewController.swift; sourceTree = ""; }; + BF718BC723C919CC00A89F2D /* CFNotificationName+AltStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CFNotificationName+AltStore.h"; sourceTree = ""; }; + BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CFNotificationName+AltStore.m"; sourceTree = ""; }; + BF718BCF23C91BD300A89F2D /* ALTWiredConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWiredConnection.h; sourceTree = ""; }; + BF718BD023C91BD300A89F2D /* ALTWiredConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTWiredConnection.m; sourceTree = ""; }; + BF718BD223C91C7000A89F2D /* ALTWiredConnection+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ALTWiredConnection+Private.h"; sourceTree = ""; }; + BF718BD323C928A300A89F2D /* ALTNotificationConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTNotificationConnection.h; sourceTree = ""; }; + BF718BD423C928A300A89F2D /* ALTNotificationConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTNotificationConnection.m; sourceTree = ""; }; + BF718BD623C92B3700A89F2D /* ALTNotificationConnection+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ALTNotificationConnection+Private.h"; sourceTree = ""; }; + BF718BD723C93DB700A89F2D /* AltKit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AltKit.m; sourceTree = ""; }; BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardingNavigationController.swift; sourceTree = ""; }; BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = ""; }; BF770E5322BC044E002A40FE /* AppOperationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOperationContext.swift; sourceTree = ""; }; @@ -444,6 +458,7 @@ BF8F69C122E659F700049BA1 /* AppContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContentViewController.swift; sourceTree = ""; }; BF8F69C322E662D300049BA1 /* AppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = ""; }; BF914C252383703800E713BA /* AltPlugin.mailbundle.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = AltPlugin.mailbundle.zip; sourceTree = ""; }; + BF9A03C523C7DD0D000D08DB /* ClientConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConnection.swift; sourceTree = ""; }; BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewController.swift; sourceTree = ""; }; BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseCollectionViewCell.swift; sourceTree = ""; }; BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotCollectionViewCell.swift; sourceTree = ""; }; @@ -651,11 +666,14 @@ isa = PBXGroup; children = ( BFD52BD222A06EFB000B7ED1 /* AltKit.h */, + BF718BD723C93DB700A89F2D /* AltKit.m */, BFBAC8852295C90300587369 /* Result+Conveniences.swift */, BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */, BF1E3128229F474900370A3C /* ServerProtocol.swift */, BF1E314822A060F400370A3C /* NSError+ALTServerError.h */, BF1E314922A060F400370A3C /* NSError+ALTServerError.m */, + BF718BC723C919CC00A89F2D /* CFNotificationName+AltStore.h */, + BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */, ); path = AltKit; sourceTree = ""; @@ -1079,6 +1097,13 @@ isa = PBXGroup; children = ( BF1E3129229F474900370A3C /* ConnectionManager.swift */, + BF9A03C523C7DD0D000D08DB /* ClientConnection.swift */, + BF718BCF23C91BD300A89F2D /* ALTWiredConnection.h */, + BF718BD223C91C7000A89F2D /* ALTWiredConnection+Private.h */, + BF718BD023C91BD300A89F2D /* ALTWiredConnection.m */, + BF718BD323C928A300A89F2D /* ALTNotificationConnection.h */, + BF718BD623C92B3700A89F2D /* ALTNotificationConnection+Private.h */, + BF718BD423C928A300A89F2D /* ALTNotificationConnection.m */, ); path = Connections; sourceTree = ""; @@ -1547,9 +1572,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BF718BD823C93DB700A89F2D /* AltKit.m in Sources */, BF1E315A22A0620000370A3C /* NSError+ALTServerError.m in Sources */, BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */, BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */, + BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */, BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1559,7 +1586,10 @@ buildActionMask = 2147483647; files = ( BF3F786422CAA41E008FBD20 /* ALTDeviceManager+Installation.swift in Sources */, + BF718BD523C928A300A89F2D /* ALTNotificationConnection.m in Sources */, BF1E312B229F474900370A3C /* ConnectionManager.swift in Sources */, + BF9A03C623C7DD0D000D08DB /* ClientConnection.swift in Sources */, + BF718BD123C91BD300A89F2D /* ALTWiredConnection.m in Sources */, BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */, BF4586C52298CDB800BD7491 /* ALTDeviceManager.mm in Sources */, BF0241AA22F29CCD00129732 /* UserDefaults+AltServer.swift in Sources */, diff --git a/AltStore/AltStore-Bridging-Header.h b/AltStore/AltStore-Bridging-Header.h index 58fcad98..3f1cb414 100644 --- a/AltStore/AltStore-Bridging-Header.h +++ b/AltStore/AltStore-Bridging-Header.h @@ -2,7 +2,8 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // -#import "NSError+ALTServerError.h" +#import "AltKit.h" + #import "ALTAppPermission.h" #import "ALTPatreonBenefitType.h" #import "ALTSourceUserInfoKey.h" diff --git a/AltStore/Operations/FetchAnisetteDataOperation.swift b/AltStore/Operations/FetchAnisetteDataOperation.swift index 265c2d3e..a6878fbf 100644 --- a/AltStore/Operations/FetchAnisetteDataOperation.swift +++ b/AltStore/Operations/FetchAnisetteDataOperation.swift @@ -40,15 +40,23 @@ class FetchAnisetteDataOperation: ResultOperation ServerManager.shared.connect(to: server) { (result) in switch result { - case .failure(let error): self.finish(.failure(error)) + case .failure(let error): + self.finish(.failure(error)) case .success(let connection): + print("Sending anisette data request...") + let request = AnisetteDataRequest() connection.send(request) { (result) in + print("Sent anisette data request!") + switch result { case .failure(let error): self.finish(.failure(error)) case .success: + print("Waiting for anisette data...") connection.receiveResponse() { (result) in + print("Receiving anisette data:", result) + switch result { case .failure(let error): self.finish(.failure(error)) diff --git a/AltStore/Operations/FindServerOperation.swift b/AltStore/Operations/FindServerOperation.swift index 3eb0ef4b..2ba2549e 100644 --- a/AltStore/Operations/FindServerOperation.swift +++ b/AltStore/Operations/FindServerOperation.swift @@ -7,13 +7,26 @@ // 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 = +{ (center, observer, name, object, userInfo) in + NotificationCenter.default.post(name: .didReceiveWiredServerConnectionResponse, object: nil) +} + @objc(FindServerOperation) class FindServerOperation: ResultOperation { let group: OperationGroup + private var isWiredServerConnectionAvailable = false + init(group: OperationGroup) { self.group = group @@ -31,21 +44,49 @@ class FindServerOperation: ResultOperation return } - 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)) + let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() + + // 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) + + // Post notification. + CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionAvailableRequest, nil, nil, true) + + // Wait for either callback or timeout. + DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { + if self.isWiredServerConnectionAvailable + { + let server = Server(isWiredConnection: true) + 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)) + } + } } } } +private extension FindServerOperation +{ + @objc func didReceiveWiredServerConnectionResponse(_ notification: Notification) + { + self.isWiredServerConnectionAvailable = true + } +} + diff --git a/AltStore/Server/Server.swift b/AltStore/Server/Server.swift index 3fe7df82..568d091d 100644 --- a/AltStore/Server/Server.swift +++ b/AltStore/Server/Server.swift @@ -44,17 +44,22 @@ enum ConnectionError: LocalizedError struct Server: Equatable { - var identifier: String - var service: NetService + var identifier: String? = nil + var service: NetService? = nil var isPreferred = false - + var isWiredConnection = false +} + +extension Server +{ + // Defined in extension so we can still use the automatically synthesized initializer. init?(service: NetService, txtData: Data) - { + { let txtDictionary = NetService.dictionary(fromTXTRecord: txtData) guard let identifierData = txtDictionary["serverID"], let identifier = String(data: identifierData, encoding: .utf8) else { return nil } - self.identifier = identifier self.service = service + self.identifier = identifier } } diff --git a/AltStore/Server/ServerManager.swift b/AltStore/Server/ServerManager.swift index 240c6eb2..1864c2df 100644 --- a/AltStore/Server/ServerManager.swift +++ b/AltStore/Server/ServerManager.swift @@ -19,11 +19,14 @@ class ServerManager: NSObject private(set) var discoveredServers = [Server]() private let serviceBrowser = NetServiceBrowser() - private var services = Set() private let dispatchQueue = DispatchQueue(label: "io.altstore.ServerManager") + private lazy var connectionListener = self.makeListener() + private var incomingConnections = [NWConnection]() + private let incomingConnectionsSemaphore = DispatchSemaphore(value: 0) + private override init() { super.init() @@ -41,6 +44,8 @@ extension ServerManager self.isDiscovering = true self.serviceBrowser.searchForServices(ofType: ALTServerServiceType, inDomain: "") + + self.connectionListener.start(queue: self.dispatchQueue) } func stopDiscovering() @@ -55,30 +60,62 @@ extension ServerManager 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 + DispatchQueue.global().async { + func finish(_ result: Result) { - case .failed(let error): - print("Failed to connect to service \(server.service.name).", error) - completion(.failure(ConnectionError.connectionFailed)) + completion(result) + } + + func start(_ connection: NWConnection) + { + connection.stateUpdateHandler = { [unowned connection] (state) in + switch state + { + case .failed(let error): + print("Failed to connect to service \(server.service?.name ?? "").", error) + finish(.failure(ConnectionError.connectionFailed)) + + case .cancelled: + finish(.failure(OperationError.cancelled)) + + case .ready: + let connection = ServerConnection(server: server, connection: connection) + finish(.success(connection)) + + case .waiting: break + case .setup: break + case .preparing: break + @unknown default: break + } + } - case .cancelled: - completion(.failure(OperationError.cancelled)) + connection.start(queue: self.dispatchQueue) + } + + if server.isWiredConnection + { + print("Waiting for new wired connection...") - case .ready: - let connection = ServerConnection(server: server, connection: connection) - completion(.success(connection)) + let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() + CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionStartRequest, nil, nil, true) - case .waiting: break - case .setup: break - case .preparing: break - @unknown default: break + _ = self.incomingConnectionsSemaphore.wait(timeout: .now() + 10.0) + + if let connection = self.incomingConnections.popLast() + { + start(connection) + } + else + { + finish(.failure(ALTServerError(.connectionFailed))) + } + } + else if let service = server.service + { + let connection = NWConnection(to: .service(name: service.name, type: service.type, domain: service.domain, interface: nil), using: .tcp) + start(connection) } } - - connection.start(queue: self.dispatchQueue) } } @@ -93,6 +130,27 @@ private extension ServerManager self.discoveredServers.append(server) } + + func makeListener() -> NWListener + { + let listener = try! NWListener(using: .tcp, on: NWEndpoint.Port(rawValue: ALTDeviceListeningSocket)!) + listener.newConnectionHandler = { [weak self] (connection) in + self?.incomingConnections.append(connection) + self?.incomingConnectionsSemaphore.signal() + } + listener.stateUpdateHandler = { (state) in + switch state + { + case .ready: break + case .waiting, .setup: print("Listener socket waiting...") + case .cancelled: print("Listener socket cancelled.") + case .failed(let error): print("Listener socket failed:", error) + @unknown default: break + } + } + + return listener + } } extension ServerManager: NetServiceBrowserDelegate diff --git a/Dependencies/AltSign b/Dependencies/AltSign index 85d1aa1b..5f3d85cf 160000 --- a/Dependencies/AltSign +++ b/Dependencies/AltSign @@ -1 +1 @@ -Subproject commit 85d1aa1bd3f10b70c417ccf8e2fa4a6db81c6bac +Subproject commit 5f3d85cf2419e55210f1f349bc15436d91fb0087