浏览代码

Initial Commit

Riley Testut 4 年之前
当前提交
e02828d16f

+ 7 - 0
.gitignore

@@ -0,0 +1,7 @@
+.DS_Store
+/.build
+/Packages
+/*.xcodeproj
+xcuserdata/
+DerivedData/
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

+ 8 - 0
.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 31 - 0
Package.swift

@@ -0,0 +1,31 @@
+// swift-tools-version:5.3
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+    name: "AltKit",
+    platforms: [
+        .iOS(.v12),
+    ],
+    products: [
+        // Products define the executables and libraries a package produces, and make them visible to other packages.
+        .library(
+            name: "AltKit",
+            targets: ["AltKit"]),
+    ],
+    dependencies: [
+        // Dependencies declare other packages that this package depends on.
+        // .package(url: /* package url */, from: "1.0.0"),
+    ],
+    targets: [
+        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+        // Targets can depend on other targets in this package, and on products in packages this package depends on.
+        .target(
+            name: "CAltKit",
+            dependencies: []),
+        .target(
+            name: "AltKit",
+            dependencies: ["CAltKit"]),
+    ]
+)

+ 84 - 0
README.md

@@ -0,0 +1,84 @@
+# AltKit
+
+AltKit allows apps to communicate with AltServers on the same WiFi network and enable features such as JIT compilation.
+
+## Installation
+
+To use AltKit in your app, add the following to your `Package.swift` file's dependencies:
+
+```
+.package(url: "https://github.com/rileytestut/AltKit.git", .upToNextMajor(from: "0.0.1")),
+```
+
+Next, add the AltKit package as a dependency for your target:
+
+```
+.product(name: "AltKit", package: "AltKit"),
+```
+
+Finally, right-click on your app's `Info.plist`, select "Open As > Source Code", then add the following entries:
+
+```
+<key>NSBonjourServices</key>
+<array>
+    <string>_altserver._tcp</string>
+</array>
+<key>NSLocalNetworkUsageDescription</key>
+<string>[Your app] uses the local network to find and communicate with AltServer.</string>
+```
+
+## Usage
+
+### Swift
+```
+import AltKit
+
+ServerManager.shared.startDiscovering()
+
+ServerManager.shared.autoconnect { result in
+    switch result
+    {
+    case .failure(let error): print("Could not auto-connect to server.", error)
+    case .success(let connection):
+        connection.enableUnsignedCodeExecution { result in
+            switch result
+            {
+            case .failure(let error): print("Could not enable JIT compilation.", error)
+            case .success: 
+                print("Successfully enabled JIT compilation!")
+                ServerManager.shared.stopDiscovering()
+            }
+            
+            connection.disconnect()
+        }
+    }
+}
+```
+
+### Objective-C
+```
+@import AltKit;
+
+[[ALTServerManager sharedManager] startDiscovering];
+
+[[ALTServerManager sharedManager] autoconnectWithCompletionHandler:^(ALTServerConnection *connection, NSError *error) {
+    if (error)
+    {
+        return NSLog(@"Could not auto-connect to server. %@", error);
+    }
+    
+    [connection enableUnsignedCodeExecutionWithCompletionHandler:^(BOOL success, NSError *error) {
+        if (success)
+        {
+            NSLog(@"Successfully enabled JIT compilation!");
+            [[ALTServerManager sharedManager] stopDiscovering];
+        }
+        else
+        {
+            NSLog(@"Could not enable JIT compilation. %@", error);
+        }
+        
+        [connection disconnect];
+    }];
+}];
+```

+ 38 - 0
Sources/AltKit/Extensions/ALTServerError+Conveniences.swift

@@ -0,0 +1,38 @@
+//
+//  ALTServerError+Conveniences.swift
+//  AltKit
+//
+//  Created by Riley Testut on 6/4/20.
+//  Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public extension ALTServerError
+{
+    init<E: Error>(_ error: E)
+    {
+        switch error
+        {
+        case let error as ALTServerError: self = error
+        case let error as ALTServerConnectionError:
+            self = ALTServerError(.connectionFailed, underlyingError: error)
+        case is DecodingError: self = ALTServerError(.invalidRequest, underlyingError: error)
+        case is EncodingError: self = ALTServerError(.invalidResponse, underlyingError: error)
+        case let error as NSError:
+            var userInfo = error.userInfo
+            if !userInfo.keys.contains(NSUnderlyingErrorKey)
+            {
+                // Assign underlying error (if there isn't already one).
+                userInfo[NSUnderlyingErrorKey] = error
+            }
+            
+            self = ALTServerError(.underlyingError, userInfo: userInfo)
+        }
+    }
+    
+    init<E: Error>(_ code: ALTServerError.Code, underlyingError: E)
+    {
+        self = ALTServerError(code, userInfo: [NSUnderlyingErrorKey: underlyingError])
+    }
+}

+ 76 - 0
Sources/AltKit/Extensions/Result+Conveniences.swift

@@ -0,0 +1,76 @@
+//
+//  Result+Conveniences.swift
+//  AltStore
+//
+//  Created by Riley Testut on 5/22/19.
+//  Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+extension Result
+{
+    var value: Success? {
+        switch self
+        {
+        case .success(let value): return value
+        case .failure: return nil
+        }
+    }
+    
+    var error: Failure? {
+        switch self
+        {
+        case .success: return nil
+        case .failure(let error): return error
+        }
+    }
+    
+    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")
+        }
+    }
+}
+
+extension Result
+{
+    init<T, U>(_ values: (T?, U?), _ error: Failure?) where Success == (T, U)
+    {
+        if let value1 = values.0, let value2 = values.1
+        {
+            self = .success((value1, value2))
+        }
+        else if let error = error
+        {
+            self = .failure(error)
+        }
+        else
+        {
+            preconditionFailure("Error must be non-nil if either provided values are nil")
+        }
+    }
+}

