iOS: AltKit source

This commit is contained in:
Flyinghead 2021-08-10 11:34:11 +02:00
parent 12f302d356
commit bb217e5c49
17 changed files with 1656 additions and 0 deletions

View File

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

View File

@ -0,0 +1,41 @@
cmake_minimum_required(VERSION 3.15.1)
project(AltKit LANGUAGES C Swift)
add_library(CAltKit
Sources/CAltKit/NSError+ALTServerError.h
Sources/CAltKit/NSError+ALTServerError.m
)
add_library(AltKit
Sources/AltKit/Extensions/ALTServerError+Conveniences.swift
Sources/AltKit/Extensions/Result+Conveniences.swift
Sources/AltKit/Server/Connection.swift
Sources/AltKit/Server/NetworkConnection.swift
Sources/AltKit/Server/Server.swift
Sources/AltKit/Server/ServerConnection.swift
Sources/AltKit/Server/ServerManager.swift
Sources/AltKit/Server/ServerProtocol.swift
Sources/AltKit/Types/CodableServerError.swift
)
target_link_libraries(AltKit PUBLIC CAltKit)
set_property(TARGET AltKit PROPERTY XCODE_ATTRIBUTE_SWIFT_VERSION "5.0")
# Make CAltKit's modulemap available to AltKit
set_property(TARGET AltKit PROPERTY XCODE_ATTRIBUTE_SWIFT_INCLUDE_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/Sources/CAltKit")
set_property(TARGET AltKit PROPERTY XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET "12.0")
# Add binary dir to interface include path to make Swift header accessible to targets using AltKit
# FIXME not working?
target_include_directories(AltKit PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/..")
# Copy generated Swift header to binary dir
add_custom_command(TARGET AltKit
POST_BUILD
COMMAND cp -v $DERIVED_SOURCES_DIR/AltKit-Swift.h ${CMAKE_CURRENT_BINARY_DIR}
)

View File

@ -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),
.tvOS(.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"]),
]
)

View File

@ -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];
}];
}];
```

View File

@ -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])
}
}

View File

@ -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")
}
}
}

View File

@ -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()
}

View File

@ -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)"
}
}

View File

@ -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).
}
}

View File

@ -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)))
}
}
}
}

View File

@ -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()
}
}
}

View File

@ -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()
{
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
module CAltKit {
umbrella "./include"
export *
}