Installs apps from AltStore via AltServer

This commit is contained in:
Riley Testut
2019-05-30 17:10:50 -07:00
parent f7beccbaa6
commit 58446d225c
18 changed files with 1137 additions and 78 deletions

View File

@@ -0,0 +1,325 @@
//
// ConnectionManager.swift
// AltServer
//
// Created by Riley Testut on 5/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Network
import AltKit
extension ALTServerError
{
init<E: Error>(_ error: E)
{
switch error
{
case let error as ALTServerError: self = error
case is DecodingError: self = ALTServerError(.invalidRequest)
case is EncodingError: self = ALTServerError(.invalidResponse)
default:
assertionFailure("Caught unknown error type")
self = ALTServerError(.unknown)
}
}
}
extension ConnectionManager
{
enum State
{
case notRunning
case connecting
case running(NWListener.Service)
case failed(Swift.Error)
}
}
class ConnectionManager
{
static let shared = ConnectionManager()
var stateUpdateHandler: ((State) -> Void)?
private(set) var state: State = .notRunning {
didSet {
self.stateUpdateHandler?(self.state)
}
}
private lazy var listener = self.makeListener()
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltServer.connections", qos: .utility)
private var connections = [NWConnection]()
private init()
{
}
func start()
{
switch self.state
{
case .notRunning, .failed: self.listener.start(queue: self.dispatchQueue)
default: break
}
}
func stop()
{
switch self.state
{
case .running: self.listener.cancel()
default: break
}
}
}
private extension ConnectionManager
{
func makeListener() -> NWListener
{
let listener = try! NWListener(using: .tcp)
listener.service = NWListener.Service(type: ALTServerServiceType)
listener.serviceRegistrationUpdateHandler = { (serviceChange) in
switch serviceChange
{
case .add(.service(let name, let type, let domain, _)):
let service = NWListener.Service(name: name, type: type, domain: domain, txtRecord: nil)
self.state = .running(service)
default: break
}
}
listener.stateUpdateHandler = { (state) in
switch state
{
case .ready: break
case .waiting, .setup: self.state = .connecting
case .cancelled: self.state = .notRunning
case .failed(let error):
self.state = .failed(error)
self.start()
@unknown default: break
}
}
listener.newConnectionHandler = { [weak self] (connection) in
self?.awaitRequest(from: connection)
}
return listener
}
func disconnect(_ 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)
connection.stateUpdateHandler = { [weak self] (state) in
switch state
{
case .setup, .preparing: break
case .ready:
print("Connected to client:", connection.endpoint)
case .waiting:
print("Waiting for connection...")
case .failed(let error):
print("Failed to connect to service \(connection.endpoint).", error)
self?.disconnect(connection)
case .cancelled:
self?.disconnect(connection)
@unknown default: break
}
}
connection.start(queue: self.dispatchQueue)
func finish(error: ALTServerError?)
{
if let error = error
{
print("Failed to process request from \(connection.endpoint).", error)
}
else
{
print("Processed request from \(connection.endpoint).")
}
let success = (error == nil)
let response = ServerResponse(success: success, error: error)
self.send(response, to: connection) { (result) in
print("Sent response to \(connection) with result:", result)
self.disconnect(connection)
}
}
self.receiveRequest(from: connection) { (result) in
switch result
{
case .failure(let error): finish(error: error)
case .success(let request, let fileURL):
ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: request.udid) { (success, error) in
let error = error.map { $0 as? ALTServerError ?? ALTServerError(.unknown) }
finish(error: error)
}
}
}
}
func receiveRequest(from connection: NWConnection, completionHandler: @escaping (Result<(ServerRequest, URL), ALTServerError>) -> Void)
{
let size = MemoryLayout<Int32>.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 request = try JSONDecoder().decode(ServerRequest.self, from: data)
self.process(request, from: connection, completionHandler: completionHandler)
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
func process(_ request: ServerRequest, from connection: NWConnection, completionHandler: @escaping (Result<(ServerRequest, URL), ALTServerError>) -> Void)
{
connection.receive(minimumIncompleteLength: request.contentSize, maximumLength: request.contentSize) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
guard ALTDeviceManager.shared.connectedDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".ipa")
try data.write(to: temporaryURL, options: .atomic)
print("Wrote app to URL:", temporaryURL)
completionHandler(.success((request, temporaryURL)))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
func send(_ response: ServerResponse, to connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
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
{
completionHandler(.failure(.init(.lostConnection)))
}
else
{
completionHandler(.success(()))
}
})
}
catch
{
completionHandler(.failure(.init(.lostConnection)))
}
})
}
catch
{
completionHandler(.failure(.init(.invalidResponse)))
}
}
}

