mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-13 00:33:28 +01:00
[AltServer] Supports enabling JIT on devices running iOS 17
AltServer embeds the AltJIT CLI tool in its app bundle and runs it as an admin subprocess.
This commit is contained in:
@@ -150,43 +150,55 @@ private extension AppDelegate
|
||||
|
||||
func enableJIT(for app: InstalledApp, on device: ALTDevice)
|
||||
{
|
||||
func finish(_ result: Result<Void, Error>)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(let error as NSError):
|
||||
let localizedTitle = String(format: NSLocalizedString("JIT could not be enabled for %@.", comment: ""), app.name)
|
||||
self.showErrorAlert(error: error.withLocalizedTitle(localizedTitle))
|
||||
|
||||
case .success:
|
||||
Task<Void, Never> {
|
||||
do
|
||||
{
|
||||
try await JITManager.shared.enableUnsignedCodeExecution(process: .name(app.executableName), device: device)
|
||||
|
||||
await MainActor.run {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = String(format: NSLocalizedString("Successfully enabled JIT for %@.", comment: ""), app.name)
|
||||
alert.informativeText = String(format: NSLocalizedString("JIT will remain enabled until you quit the app. You can now disconnect %@ from your computer.", comment: ""), device.name)
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTDeviceManager.shared.prepare(device) { (result) in
|
||||
switch result
|
||||
catch let error as JITError where error.code == .dependencyNotFound
|
||||
{
|
||||
case .failure(let error as NSError): return finish(.failure(error))
|
||||
case .success:
|
||||
ALTDeviceManager.shared.startDebugConnection(to: device) { (connection, error) in
|
||||
guard let connection = connection else {
|
||||
return finish(.failure(error! as NSError))
|
||||
}
|
||||
var errorMessage = error.localizedDescription
|
||||
if let recoverySuggestion = error.recoverySuggestion
|
||||
{
|
||||
errorMessage += "\n\n" + recoverySuggestion
|
||||
}
|
||||
|
||||
await MainActor.run { [errorMessage] in
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = NSLocalizedString("Missing AltJIT Dependencies", comment: "")
|
||||
alert.informativeText = errorMessage
|
||||
|
||||
connection.enableUnsignedCodeExecutionForProcess(withName: app.executableName) { (success, error) in
|
||||
guard success else {
|
||||
return finish(.failure(error!))
|
||||
}
|
||||
|
||||
finish(.success(()))
|
||||
alert.addButton(withTitle: NSLocalizedString("View Instructions", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
if response == .alertFirstButtonReturn
|
||||
{
|
||||
let faqURL = URL(string: "https://faq.altstore.io/how-to-use-altstore/altjit")!
|
||||
NSWorkspace.shared.open(faqURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
await MainActor.run {
|
||||
let localizedTitle = String(format: NSLocalizedString("JIT could not be enabled for %@.", comment: ""), app.name)
|
||||
self.showErrorAlert(error: error.withLocalizedTitle(localizedTitle))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,44 +147,36 @@ struct ServerRequestHandler: RequestHandler
|
||||
func handleEnableUnsignedCodeExecutionRequest(_ request: EnableUnsignedCodeExecutionRequest, for connection: Connection, completionHandler: @escaping (Result<EnableUnsignedCodeExecutionResponse, Error>) -> Void)
|
||||
{
|
||||
guard let device = ALTDeviceManager.shared.availableDevices.first(where: { $0.identifier == request.udid }) else { return completionHandler(.failure(ALTServerError(.deviceNotFound))) }
|
||||
|
||||
let process: AppProcess
|
||||
|
||||
ALTDeviceManager.shared.prepare(device) { result in
|
||||
switch result
|
||||
if let processID = request.processID
|
||||
{
|
||||
process = .pid(processID)
|
||||
}
|
||||
else if let processName = request.processName
|
||||
{
|
||||
process = .name(processName)
|
||||
}
|
||||
else
|
||||
{
|
||||
return completionHandler(.failure(ALTServerError(.invalidRequest)))
|
||||
}
|
||||
|
||||
Task<Void, Never> {
|
||||
do
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success:
|
||||
ALTDeviceManager.shared.startDebugConnection(to: device) { (connection, error) in
|
||||
guard let connection = connection else { return completionHandler(.failure(error!)) }
|
||||
|
||||
func finish(success: Bool, error: Error?)
|
||||
{
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to enable unsigned code execution for process \(request.processID?.description ?? request.processName ?? "nil"):", error)
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Enabled unsigned code execution for process:", request.processID ?? request.processName ?? "nil")
|
||||
|
||||
let response = EnableUnsignedCodeExecutionResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
if let processID = request.processID
|
||||
{
|
||||
connection.enableUnsignedCodeExecutionForProcess(withID: processID, completionHandler: finish)
|
||||
}
|
||||
else if let processName = request.processName
|
||||
{
|
||||
connection.enableUnsignedCodeExecutionForProcess(withName: processName, completionHandler: finish)
|
||||
}
|
||||
else
|
||||
{
|
||||
finish(success: false, error: ALTServerError(.invalidRequest))
|
||||
}
|
||||
}
|
||||
try await JITManager.shared.enableUnsignedCodeExecution(process: process, device: device)
|
||||
|
||||
print("Enabled unsigned code execution for process:", request.processID ?? request.processName ?? "nil")
|
||||
|
||||
let response = EnableUnsignedCodeExecutionResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to enable unsigned code execution for process \(request.processID?.description ?? request.processName ?? "nil"):", error)
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,39 +255,19 @@ extension ALTDeviceManager
|
||||
}
|
||||
}
|
||||
|
||||
extension ALTDeviceManager
|
||||
private extension ALTDeviceManager
|
||||
{
|
||||
func prepare(_ device: ALTDevice, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
ALTDeviceManager.shared.isDeveloperDiskImageMounted(for: device) { (isMounted, error) in
|
||||
switch (isMounted, error)
|
||||
Task<Void, Never> {
|
||||
do
|
||||
{
|
||||
case (_, let error?): return completionHandler(.failure(error))
|
||||
case (true, _): return completionHandler(.success(()))
|
||||
case (false, _):
|
||||
developerDiskManager.downloadDeveloperDisk(for: device) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success((let diskFileURL, let signatureFileURL)):
|
||||
ALTDeviceManager.shared.installDeveloperDiskImage(at: diskFileURL, signatureURL: signatureFileURL, to: device) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure(let error as ALTServerError) where error.code == .incompatibleDeveloperDisk:
|
||||
developerDiskManager.setDeveloperDiskCompatible(false, with: device)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .failure(let error):
|
||||
// Don't mark developer disk as incompatible because it probably failed for a different reason.
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
developerDiskManager.setDeveloperDiskCompatible(true, with: device)
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try await JITManager.shared.prepare(device)
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
AltServer/Extensions/Logger+AltServer.swift
Normal file
16
AltServer/Extensions/Logger+AltServer.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// Logger+AltServer.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/6/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import OSLog
|
||||
|
||||
extension Logger
|
||||
{
|
||||
static let altserverSubsystem = Bundle.main.bundleIdentifier!
|
||||
|
||||
static let main = Logger(subsystem: altserverSubsystem, category: "AltServer")
|
||||
}
|
||||
71
AltServer/Extensions/Process+STPrivilegedTask.swift
Normal file
71
AltServer/Extensions/Process+STPrivilegedTask.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Process+STPrivilegedTask.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 8/22/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
import OSLog
|
||||
|
||||
import STPrivilegedTask
|
||||
|
||||
extension Process
|
||||
{
|
||||
class func runAsAdmin(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef?
|
||||
{
|
||||
var launchPath = "/usr/bin/" + program
|
||||
if !FileManager.default.fileExists(atPath: launchPath)
|
||||
{
|
||||
launchPath = "/bin/" + program
|
||||
}
|
||||
|
||||
if !FileManager.default.fileExists(atPath: launchPath)
|
||||
{
|
||||
launchPath = program
|
||||
}
|
||||
|
||||
Logger.main.info("Launching admin process: \(launchPath, privacy: .public)")
|
||||
|
||||
let task = STPrivilegedTask()
|
||||
task.launchPath = launchPath
|
||||
task.arguments = arguments
|
||||
task.freeAuthorizationWhenDone = false
|
||||
|
||||
let errorCode: OSStatus
|
||||
|
||||
if let authorization = authorization
|
||||
{
|
||||
errorCode = task.launch(withAuthorization: authorization)
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCode = task.launch()
|
||||
}
|
||||
|
||||
let executableURL = URL(fileURLWithPath: launchPath)
|
||||
guard errorCode == 0 else { throw ProcessError.failed(executableURL: executableURL, exitCode: errorCode, output: nil) }
|
||||
|
||||
task.waitUntilExit()
|
||||
|
||||
Logger.main.info("Admin process \(launchPath, privacy: .public) terminated with exit code \(task.terminationStatus, privacy: .public).")
|
||||
|
||||
guard task.terminationStatus == 0 else {
|
||||
let executableURL = URL(fileURLWithPath: launchPath)
|
||||
|
||||
let outputData = task.outputFileHandle.readDataToEndOfFile()
|
||||
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
|
||||
{
|
||||
throw ProcessError.failed(executableURL: executableURL, exitCode: task.terminationStatus, output: outputString)
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ProcessError.failed(executableURL: executableURL, exitCode: task.terminationStatus, output: nil)
|
||||
}
|
||||
}
|
||||
|
||||
return task.authorization
|
||||
}
|
||||
}
|
||||
153
AltServer/JIT/JITManager.swift
Normal file
153
AltServer/JIT/JITManager.swift
Normal file
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// JITManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 8/30/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import RegexBuilder
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension URL
|
||||
{
|
||||
static let python3 = URL(fileURLWithPath: "/usr/bin/python3")
|
||||
static let altjit = Bundle.main.executableURL!.deletingLastPathComponent().appendingPathComponent("altjit")
|
||||
}
|
||||
|
||||
class JITManager
|
||||
{
|
||||
static let shared = JITManager()
|
||||
|
||||
private let diskManager = DeveloperDiskManager()
|
||||
|
||||
private var authorization: AuthorizationRef?
|
||||
|
||||
private init()
|
||||
{
|
||||
}
|
||||
|
||||
func prepare(_ device: ALTDevice) async throws
|
||||
{
|
||||
let isMounted = try await ALTDeviceManager.shared.isDeveloperDiskImageMounted(for: device)
|
||||
guard !isMounted else { return }
|
||||
|
||||
if #available(macOS 13, *), device.osVersion.majorVersion >= 17
|
||||
{
|
||||
// iOS 17+
|
||||
try await self.installPersonalizedDeveloperDisk(onto: device)
|
||||
}
|
||||
else
|
||||
{
|
||||
try await self.installDeveloperDisk(onto: device)
|
||||
}
|
||||
}
|
||||
|
||||
func enableUnsignedCodeExecution(process: AppProcess, device: ALTDevice) async throws
|
||||
{
|
||||
try await self.prepare(device)
|
||||
|
||||
if #available(macOS 13, *), device.osVersion.majorVersion >= 17
|
||||
{
|
||||
// iOS 17+
|
||||
try await self.enableModernUnsignedCodeExecution(process: process, device: device)
|
||||
}
|
||||
else
|
||||
{
|
||||
try await self.enableLegacyUnsignedCodeExecution(process: process, device: device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension JITManager
|
||||
{
|
||||
func installDeveloperDisk(onto device: ALTDevice) async throws
|
||||
{
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
self.diskManager.downloadDeveloperDisk(for: device) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): continuation.resume(throwing: error)
|
||||
case .success((let diskFileURL, let signatureFileURL)):
|
||||
ALTDeviceManager.shared.installDeveloperDiskImage(at: diskFileURL, signatureURL: signatureFileURL, to: device) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure(let error as ALTServerError) where error.code == .incompatibleDeveloperDisk:
|
||||
self.diskManager.setDeveloperDiskCompatible(false, with: device)
|
||||
continuation.resume(throwing: error)
|
||||
|
||||
case .failure(let error):
|
||||
// Don't mark developer disk as incompatible because it probably failed for a different reason.
|
||||
continuation.resume(throwing: error)
|
||||
|
||||
case .success:
|
||||
self.diskManager.setDeveloperDiskCompatible(true, with: device)
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enableLegacyUnsignedCodeExecution(process: AppProcess, device: ALTDevice) async throws
|
||||
{
|
||||
let connection = try await ALTDeviceManager.shared.startDebugConnection(to: device)
|
||||
|
||||
switch process
|
||||
{
|
||||
case .name(let name): try await connection.enableUnsignedCodeExecutionForProcess(withName: name)
|
||||
case .pid(let pid): try await connection.enableUnsignedCodeExecutionForProcess(withID: pid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13, *)
|
||||
private extension JITManager
|
||||
{
|
||||
func installPersonalizedDeveloperDisk(onto device: ALTDevice) async throws
|
||||
{
|
||||
_ = try await Process.launchAndWait(.altjit, arguments: ["mount", "--udid", device.identifier])
|
||||
}
|
||||
|
||||
func enableModernUnsignedCodeExecution(process: AppProcess, device: ALTDevice) async throws
|
||||
{
|
||||
do
|
||||
{
|
||||
if self.authorization == nil
|
||||
{
|
||||
// runAsAdmin() only returns authorization if the process completes successfully,
|
||||
// so we request authorization for a command that can't fail, then re-use it for the failable command below.
|
||||
self.authorization = try Process.runAsAdmin("echo", arguments: ["altstore"], authorization: self.authorization)
|
||||
}
|
||||
|
||||
var arguments = ["enable"]
|
||||
switch process
|
||||
{
|
||||
case .name(let name): arguments.append(name)
|
||||
case .pid(let pid): arguments.append(String(pid))
|
||||
}
|
||||
arguments += ["--udid", device.identifier]
|
||||
|
||||
self.authorization = try Process.runAsAdmin(URL.altjit.path, arguments: arguments, authorization: self.authorization)
|
||||
}
|
||||
catch let error as ProcessError where error.code == .failed
|
||||
{
|
||||
let regex = Regex {
|
||||
"No module named"
|
||||
|
||||
OneOrMore(.whitespace)
|
||||
|
||||
Capture {
|
||||
OneOrMore(.anyNonNewline)
|
||||
}
|
||||
}
|
||||
|
||||
guard let output = error.output, let match = output.firstMatch(of: regex) else { throw error }
|
||||
|
||||
let dependency = String(match.1)
|
||||
throw JITError.dependencyNotFound(dependency)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user