+ 18 - 0
Sources/AltKit/Server/Connection.swift

@@ -0,0 +1,18 @@
+//
+//  Connection.swift
+//  AltKit
+//
+//  Created by Riley Testut on 6/1/20.
+//  Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+import Foundation
+import Network
+
+public protocol Connection
+{
+    func send(_ data: Data, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
+    func receiveData(expectedSize: Int, completionHandler: @escaping (Result<Data, ALTServerError>) -> Void)
+    
+    func disconnect()
+}

+ 62 - 0
Sources/AltKit/Server/NetworkConnection.swift

@@ -0,0 +1,62 @@
+//
+//  NetworkConnection.swift
+//  AltKit
+//
+//  Created by Riley Testut on 6/1/20.
+//  Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+import Foundation
+import Network
+
+public class NetworkConnection: NSObject, Connection
+{
+    public let nwConnection: NWConnection
+    
+    public init(_ nwConnection: NWConnection)
+    {
+        self.nwConnection = nwConnection
+    }
+    
+    public func send(_ data: Data, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
+    {
+        self.nwConnection.send(content: data, completion: .contentProcessed { (error) in
+            if let error = error
+            {
+                completionHandler(.failure(.init(.lostConnection, underlyingError: error)))
+            }
+            else
+            {
+                completionHandler(.success(()))
+            }
+        })
+    }
+    
+    public func receiveData(expectedSize: Int, completionHandler: @escaping (Result<Data, ALTServerError>) -> Void)
+    {
+        self.nwConnection.receive(minimumIncompleteLength: expectedSize, maximumLength: expectedSize) { (data, context, isComplete, error) in
+            switch (data, error)
+            {
+            case (let data?, _): completionHandler(.success(data))
+            case (_, let error?): completionHandler(.failure(.init(.lostConnection, underlyingError: error)))
+            case (nil, nil): completionHandler(.failure(ALTServerError(.lostConnection)))
+            }
+        }
+    }
+    
+    public func disconnect()
+    {
+        switch self.nwConnection.state
+        {
+        case .cancelled, .failed: break
+        default: self.nwConnection.cancel()
+        }
+    }
+}
+
+extension NetworkConnection
+{
+    override public var description: String {
+        return "\(self.nwConnection.endpoint) (Network)"
+    }
+}

+ 44 - 0
Sources/AltKit/Server/Server.swift

@@ -0,0 +1,44 @@
+//
+//  Server.swift
+//  AltStore
+//
+//  Created by Riley Testut on 6/20/19.
+//  Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+@objc(ALTServer)
+public class Server: NSObject, Identifiable
+{
+    public let id: String
+    public let service: NetService
+    
+    public var name: String? {
+        return self.service.hostName
+    }
+    
+    public internal(set) var isPreferred = false
+    
+    public override var hash: Int {
+        return self.id.hashValue ^ self.service.name.hashValue
+    }
+    
+    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.id = identifier
+        self.service = service
+        
+        super.init()
+    }
+    
+    public override func isEqual(_ object: Any?) -> Bool
+    {
+        guard let server = object as? Server else { return false }
+        
+        return self.id == server.id && self.service.name == server.service.name // service.name is consistent, and is not the human readable name (hostName).
+    }
+}

+ 180 - 0
Sources/AltKit/Server/ServerConnection.swift

@@ -0,0 +1,180 @@
+//
+//  ServerConnection.swift
+//  AltStore
+//
+//  Created by Riley Testut on 1/7/20.
+//  Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+@objc(ALTServerConnection) @objcMembers
+public class ServerConnection: NSObject
+{
+    public let server: Server
+    public let connection: Connection
+    
+    init(server: Server, connection: Connection)
+    {
+        self.server = server
+        self.connection = connection
+    }
+    
+    deinit
+    {
+        self.connection.disconnect()
+    }
+    
+    @objc
+    public func disconnect()
+    {
+        self.connection.disconnect()
+    }
+}
+
+public extension ServerConnection
+{
+    func enableUnsignedCodeExecution(completion: @escaping (Result<Void, Error>) -> Void)
+    {
+        guard let udid = Bundle.main.object(forInfoDictionaryKey: "ALTDeviceID") as? String else {
+            return ServerManager.shared.callbackQueue.async {
+                completion(.failure(ConnectionError.unknownUDID))
+            }
+        }
+        
+        self.enableUnsignedCodeExecution(udid: udid, completion: completion)
+    }
+    
+    func enableUnsignedCodeExecution(udid: String, completion: @escaping (Result<Void, Error>) -> Void)
+    {
+        func finish(_ result: Result<Void, Error>)
+        {
+            ServerManager.shared.callbackQueue.async {
+                completion(result)
+            }
+        }
+        
+        let request = EnableUnsignedCodeExecutionRequest(udid: udid, processID: ProcessInfo.processInfo.processIdentifier)
+        
+        self.send(request) { (result) in
+            switch result
+            {
+            case .failure(let error): finish(.failure(error))
+            case .success:
+                self.receiveResponse() { (result) in
+                    switch result
+                    {
+                    case .failure(let error): finish(.failure(error))
+                    case .success(.error(let response)): finish(.failure(response.error))
+                    case .success(.enableUnsignedCodeExecution): finish(.success(()))
+                    case .success: finish(.failure(ALTServerError(.unknownResponse)))
+                    }
+                }
+            }
+        }
+    }
+}
+
+public extension ServerConnection
+{
+    @objc(enableUnsignedCodeExecutionWithCompletionHandler:)
+    func __enableUnsignedCodeExecution(completion: @escaping (Bool, Error?) -> Void)
+    {
+        self.enableUnsignedCodeExecution { result in
+            switch result {
+            case .failure(let error): completion(false, error)
+            case .success: completion(true, nil)
+            }
+        }
+    }
+    
+    @objc(enableUnsignedCodeExecutionWithUDID:completionHandler:)
+    func __enableUnsignedCodeExecution(udid: String, completion: @escaping (Bool, Error?) -> Void)
+    {
+        self.enableUnsignedCodeExecution(udid: udid) { result in
+            switch result {
+            case .failure(let error): completion(false, error)
+            case .success: completion(true, nil)
+            }
+        }
+    }
+}
+
+private extension ServerConnection
+{
+    func send<T: Encodable>(_ payload: T, completionHandler: @escaping (Result<Void, Error>) -> Void)
+    {
+        do
+        {
+            let data: Data
+            
+            if let payload = payload as? Data
+            {
+                data = payload
+            }
+            else
+            {
+                data = try JSONEncoder().encode(payload)
+            }
+            
+            func process<T>(_ result: Result<T, ALTServerError>) -> Bool
+            {
+                switch result
+                {
+                case .success: return true
+                case .failure(let error):
+                    completionHandler(.failure(error))
+                    return false
+                }
+            }
+            
+            let requestSize = Int32(data.count)
+            let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
+            
+            self.connection.send(requestSizeData) { (result) in
+                guard process(result) else { return }
+                
+                self.connection.send(data) { (result) in
+                    guard process(result) else { return }
+                    completionHandler(.success(()))
+                }
+            }
+        }
+        catch
+        {
+            print("Invalid request.", error)
+            completionHandler(.failure(ALTServerError(.invalidRequest)))
+        }
+    }
+    
+    func receiveResponse(completionHandler: @escaping (Result<ServerResponse, Error>) -> Void)
+    {
+        let size = MemoryLayout<Int32>.size
+        
+        self.connection.receiveData(expectedSize: size) { (result) in
+            do
+            {
+                let data = try result.get()
+                
+                let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
+                self.connection.receiveData(expectedSize: expectedBytes) { (result) in
+                    do
+                    {
+                        let data = try result.get()
+                        
+                        let response = try JSONDecoder().decode(ServerResponse.self, from: data)
+                        completionHandler(.success(response))
+                    }
+                    catch
+                    {
+                        completionHandler(.failure(ALTServerError(error)))
+                    }
+                }
+            }
+            catch
+            {
+                completionHandler(.failure(ALTServerError(error)))
+            }
+        }
+    }
+}

+ 341 - 0
Sources/AltKit/Server/ServerManager.swift

@@ -0,0 +1,341 @@
+//
+//  ServerManager.swift
+//  AltStore
+//
+//  Created by Riley Testut on 5/30/19.
+//  Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import Foundation
+import Network
+
+import UIKit
+
+@_exported import CAltKit
+
+public enum ConnectionError: LocalizedError
+{
+    case serverNotFound
+    case connectionFailed(Server)
+    case connectionDropped(Server)
+    case unknownUDID
+    
+    public var errorDescription: String? {
+        switch self
+        {
+        case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "")
+        case .connectionFailed: return NSLocalizedString("Could not connect to AltServer.", comment: "")
+        case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
+        case .unknownUDID: return NSLocalizedString("This device's UDID could not be determined.", comment: "")
+        }
+    }
+}
+
+@objc(ALTServerManager) @objcMembers
+public class ServerManager: NSObject
+{
+    public static let shared = ServerManager()
+    
+    private(set) var isDiscovering = false
+    private(set) var discoveredServers = [Server]()
+    
+    public var discoveredServerHandler: ((Server) -> Void)?
+    public var lostServerHandler: ((Server) -> Void)?
+    
+    public var callbackQueue: DispatchQueue = .main
+    
+    // Allow other AltKit queues to target this one.
+    internal let dispatchQueue = DispatchQueue(label: "io.altstore.altkit.ServerManager", qos: .utility, autoreleaseFrequency: .workItem)
+    
+    private let serviceBrowser = NetServiceBrowser()
+    private var resolvingServices = Set<NetService>()
+    
+    private var autoconnectGroup: DispatchGroup?
+    private var ignoredServers = Set<Server>()
+    
+    private override init()
+    {
+        super.init()
+        
+        self.serviceBrowser.delegate = self
+        self.serviceBrowser.includesPeerToPeer = false
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
+    }
+}
+
+public extension ServerManager
+{
+    @objc
+    func startDiscovering()
+    {
+        guard !self.isDiscovering else { return }
+        self.isDiscovering = true
+        
+        self.serviceBrowser.searchForServices(ofType: ALTServerServiceType, inDomain: "")
+    }
+    
+    @objc
+    func stopDiscovering()
+    {
+        guard self.isDiscovering else { return }
+        self.isDiscovering = false
+        
+        self.discoveredServers.removeAll()
+        self.ignoredServers.removeAll()
+        self.resolvingServices.removeAll()
+        
+        self.serviceBrowser.stop()
+    }
+    
+    func connect(to server: Server, completion: @escaping (Result<ServerConnection, Error>) -> Void)
+    {
+        var didFinish = false
+        
+        func finish(_ result: Result<ServerConnection, Error>)
+        {
+            guard !didFinish else { return }
+            didFinish = true
+            
+            self.ignoredServers.insert(server)
+            
+            self.callbackQueue.async {
+                completion(result)
+            }
+        }
+        
+        self.dispatchQueue.async {
+
+            print("Connecting to service:", server.service)
+            
+            let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp)
+            connection.stateUpdateHandler = { [unowned connection] (state) in
+                switch state
+                {
+                case .failed(let error):
+                    print("Failed to connect to service \(server.service.name).", error)
+                    finish(.failure(ConnectionError.connectionFailed(server)))
+                    
+                case .cancelled: finish(.failure(CocoaError(.userCancelled)))
+                    
+                case .ready:
+                    let networkConnection = NetworkConnection(connection)
+                    let serverConnection = ServerConnection(server: server, connection: networkConnection)
+                    finish(.success(serverConnection))
+                    
+                case .waiting: break
+                case .setup: break
+                case .preparing: break
+                @unknown default: break
+                }
+            }
+            
+            connection.start(queue: self.dispatchQueue)
+        }
+    }
+    
+    func autoconnect(completion: @escaping (Result<ServerConnection, Error>) -> Void)
+    {
+        self.dispatchQueue.async {
+            if case let availableServers = self.discoveredServers.filter({ !self.ignoredServers.contains($0) }),
+               let server = availableServers.first(where: { $0.isPreferred }) ?? availableServers.first
+            {
+                return self.connect(to: server, completion: completion)
+            }
+            
+            self.autoconnectGroup = DispatchGroup()
+            self.autoconnectGroup?.enter()
+            self.autoconnectGroup?.notify(queue: self.dispatchQueue) {
+                self.autoconnectGroup = nil
+                
+                guard
+                    case let availableServers = self.discoveredServers.filter({ !self.ignoredServers.contains($0) }),
+                    let server = availableServers.first(where: { $0.isPreferred }) ?? availableServers.first
+                else { return self.autoconnect(completion: completion)  }
+                
+                self.connect(to: server, completion: completion)
+            }
+        }
+    }
+}
+
+public extension ServerManager
+{
+    @objc(sharedManager)
+    class var __shared: ServerManager {
+        return ServerManager.shared
+    }
+    
+    @objc(connectToServer:completionHandler:)
+    func __connect(to server: Server, completion: @escaping (ServerConnection?, Error?) -> Void)
+    {
+        self.connect(to: server) { result in
+            completion(result.value, result.error)
+        }
+    }
+    
+    @objc(autoconnectWithCompletionHandler:)
+    func __autoconnect(completion: @escaping (ServerConnection?, Error?) -> Void)
+    {
+        self.autoconnect { result in
+            completion(result.value, result.error)
+        }
+    }
+}
+
+private extension ServerManager
+{
+    func addDiscoveredServer(_ server: Server)
+    {
+        self.dispatchQueue.async {
+            let serverID = Bundle.main.object(forInfoDictionaryKey: "ALTServerID") as? String
+            server.isPreferred = (server.id == serverID)
+            
+            guard !self.discoveredServers.contains(server) else { return }
+            
+            self.discoveredServers.append(server)
+            
+            if let callback = self.discoveredServerHandler
+            {
+                self.callbackQueue.async {
+                    callback(server)
+                }
+            }
+        }
+    }
+    
+    func removeDiscoveredServer(_ server: Server)
+    {
+        self.dispatchQueue.async {
+            guard let index = self.discoveredServers.firstIndex(of: server) else { return }
+            
+            self.discoveredServers.remove(at: index)
+            
+            if let callback = self.lostServerHandler
+            {
+                self.callbackQueue.async {
+                    callback(server)
+                }
+            }
+        }
+    }
+}
+
+@objc
+private extension ServerManager
+{
+    @objc
+    func didEnterBackground(_ notification: Notification)
+    {
+        guard self.isDiscovering else { return }
+        
+        self.resolvingServices.removeAll()
+        self.discoveredServers.removeAll()
+        self.serviceBrowser.stop()
+    }
+    
+    @objc
+    func willEnterForeground(_ notification: Notification)
+    {
+        guard self.isDiscovering else { return }
+        
+        self.serviceBrowser.searchForServices(ofType: ALTServerServiceType, inDomain: "")
+    }
+}
+
+extension ServerManager: NetServiceBrowserDelegate
+{
+    public func netServiceBrowserWillSearch(_ browser: NetServiceBrowser)
+    {
+        print("Discovering servers...")
+    }
+    
+    public func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser)
+    {
+        print("Stopped discovering servers.")
+    }
+    
+    public func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber])
+    {
+        print("Failed to discover servers.", errorDict)
+    }
+    
+    public func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool)
+    {
+        self.dispatchQueue.async {
+            service.delegate = self
+            
+            if let txtData = service.txtRecordData(), let server = Server(service: service, txtData: txtData)
+            {
+                self.addDiscoveredServer(server)
+            }
+            else
+            {
+                service.resolve(withTimeout: 3.0)
+                self.resolvingServices.insert(service)
+            }
+            
+            self.autoconnectGroup?.enter()
+            
+            if !moreComing
+            {
+                self.autoconnectGroup?.leave()
+            }
+        }
+    }
+    
+    public func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool)
+    {
+        if let server = self.discoveredServers.first(where: { $0.service == service })
+        {
+            self.removeDiscoveredServer(server)
+        }
+    }
+}
+
+extension ServerManager: NetServiceDelegate
+{
+    public func netServiceDidResolveAddress(_ service: NetService)
+    {
+        defer {
+            self.dispatchQueue.async {
+                guard self.resolvingServices.contains(service) else { return }
+                self.resolvingServices.remove(service)
+                
+                self.autoconnectGroup?.leave()
+            }
+        }
+        
+        guard let data = service.txtRecordData(), let server = Server(service: service, txtData: data) else { return }
+        self.addDiscoveredServer(server)
+    }
+    
+    public func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber])
+    {
+        print("Error resolving net service \(sender).", errorDict)
+        
+        self.dispatchQueue.async {
+            guard self.resolvingServices.contains(sender) else { return }
+            self.resolvingServices.remove(sender)
+            
+            self.autoconnectGroup?.leave()
+        }
+    }
+    
+    public func netService(_ sender: NetService, didUpdateTXTRecord data: Data)
+    {
+        let txtDict = NetService.dictionary(fromTXTRecord: data)
+        print("Service \(sender) updated TXT Record:", txtDict)
+    }
+    
+    public func netServiceDidStop(_ sender: NetService)
+    {
+        self.dispatchQueue.async {
+            guard self.resolvingServices.contains(sender) else { return }
+            self.resolvingServices.remove(sender)
+            
+            self.autoconnectGroup?.leave()
+        }
+    }
+}

