├── .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 | [](https://github.com/SideStore/SideKit/actions/workflows/test.yml)
6 |
7 | 
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 |
--------------------------------------------------------------------------------