From 4257f58f96a9ddf4f00855f44598891b4fec8044 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Tue, 15 Feb 2022 14:44:11 -0600 Subject: [PATCH] [AltServer] Updates AltPlugin separately from AltServer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code written and committed with Lil’ Dude “Weedles” by my side <3 --- AltServer/AppDelegate.swift | 109 ++++---- AltServer/Plugin/PluginManager.swift | 371 +++++++++++++++++++++++++++ AltServer/Plugin/PluginVersion.swift | 37 +++ AltServer/PluginManager.swift | 310 ---------------------- AltStore.xcodeproj/project.pbxproj | 14 +- 5 files changed, 486 insertions(+), 355 deletions(-) create mode 100644 AltServer/Plugin/PluginManager.swift create mode 100644 AltServer/Plugin/PluginVersion.swift delete mode 100644 AltServer/PluginManager.swift diff --git a/AltServer/AppDelegate.swift b/AltServer/AppDelegate.swift index 685b70fd..1449b2f0 100644 --- a/AltServer/AppDelegate.swift +++ b/AltServer/AppDelegate.swift @@ -54,6 +54,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var _jitAppListMenuControllers = [AnyObject]() + private var isAltPluginUpdateAvailable = false + func applicationDidFinishLaunching(_ aNotification: Notification) { UserDefaults.standard.registerDefaults() @@ -110,9 +112,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - if self.pluginManager.isUpdateAvailable - { - self.installMailPlugin() + self.pluginManager.isUpdateAvailable { result in + guard let isUpdateAvailable = try? result.get() else { return } + self.isAltPluginUpdateAvailable = isUpdateAvailable + + if isUpdateAvailable + { + self.installMailPlugin() + } } } @@ -228,57 +235,69 @@ private extension AppDelegate let username = appleIDTextField.stringValue let password = passwordTextField.stringValue - - func install() + + func finish(_ result: Result) { - ALTDeviceManager.shared.installApplication(at: url, to: device, appleID: username, password: password) { (result) in - switch result - { - case .success(let application): - let content = UNMutableNotificationContent() - content.title = NSLocalizedString("Installation Succeeded", comment: "") - content.body = String(format: NSLocalizedString("%@ was successfully installed on %@.", comment: ""), application.name, device.name) - - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) - UNUserNotificationCenter.current().add(request) - - case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication): - // Ignore - break - - case .failure(let error): + switch result + { + case .success(let application): + let content = UNMutableNotificationContent() + content.title = NSLocalizedString("Installation Succeeded", comment: "") + content.body = String(format: NSLocalizedString("%@ was successfully installed on %@.", comment: ""), application.name, device.name) + + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) + + case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication): + // Ignore + break + + case .failure(let error): + DispatchQueue.main.async { self.showErrorAlert(error: error, localizedFailure: String(format: NSLocalizedString("Could not install app to %@.", comment: ""), device.name)) } } } - - if !self.pluginManager.isMailPluginInstalled || self.pluginManager.isUpdateAvailable + + func install() { - AnisetteDataManager.shared.isXPCAvailable { (isAvailable) in - if isAvailable - { - // XPC service is available, so we don't need to install/update Mail plug-in. - // Users can still manually do so from the AltServer menu. - install() - } - else - { - DispatchQueue.main.async { - self.installMailPlugin { (result) in - switch result - { - case .failure: break - case .success: install() + ALTDeviceManager.shared.installApplication(at: url, to: device, appleID: username, password: password, completion: finish(_:)) + } + + AnisetteDataManager.shared.isXPCAvailable { isAvailable in + if isAvailable + { + // XPC service is available, so we don't need to install/update Mail plug-in. + // Users can still manually do so from the AltServer menu. + install() + } + else + { + self.pluginManager.isUpdateAvailable { result in + switch result + { + case .failure(let error): finish(.failure(error)) + case .success(let isUpdateAvailable): + self.isAltPluginUpdateAvailable = isUpdateAvailable + + if !self.pluginManager.isMailPluginInstalled || isUpdateAvailable + { + self.installMailPlugin { result in + switch result + { + case .failure: break + case .success: install() + } } } + else + { + install() + } } } } } - else - { - install() - } } func showErrorAlert(error: Error, localizedFailure: String) @@ -356,7 +375,7 @@ private extension AppDelegate @objc func handleInstallMailPluginMenuItem(_ item: NSMenuItem) { - if !self.pluginManager.isMailPluginInstalled || self.pluginManager.isUpdateAvailable + if !self.pluginManager.isMailPluginInstalled || self.isAltPluginUpdateAvailable { self.installMailPlugin() } @@ -384,6 +403,8 @@ private extension AppDelegate alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "") alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "") alert.runModal() + + self.isAltPluginUpdateAvailable = false } completion?(result) @@ -434,7 +455,7 @@ extension AppDelegate: NSMenuDelegate self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:)) self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off - if self.pluginManager.isUpdateAvailable + if self.isAltPluginUpdateAvailable { self.installMailPluginMenuItem.title = NSLocalizedString("Update Mail Plug-in", comment: "") } diff --git a/AltServer/Plugin/PluginManager.swift b/AltServer/Plugin/PluginManager.swift new file mode 100644 index 00000000..b35b21dc --- /dev/null +++ b/AltServer/Plugin/PluginManager.swift @@ -0,0 +1,371 @@ +// +// PluginManager.swift +// AltServer +// +// Created by Riley Testut on 9/16/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation +import AppKit +import CryptoKit + +import STPrivilegedTask + +private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true) +private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle") + +enum PluginError: LocalizedError +{ + case cancelled + case unknown + case notFound + case mismatchedHash(hash: String, expectedHash: String) + case taskError(String) + case taskErrorCode(Int) + + var errorDescription: String? { + switch self + { + case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "") + case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "") + case .notFound: return NSLocalizedString("The Mail plug-in does not exist at the requested URL.", comment: "") + case .mismatchedHash(let hash, let expectedHash): return String(format: NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.\n\nHash:\n%@\n\nExpected Hash:\n%@", comment: ""), hash, expectedHash) + case .taskError(let output): return output + case .taskErrorCode(let errorCode): return String(format: NSLocalizedString("There was an error installing the Mail plug-in. (Error Code: %@)", comment: ""), NSNumber(value: errorCode)) + } + } +} + +private extension URL +{ + #if STAGING + static let altPluginUpdateURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altserver/altplugin/altplugin2.json")! + #else + static let altPluginUpdateURL = URL(string: "https://cdn.altstore.io/file/altstore/altserver/altplugin/altplugin.json")! + #endif +} + +class PluginManager +{ + private let session = URLSession(configuration: .ephemeral) + private var latestPluginVersion: PluginVersion? + + var isMailPluginInstalled: Bool { + let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path) + return isMailPluginInstalled + } + + func isUpdateAvailable(completionHandler: @escaping (Result) -> Void) + { + self.isUpdateAvailable(useCache: false, completionHandler: completionHandler) + } + + private func isUpdateAvailable(useCache: Bool, completionHandler: @escaping (Result) -> Void) + { + do + { + // If Mail plug-in is not yet installed, then there is no update available. + guard let bundle = Bundle(url: pluginURL) else { return completionHandler(.success(false)) } + + // Load Info.plist from disk because Bundle.infoDictionary is cached by system. + let infoDictionaryURL = bundle.bundleURL.appendingPathComponent("Contents/Info.plist") + guard let infoDictionary = NSDictionary(contentsOf: infoDictionaryURL) as? [String: Any], + let localVersion = infoDictionary["CFBundleShortVersionString"] as? String + else { throw CocoaError(.fileReadCorruptFile, userInfo: [NSURLErrorKey: infoDictionaryURL]) } + + if let pluginVersion = self.latestPluginVersion, useCache + { + let isUpdateAvailable = (localVersion != pluginVersion.version) + completionHandler(.success(isUpdateAvailable)) + } + else + { + self.fetchLatestPluginVersion(useCache: useCache) { result in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let pluginVersion): + let isUpdateAvailable = (localVersion != pluginVersion.version) + completionHandler(.success(isUpdateAvailable)) + } + } + } + } + catch + { + completionHandler(.failure(error)) + } + } +} + +extension PluginManager +{ + func installMailPlugin(completionHandler: @escaping (Result) -> Void) + { + self.isUpdateAvailable(useCache: true) { result in + DispatchQueue.main.async { + do + { + let isUpdateAvailable = try result.get() + + let alert = NSAlert() + if isUpdateAvailable + { + alert.messageText = NSLocalizedString("Update Mail Plug-in", comment: "") + alert.informativeText = NSLocalizedString("An update is available for AltServer's Mail plug-in. Please update the plug-in now in order to keep using AltStore.", comment: "") + + alert.addButton(withTitle: NSLocalizedString("Update Plug-in", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) + } + else + { + alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "") + alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "") + + alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) + } + + NSRunningApplication.current.activate(options: .activateIgnoringOtherApps) + + let response = alert.runModal() + guard response == .alertFirstButtonReturn else { throw PluginError.cancelled } + + self.downloadPlugin { (result) in + do + { + let fileURL = try result.get() + + // Ensure plug-in directory exists. + let authorization = try self.runAndKeepAuthorization("mkdir", arguments: ["-p", pluginDirectoryURL.path]) + + // Create temporary directory. + let temporaryDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) + defer { try? FileManager.default.removeItem(at: temporaryDirectoryURL) } + + // Unzip AltPlugin to temporary directory. + try self.runAndKeepAuthorization("unzip", arguments: ["-o", fileURL.path, "-d", temporaryDirectoryURL.path], authorization: authorization) + + if FileManager.default.fileExists(atPath: pluginURL.path) + { + // Delete existing Mail plug-in. + try self.runAndKeepAuthorization("rm", arguments: ["-rf", pluginURL.path], authorization: authorization) + } + + // Copy AltPlugin to Mail plug-ins directory. + // Must be separate step than unzip to prevent macOS from considering plug-in corrupted. + let unzippedPluginURL = temporaryDirectoryURL.appendingPathComponent(pluginURL.lastPathComponent) + try self.runAndKeepAuthorization("cp", arguments: ["-R", unzippedPluginURL.path, pluginDirectoryURL.path], authorization: authorization) + + guard self.isMailPluginInstalled else { throw PluginError.unknown } + + // Enable Mail plug-in preferences. + try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization) + + print("Finished installing Mail plug-in!") + + completionHandler(.success(())) + } + catch + { + completionHandler(.failure(error)) + } + } + } + catch + { + completionHandler(.failure(error)) + } + } + } + } + + func uninstallMailPlugin(completionHandler: @escaping (Result) -> Void) + { + let alert = NSAlert() + alert.messageText = NSLocalizedString("Uninstall Mail Plug-in", comment: "") + alert.informativeText = NSLocalizedString("Are you sure you want to uninstall the AltServer Mail plug-in? You will no longer be able to install or refresh apps with AltStore.", comment: "") + + alert.addButton(withTitle: NSLocalizedString("Uninstall Plug-in", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) + + NSRunningApplication.current.activate(options: .activateIgnoringOtherApps) + + let response = alert.runModal() + guard response == .alertFirstButtonReturn else { return completionHandler(.failure(PluginError.cancelled)) } + + DispatchQueue.global().async { + do + { + if FileManager.default.fileExists(atPath: pluginURL.path) + { + // Delete Mail plug-in from privileged directory. + try self.run("rm", arguments: ["-rf", pluginURL.path]) + } + + completionHandler(.success(())) + } + catch + { + completionHandler(.failure(error)) + } + } + } +} + +private extension PluginManager +{ + func fetchLatestPluginVersion(useCache: Bool, completionHandler: @escaping (Result) -> Void) + { + if let pluginVersion = self.latestPluginVersion, useCache + { + return completionHandler(.success(pluginVersion)) + } + + guard #available(macOS 11, *) else { + // macOS versions prior to 11.0 require Mail plug-ins be *unsigned*, + // so we hardcode these versions to use the unsigned AltPlugin v1.0. + return completionHandler(.success(.v1_0)) + } + + let dataTask = self.session.dataTask(with: .altPluginUpdateURL) { (data, response, error) in + do + { + if let response = response as? HTTPURLResponse + { + guard response.statusCode != 404 else { return completionHandler(.failure(PluginError.notFound)) } + } + + guard let data = data else { throw error! } + + let response = try JSONDecoder().decode(PluginVersionResponse.self, from: data) + completionHandler(.success(response.pluginVersion)) + } + catch + { + completionHandler(.failure(error)) + } + } + + dataTask.resume() + } + + func downloadPlugin(completion: @escaping (Result) -> Void) + { + self.fetchLatestPluginVersion(useCache: true) { result in + switch result + { + case .failure(let error): completion(.failure(error)) + case .success(let pluginVersion): + + func finish(_ result: Result) + { + do + { + let fileURL = try result.get() + + if #available(OSX 10.15, *) + { + let data = try Data(contentsOf: fileURL) + let sha256Hash = SHA256.hash(data: data) + let hashString = sha256Hash.compactMap { String(format: "%02x", $0) }.joined() + + print("Comparing Mail plug-in hash (\(hashString)) against expected hash (\(pluginVersion.sha256Hash))...") + guard hashString == pluginVersion.sha256Hash else { throw PluginError.mismatchedHash(hash: hashString, expectedHash: pluginVersion.sha256Hash) } + } + + completion(.success(fileURL)) + } + catch + { + completion(.failure(error)) + } + } + + if pluginVersion.url.isFileURL + { + finish(.success(pluginVersion.url)) + } + else + { + let downloadTask = URLSession.shared.downloadTask(with: pluginVersion.url) { (fileURL, response, error) in + if let response = response as? HTTPURLResponse + { + guard response.statusCode != 404 else { return finish(.failure(PluginError.notFound)) } + } + + let result = Result(fileURL, error) + finish(result) + + if let fileURL = fileURL + { + try? FileManager.default.removeItem(at: fileURL) + } + } + + downloadTask.resume() + } + } + } + } + + func run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws + { + _ = try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: true) + } + + @discardableResult + func runAndKeepAuthorization(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef + { + return try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: false) + } + + func _run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil, freeAuthorization: Bool) throws -> AuthorizationRef + { + var launchPath = "/usr/bin/" + program + if !FileManager.default.fileExists(atPath: launchPath) + { + launchPath = "/bin/" + program + } + + print("Running program:", launchPath) + + let task = STPrivilegedTask() + task.launchPath = launchPath + task.arguments = arguments + task.freeAuthorizationWhenDone = freeAuthorization + + let errorCode: OSStatus + + if let authorization = authorization + { + errorCode = task.launch(withAuthorization: authorization) + } + else + { + errorCode = task.launch() + } + + guard errorCode == 0 else { throw PluginError.taskErrorCode(Int(errorCode)) } + + task.waitUntilExit() + + print("Exit code:", task.terminationStatus) + + guard task.terminationStatus == 0 else { + let outputData = task.outputFileHandle.readDataToEndOfFile() + + if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty + { + throw PluginError.taskError(outputString) + } + + throw PluginError.taskErrorCode(Int(task.terminationStatus)) + } + + guard let authorization = task.authorization else { throw PluginError.unknown } + return authorization + } +} diff --git a/AltServer/Plugin/PluginVersion.swift b/AltServer/Plugin/PluginVersion.swift new file mode 100644 index 00000000..72f7bcb5 --- /dev/null +++ b/AltServer/Plugin/PluginVersion.swift @@ -0,0 +1,37 @@ +// +// PluginVersion.swift +// AltServer +// +// Created by Riley Testut and Weedles on 2/15/22 <3 +// Copyright © 2022 Riley Testut. All rights reserved. +// + +import Foundation + +struct PluginVersion: Decodable +{ + var url: URL + var sha256Hash: String + var version: String + + static let v1_0 = PluginVersion(url: URL(string: "https://f000.backblazeb2.com/file/altstore/altserver/altplugin/1_0.zip")!, + sha256Hash: "070e9b7e1f74e7a6474d36253ab5a3623ff93892acc9e1043c3581f2ded12200", + version: "1.0") + + static let v1_9 = PluginVersion(url: Bundle.main.url(forResource: "AltPlugin", withExtension: "zip")!, + sha256Hash: "83ead26d8776ef6850e06fe3d1c5c5559aca284718b1cf3cc49785ba6b1e2849", + version: "1.9") + + private enum CodingKeys: String, CodingKey + { + case url + case sha256Hash = "sha256" + case version + } +} + +struct PluginVersionResponse: Decodable +{ + var version: Int + var pluginVersion: PluginVersion +} diff --git a/AltServer/PluginManager.swift b/AltServer/PluginManager.swift deleted file mode 100644 index 7114ecd3..00000000 --- a/AltServer/PluginManager.swift +++ /dev/null @@ -1,310 +0,0 @@ -// -// PluginManager.swift -// AltServer -// -// Created by Riley Testut on 9/16/20. -// Copyright © 2020 Riley Testut. All rights reserved. -// - -import Foundation -import AppKit -import CryptoKit - -import STPrivilegedTask - -private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true) -private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle") - -enum PluginError: LocalizedError -{ - case cancelled - case unknown - case notFound - case mismatchedHash(hash: String, expectedHash: String) - case taskError(String) - case taskErrorCode(Int) - - var errorDescription: String? { - switch self - { - case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "") - case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "") - case .notFound: return NSLocalizedString("The Mail plug-in does not exist at the requested URL.", comment: "") - case .mismatchedHash(let hash, let expectedHash): return String(format: NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.\n\nHash:\n%@\n\nExpected Hash:\n%@", comment: ""), hash, expectedHash) - case .taskError(let output): return output - case .taskErrorCode(let errorCode): return String(format: NSLocalizedString("There was an error installing the Mail plug-in. (Error Code: %@)", comment: ""), NSNumber(value: errorCode)) - } - } -} - -struct PluginVersion -{ - var url: URL - var sha256Hash: String - var version: String - - static let v1_0 = PluginVersion(url: URL(string: "https://f000.backblazeb2.com/file/altstore/altserver/altplugin/1_0.zip")!, - sha256Hash: "070e9b7e1f74e7a6474d36253ab5a3623ff93892acc9e1043c3581f2ded12200", - version: "1.0") - - static let v1_9 = PluginVersion(url: Bundle.main.url(forResource: "AltPlugin", withExtension: "zip")!, - sha256Hash: "83ead26d8776ef6850e06fe3d1c5c5559aca284718b1cf3cc49785ba6b1e2849", - version: "1.9") -} - -class PluginManager -{ - var isMailPluginInstalled: Bool { - let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path) - return isMailPluginInstalled - } - - var isUpdateAvailable: Bool { - guard let bundle = Bundle(url: pluginURL) else { return false } - - // Load Info.plist from disk because Bundle.infoDictionary is cached by system. - let infoDictionaryURL = bundle.bundleURL.appendingPathComponent("Contents/Info.plist") - guard let infoDictionary = NSDictionary(contentsOf: infoDictionaryURL) as? [String: Any], - let version = infoDictionary["CFBundleShortVersionString"] as? String - else { return false } - - let isUpdateAvailable = (version != self.preferredVersion.version) - return isUpdateAvailable - } - - private var preferredVersion: PluginVersion { - if #available(macOS 11, *) - { - return .v1_9 - } - else - { - return .v1_0 - } - } -} - -extension PluginManager -{ - func installMailPlugin(completionHandler: @escaping (Result) -> Void) - { - do - { - let alert = NSAlert() - - if self.isUpdateAvailable - { - alert.messageText = NSLocalizedString("Update Mail Plug-in", comment: "") - alert.informativeText = NSLocalizedString("An update is available for AltServer's Mail plug-in. Please update the plug-in now in order to keep using AltStore.", comment: "") - - alert.addButton(withTitle: NSLocalizedString("Update Plug-in", comment: "")) - alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) - } - else - { - alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "") - alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "") - - alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: "")) - alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) - } - - NSRunningApplication.current.activate(options: .activateIgnoringOtherApps) - - let response = alert.runModal() - guard response == .alertFirstButtonReturn else { throw PluginError.cancelled } - - self.downloadPlugin { (result) in - do - { - let fileURL = try result.get() - - // Ensure plug-in directory exists. - let authorization = try self.runAndKeepAuthorization("mkdir", arguments: ["-p", pluginDirectoryURL.path]) - - // Create temporary directory. - let temporaryDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) - defer { try? FileManager.default.removeItem(at: temporaryDirectoryURL) } - - // Unzip AltPlugin to temporary directory. - try self.runAndKeepAuthorization("unzip", arguments: ["-o", fileURL.path, "-d", temporaryDirectoryURL.path], authorization: authorization) - - if FileManager.default.fileExists(atPath: pluginURL.path) - { - // Delete existing Mail plug-in. - try self.runAndKeepAuthorization("rm", arguments: ["-rf", pluginURL.path], authorization: authorization) - } - - // Copy AltPlugin to Mail plug-ins directory. - // Must be separate step than unzip to prevent macOS from considering plug-in corrupted. - let unzippedPluginURL = temporaryDirectoryURL.appendingPathComponent(pluginURL.lastPathComponent) - try self.runAndKeepAuthorization("cp", arguments: ["-R", unzippedPluginURL.path, pluginDirectoryURL.path], authorization: authorization) - - guard self.isMailPluginInstalled else { throw PluginError.unknown } - - // Enable Mail plug-in preferences. - try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization) - - print("Finished installing Mail plug-in!") - - completionHandler(.success(())) - } - catch - { - completionHandler(.failure(error)) - } - } - } - catch - { - completionHandler(.failure(PluginError.cancelled)) - } - } - - func uninstallMailPlugin(completionHandler: @escaping (Result) -> Void) - { - let alert = NSAlert() - alert.messageText = NSLocalizedString("Uninstall Mail Plug-in", comment: "") - alert.informativeText = NSLocalizedString("Are you sure you want to uninstall the AltServer Mail plug-in? You will no longer be able to install or refresh apps with AltStore.", comment: "") - - alert.addButton(withTitle: NSLocalizedString("Uninstall Plug-in", comment: "")) - alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) - - NSRunningApplication.current.activate(options: .activateIgnoringOtherApps) - - let response = alert.runModal() - guard response == .alertFirstButtonReturn else { return completionHandler(.failure(PluginError.cancelled)) } - - DispatchQueue.global().async { - do - { - if FileManager.default.fileExists(atPath: pluginURL.path) - { - // Delete Mail plug-in from privileged directory. - try self.run("rm", arguments: ["-rf", pluginURL.path]) - } - - completionHandler(.success(())) - } - catch - { - completionHandler(.failure(error)) - } - } - } -} - -private extension PluginManager -{ - func downloadPlugin(completion: @escaping (Result) -> Void) - { - let pluginVersion = self.preferredVersion - - func finish(_ result: Result) - { - do - { - let fileURL = try result.get() - - if #available(OSX 10.15, *) - { - let data = try Data(contentsOf: fileURL) - let sha256Hash = SHA256.hash(data: data) - let hashString = sha256Hash.compactMap { String(format: "%02x", $0) }.joined() - - print("Comparing Mail plug-in hash (\(hashString)) against expected hash (\(pluginVersion.sha256Hash))...") - guard hashString == pluginVersion.sha256Hash else { throw PluginError.mismatchedHash(hash: hashString, expectedHash: pluginVersion.sha256Hash) } - } - - completion(.success(fileURL)) - } - catch - { - completion(.failure(error)) - } - } - - if pluginVersion.url.isFileURL - { - finish(.success(pluginVersion.url)) - } - else - { - let downloadTask = URLSession.shared.downloadTask(with: pluginVersion.url) { (fileURL, response, error) in - if let response = response as? HTTPURLResponse - { - guard response.statusCode != 404 else { return finish(.failure(PluginError.notFound)) } - } - - let result = Result(fileURL, error) - finish(result) - - if let fileURL = fileURL - { - try? FileManager.default.removeItem(at: fileURL) - } - } - - downloadTask.resume() - } - } - - func run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws - { - _ = try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: true) - } - - @discardableResult - func runAndKeepAuthorization(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef - { - return try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: false) - } - - func _run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil, freeAuthorization: Bool) throws -> AuthorizationRef - { - var launchPath = "/usr/bin/" + program - if !FileManager.default.fileExists(atPath: launchPath) - { - launchPath = "/bin/" + program - } - - print("Running program:", launchPath) - - let task = STPrivilegedTask() - task.launchPath = launchPath - task.arguments = arguments - task.freeAuthorizationWhenDone = freeAuthorization - - let errorCode: OSStatus - - if let authorization = authorization - { - errorCode = task.launch(withAuthorization: authorization) - } - else - { - errorCode = task.launch() - } - - guard errorCode == 0 else { throw PluginError.taskErrorCode(Int(errorCode)) } - - task.waitUntilExit() - - print("Exit code:", task.terminationStatus) - - guard task.terminationStatus == 0 else { - let outputData = task.outputFileHandle.readDataToEndOfFile() - - if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty - { - throw PluginError.taskError(outputString) - } - - throw PluginError.taskErrorCode(Int(task.terminationStatus)) - } - - guard let authorization = task.authorization else { throw PluginError.unknown } - return authorization - } -} diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 6344a1c1..8d63bb78 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -239,6 +239,7 @@ BFBE0004250ACFFB0080826E /* ViewApp.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = BF989191250AAE86002ACF50 /* ViewApp.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF989190250AAE86002ACF50 /* ViewAppIntentHandler.swift */; }; BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BFBF331A2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel */; }; + BFC15ADA27BC352300ED2FB4 /* PluginVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC15AD927BC352300ED2FB4 /* PluginVersion.swift */; }; BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */; }; BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */; }; BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */; }; @@ -706,6 +707,7 @@ BFBAC8852295C90300587369 /* Result+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Conveniences.swift"; sourceTree = ""; }; BFBF33142526754700B7B8C9 /* AltStore 9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 9.xcdatamodel"; sourceTree = ""; }; BFBF331A2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore8ToAltStore9.xcmappingmodel; sourceTree = ""; }; + BFC15AD927BC352300ED2FB4 /* PluginVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginVersion.swift; sourceTree = ""; }; BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAppOperation.swift; sourceTree = ""; }; BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeactivateAppOperation.swift; sourceTree = ""; }; BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppsCollectionHeaderView.swift; sourceTree = ""; }; @@ -998,10 +1000,10 @@ BF45868F229872EA00BD7491 /* AppDelegate.swift */, BF458695229872EA00BD7491 /* Main.storyboard */, BFE48974238007CE003239E0 /* AnisetteDataManager.swift */, - BFC712BA2512B9CF00AB5EBE /* PluginManager.swift */, BFAD67A225E0854500D4C4D1 /* DeveloperDiskManager.swift */, BFF0394A25F0551600BE607D /* MenuController.swift */, BF904DE9265DAE9A00E86C2A /* InstalledApp.swift */, + BFC15ADB27BC3AD100ED2FB4 /* Plugin */, BF703195229F36FF006E110F /* Devices */, BFD52BDC22A0A659000B7ED1 /* Connections */, BF055B4A233B528B0086DEA9 /* Extensions */, @@ -1431,6 +1433,15 @@ path = "My Apps"; sourceTree = ""; }; + BFC15ADB27BC3AD100ED2FB4 /* Plugin */ = { + isa = PBXGroup; + children = ( + BFC15AD927BC352300ED2FB4 /* PluginVersion.swift */, + BFC712BA2512B9CF00AB5EBE /* PluginManager.swift */, + ); + path = Plugin; + sourceTree = ""; + }; BFC51D7922972F1F00388324 /* Server */ = { isa = PBXGroup; children = ( @@ -2332,6 +2343,7 @@ BFF767C82489A74E0097E58C /* WirelessConnectionHandler.swift in Sources */, BFF0394B25F0551600BE607D /* MenuController.swift in Sources */, BFECAC8024FD950B0077C41F /* ConnectionManager.swift in Sources */, + BFC15ADA27BC352300ED2FB4 /* PluginVersion.swift in Sources */, BFECAC8324FD950B0077C41F /* NetworkConnection.swift in Sources */, BF541C0B25E5A5FA00CD46B2 /* FileManager+URLs.swift in Sources */, BFECAC8724FD950B0077C41F /* Bundle+AltStore.swift in Sources */,