+ 163 - 0
Sources/AltKit/Server/ServerProtocol.swift

@@ -0,0 +1,163 @@
+//
+//  ServerProtocol.swift
+//  AltServer
+//
+//  Created by Riley Testut on 5/24/19.
+//  Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public let ALTServerServiceType = "_altserver._tcp"
+
+protocol ServerMessageProtocol: Codable
+{
+    var version: Int { get }
+    var identifier: String { get }
+}
+
+public enum ServerRequest: Decodable
+{
+    case enableUnsignedCodeExecution(EnableUnsignedCodeExecutionRequest)
+    case unknown(identifier: String, version: Int)
+    
+    var identifier: String {
+        switch self
+        {
+        case .enableUnsignedCodeExecution(let request): return request.identifier
+        case .unknown(let identifier, _): return identifier
+        }
+    }
+    
+    var version: Int {
+        switch self
+        {
+        case .enableUnsignedCodeExecution(let request): return request.version
+        case .unknown(_, let version): return version
+        }
+    }
+    
+    private enum CodingKeys: String, CodingKey
+    {
+        case identifier
+        case version
+    }
+    
+    public init(from decoder: Decoder) throws
+    {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        
+        let version = try container.decode(Int.self, forKey: .version)
+        
+        let identifier = try container.decode(String.self, forKey: .identifier)
+        switch identifier
+        {
+        case "EnableUnsignedCodeExecutionRequest":
+            let request = try EnableUnsignedCodeExecutionRequest(from: decoder)
+            self = .enableUnsignedCodeExecution(request)
+            
+        default:
+            self = .unknown(identifier: identifier, version: version)
+        }
+    }
+}
+
+public enum ServerResponse: Decodable
+{
+    case enableUnsignedCodeExecution(EnableUnsignedCodeExecutionResponse)
+    case error(ErrorResponse)
+    case unknown(identifier: String, version: Int)
+    
+    var identifier: String {
+        switch self
+        {
+        case .enableUnsignedCodeExecution(let response): return response.identifier
+        case .error(let response): return response.identifier
+        case .unknown(let identifier, _): return identifier
+        }
+    }
+    
+    var version: Int {
+        switch self
+        {
+        case .enableUnsignedCodeExecution(let response): return response.version
+        case .error(let response): return response.version
+        case .unknown(_, let version): return version
+        }
+    }
+    
+    private enum CodingKeys: String, CodingKey
+    {
+        case identifier
+        case version
+    }
+    
+    public init(from decoder: Decoder) throws
+    {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        
+        let version = try container.decode(Int.self, forKey: .version)
+        
+        let identifier = try container.decode(String.self, forKey: .identifier)
+        switch identifier
+        {
+        case "EnableUnsignedCodeExecutionResponse":
+            let response = try EnableUnsignedCodeExecutionResponse(from: decoder)
+            self = .enableUnsignedCodeExecution(response)
+            
+        case "ErrorResponse":
+            let response = try ErrorResponse(from: decoder)
+            self = .error(response)
+            
+        default:
+            self = .unknown(identifier: identifier, version: version)
+        }
+    }
+}
+
+// _Don't_ provide generic SuccessResponse, as that would prevent us
+// from easily changing response format for a request in the future.
+public struct ErrorResponse: ServerMessageProtocol
+{
+    public var version = 2
+    public var identifier = "ErrorResponse"
+    
+    public var error: ALTServerError {
+        return self.serverError?.error ?? ALTServerError(self.errorCode)
+    }
+    private var serverError: CodableServerError?
+    
+    // Legacy (v1)
+    private var errorCode: ALTServerError.Code
+    
+    public init(error: ALTServerError)
+    {
+        self.serverError = CodableServerError(error: error)
+        self.errorCode = error.code
+    }
+}
+
+public struct EnableUnsignedCodeExecutionRequest: ServerMessageProtocol
+{
+    public var version = 1
+    public var identifier = "EnableUnsignedCodeExecutionRequest"
+    
+    public var udid: String
+    public var processID: Int32
+
+    public init(udid: String, processID: Int32)
+    {
+        self.udid = udid
+        self.processID = processID
+    }
+}
+
+public struct EnableUnsignedCodeExecutionResponse: ServerMessageProtocol
+{
+    public var version = 1
+    public var identifier = "EnableUnsignedCodeExecutionResponse"
+    
+    public init()
+    {
+    }
+}

