2020-10-06 18:11:03 -07:00
//
// P l u g i n M a n a g e r . s w i f t
// A l t S e r v e r
//
// C r e a t e d b y R i l e y T e s t u t o n 9 / 1 6 / 2 0 .
// C o p y r i g h t © 2 0 2 0 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
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 \n Hash: \n %@ \n \n Expected 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 " )
2021-09-01 12:28:53 -05:00
static let v1_6 = PluginVersion ( url : Bundle . main . url ( forResource : " AltPlugin " , withExtension : " zip " ) ! ,
sha256Hash : " e660d91612b9e7f43ffc0966808a93f0b1024adcc7c04e1c3f9bdb7bd7bcd228 " ,
version : " 1.6 " )
2020-10-06 18:11:03 -07:00
}
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 }
// L o a d I n f o . p l i s t f r o m d i s k b e c a u s e B u n d l e . i n f o D i c t i o n a r y i s c a c h e d b y s y s t e m .
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 , * )
{
2021-09-01 12:28:53 -05:00
return . v1_6
2020-10-06 18:11:03 -07:00
}
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 ( )
// E n s u r e p l u g - i n d i r e c t o r y e x i s t s .
let authorization = try self . runAndKeepAuthorization ( " mkdir " , arguments : [ " -p " , pluginDirectoryURL . path ] )
// C r e a t e t e m p o r a r y d i r e c t o r y .
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 ) }
// U n z i p A l t P l u g i n t o t e m p o r a r y d i r e c t o r y .
try self . runAndKeepAuthorization ( " unzip " , arguments : [ " -o " , fileURL . path , " -d " , temporaryDirectoryURL . path ] , authorization : authorization )
if FileManager . default . fileExists ( atPath : pluginURL . path )
{
// D e l e t e e x i s t i n g M a i l p l u g - i n .
try self . runAndKeepAuthorization ( " rm " , arguments : [ " -rf " , pluginURL . path ] , authorization : authorization )
}
// C o p y A l t P l u g i n t o M a i l p l u g - i n s d i r e c t o r y .
// M u s t b e s e p a r a t e s t e p t h a n u n z i p t o p r e v e n t m a c O S f r o m c o n s i d e r i n g p l u g - i n c o r r u p t e d .
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 }
// E n a b l e M a i l p l u g - i n p r e f e r e n c e s .
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 )
{
// D e l e t e M a i l p l u g - i n f r o m p r i v i l e g e d d i r e c t o r y .
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
}
}