mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[AltServer] Updates AltPlugin separately from AltServer
Code written and committed with Lil’ Dude “Weedles” by my side <3
This commit is contained in:
@@ -54,6 +54,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
private var _jitAppListMenuControllers = [AnyObject]()
|
||||
|
||||
private var isAltPluginUpdateAvailable = false
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification)
|
||||
{
|
||||
UserDefaults.standard.registerDefaults()
|
||||
@@ -110,11 +112,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if self.pluginManager.isUpdateAvailable
|
||||
self.pluginManager.isUpdateAvailable { result in
|
||||
guard let isUpdateAvailable = try? result.get() else { return }
|
||||
self.isAltPluginUpdateAvailable = isUpdateAvailable
|
||||
|
||||
if isUpdateAvailable
|
||||
{
|
||||
self.installMailPlugin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification)
|
||||
{
|
||||
@@ -229,9 +236,8 @@ private extension AppDelegate
|
||||
let username = appleIDTextField.stringValue
|
||||
let password = passwordTextField.stringValue
|
||||
|
||||
func install()
|
||||
func finish(_ result: Result<ALTApplication, Error>)
|
||||
{
|
||||
ALTDeviceManager.shared.installApplication(at: url, to: device, appleID: username, password: password) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let application):
|
||||
@@ -247,14 +253,18 @@ private extension AppDelegate
|
||||
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
|
||||
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.
|
||||
@@ -263,8 +273,16 @@ private extension AppDelegate
|
||||
}
|
||||
else
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.installMailPlugin { (result) in
|
||||
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
|
||||
@@ -272,14 +290,15 @@ private extension AppDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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: "")
|
||||
}
|
||||
|
||||
371
AltServer/Plugin/PluginManager.swift
Normal file
371
AltServer/Plugin/PluginManager.swift
Normal file
@@ -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<Bool, Error>) -> Void)
|
||||
{
|
||||
self.isUpdateAvailable(useCache: false, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func isUpdateAvailable(useCache: Bool, completionHandler: @escaping (Result<Bool, Error>) -> 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, Error>) -> 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, Error>) -> 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<PluginVersion, Error>) -> 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<URL, Error>) -> Void)
|
||||
{
|
||||
self.fetchLatestPluginVersion(useCache: true) { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success(let pluginVersion):
|
||||
|
||||
func finish(_ result: Result<URL, Error>)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
37
AltServer/Plugin/PluginVersion.swift
Normal file
37
AltServer/Plugin/PluginVersion.swift
Normal file
@@ -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
|
||||
}
|
||||
@@ -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, Error>) -> 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, Error>) -> 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<URL, Error>) -> Void)
|
||||
{
|
||||
let pluginVersion = self.preferredVersion
|
||||
|
||||
func finish(_ result: Result<URL, Error>)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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 = "<group>"; };
|
||||
BFBF33142526754700B7B8C9 /* AltStore 9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 9.xcdatamodel"; sourceTree = "<group>"; };
|
||||
BFBF331A2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore8ToAltStore9.xcmappingmodel; sourceTree = "<group>"; };
|
||||
BFC15AD927BC352300ED2FB4 /* PluginVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginVersion.swift; sourceTree = "<group>"; };
|
||||
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAppOperation.swift; sourceTree = "<group>"; };
|
||||
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeactivateAppOperation.swift; sourceTree = "<group>"; };
|
||||
BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppsCollectionHeaderView.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
};
|
||||
BFC15ADB27BC3AD100ED2FB4 /* Plugin */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFC15AD927BC352300ED2FB4 /* PluginVersion.swift */,
|
||||
BFC712BA2512B9CF00AB5EBE /* PluginManager.swift */,
|
||||
);
|
||||
path = Plugin;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
||||
Reference in New Issue
Block a user