+ 126 - 0
Sources/AltKit/Types/CodableServerError.swift

@@ -0,0 +1,126 @@
+//
+//  CodableServerError.swift
+//  AltKit
+//
+//  Created by Riley Testut on 3/5/20.
+//  Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
+extension ALTServerError.Code: Codable {}
+
+extension CodableServerError
+{
+    enum UserInfoValue: Codable
+    {
+        case string(String)
+        case error(NSError)
+        
+        public init(from decoder: Decoder) throws
+        {
+            let container = try decoder.singleValueContainer()
+
+            if
+                let data = try? container.decode(Data.self),
+                let error = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: data)
+            {
+                self = .error(error)
+            }
+            else if let string = try? container.decode(String.self)
+            {
+                self = .string(string)
+            }
+            else
+            {
+                throw DecodingError.dataCorruptedError(in: container, debugDescription: "UserInfoValue value cannot be decoded")
+            }
+        }
+        
+        func encode(to encoder: Encoder) throws
+        {
+            var container = encoder.singleValueContainer()
+            
+            switch self
+            {
+            case .string(let string): try container.encode(string)
+            case .error(let error):
+                guard let data = try? NSKeyedArchiver.archivedData(withRootObject: error, requiringSecureCoding: true) else {
+                    let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "UserInfoValue value \(self) cannot be encoded")
+                    throw EncodingError.invalidValue(self, context)
+                }
+                
+                try container.encode(data)
+            }
+        }
+    }
+}
+
+struct CodableServerError: Codable
+{
+    var error: ALTServerError {
+        return ALTServerError(self.errorCode, userInfo: self.userInfo ?? [:])
+    }
+    
+    private var errorCode: ALTServerError.Code
+    private var userInfo: [String: Any]?
+    
+    private enum CodingKeys: String, CodingKey
+    {
+        case errorCode
+        case userInfo
+    }
+
+    init(error: ALTServerError)
+    {
+        self.errorCode = error.code
+        
+        var userInfo = error.userInfo
+        if let localizedRecoverySuggestion = (error as NSError).localizedRecoverySuggestion
+        {
+            userInfo[NSLocalizedRecoverySuggestionErrorKey] = localizedRecoverySuggestion
+        }
+        
+        if !userInfo.isEmpty
+        {
+            self.userInfo = userInfo
+        }
+    }
+    
+    init(from decoder: Decoder) throws
+    {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        
+        let errorCode = try container.decode(Int.self, forKey: .errorCode)
+        self.errorCode = ALTServerError.Code(rawValue: errorCode) ?? .unknown
+        
+        let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .userInfo)
+        
+        let userInfo = rawUserInfo?.mapValues { (value) -> Any in
+            switch value
+            {
+            case .string(let string): return string
+            case .error(let error): return error
+            }
+        }
+        self.userInfo = userInfo
+    }
+    
+    func encode(to encoder: Encoder) throws
+    {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(self.error.code.rawValue, forKey: .errorCode)
+        
+        let rawUserInfo = self.userInfo?.compactMapValues { (value) -> UserInfoValue? in
+            switch value
+            {
+            case let string as String: return .string(string)
+            case let error as NSError: return .error(error)
+            default: return nil
+            }
+        }
+        try container.encodeIfPresent(rawUserInfo, forKey: .userInfo)
+    }
+}
+

