├── .github ├── CODEOWNERS ├── FUNDING.yml ├── auto_assign.yml ├── issue_template.md ├── stale.yml └── workflows │ ├── rebase.yml │ ├── swiftlint.yml │ └── test.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── CMakeLists.txt ├── Package.swift ├── README.md ├── Sources └── SideKit │ ├── Errors │ ├── ALTServerConnectionError+NSError.swift │ ├── ALTServerConnectionError.swift │ ├── ALTServerError+NSError.swift │ └── ALTServerError.swift │ ├── Extensions │ ├── ALTServerError+Conveniences.swift │ └── Result+Conveniences.swift │ ├── Server │ ├── Connection.swift │ ├── NetworkConnection.swift │ ├── Server.swift │ ├── ServerConnection.swift │ ├── ServerManager.swift │ └── ServerProtocol.swift │ └── Types │ └── CodableServerError.swift └── Tests └── SideKitTests └── SideKitTests.swift /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS 2 | # Lines starting with '#' are comments. 3 | # Each line is a file pattern followed by one or more owners. 4 | # https://help.github.com/en/articles/about-code-owners 5 | 6 | # These owners will be the default owners for everything in the repo. 7 | * @JoeMatt 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [JoeMatt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: provenanceemu # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: true 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: false 6 | 7 | # A list of reviewers to be added to pull requests (GitHub user name) 8 | reviewers: 9 | - JoeMatt 10 | 11 | # A number of reviewers added to the pull request 12 | # Set 0 to add all the reviewers (default: 0) 13 | numberOfReviewers: 0 14 | 15 | # A list of assignees, overrides reviewers if set 16 | # assignees: 17 | 18 | # A number of assignees to add to the pull request 19 | # Set to 0 to add all of the assignees. 20 | # Uses numberOfReviewers if unset. 21 | numberOfAssignees: 1 22 | 23 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 24 | skipKeywords: 25 | - wip 26 | - WIP 27 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## What did you do? 8 | 9 | 10 | 11 | ## What did you expect to happen? 12 | 13 | 14 | 15 | ## What happened instead? 16 | 17 | 18 | 19 | ## General Information 20 | 21 | - Hero Version: 22 | 23 | - iOS Version(s): 24 | 25 | - Swift Version: 26 | 27 | - Devices/Simulators: 28 | 29 | - Reproducible in Examples? (Yes/No): 30 | 31 | ## Demo Project 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 180 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - confirmed bug 10 | - investigating 11 | - bug? 12 | - WIP 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: true 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | # Rebase PR branch when someone comments /rebase 2 | on: 3 | issue_comment: 4 | types: [created] 5 | name: Automatic Rebase 6 | jobs: 7 | rebase: 8 | name: Rebase 9 | if: contains(github.event.comment.body, '/rebase') 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: Automatic Rebase 14 | uses: cirrus-actions/rebase@master 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: Swift Lint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | 10 | jobs: 11 | swift-lint: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: GitHub Action for SwiftLint 17 | uses: norio-nomura/action-swiftlint@3.2.1 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | DIFF_BASE: ${{ github.base_ref }} 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "**.swift" 7 | - "**.xcodeproj" 8 | - "**.m" 9 | - "**.h" 10 | - "**.podspec" 11 | - "Podfile" 12 | - "Podfile.lock" 13 | - "test.yml" 14 | jobs: 15 | swiftpm: 16 | name: Test iOS (swiftpm) 17 | runs-on: macOS-latest 18 | env: 19 | DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@master 23 | 24 | - name: iOS - Swift PM 25 | run: | 26 | set -o pipefail && swift test --parallel | xcpretty -c --test --color --report junit --output build/reports/junit.xml --report html --output build/reports/html 27 | 28 | - name: Upload Test Results 29 | uses: actions/upload-artifact@v2 30 | if: always() 31 | with: 32 | name: Test Results 33 | path: build/reports 34 | 35 | - name: Upload Coverage Results 36 | uses: actions/upload-artifact@v2 37 | if: always() 38 | with: 39 | name: Coverage Results 40 | path: build/coverage 41 | 42 | - name: Upload Logs 43 | uses: actions/upload-artifact@v2 44 | if: always() 45 | with: 46 | name: Logs 47 | path: build/logs 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | /build 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15.1) 2 | 3 | project(SideKit LANGUAGES Swift) 4 | 5 | add_library(SideKit 6 | Sources/SideKit/Extensions/ALTServerError+Conveniences.swift 7 | Sources/SideKit/Extensions/Result+Conveniences.swift 8 | 9 | Sources/SideKit/Errors/ALTServerError.swift 10 | Sources/SideKit/Errors/ALTServerError+NSError.swift 11 | Sources/SideKit/Errors/ALTServerConnectionError.swift 12 | Sources/SideKit/Errors/ALTServerConnectionError+NSError.swift 13 | 14 | Sources/SideKit/Server/Connection.swift 15 | Sources/SideKit/Server/NetworkConnection.swift 16 | Sources/SideKit/Server/Server.swift 17 | Sources/SideKit/Server/ServerConnection.swift 18 | Sources/SideKit/Server/ServerManager.swift 19 | Sources/SideKit/Server/ServerProtocol.swift 20 | 21 | Sources/SideKit/Types/CodableServerError.swift 22 | ) 23 | 24 | target_link_libraries(SideKit PRIVATE CSideKit) 25 | 26 | set_property(TARGET SideKit PROPERTY XCODE_ATTRIBUTE_SWIFT_VERSION "5.0") 27 | 28 | # Make CSideKit's modulemap available to SideKit 29 | set_property(TARGET SideKit PROPERTY XCODE_ATTRIBUTE_SWIFT_INCLUDE_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/Sources/CSideKit") 30 | 31 | # Add binary dir to interface include path to make Swift header accessible to targets using SideKit 32 | target_include_directories(SideKit INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) 33 | 34 | # Copy generated Swift header to binary dir 35 | add_custom_command(TARGET SideKit 36 | POST_BUILD 37 | COMMAND cp $DERIVED_SOURCES_DIR/SideKit-Swift.h ${CMAKE_CURRENT_BINARY_DIR} 38 | ) 39 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SideKit", 8 | platforms: [ 9 | .iOS(.v11), 10 | .tvOS(.v11), 11 | .macCatalyst(.v13), 12 | .macOS(.v11) 13 | ], 14 | products: [ 15 | .library( 16 | name: "SideKit", 17 | targets: ["SideKit"]), 18 | .library( 19 | name: "SideKit-Static", 20 | type: .static, 21 | targets: ["SideKit"]), 22 | .library( 23 | name: "SideKit-Dynamic", 24 | type: .dynamic, 25 | targets: ["SideKit"]) 26 | ], 27 | dependencies: [ 28 | ], 29 | targets: [ 30 | .target( 31 | name: "SideKit", 32 | dependencies: [], 33 | linkerSettings: [ 34 | .linkedFramework("UIKit", .when(platforms: [.iOS, .tvOS, .macCatalyst])), 35 | .linkedFramework("Network") 36 | ] 37 | ), 38 | .testTarget( 39 | name: "SideKitTests", 40 | dependencies: ["SideKit"] 41 | ) 42 | ], 43 | swiftLanguageVersions: [.v5] 44 | ) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SideKit 2 | 3 | _SideKit allows apps to communicate with [SideServer mobile](https://github.com/SideStore/em_proxy) on **any** WiFi network and enable various features such as JIT compilation._ 4 | 5 | [![Unit Test](https://github.com/SideStore/SideKit/actions/workflows/test.yml/badge.svg)](https://github.com/SideStore/SideKit/actions/workflows/test.yml) 6 | 7 | ![Alt](https://repobeats.axiom.co/api/embed/c3dd1299b57265454681dab327b40e2dd52322e1.svg "Repobeats analytics image") 8 | 9 | ## Installation 10 | 11 | To use AltKit in your app, add the following to your `Package.swift` file's dependencies: 12 | 13 | ```swift 14 | .package(url: "https://github.com/SideStore/SideKit.git", .upToNextMajor(from: "0.0.1")), 15 | ``` 16 | 17 | Next, add the SideKit package as a dependency for your target: 18 | 19 | ```swift 20 | .product(name: "SideKit", package: "SideKit), 21 | ``` 22 | 23 | Finally, right-click on your app's `Info.plist`, select "Open As > Source Code", then add the following entries: 24 | 25 | ```xml 26 | ALTDeviceID 27 | 28 | ``` 29 | 30 | ⚠️ The `ALTDeviceID` key must be present in your app's `Info.plist` to let AltStore know it should replace that entry with the user's device's UDID when sideloading your app, which is required for AltKit to work. For local development with AltKit, we recommend setting `ALTDeviceID` to your development device's UDID to ensure everything works as expected. Otherwise, the exact value doesn't matter as long as the entry exists, since it will be replaced by AltStore during installation. 31 | 32 | ### CMake Integration 33 | 34 | Note: CMake 3.15 is required for the integration. The integration only works with the Xcode generator. 35 | 36 | Steps: 37 | - Add the AltKit CMake project to your CMake project using `add_subdirectory`. 38 | - Add Swift to your project's supported languages. (ex.: `project(projName LANGUAGES C Swift)`) 39 | 40 | If you're using `add_compile_options` or `target_compile_options` and the Swift compiler complains about the option not being supported, it's possible to use CMake's generator expressions to limit the options to non-Swift source files. 41 | 42 | Example: 43 | 44 | ```cmake 45 | add_compile_options($<$>:-fPIC>) 46 | ``` 47 | 48 | ## Usage 49 | 50 | ### Swift 51 | 52 | ```swift 53 | import SideKit 54 | 55 | ServerManager.shared.startDiscovering() 56 | 57 | ServerManager.shared.autoconnect { result in 58 | switch result 59 | { 60 | case .failure(let error): print("Could not auto-connect to server.", error) 61 | case .success(let connection): 62 | connection.enableUnsignedCodeExecution { result in 63 | switch result 64 | { 65 | case .failure(let error): print("Could not enable JIT compilation.", error) 66 | case .success: 67 | print("Successfully enabled JIT compilation!") 68 | ServerManager.shared.stopDiscovering() 69 | } 70 | 71 | connection.disconnect() 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | ### Objective-C 78 | 79 | ```objc 80 | @import SideKit; 81 | 82 | [[ALTServerManager sharedManager] startDiscovering]; 83 | 84 | [[ALTServerManager sharedManager] autoconnectWithCompletionHandler:^(ALTServerConnection *connection, NSError *error) { 85 | if (error) 86 | { 87 | return NSLog(@"Could not auto-connect to server. %@", error); 88 | } 89 | 90 | [connection enableUnsignedCodeExecutionWithCompletionHandler:^(BOOL success, NSError *error) { 91 | if (success) 92 | { 93 | NSLog(@"Successfully enabled JIT compilation!"); 94 | [[ALTServerManager sharedManager] stopDiscovering]; 95 | } 96 | else 97 | { 98 | NSLog(@"Could not enable JIT compilation. %@", error); 99 | } 100 | 101 | [connection disconnect]; 102 | }]; 103 | }]; 104 | ``` 105 | -------------------------------------------------------------------------------- /Sources/SideKit/Errors/ALTServerConnectionError+NSError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ALTServerConnectionError+NSError.swift 3 | // 4 | // 5 | // Created by Joseph Mattiello on 2/24/23. 6 | // 7 | 8 | import Foundation 9 | 10 | #if false 11 | public extension ALTServerConnectionError { 12 | static func setUserInfoProvider(name: String, device: String, bundleId: String = Bundle.main.bundleIdentifier!) { 13 | // Set the user info value provider for the AltServerErrorDomain 14 | NSError.setUserInfoValueProvider(forDomain: AltServerErrorConnectionDomain) { error, key in 15 | switch key { 16 | case ALTUnderlyingErrorDomainErrorKey: 17 | let error = error as NSError 18 | return (error.userInfo[NSUnderlyingErrorKey] as? NSError)?.domain 19 | case ALTUnderlyingErrorCodeErrorKey: 20 | let error = error as NSError 21 | return (error.userInfo[NSUnderlyingErrorKey] as? NSError)?.code 22 | case ALTProvisioningProfileBundleIDErrorKey: 23 | return bundleId 24 | case ALTAppNameErrorKey: 25 | return device 26 | case ALTDeviceNameErrorKey: 27 | return name 28 | default: 29 | return nil 30 | } 31 | } 32 | } 33 | } 34 | #endif 35 | -------------------------------------------------------------------------------- /Sources/SideKit/Errors/ALTServerConnectionError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ALTServerConnectionError.swift 3 | // 4 | // 5 | // Created by Joseph Mattiello on 2/24/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public let AltServerInstallationErrorDomain = "com.rileytestut.AltServer.Installation" 11 | public let AltServerConnectionErrorDomain = "com.rileytestut.AltServer.Connection" 12 | 13 | public enum ALTServerConnectionError: Int, LocalizedError { 14 | case unknown 15 | case deviceLocked 16 | case invalidRequest 17 | case invalidResponse 18 | case usbmuxd 19 | case ssl 20 | case timedOut 21 | 22 | public static var errorDomain: String { AltServerConnectionErrorDomain } 23 | } 24 | 25 | public extension ALTServerConnectionError { 26 | var errorDescription: String? { 27 | switch self { 28 | case .unknown: 29 | return NSLocalizedString("Unknown connection error", comment: "ALTServerConnectionError") 30 | case .deviceLocked: 31 | return NSLocalizedString("Device locked", comment: "ALTServerConnectionError") 32 | case .invalidRequest: 33 | return NSLocalizedString("Invalid request", comment: "ALTServerConnectionError") 34 | case .invalidResponse: 35 | return NSLocalizedString("Invalid response", comment: "ALTServerConnectionError") 36 | case .usbmuxd: 37 | return NSLocalizedString("USBMuxd error", comment: "ALTServerConnectionError") 38 | case .ssl: 39 | return NSLocalizedString("SSL error", comment: "ALTServerConnectionError") 40 | case .timedOut: 41 | return NSLocalizedString("Timed out", comment: "ALTServerConnectionError") 42 | } 43 | } 44 | } 45 | 46 | public extension ALTServerConnectionError { 47 | var recoverySuggestion: String? { 48 | switch self { 49 | case .deviceLocked: 50 | return NSLocalizedString("Unlock your device and try again.", comment: "ALTServerConnectionError recovery suggestion") 51 | case .invalidRequest: 52 | return NSLocalizedString("Make sure AltServer is running and try again.", comment: "ALTServerConnectionError recovery suggestion") 53 | case .invalidResponse: 54 | return NSLocalizedString("Make sure AltServer is running and try again.", comment: "ALTServerConnectionError recovery suggestion") 55 | case .usbmuxd: 56 | return NSLocalizedString("Make sure iTunes is not running, and no other software is using the device over USB.", comment: "ALTServerConnectionError recovery suggestion") 57 | case .ssl: 58 | return NSLocalizedString("Make sure your computer's time and date are correct, and try again.", comment: "ALTServerConnectionError recovery suggestion") 59 | case .timedOut: 60 | return NSLocalizedString("Make sure your device is unlocked and try again.", comment: "ALTServerConnectionError recovery suggestion") 61 | default: 62 | return nil 63 | } 64 | } 65 | } 66 | 67 | public extension ALTServerConnectionError { 68 | var failureReason: String? { 69 | switch self { 70 | case .deviceLocked: 71 | return NSLocalizedString("Your device is locked. Unlock your device and try again.", comment: "ALTServerConnectionError failure reason") 72 | case .invalidRequest: 73 | return NSLocalizedString("The request sent to AltServer is invalid.", comment: "ALTServerConnectionError failure reason") 74 | case .invalidResponse: 75 | return NSLocalizedString("The response from AltServer is invalid.", comment: "ALTServerConnectionError failure reason") 76 | case .usbmuxd: 77 | return NSLocalizedString("AltServer cannot communicate with your device over USB. Make sure iTunes is not running, and no other software is using the device over USB.", comment: "ALTServerConnectionError failure reason") 78 | case .ssl: 79 | return NSLocalizedString("There is a problem with the SSL connection to AltServer. Make sure your computer's time and date are correct, and try again.", comment: "ALTServerConnectionError failure reason") 80 | case .timedOut: 81 | return NSLocalizedString("The connection to AltServer timed out. Make sure your device is unlocked and try again.", comment: "ALTServerConnectionError failure reason") 82 | case .unknown: 83 | return NSLocalizedString("An unknown error occurred with AltServer.", comment: "ALTServerConnectionError failure reason") 84 | } 85 | } 86 | } 87 | 88 | public extension ALTServerConnectionError { 89 | var code: Int { 90 | return rawValue 91 | } 92 | } 93 | 94 | extension ALTServerConnectionError: Codable { 95 | enum CodingKeys: String, CodingKey { 96 | case deviceLocked 97 | case invalidRequest 98 | case invalidResponse 99 | case usbmuxd 100 | case ssl 101 | case timedOut 102 | case unknown 103 | } 104 | 105 | public init(from decoder: Decoder) throws { 106 | let values = try decoder.container(keyedBy: CodingKeys.self) 107 | 108 | if let _ = try? values.decode(Bool.self, forKey: .deviceLocked) { 109 | self = .deviceLocked 110 | return 111 | } 112 | if let _ = try? values.decode(Bool.self, forKey: .invalidRequest) { 113 | self = .invalidRequest 114 | return 115 | } 116 | if let _ = try? values.decode(Bool.self, forKey: .invalidResponse) { 117 | self = .invalidResponse 118 | return 119 | } 120 | if let _ = try? values.decode(Bool.self, forKey: .usbmuxd) { 121 | self = .usbmuxd 122 | return 123 | } 124 | if let _ = try? values.decode(Bool.self, forKey: .ssl) { 125 | self = .ssl 126 | return 127 | } 128 | if let _ = try? values.decode(Bool.self, forKey: .timedOut) { 129 | self = .timedOut 130 | return 131 | } 132 | 133 | self = .unknown 134 | } 135 | 136 | public func encode(to encoder: Encoder) throws { 137 | var container = encoder.container(keyedBy: CodingKeys.self) 138 | 139 | switch self { 140 | case .deviceLocked: 141 | try container.encode(true, forKey: .deviceLocked) 142 | case .invalidRequest: 143 | try container.encode(true, forKey: .invalidRequest) 144 | case .invalidResponse: 145 | try container.encode(true, forKey: .invalidResponse) 146 | case .usbmuxd: 147 | try container.encode(true, forKey: .usbmuxd) 148 | case .ssl: 149 | try container.encode(true, forKey: .ssl) 150 | case .timedOut: 151 | try container.encode(true, forKey: .timedOut) 152 | case .unknown: 153 | break 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Sources/SideKit/Errors/ALTServerError+NSError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Joseph Mattiello on 2/24/23. 6 | // 7 | 8 | import Foundation 9 | 10 | #if false 11 | public extension ALTServerError { 12 | static func setUserInfoProvider(name: String, device: String, bundleId: String = Bundle.main.bundleIdentifier!) { 13 | // Set the user info value provider for the AltServerErrorDomain 14 | NSError.setUserInfoValueProvider(forDomain: AltServerErrorDomain) { error, key in 15 | switch key { 16 | case ALTUnderlyingErrorDomainErrorKey: 17 | let error = error as NSError 18 | return (error.userInfo[NSUnderlyingErrorKey] as? NSError)?.domain 19 | case ALTUnderlyingErrorCodeErrorKey: 20 | let error = error as NSError 21 | return (error.userInfo[NSUnderlyingErrorKey] as? NSError)?.code 22 | case ALTProvisioningProfileBundleIDErrorKey: 23 | return bundleId 24 | case ALTAppNameErrorKey: 25 | return device 26 | case ALTDeviceNameErrorKey: 27 | return name 28 | default: 29 | return nil 30 | } 31 | } 32 | } 33 | } 34 | #endif 35 | -------------------------------------------------------------------------------- /Sources/SideKit/Errors/ALTServerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ALTServerError.swift 3 | // 4 | // 5 | // Created by Joseph Mattiello on 2/24/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public let AltServerErrorDomain = "com.rileytestut.AltServer" 11 | 12 | public let ALTUnderlyingErrorDomainErrorKey = "underlyingErrorDomain" 13 | public let ALTUnderlyingErrorCodeErrorKey = "underlyingErrorCode" 14 | public let ALTProvisioningProfileBundleIDErrorKey = "bundleIdentifier" 15 | public let ALTAppNameErrorKey = "appName" 16 | public let ALTDeviceNameErrorKey = "deviceName" 17 | 18 | public enum ALTServerError: LocalizedError, Codable, RawRepresentable { 19 | public var rawValue: Int { 20 | switch self { 21 | case .underlyingError: return -1 22 | default: return errorCode 23 | } 24 | } 25 | 26 | public typealias RawValue = Int 27 | public typealias Code = ALTServerError 28 | 29 | case underlyingError(domain: String, code: Int) 30 | case unknown(code: Int = 0) 31 | case connectionFailed(underlyingError: Error? = nil) 32 | case lostConnection(underlyingError: Error? = nil) 33 | case deviceNotFound 34 | case deviceWriteFailed 35 | case invalidRequest(underlyingError: Error? = nil) 36 | case invalidResponse(underlyingError: Error? = nil) 37 | case invalidApp 38 | case installationFailed 39 | case maximumFreeAppLimitReached 40 | case unsupportediOSVersion 41 | case unknownRequest 42 | case unknownResponse 43 | case invalidAnisetteData 44 | case pluginNotFound 45 | case profileNotFound 46 | case appDeletionFailed 47 | case requestedAppNotRunning(appName: String = "Unknown", deviceName: String = "UnknownDevice") 48 | 49 | enum CodingKeys: String, CodingKey { 50 | case errorDomain 51 | case errorCode 52 | case rawValue 53 | } 54 | 55 | public static var errorDomain: String { "com.rileytestut.AltServer" } 56 | 57 | public var errorDomain: String { 58 | switch self { 59 | case let .underlyingError(domain, _): 60 | return domain 61 | default: 62 | return Self.errorDomain 63 | } 64 | } 65 | 66 | public var code: RawValue { errorCode } 67 | public var errorCode: RawValue { 68 | switch self { 69 | case let .underlyingError(_, code): 70 | return code 71 | case let .unknown(code): 72 | return code 73 | default: 74 | return ALTServerError.code(for: self) 75 | } 76 | } 77 | 78 | public static func code(for error: ALTServerError) -> RawValue { 79 | switch error { 80 | case .underlyingError: 81 | return -1 82 | case .connectionFailed: 83 | return 1 84 | case .lostConnection: 85 | return 2 86 | case .deviceNotFound: 87 | return 3 88 | case .deviceWriteFailed: 89 | return 4 90 | case .invalidRequest: 91 | return 5 92 | case .invalidResponse: 93 | return 6 94 | case .invalidApp: 95 | return 7 96 | case .installationFailed: 97 | return 8 98 | case .maximumFreeAppLimitReached: 99 | return 9 100 | case .unsupportediOSVersion: 101 | return 10 102 | case .unknownRequest: 103 | return 11 104 | case .unknownResponse: 105 | return 12 106 | case .invalidAnisetteData: 107 | return 13 108 | case .pluginNotFound: 109 | return 14 110 | case .profileNotFound: 111 | return 15 112 | case .appDeletionFailed: 113 | return 16 114 | case .requestedAppNotRunning: 115 | return 100 116 | case .unknown: 117 | return 0 118 | } 119 | } 120 | } 121 | 122 | public extension ALTServerError { 123 | init?(rawValue: Int) { 124 | switch rawValue { 125 | case -1: 126 | self = .underlyingError(domain: "", code: -1) 127 | case 0: 128 | self = .unknown() 129 | case 1: 130 | self = .connectionFailed(underlyingError: nil) 131 | case ALTServerError.lostConnection().errorCode: 132 | self = .lostConnection() 133 | case ALTServerError.deviceNotFound.errorCode: 134 | self = .deviceNotFound 135 | case ALTServerError.deviceWriteFailed.errorCode: 136 | self = .deviceWriteFailed 137 | case ALTServerError.invalidRequest().errorCode: 138 | self = .invalidRequest() 139 | case ALTServerError.invalidResponse().errorCode: 140 | self = .invalidResponse() 141 | case ALTServerError.invalidApp.errorCode: 142 | self = .invalidApp 143 | case ALTServerError.installationFailed.errorCode: 144 | self = .installationFailed 145 | case ALTServerError.maximumFreeAppLimitReached.errorCode: 146 | self = .maximumFreeAppLimitReached 147 | case ALTServerError.unsupportediOSVersion.errorCode: 148 | self = .unsupportediOSVersion 149 | case ALTServerError.unknownRequest.errorCode: 150 | self = .unknownRequest 151 | case ALTServerError.unknownResponse.errorCode: 152 | self = .unknownResponse 153 | case ALTServerError.invalidAnisetteData.errorCode: 154 | self = .invalidAnisetteData 155 | case ALTServerError.pluginNotFound.errorCode: 156 | self = .pluginNotFound 157 | case ALTServerError.profileNotFound.errorCode: 158 | self = .profileNotFound 159 | case ALTServerError.appDeletionFailed.errorCode: 160 | self = .appDeletionFailed 161 | case ALTServerError.requestedAppNotRunning().errorCode: 162 | self = .requestedAppNotRunning() 163 | default: 164 | return nil 165 | } 166 | } 167 | } 168 | 169 | // extension ALTServerError: CaseIterable { 170 | // public static var allCases: [ALTServerError] { 171 | // return [.underlyingError(domain: "", code: 0), 172 | // .unknown, 173 | // .connectionFailed, 174 | // .lostConnection, 175 | // .deviceNotFound, 176 | // .deviceWriteFailed, 177 | // .invalidRequest, 178 | // .invalidResponse, 179 | // .invalidApp, 180 | // .installationFailed, 181 | // .maximumFreeAppLimitReached, 182 | // .unsupportediOSVersion, 183 | // .unknownRequest, 184 | // .unknownResponse, 185 | // .invalidAnisetteData, 186 | // .pluginNotFound, 187 | // .profileNotFound, 188 | // .appDeletionFailed, 189 | // .requestedAppNotRunning, 190 | // .unknown(code: 0)] 191 | // } 192 | // } 193 | 194 | public extension ALTServerError { 195 | internal var userInfo: [String: Any] { (self as NSError).userInfo } 196 | 197 | private func profileErrorLocalizedDescription(baseDescription: String) -> String { 198 | let bundleID = userInfo[ALTProvisioningProfileBundleIDErrorKey] as? String ?? NSLocalizedString("this app", comment: "") 199 | let profileType = (bundleID == "com.apple.configurator.profile-install") ? NSLocalizedString("Configuration Profile", comment: "") : NSLocalizedString("Provisioning Profile", comment: "") 200 | let fullDescription = String(format: NSLocalizedString("%@ %@ is not valid.", comment: ""), profileType, bundleID) 201 | return [baseDescription, fullDescription].filter { !$0.isEmpty }.joined(separator: " ") 202 | } 203 | 204 | var errorDescription: String? { 205 | switch self { 206 | case let .underlyingError(domain, code): 207 | return String(format: NSLocalizedString("Underlying error (%@, %d)", comment: "ALTServerError"), domain, code) 208 | case .connectionFailed: 209 | return NSLocalizedString("Could not connect to AltServer.", comment: "") 210 | case .lostConnection: 211 | return NSLocalizedString("Lost connection to AltServer.", comment: "") 212 | case .deviceNotFound: 213 | return NSLocalizedString("AltServer could not find this device.", comment: "") 214 | case .deviceWriteFailed: 215 | return NSLocalizedString("Failed to write app data to device.", comment: "") 216 | case .invalidRequest: 217 | return NSLocalizedString("AltServer received an invalid request.", comment: "") 218 | case .invalidResponse: 219 | return NSLocalizedString("AltServer sent an invalid response.", comment: "") 220 | case .invalidApp: 221 | return NSLocalizedString("The app is invalid.", comment: "") 222 | case .installationFailed: 223 | return NSLocalizedString("An error occurred while installing the app.", comment: "") 224 | case .maximumFreeAppLimitReached: 225 | return NSLocalizedString("Cannot activate more than 3 apps and app extensions.", comment: "") 226 | case .unsupportediOSVersion: 227 | return NSLocalizedString("Your device must be running iOS 12.2 or later to install AltStore.", comment: "") 228 | case .unknownRequest: 229 | return NSLocalizedString("AltServer does not support this request.", comment: "") 230 | case .unknownResponse: 231 | return NSLocalizedString("Received an unknown response from AltServer.", comment: "") 232 | case .invalidAnisetteData: 233 | return NSLocalizedString("The provided Anisette data is invalid.", comment: "") 234 | case .pluginNotFound: 235 | return NSLocalizedString("AltServer could not connect to Mail plug-in.", comment: "") 236 | case .profileNotFound: 237 | return NSLocalizedString("Could not find profile.", comment: "") 238 | case .appDeletionFailed: 239 | return NSLocalizedString("An error occurred while removing the app.", comment: "") 240 | case let .requestedAppNotRunning(appName, deviceName): 241 | return String(format: NSLocalizedString("The requested app %@ is not currently running on device %@.", comment: ""), appName, deviceName) 242 | case let .unknown(code): 243 | return String(format: NSLocalizedString("An unknown error occurred with code %d.", comment: ""), code) 244 | } 245 | } 246 | } 247 | 248 | public extension ALTServerError { 249 | var failureReason: String? { 250 | switch self { 251 | case let .underlyingError(domain, code): 252 | let underlyingError = NSError(domain: domain, code: code, userInfo: nil) 253 | if let localizedFailureReason = underlyingError.localizedFailureReason { 254 | return localizedFailureReason 255 | } else if let underlyingErrorCode = underlyingError.userInfo[ALTUnderlyingErrorCodeErrorKey] as? String { 256 | return String(format: NSLocalizedString("Error code: %@", comment: ""), underlyingErrorCode) 257 | } 258 | return nil 259 | 260 | case .unknown: 261 | return NSLocalizedString("An unknown error occured.", comment: "Unknown ALTServerError") 262 | 263 | case .connectionFailed: 264 | #if TARGET_OS_OSX 265 | return NSLocalizedString("There was an error connecting to the device.", comment: "Failed to connect ALTServerError") 266 | #else 267 | return NSLocalizedString("Could not connect to AltServer.", comment: "Could not connect ALTServerError") 268 | #endif 269 | 270 | case .lostConnection: 271 | return NSLocalizedString("Lost connection to AltServer.", comment: "Lost connection ALTServerError") 272 | 273 | case .deviceNotFound: 274 | return NSLocalizedString("AltServer could not find this device.", comment: "Device not found ALTServerError") 275 | 276 | case .deviceWriteFailed: 277 | return NSLocalizedString("Failed to write app data to device.", comment: "Failed to write data ALTServerError") 278 | 279 | case .invalidRequest: 280 | return NSLocalizedString("AltServer received an invalid request.", comment: "Invalid request ALTServerError") 281 | 282 | case .invalidResponse: 283 | return NSLocalizedString("AltServer sent an invalid response.", comment: "Invalid response ALTServerError") 284 | 285 | case .invalidApp: 286 | return NSLocalizedString("The app is invalid.", comment: "Invalid app ALTServerError") 287 | 288 | case .installationFailed: 289 | return NSLocalizedString("An error occured while installing the app.", comment: "Installation failed ALTServerError") 290 | 291 | case .maximumFreeAppLimitReached: 292 | return NSLocalizedString("Cannot activate more than 3 apps and app extensions.", comment: "Maximum app limit reached ALTServerError") 293 | 294 | case .unsupportediOSVersion: 295 | return NSLocalizedString("Your device must be running iOS 12.2 or later to install AltStore.", comment: "Unsupported iOS version ALTServerError") 296 | 297 | case .unknownRequest: 298 | return NSLocalizedString("AltServer does not support this request.", comment: "Unknown request ALTServerError") 299 | 300 | case .unknownResponse: 301 | return NSLocalizedString("Received an unknown response from AltServer.", comment: "Unknown response ALTServerError") 302 | 303 | case .invalidAnisetteData: 304 | return NSLocalizedString("The provided anisette data is invalid.", comment: "Invalid anisette data ALTServerError") 305 | 306 | case .pluginNotFound: 307 | return NSLocalizedString("AltServer could not connect to Mail plug-in.", comment: "Plugin not found ALTServerError") 308 | 309 | case .profileNotFound: 310 | return profileErrorLocalizedDescription(baseDescription: NSLocalizedString("Could not find profile", comment: "")) 311 | 312 | case .appDeletionFailed: 313 | return NSLocalizedString("An error occured while removing the app.", comment: "App deletion failed ALTServerError") 314 | 315 | case let .requestedAppNotRunning(appName, deviceName): 316 | return String(format: NSLocalizedString("%@ is not currently running on %@.", comment: "Requested app not running ALTServerError"), appName, deviceName) 317 | } 318 | } 319 | } 320 | 321 | public extension ALTServerError { 322 | var recoverySuggestion: String? { 323 | switch self { 324 | case .connectionFailed, .deviceNotFound: 325 | return NSLocalizedString("Make sure you have trusted this device with your computer and WiFi sync is enabled.", comment: "ALTServerError recovery suggestion") 326 | case .pluginNotFound: 327 | return NSLocalizedString("Make sure Mail is running and the plug-in is enabled in Mail's preferences.", comment: "ALTServerError recovery suggestion") 328 | case .maximumFreeAppLimitReached: 329 | return NSLocalizedString("Make sure “Offload Unused Apps” is disabled in Settings > iTunes & App Stores, then install or delete all offloaded apps.", comment: "ALTServerError recovery suggestion") 330 | case .requestedAppNotRunning: 331 | let deviceName = userInfo[ALTDeviceNameErrorKey] as? String ?? NSLocalizedString("your device", comment: "ALTServerError recovery suggestion") 332 | return String(format: NSLocalizedString("Make sure the app is running in the foreground on %@ then try again.", comment: "ALTServerError recovery suggestion"), deviceName) 333 | default: 334 | return nil 335 | } 336 | } 337 | } 338 | 339 | public extension NSError { 340 | convenience init(altServerError code: ALTServerError, userInfo: [String: Any]? = nil) { 341 | self.init(domain: AltServerErrorDomain, code: code.errorCode, userInfo: userInfo) 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /Sources/SideKit/Extensions/ALTServerError+Conveniences.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ALTServerError+Conveniences.swift 3 | // AltKit 4 | // 5 | // Created by Riley Testut on 6/4/20. 6 | // Copyright © 2020 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension ALTServerError { 12 | init(_ error: E) { 13 | switch error { 14 | case let error as ALTServerError: self = error 15 | case let error as ALTServerConnectionError: 16 | self = .connectionFailed(underlyingError: error) 17 | case is DecodingError: self = .invalidRequest(underlyingError: error) 18 | case let error as NSError: 19 | var userInfo = error.userInfo 20 | if !userInfo.keys.contains(NSUnderlyingErrorKey) { 21 | // Assign underlying error (if there isn't already one). 22 | userInfo[NSUnderlyingErrorKey] = error 23 | } 24 | 25 | self = .underlyingError(domain: error.domain, code: error.code) 26 | } 27 | } 28 | 29 | init(_ code: ALTServerError, underlyingError: E) { 30 | switch code { 31 | case .connectionFailed: self = .connectionFailed(underlyingError: underlyingError) 32 | case .lostConnection: self = .lostConnection(underlyingError: underlyingError) 33 | case .invalidRequest: self = .invalidRequest(underlyingError: underlyingError) 34 | case .invalidResponse: self = .invalidResponse(underlyingError: underlyingError) 35 | default: self = code 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SideKit/Extensions/Result+Conveniences.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+Conveniences.swift 3 | // AltStore 4 | // 5 | // Created by Riley Testut on 5/22/19. 6 | // Copyright © 2019 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Result { 12 | var value: Success? { 13 | switch self { 14 | case let .success(value): return value 15 | case .failure: return nil 16 | } 17 | } 18 | 19 | var error: Failure? { 20 | switch self { 21 | case .success: return nil 22 | case let .failure(error): return error 23 | } 24 | } 25 | 26 | init(_ value: Success?, _ error: Failure?) { 27 | switch (value, error) { 28 | case let (value?, _): self = .success(value) 29 | case let (_, error?): self = .failure(error) 30 | case (nil, nil): preconditionFailure("Either value or error must be non-nil") 31 | } 32 | } 33 | } 34 | 35 | extension Result where Success == Void { 36 | init(_ success: Bool, _ error: Failure?) { 37 | if success { 38 | self = .success(()) 39 | } else if let error = error { 40 | self = .failure(error) 41 | } else { 42 | preconditionFailure("Error must be non-nil if success is false") 43 | } 44 | } 45 | } 46 | 47 | extension Result { 48 | init(_ values: (T?, U?), _ error: Failure?) where Success == (T, U) { 49 | if let value1 = values.0, let value2 = values.1 { 50 | self = .success((value1, value2)) 51 | } else if let error = error { 52 | self = .failure(error) 53 | } else { 54 | preconditionFailure("Error must be non-nil if either provided values are nil") 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SideKit/Server/Connection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connection.swift 3 | // AltKit 4 | // 5 | // Created by Riley Testut on 6/1/20. 6 | // Copyright © 2020 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Network 11 | 12 | public protocol Connection { 13 | func send(_ data: Data, completionHandler: @escaping (Result) -> Void) 14 | func receiveData(expectedSize: Int, completionHandler: @escaping (Result) -> Void) 15 | 16 | func disconnect() 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SideKit/Server/NetworkConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkConnection.swift 3 | // AltKit 4 | // 5 | // Created by Riley Testut on 6/1/20. 6 | // Copyright © 2020 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Network 11 | 12 | @available(iOS 12, tvOS 12, watchOS 5, macOS 10.14, *) 13 | public class NetworkConnection: NSObject, Connection { 14 | public let nwConnection: NWConnection 15 | 16 | public init(_ nwConnection: NWConnection) { 17 | self.nwConnection = nwConnection 18 | } 19 | 20 | public func send(_ data: Data, completionHandler: @escaping (Result) -> Void) { 21 | nwConnection.send(content: data, completion: .contentProcessed { error in 22 | if let error = error { 23 | completionHandler(.failure(.lostConnection(underlyingError: error))) 24 | } else { 25 | completionHandler(.success(())) 26 | } 27 | }) 28 | } 29 | 30 | public func receiveData(expectedSize: Int, completionHandler: @escaping (Result) -> Void) { 31 | nwConnection.receive(minimumIncompleteLength: expectedSize, maximumLength: expectedSize) { data, _, _, error in 32 | switch (data, error) { 33 | case let (data?, _): completionHandler(.success(data)) 34 | case let (_, error?): completionHandler(.failure(.lostConnection(underlyingError: error))) 35 | case (nil, nil): completionHandler(.failure(.lostConnection(underlyingError: nil))) 36 | } 37 | } 38 | } 39 | 40 | public func disconnect() { 41 | switch nwConnection.state { 42 | case .cancelled, .failed: break 43 | default: nwConnection.cancel() 44 | } 45 | } 46 | 47 | override public var description: String { 48 | return "\(nwConnection.endpoint) (Network)" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/SideKit/Server/Server.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Server.swift 3 | // AltStore 4 | // 5 | // Created by Riley Testut on 6/20/19. 6 | // Copyright © 2019 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc(ALTServer) 12 | public class Server: NSObject, Identifiable { 13 | public let id: String 14 | public let service: NetService 15 | 16 | public var name: String? { 17 | return service.hostName 18 | } 19 | 20 | public internal(set) var isPreferred = false 21 | 22 | override public var hash: Int { 23 | return id.hashValue ^ service.name.hashValue 24 | } 25 | 26 | init?(service: NetService, txtData: Data) { 27 | let txtDictionary = NetService.dictionary(fromTXTRecord: txtData) 28 | guard let identifierData = txtDictionary["serverID"], let identifier = String(data: identifierData, encoding: .utf8) else { return nil } 29 | 30 | id = identifier 31 | self.service = service 32 | 33 | super.init() 34 | } 35 | 36 | override public func isEqual(_ object: Any?) -> Bool { 37 | guard let server = object as? Server else { return false } 38 | 39 | return id == server.id && service.name == server.service.name // service.name is consistent, and is not the human readable name (hostName). 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/SideKit/Server/ServerConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerConnection.swift 3 | // AltStore 4 | // 5 | // Created by Riley Testut on 1/7/20. 6 | // Copyright © 2020 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc(ALTServerConnection) @objcMembers 12 | public class ServerConnection: NSObject { 13 | public let server: Server 14 | public let connection: Connection 15 | 16 | init(server: Server, connection: Connection) { 17 | self.server = server 18 | self.connection = connection 19 | } 20 | 21 | deinit { 22 | self.connection.disconnect() 23 | } 24 | 25 | public func disconnect() { 26 | connection.disconnect() 27 | } 28 | } 29 | 30 | public extension ServerConnection { 31 | func enableUnsignedCodeExecution(completion: @escaping (Result) -> Void) { 32 | guard let udid = Bundle.main.object(forInfoDictionaryKey: "ALTDeviceID") as? String else { 33 | return ServerManager.shared.callbackQueue.async { 34 | completion(.failure(ConnectionError.unknownUDID)) 35 | } 36 | } 37 | 38 | enableUnsignedCodeExecution(udid: udid, completion: completion) 39 | } 40 | 41 | func enableUnsignedCodeExecution(udid: String, completion: @escaping (Result) -> Void) { 42 | func finish(_ result: Result) { 43 | ServerManager.shared.callbackQueue.async { 44 | completion(result) 45 | } 46 | } 47 | 48 | let request = EnableUnsignedCodeExecutionRequest(udid: udid, processID: ProcessInfo.processInfo.processIdentifier) 49 | 50 | send(request) { result in 51 | switch result { 52 | case let .failure(error): finish(.failure(error)) 53 | case .success: 54 | self.receiveResponse { result in 55 | switch result { 56 | case let .failure(error): finish(.failure(error)) 57 | case let .success(.error(response)): finish(.failure(response.error)) 58 | case .success(.enableUnsignedCodeExecution): finish(.success(())) 59 | case .success: finish(.failure(ALTServerError.unknownResponse)) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | public extension ServerConnection { 68 | @objc(enableUnsignedCodeExecutionWithCompletionHandler:) 69 | func __enableUnsignedCodeExecution(completion: @escaping (Bool, Error?) -> Void) { 70 | enableUnsignedCodeExecution { result in 71 | switch result { 72 | case let .failure(error): completion(false, error) 73 | case .success: completion(true, nil) 74 | } 75 | } 76 | } 77 | 78 | @objc(enableUnsignedCodeExecutionWithUDID:completionHandler:) 79 | func __enableUnsignedCodeExecution(udid: String, completion: @escaping (Bool, Error?) -> Void) { 80 | enableUnsignedCodeExecution(udid: udid) { result in 81 | switch result { 82 | case let .failure(error): completion(false, error) 83 | case .success: completion(true, nil) 84 | } 85 | } 86 | } 87 | } 88 | 89 | private extension ServerConnection { 90 | func send(_ payload: T, completionHandler: @escaping (Result) -> Void) { 91 | do { 92 | let data: Data 93 | 94 | if let payload = payload as? Data { 95 | data = payload 96 | } else { 97 | data = try JSONEncoder().encode(payload) 98 | } 99 | 100 | func process(_ result: Result) -> Bool { 101 | switch result { 102 | case .success: return true 103 | case let .failure(error): 104 | completionHandler(.failure(error)) 105 | return false 106 | } 107 | } 108 | 109 | let requestSize = Int32(data.count) 110 | let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) } 111 | 112 | connection.send(requestSizeData) { result in 113 | guard process(result) else { return } 114 | 115 | self.connection.send(data) { result in 116 | guard process(result) else { return } 117 | completionHandler(.success(())) 118 | } 119 | } 120 | } catch { 121 | print("Invalid request.", error) 122 | completionHandler(.failure(ALTServerError.invalidRequest(underlyingError: error))) 123 | } 124 | } 125 | 126 | func receiveResponse(completionHandler: @escaping (Result) -> Void) { 127 | let size = MemoryLayout.size 128 | 129 | connection.receiveData(expectedSize: size) { result in 130 | do { 131 | let data = try result.get() 132 | 133 | let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) }) 134 | self.connection.receiveData(expectedSize: expectedBytes) { result in 135 | do { 136 | let data = try result.get() 137 | 138 | let response = try JSONDecoder().decode(ServerResponse.self, from: data) 139 | completionHandler(.success(response)) 140 | } catch { 141 | completionHandler(.failure(ALTServerError(error))) 142 | } 143 | } 144 | } catch { 145 | completionHandler(.failure(ALTServerError(error))) 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Sources/SideKit/Server/ServerManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerManager.swift 3 | // AltStore 4 | // 5 | // Created by Riley Testut on 5/30/19. 6 | // Copyright © 2019 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Network 11 | 12 | #if canImport(UIKit) 13 | import UIKit 14 | #elseif canImport(AppKit) 15 | import AppKit 16 | #endif 17 | 18 | public enum ConnectionError: LocalizedError { 19 | case serverNotFound 20 | case connectionFailed(Server) 21 | case connectionDropped(Server) 22 | case unknownUDID 23 | case unsupportedOS 24 | 25 | public var errorDescription: String? { 26 | switch self { 27 | case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "") 28 | case .connectionFailed: return NSLocalizedString("Could not connect to AltServer.", comment: "") 29 | case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "") 30 | case .unknownUDID: return NSLocalizedString("This device's UDID could not be determined.", comment: "") 31 | case .unsupportedOS: return NSLocalizedString("This device's OS version is too old to run AltKit.", comment: "") 32 | } 33 | } 34 | } 35 | 36 | @objc(ALTServerManager) @objcMembers 37 | public class ServerManager: NSObject { 38 | public static let shared = ServerManager() 39 | 40 | public private(set) dynamic var isDiscovering = false 41 | public private(set) dynamic var discoveredServers = [Server]() 42 | 43 | public var discoveredServerHandler: ((Server) -> Void)? 44 | public var lostServerHandler: ((Server) -> Void)? 45 | 46 | public var callbackQueue: DispatchQueue = .main 47 | 48 | // Allow other AltKit queues to target this one. 49 | internal let dispatchQueue = DispatchQueue(label: "io.altstore.altkit.ServerManager", qos: .utility, autoreleaseFrequency: .workItem) 50 | 51 | private var serviceBrowser: NetServiceBrowser? 52 | private var resolvingServices = Set() 53 | 54 | private var autoconnectGroup: DispatchGroup? 55 | private var ignoredServers = Set() 56 | 57 | override private init() { 58 | super.init() 59 | #if canImport(UIKit) 60 | NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) 61 | NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) 62 | #elseif canImport(AppKit) 63 | NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.didEnterBackground(_:)), name: NSApplication.didResignActiveNotification, object: nil) 64 | NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.willEnterForeground(_:)), name: NSApplication.willBecomeActiveNotification, object: nil) 65 | #endif 66 | } 67 | } 68 | 69 | public extension ServerManager { 70 | @objc 71 | func startDiscovering() { 72 | guard !isDiscovering else { return } 73 | isDiscovering = true 74 | 75 | DispatchQueue.main.async { 76 | // NetServiceBrowser must be initialized on main thread. 77 | // https://stackoverflow.com/questions/3526661/nsnetservicebrowser-delegate-not-called-when-searching 78 | 79 | let serviceBrowser = NetServiceBrowser() 80 | serviceBrowser.delegate = self 81 | serviceBrowser.includesPeerToPeer = false 82 | serviceBrowser.searchForServices(ofType: ALTServerServiceType, inDomain: "") 83 | 84 | self.serviceBrowser = serviceBrowser 85 | } 86 | } 87 | 88 | @objc 89 | func stopDiscovering() { 90 | guard isDiscovering else { return } 91 | isDiscovering = false 92 | 93 | discoveredServers.removeAll() 94 | ignoredServers.removeAll() 95 | resolvingServices.removeAll() 96 | 97 | serviceBrowser?.stop() 98 | serviceBrowser = nil 99 | } 100 | 101 | func connect(to server: Server, completion: @escaping (Result) -> Void) { 102 | var didFinish = false 103 | 104 | func finish(_ result: Result) { 105 | guard !didFinish else { return } 106 | didFinish = true 107 | 108 | ignoredServers.insert(server) 109 | 110 | callbackQueue.async { 111 | completion(result) 112 | } 113 | } 114 | 115 | dispatchQueue.async { 116 | guard #available(iOS 12, tvOS 12, watchOS 5, macOS 10.14, *) else { 117 | finish(.failure(ConnectionError.unsupportedOS)) 118 | return 119 | } 120 | print("Connecting to service:", server.service) 121 | 122 | let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp) 123 | connection.stateUpdateHandler = { [unowned connection] state in 124 | switch state { 125 | case let .failed(error): 126 | print("Failed to connect to service \(server.service.name).", error) 127 | finish(.failure(ConnectionError.connectionFailed(server))) 128 | 129 | case .cancelled: finish(.failure(CocoaError(.userCancelled))) 130 | 131 | case .ready: 132 | let networkConnection = NetworkConnection(connection) 133 | let serverConnection = ServerConnection(server: server, connection: networkConnection) 134 | finish(.success(serverConnection)) 135 | 136 | case .waiting: break 137 | case .setup: break 138 | case .preparing: break 139 | @unknown default: break 140 | } 141 | } 142 | 143 | connection.start(queue: self.dispatchQueue) 144 | } 145 | } 146 | 147 | func autoconnect(completion: @escaping (Result) -> Void) { 148 | dispatchQueue.async { 149 | if case let availableServers = self.discoveredServers.filter({ !self.ignoredServers.contains($0) }), 150 | let server = availableServers.first(where: { $0.isPreferred }) ?? availableServers.first 151 | { 152 | return self.connect(to: server, completion: completion) 153 | } 154 | 155 | self.autoconnectGroup = DispatchGroup() 156 | self.autoconnectGroup?.enter() 157 | self.autoconnectGroup?.notify(queue: self.dispatchQueue) { 158 | self.autoconnectGroup = nil 159 | 160 | guard 161 | case let availableServers = self.discoveredServers.filter({ !self.ignoredServers.contains($0) }), 162 | let server = availableServers.first(where: { $0.isPreferred }) ?? availableServers.first 163 | else { return self.autoconnect(completion: completion) } 164 | 165 | self.connect(to: server, completion: completion) 166 | } 167 | } 168 | } 169 | } 170 | 171 | public extension ServerManager { 172 | @objc(sharedManager) 173 | class var __shared: ServerManager { 174 | return ServerManager.shared 175 | } 176 | 177 | @objc(connectToServer:completionHandler:) 178 | func __connect(to server: Server, completion: @escaping (ServerConnection?, Error?) -> Void) { 179 | connect(to: server) { result in 180 | completion(result.value, result.error) 181 | } 182 | } 183 | 184 | @objc(autoconnectWithCompletionHandler:) 185 | func __autoconnect(completion: @escaping (ServerConnection?, Error?) -> Void) { 186 | autoconnect { result in 187 | completion(result.value, result.error) 188 | } 189 | } 190 | } 191 | 192 | private extension ServerManager { 193 | func addDiscoveredServer(_ server: Server) { 194 | dispatchQueue.async { 195 | let serverID = Bundle.main.object(forInfoDictionaryKey: "ALTServerID") as? String 196 | server.isPreferred = (server.id == serverID) 197 | 198 | guard !self.discoveredServers.contains(server) else { return } 199 | 200 | self.discoveredServers.append(server) 201 | 202 | if let callback = self.discoveredServerHandler { 203 | self.callbackQueue.async { 204 | callback(server) 205 | } 206 | } 207 | } 208 | } 209 | 210 | func removeDiscoveredServer(_ server: Server) { 211 | dispatchQueue.async { 212 | guard let index = self.discoveredServers.firstIndex(of: server) else { return } 213 | 214 | self.discoveredServers.remove(at: index) 215 | 216 | if let callback = self.lostServerHandler { 217 | self.callbackQueue.async { 218 | callback(server) 219 | } 220 | } 221 | } 222 | } 223 | } 224 | 225 | @objc 226 | private extension ServerManager { 227 | func didEnterBackground(_: Notification) { 228 | guard isDiscovering else { return } 229 | 230 | resolvingServices.removeAll() 231 | discoveredServers.removeAll() 232 | serviceBrowser?.stop() 233 | } 234 | 235 | func willEnterForeground(_: Notification) { 236 | guard isDiscovering else { return } 237 | 238 | serviceBrowser?.searchForServices(ofType: ALTServerServiceType, inDomain: "") 239 | } 240 | } 241 | 242 | extension ServerManager: NetServiceBrowserDelegate { 243 | public func netServiceBrowserWillSearch(_: NetServiceBrowser) { 244 | print("Discovering servers...") 245 | } 246 | 247 | public func netServiceBrowserDidStopSearch(_: NetServiceBrowser) { 248 | print("Stopped discovering servers.") 249 | } 250 | 251 | public func netServiceBrowser(_: NetServiceBrowser, didNotSearch errorDict: [String: NSNumber]) { 252 | print("Failed to discover servers.", errorDict) 253 | } 254 | 255 | public func netServiceBrowser(_: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { 256 | dispatchQueue.async { 257 | service.delegate = self 258 | 259 | if let txtData = service.txtRecordData(), let server = Server(service: service, txtData: txtData) { 260 | self.addDiscoveredServer(server) 261 | } else { 262 | service.resolve(withTimeout: 3.0) 263 | self.resolvingServices.insert(service) 264 | } 265 | 266 | self.autoconnectGroup?.enter() 267 | 268 | if !moreComing { 269 | self.autoconnectGroup?.leave() 270 | } 271 | } 272 | } 273 | 274 | public func netServiceBrowser(_: NetServiceBrowser, didRemove service: NetService, moreComing _: Bool) { 275 | if let server = discoveredServers.first(where: { $0.service == service }) { 276 | removeDiscoveredServer(server) 277 | } 278 | } 279 | } 280 | 281 | extension ServerManager: NetServiceDelegate { 282 | public func netServiceDidResolveAddress(_ service: NetService) { 283 | defer { 284 | self.dispatchQueue.async { 285 | guard self.resolvingServices.contains(service) else { return } 286 | self.resolvingServices.remove(service) 287 | 288 | self.autoconnectGroup?.leave() 289 | } 290 | } 291 | 292 | guard let data = service.txtRecordData(), let server = Server(service: service, txtData: data) else { return } 293 | addDiscoveredServer(server) 294 | } 295 | 296 | public func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) { 297 | print("Error resolving net service \(sender).", errorDict) 298 | 299 | dispatchQueue.async { 300 | guard self.resolvingServices.contains(sender) else { return } 301 | self.resolvingServices.remove(sender) 302 | 303 | self.autoconnectGroup?.leave() 304 | } 305 | } 306 | 307 | public func netService(_ sender: NetService, didUpdateTXTRecord data: Data) { 308 | let txtDict = NetService.dictionary(fromTXTRecord: data) 309 | print("Service \(sender) updated TXT Record:", txtDict) 310 | } 311 | 312 | public func netServiceDidStop(_ sender: NetService) { 313 | dispatchQueue.async { 314 | guard self.resolvingServices.contains(sender) else { return } 315 | self.resolvingServices.remove(sender) 316 | 317 | self.autoconnectGroup?.leave() 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /Sources/SideKit/Server/ServerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerProtocol.swift 3 | // AltServer 4 | // 5 | // Created by Riley Testut on 5/24/19. 6 | // Copyright © 2019 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public let ALTServerServiceType = "_altserver._tcp" 12 | 13 | public protocol ServerMessageProtocol: Codable { 14 | var version: Int { get } 15 | var identifier: String { get } 16 | } 17 | 18 | public enum ServerRequest: Decodable { 19 | case enableUnsignedCodeExecution(EnableUnsignedCodeExecutionRequest) 20 | case unknown(identifier: String, version: Int) 21 | 22 | var identifier: String { 23 | switch self { 24 | case let .enableUnsignedCodeExecution(request): return request.identifier 25 | case let .unknown(identifier, _): return identifier 26 | } 27 | } 28 | 29 | var version: Int { 30 | switch self { 31 | case let .enableUnsignedCodeExecution(request): return request.version 32 | case let .unknown(_, version): return version 33 | } 34 | } 35 | 36 | private enum CodingKeys: String, CodingKey { 37 | case identifier 38 | case version 39 | } 40 | 41 | public init(from decoder: Decoder) throws { 42 | let container = try decoder.container(keyedBy: CodingKeys.self) 43 | 44 | let version = try container.decode(Int.self, forKey: .version) 45 | 46 | let identifier = try container.decode(String.self, forKey: .identifier) 47 | switch identifier { 48 | case "EnableUnsignedCodeExecutionRequest": 49 | let request = try EnableUnsignedCodeExecutionRequest(from: decoder) 50 | self = .enableUnsignedCodeExecution(request) 51 | 52 | default: 53 | self = .unknown(identifier: identifier, version: version) 54 | } 55 | } 56 | } 57 | 58 | public enum ServerResponse: Decodable { 59 | case enableUnsignedCodeExecution(EnableUnsignedCodeExecutionResponse) 60 | case error(ErrorResponse) 61 | case unknown(identifier: String, version: Int) 62 | 63 | var identifier: String { 64 | switch self { 65 | case let .enableUnsignedCodeExecution(response): return response.identifier 66 | case let .error(response): return response.identifier 67 | case let .unknown(identifier, _): return identifier 68 | } 69 | } 70 | 71 | var version: Int { 72 | switch self { 73 | case let .enableUnsignedCodeExecution(response): return response.version 74 | case let .error(response): return response.version 75 | case let .unknown(_, version): return version 76 | } 77 | } 78 | 79 | private enum CodingKeys: String, CodingKey { 80 | case identifier 81 | case version 82 | } 83 | 84 | public init(from decoder: Decoder) throws { 85 | let container = try decoder.container(keyedBy: CodingKeys.self) 86 | 87 | let version = try container.decode(Int.self, forKey: .version) 88 | 89 | let identifier = try container.decode(String.self, forKey: .identifier) 90 | switch identifier { 91 | case "EnableUnsignedCodeExecutionResponse": 92 | let response = try EnableUnsignedCodeExecutionResponse(from: decoder) 93 | self = .enableUnsignedCodeExecution(response) 94 | 95 | case "ErrorResponse": 96 | let response = try ErrorResponse(from: decoder) 97 | self = .error(response) 98 | 99 | default: 100 | self = .unknown(identifier: identifier, version: version) 101 | } 102 | } 103 | } 104 | 105 | // _Don't_ provide generic SuccessResponse, as that would prevent us 106 | // from easily changing response format for a request in the future. 107 | public struct ErrorResponse: ServerMessageProtocol { 108 | public var version = 2 109 | public var identifier = "ErrorResponse" 110 | 111 | public var error: ALTServerError 112 | 113 | // Legacy (v1) 114 | private var errorCode: ALTServerError { error } 115 | 116 | public init(error: ALTServerError) { 117 | self.error = error 118 | } 119 | } 120 | 121 | public struct EnableUnsignedCodeExecutionRequest: ServerMessageProtocol { 122 | public var version = 1 123 | public var identifier = "EnableUnsignedCodeExecutionRequest" 124 | 125 | public var udid: String 126 | public var processID: Int32? 127 | public var processName: String? 128 | 129 | public init(udid: String, processID: Int32? = nil, processName: String? = nil) { 130 | self.udid = udid 131 | self.processID = processID 132 | self.processName = processName 133 | } 134 | } 135 | 136 | public struct EnableUnsignedCodeExecutionResponse: ServerMessageProtocol { 137 | public var version = 1 138 | public var identifier = "EnableUnsignedCodeExecutionResponse" 139 | 140 | public init() {} 141 | } 142 | -------------------------------------------------------------------------------- /Sources/SideKit/Types/CodableServerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableServerError.swift 3 | // AltKit 4 | // 5 | // Created by Riley Testut on 3/5/20. 6 | // Copyright © 2020 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @available(*, deprecated, renamed: "ALTServerError", message: "This type alias is deprecated, use `ALTServerError` directly instead") 12 | public typealias CodableServerError = ALTServerError 13 | -------------------------------------------------------------------------------- /Tests/SideKitTests/SideKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SideKitTests.swift 3 | // SideKit 4 | // 5 | // Created by Joseph Mattiello on 2/24/23. 6 | // Copyright © 2023 Joseph Mattiello. All rights reserved. 7 | // 8 | 9 | @testable import SideKit 10 | import XCTest 11 | 12 | class PVAppTests: XCTestCase { 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | super.tearDown() 21 | } 22 | 23 | func testExample() { 24 | // This is an example of a functional test case. 25 | // Use XCTAssert and related functions to verify your tests produce the correct results. 26 | } 27 | 28 | func testPerformanceExample() { 29 | // This is an example of a performance test case. 30 | measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | } 35 | --------------------------------------------------------------------------------