View File

@@ -11,23 +11,13 @@
NS_ASSUME_NONNULL_BEGIN
extern NSErrorDomain const ALTDeviceErrorDomain;
typedef NS_ERROR_ENUM(ALTDeviceErrorDomain, ALTDeviceError)
{
ALTDeviceErrorUnknown,
ALTDeviceErrorNotConnected,
ALTDeviceErrorConnectionFailed,
ALTDeviceErrorWriteFailed,
};
@interface ALTDeviceManager : NSObject
@property (class, nonatomic, readonly) ALTDeviceManager *sharedManager;
@property (nonatomic, readonly) NSArray<ALTDevice *> *connectedDevices;
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDevice:(ALTDevice *)altDevice completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
@end

View File

@@ -7,6 +7,7 @@
//
#import "ALTDeviceManager.h"
#import "NSError+ALTServerError.h"
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
@@ -51,7 +52,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
return self;
}
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDevice:(ALTDevice *)altDevice completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
{
NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:100];
@@ -119,29 +120,29 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
}
/* Find Device */
if (idevice_new(&device, altDevice.identifier.UTF8String) != IDEVICE_E_SUCCESS)
if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS)
{
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorNotConnected userInfo:nil]);
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
return progress;
}
/* Connect to Device */
if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS)
{
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
/* Connect to Notification Proxy */
if ((lockdownd_start_service(client, "com.apple.mobile.notification_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
{
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
if (np_client_new(device, service, &np) != NP_E_SUCCESS)
{
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
@@ -159,13 +160,13 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
/* Connect to Installation Proxy */
if ((lockdownd_start_service(client, "com.apple.mobile.installation_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
{
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
if (instproxy_client_new(device, service, &ipc) != INSTPROXY_E_SUCCESS)
{
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
@@ -181,7 +182,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
/* Connect to AFC service */
if ((lockdownd_start_service(client, "com.apple.afc", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
{
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
@@ -190,7 +191,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
if (afc_client_new(device, service, &afc) != AFC_E_SUCCESS)
{
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
@@ -202,7 +203,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
{
if (afc_make_directory(afc, stagingURL.relativePath.fileSystemRepresentation) != AFC_E_SUCCESS)
{
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorWriteFailed userInfo:nil]);
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceWriteFailed userInfo:nil]);
return progress;
}
}
@@ -454,4 +455,6 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *udid)
{
progress.completedUnitCount = percent;
}
NSLog(@"Installation Progress: %@", @(percent));
}

View File

@@ -1,41 +0,0 @@
//
// Result+Conveniences.swift
// AltStore
//
// Created by Riley Testut on 5/22/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension Result
{
init(_ value: Success?, _ error: Failure?)
{
switch (value, error)
{
case (let value?, _): self = .success(value)
case (_, let error?): self = .failure(error)
case (nil, nil): preconditionFailure("Either value or error must be non-nil")
}
}
}
extension Result where Success == Void
{
init(_ success: Bool, _ error: Failure?)
{
if success
{
self = .success(())
}
else if let error = error
{
self = .failure(error)
}
else
{
preconditionFailure("Error must be non-nil if success is false")
}
}
}

View File

@@ -39,6 +39,20 @@ class ViewController: NSViewController
{
super.viewDidLoad()
ConnectionManager.shared.stateUpdateHandler = { (state) in
DispatchQueue.main.async {
switch state
{
case .notRunning: self.view.window?.title = ""
case .connecting: self.view.window?.title = "Connecting..."
case .running(let service): self.view.window?.title = service.name ?? ""
case .failed(let error): self.view.window?.title = error.localizedDescription
}
}
}
ConnectionManager.shared.start()
self.update()
}
@@ -336,7 +350,7 @@ private extension ViewController
let infoPlistURL = appBundleURL.appendingPathComponent("Info.plist")
guard var infoDictionary = NSDictionary(contentsOf: infoPlistURL) as? [String: Any] else { throw ALTError(.missingInfoPlist) }
infoDictionary["altstore"] = ["udid": device.identifier]
infoDictionary[Bundle.Info.deviceID] = device.identifier
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
let zippedURL = try FileManager.default.zipAppBundle(at: appBundleURL)
@@ -346,7 +360,7 @@ private extension ViewController
do
{
let resignedURL = try Result(resignedURL, error).get()
ALTDeviceManager.shared.installApp(at: resignedURL, to: device) { (success, error) in
ALTDeviceManager.shared.installApp(at: resignedURL, toDeviceWithUDID: device.identifier) { (success, error) in
let result = Result(success, error)
print(result)
}