+ 69 - 0
Sources/CAltKit/NSError+ALTServerError.h

@@ -0,0 +1,69 @@
+//
+//  NSError+ALTServerError.h
+//  AltStore
+//
+//  Created by Riley Testut on 5/30/19.
+//  Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+extern NSErrorDomain const AltServerErrorDomain;
+extern NSErrorDomain const AltServerInstallationErrorDomain;
+extern NSErrorDomain const AltServerConnectionErrorDomain;
+
+extern NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey;
+extern NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey;
+extern NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey;
+extern NSErrorUserInfoKey const ALTAppNameErrorKey;
+extern NSErrorUserInfoKey const ALTDeviceNameErrorKey;
+
+typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError)
+{
+    ALTServerErrorUnderlyingError = -1,
+    
+    ALTServerErrorUnknown = 0,
+    ALTServerErrorConnectionFailed = 1,
+    ALTServerErrorLostConnection = 2,
+    
+    ALTServerErrorDeviceNotFound = 3,
+    ALTServerErrorDeviceWriteFailed = 4,
+    
+    ALTServerErrorInvalidRequest = 5,
+    ALTServerErrorInvalidResponse = 6,
+    
+    ALTServerErrorInvalidApp = 7,
+    ALTServerErrorInstallationFailed = 8,
+    ALTServerErrorMaximumFreeAppLimitReached = 9,
+    ALTServerErrorUnsupportediOSVersion = 10,
+    
+    ALTServerErrorUnknownRequest = 11,
+    ALTServerErrorUnknownResponse = 12,
+    
+    ALTServerErrorInvalidAnisetteData = 13,
+    ALTServerErrorPluginNotFound = 14,
+    
+    ALTServerErrorProfileNotFound = 15,
+    
+    ALTServerErrorAppDeletionFailed = 16,
+    
+    ALTServerErrorRequestedAppNotRunning = 100,
+};
+
+typedef NS_ERROR_ENUM(AltServerConnectionErrorDomain, ALTServerConnectionError)
+{
+    ALTServerConnectionErrorUnknown,
+    ALTServerConnectionErrorDeviceLocked,
+    ALTServerConnectionErrorInvalidRequest,
+    ALTServerConnectionErrorInvalidResponse,
+    ALTServerConnectionErrorUSBMUXD,
+    ALTServerConnectionErrorSSL,
+    ALTServerConnectionErrorTimedOut,
+};
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface NSError (ALTServerError)
+@end
+
+NS_ASSUME_NONNULL_END

+ 305 - 0
Sources/CAltKit/NSError+ALTServerError.m

@@ -0,0 +1,305 @@
+//
+//  NSError+ALTServerError.m
+//  AltStore
+//
+//  Created by Riley Testut on 5/30/19.
+//  Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import "NSError+ALTServerError.h"
+
+NSErrorDomain const AltServerErrorDomain = @"com.rileytestut.AltServer";
+NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServer.Installation";
+NSErrorDomain const AltServerConnectionErrorDomain = @"com.rileytestut.AltServer.Connection";
+
+NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey = @"underlyingErrorDomain";
+NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode";
+NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdentifier";
+NSErrorUserInfoKey const ALTAppNameErrorKey = @"appName";
+NSErrorUserInfoKey const ALTDeviceNameErrorKey = @"deviceName";
+
+@implementation NSError (ALTServerError)
+
++ (void)load
+{
+    [NSError setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey  _Nonnull userInfoKey) {
+        if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
+        {
+            return [error altserver_localizedFailureReason];
+        }
+        else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey])
+        {
+            return [error altserver_localizedRecoverySuggestion];
+        }
+        
+        return nil;
+    }];
+    
+    [NSError setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey  _Nonnull userInfoKey) {
+        if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey])
+        {
+            return [error altserver_connection_localizedDescription];
+        }
+        else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey])
+        {
+            return [error altserver_connection_localizedRecoverySuggestion];
+        }
+//        else if ([userInfoKey isEqualToString:NSLocalizedFailureErrorKey])
+//        {
+//            return @"";
+//        }
+        
+        return nil;
+    }];
+}
+
+//- (nullable NSString *)altserver_localizedDescription
+//{
+//    switch ((ALTServerError)self.code)
+//    {
+//        case ALTServerErrorUnderlyingError:
+//        {
+//            NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
+//            return [underlyingError localizedDescription];
+//        }
+//
+//        default:
+//        {
+//            return [self altserver_localizedFailureReason];
+//        }
+//    }
+//}
+
+- (nullable NSString *)altserver_localizedFailureReason
+{
+    switch ((ALTServerError)self.code)
+    {
+        case ALTServerErrorUnderlyingError:
+        {
+            NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
+            if (underlyingError.localizedFailureReason != nil)
+            {
+                return underlyingError.localizedFailureReason;
+            }
+
+            NSString *underlyingErrorCode = self.userInfo[ALTUnderlyingErrorCodeErrorKey];
+            if (underlyingErrorCode != nil)
+            {
+                return [NSString stringWithFormat:NSLocalizedString(@"Error code: %@", @""), underlyingErrorCode];
+            }
+            
+            return nil;
+        }
+        
+        case ALTServerErrorUnknown:
+            return NSLocalizedString(@"An unknown error occured.", @"");
+            
+        case ALTServerErrorConnectionFailed:
+#if TARGET_OS_OSX
+            return NSLocalizedString(@"There was an error connecting to the device.", @"");
+#else
+            return NSLocalizedString(@"Could not connect to AltServer.", @"");
+#endif
+            
+        case ALTServerErrorLostConnection:
+            return NSLocalizedString(@"Lost connection to AltServer.", @"");
+            
+        case ALTServerErrorDeviceNotFound:
+            return NSLocalizedString(@"AltServer could not find this device.", @"");
+            
+        case ALTServerErrorDeviceWriteFailed:
+            return NSLocalizedString(@"Failed to write app data to device.", @"");
+            
+        case ALTServerErrorInvalidRequest:
+            return NSLocalizedString(@"AltServer received an invalid request.", @"");
+            
+        case ALTServerErrorInvalidResponse:
+            return NSLocalizedString(@"AltServer sent an invalid response.", @"");
+            
+        case ALTServerErrorInvalidApp:
+            return NSLocalizedString(@"The app is invalid.", @"");
+            
+        case ALTServerErrorInstallationFailed:
+            return NSLocalizedString(@"An error occured while installing the app.", @"");
+            
+        case ALTServerErrorMaximumFreeAppLimitReached:
+            return NSLocalizedString(@"Cannot activate more than 3 apps and app extensions.", @"");
+            
+        case ALTServerErrorUnsupportediOSVersion:
+            return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
+            
+        case ALTServerErrorUnknownRequest:
+            return NSLocalizedString(@"AltServer does not support this request.", @"");
+            
+        case ALTServerErrorUnknownResponse:
+            return NSLocalizedString(@"Received an unknown response from AltServer.", @"");
+            
+        case ALTServerErrorInvalidAnisetteData:
+            return NSLocalizedString(@"The provided anisette data is invalid.", @"");
+            
+        case ALTServerErrorPluginNotFound:
+            return NSLocalizedString(@"AltServer could not connect to Mail plug-in.", @"");
+            
+        case ALTServerErrorProfileNotFound:
+            return [self profileErrorLocalizedDescriptionWithBaseDescription:NSLocalizedString(@"Could not find profile", "")];
+            
+        case ALTServerErrorAppDeletionFailed:
+            return NSLocalizedString(@"An error occured while removing the app.", @"");
+            
+        case ALTServerErrorRequestedAppNotRunning:
+        {
+            NSString *appName = self.userInfo[ALTAppNameErrorKey] ?: NSLocalizedString(@"The requested app", @"");
+            NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @"");
+            return [NSString stringWithFormat:NSLocalizedString(@"%@ is not currently running on %@.", ""), appName, deviceName];
+        }
+    }
+}
+
+- (nullable NSString *)altserver_localizedRecoverySuggestion
+{
+    switch ((ALTServerError)self.code)
+    {
+        case ALTServerErrorConnectionFailed:
+        case ALTServerErrorDeviceNotFound:
+            return NSLocalizedString(@"Make sure you have trusted this device with your computer and WiFi sync is enabled.", @"");
+            
+        case ALTServerErrorPluginNotFound:
+            return NSLocalizedString(@"Make sure Mail is running and the plug-in is enabled in Mail's preferences.", @"");
+            
+        case ALTServerErrorMaximumFreeAppLimitReached:
+            return NSLocalizedString(@"Make sure “Offload Unused Apps” is disabled in Settings > iTunes & App Stores, then install or delete all offloaded apps.", @"");
+            
+        case ALTServerErrorRequestedAppNotRunning:
+        {
+            NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"your device", @"");
+            return [NSString stringWithFormat:NSLocalizedString(@"Make sure the app is running in the foreground on %@ then try again.", @""), deviceName];
+        }
+            
+        default:
+            return nil;
+    }
+}
+
+- (NSString *)profileErrorLocalizedDescriptionWithBaseDescription:(NSString *)baseDescription
+{
+    NSString *localizedDescription = nil;
+    
+    NSString *bundleID = self.userInfo[ALTProvisioningProfileBundleIDErrorKey];
+    if (bundleID)
+    {
+        localizedDescription = [NSString stringWithFormat:@"%@ “%@”", baseDescription, bundleID];
+    }
+    else
+    {
+        localizedDescription = [NSString stringWithFormat:@"%@.", baseDescription];
+    }
+    
+    return localizedDescription;
+}
+
+#pragma mark - AltServerConnectionErrorDomain -
+
+- (nullable NSString *)altserver_connection_localizedDescription
+{
+    switch ((ALTServerConnectionError)self.code)
+    {
+        case ALTServerConnectionErrorUnknown:
+        {
+            NSString *underlyingErrorDomain = self.userInfo[ALTUnderlyingErrorDomainErrorKey];
+            NSString *underlyingErrorCode = self.userInfo[ALTUnderlyingErrorCodeErrorKey];
+            
+            if (underlyingErrorDomain != nil && underlyingErrorCode != nil)
+            {
+                return [NSString stringWithFormat:NSLocalizedString(@"%@ error %@.", @""), underlyingErrorDomain, underlyingErrorCode];
+            }
+            else if (underlyingErrorCode != nil)
+            {
+                return [NSString stringWithFormat:NSLocalizedString(@"Connection error code: %@", @""), underlyingErrorCode];
+            }
+            
+            return nil;
+        }
+            
+        case ALTServerConnectionErrorDeviceLocked:
+        {
+            NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"The device", @"");
+            return [NSString stringWithFormat:NSLocalizedString(@"%@ is currently locked.", @""), deviceName];
+        }
+            
+        case ALTServerConnectionErrorInvalidRequest:
+        {
+            NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"The device", @"");
+            return [NSString stringWithFormat:NSLocalizedString(@"%@ received an invalid request from AltServer.", @""), deviceName];
+        }
+            
+        case ALTServerConnectionErrorInvalidResponse:
+        {
+            NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @"");
+            return [NSString stringWithFormat:NSLocalizedString(@"AltServer received an invalid response from %@.", @""), deviceName];
+        }
+            
+        case ALTServerConnectionErrorUSBMUXD:
+        {
+            return NSLocalizedString(@"A connection to usbmuxd could not be established.", @"");
+        }
+            
+        case ALTServerConnectionErrorSSL:
+        {
+            NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @"");
+            return [NSString stringWithFormat:NSLocalizedString(@"A secure connection between AltServer and %@ could not be established.", @""), deviceName];
+        }
+            
+        case ALTServerConnectionErrorTimedOut:
+        {
+            NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @"");
+            return [NSString stringWithFormat:NSLocalizedString(@"The connection to %@ timed out.", @""), deviceName];
+        }
+    }
+    
+    return nil;
+}
+
+- (nullable NSString *)altserver_connection_localizedRecoverySuggestion
+{
+    switch ((ALTServerConnectionError)self.code)
+    {
+        case ALTServerConnectionErrorUnknown:
+        {
+            return nil;
+        }
+            
+        case ALTServerConnectionErrorDeviceLocked:
+        {
+            return NSLocalizedString(@"Please unlock the device with your passcode and try again.", @"");
+        }
+            
+        case ALTServerConnectionErrorInvalidRequest:
+        {
+            break;
+        }
+            
+        case ALTServerConnectionErrorInvalidResponse:
+        {
+            break;
+        }
+            
+        case ALTServerConnectionErrorUSBMUXD:
+        {
+            break;
+        }
+            
+        case ALTServerConnectionErrorSSL:
+        {
+            break;
+        }
+            
+        case ALTServerConnectionErrorTimedOut:
+        {
+            break;
+        }
+    }
+    
+    return nil;
+}
+    
+@end

+ 1 - 0
Sources/CAltKit/include/NSError+ALTServerError.h

@@ -0,0 +1 @@
+../NSError+ALTServerError.h