2019-05-24 11:29:27 -07:00
//
2019-07-01 15:19:22 -07:00
// A L T D e v i c e M a n a g e r + I n s t a l l a t i o n . s w i f t
2019-05-24 11:29:27 -07:00
// A l t S e r v e r
//
2019-07-01 15:19:22 -07:00
// C r e a t e d b y R i l e y T e s t u t o n 7 / 1 / 1 9 .
2019-05-24 11:29:27 -07:00
// C o p y r i g h t © 2 0 1 9 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 Cocoa
2019-07-01 15:19:22 -07:00
import UserNotifications
2019-05-24 11:29:27 -07:00
2019-09-14 11:28:57 -07:00
enum InstallError : LocalizedError
2019-05-29 15:50:53 -07:00
{
2019-09-14 11:28:57 -07:00
case cancelled
2019-05-29 15:50:53 -07:00
case noTeam
case missingPrivateKey
case missingCertificate
2019-09-14 11:28:57 -07:00
var errorDescription : String ? {
2019-05-29 15:50:53 -07:00
switch self
{
2019-09-14 11:28:57 -07:00
case . cancelled : return NSLocalizedString ( " The operation was cancelled. " , comment : " " )
2019-05-29 15:50:53 -07:00
case . noTeam : return " You are not a member of any developer teams. "
case . missingPrivateKey : return " The developer certificate's private key could not be found. "
case . missingCertificate : return " The developer certificate could not be found. "
}
}
}
2019-05-24 11:29:27 -07:00
2019-07-01 15:19:22 -07:00
extension ALTDeviceManager
2019-05-29 15:50:53 -07:00
{
2019-07-01 15:19:22 -07:00
func installAltStore ( to device : ALTDevice , appleID : String , password : String , completion : @ escaping ( Result < Void , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-06-25 13:34:12 -07:00
let destinationDirectoryURL = FileManager . default . temporaryDirectory . appendingPathComponent ( UUID ( ) . uuidString )
func finish ( _ error : Error ? , title : String = " " )
2019-05-29 15:50:53 -07:00
{
DispatchQueue . main . async {
2019-06-25 13:34:12 -07:00
if let error = error
{
2019-07-01 15:19:22 -07:00
completion ( . failure ( error ) )
2019-06-25 13:34:12 -07:00
}
else
{
2019-07-01 15:19:22 -07:00
completion ( . success ( ( ) ) )
2019-06-25 13:34:12 -07:00
}
2019-05-29 15:50:53 -07:00
}
2019-06-25 13:34:12 -07:00
try ? FileManager . default . removeItem ( at : destinationDirectoryURL )
}
2019-07-01 15:19:22 -07:00
self . authenticate ( appleID : appleID , password : password ) { ( result ) in
2019-05-29 15:50:53 -07:00
do
{
let account = try result . get ( )
2019-07-01 15:19:22 -07:00
2019-05-29 15:50:53 -07:00
self . fetchTeam ( for : account ) { ( result ) in
do
{
let team = try result . get ( )
self . register ( device , team : team ) { ( result ) in
do
{
let device = try result . get ( )
self . fetchCertificate ( for : team ) { ( result ) in
do
{
let certificate = try result . get ( )
2019-09-14 11:28:57 -07:00
let content = UNMutableNotificationContent ( )
content . title = String ( format : NSLocalizedString ( " Installing AltStore to %@... " , comment : " " ) , device . name )
content . body = NSLocalizedString ( " This may take a few seconds. " , comment : " " )
let request = UNNotificationRequest ( identifier : UUID ( ) . uuidString , content : content , trigger : nil )
UNUserNotificationCenter . current ( ) . add ( request )
2019-06-26 17:05:52 -07:00
self . downloadApp { ( result ) in
2019-05-29 15:50:53 -07:00
do
{
2019-06-26 17:05:52 -07:00
let fileURL = try result . get ( )
try FileManager . default . createDirectory ( at : destinationDirectoryURL , withIntermediateDirectories : true , attributes : nil )
2019-06-25 13:34:12 -07:00
2019-06-26 17:05:52 -07:00
let appBundleURL = try FileManager . default . unzipAppBundle ( at : fileURL , toDirectory : destinationDirectoryURL )
2019-09-13 14:48:01 -07:00
do
{
try FileManager . default . removeItem ( at : fileURL )
}
catch
{
print ( " Failed to remove downloaded .ipa. " , error )
}
2019-06-26 17:05:52 -07:00
guard let application = ALTApplication ( fileURL : appBundleURL ) else { throw ALTError ( . invalidApp ) }
self . registerAppID ( name : " AltStore " , identifier : " com.rileytestut.AltStore " , team : team ) { ( result ) in
2019-05-29 15:50:53 -07:00
do
{
2019-06-25 13:34:12 -07:00
let appID = try result . get ( )
2019-06-26 17:05:52 -07:00
self . updateFeatures ( for : appID , app : application , team : team ) { ( result ) in
2019-06-25 13:34:12 -07:00
do
{
2019-06-26 17:05:52 -07:00
let appID = try result . get ( )
2019-06-25 13:34:12 -07:00
2019-06-26 17:05:52 -07:00
self . fetchProvisioningProfile ( for : appID , team : team ) { ( result ) in
do
{
let provisioningProfile = try result . get ( )
self . install ( application , to : device , team : team , appID : appID , certificate : certificate , profile : provisioningProfile ) { ( result ) in
finish ( result . error , title : " Failed to Install AltStore " )
}
}
catch
{
finish ( error , title : " Failed to Fetch Provisioning Profile " )
}
2019-06-25 13:34:12 -07:00
}
}
catch
{
2019-06-26 17:05:52 -07:00
finish ( error , title : " Failed to Update App ID " )
2019-06-25 13:34:12 -07:00
}
}
2019-05-29 15:50:53 -07:00
}
catch
{
2019-06-26 17:05:52 -07:00
finish ( error , title : " Failed to Register App " )
2019-05-29 15:50:53 -07:00
}
}
}
catch
{
2019-06-26 17:05:52 -07:00
finish ( error , title : " Failed to Download AltStore " )
return
2019-05-29 15:50:53 -07:00
}
}
}
catch
{
2019-06-25 13:34:12 -07:00
finish ( error , title : " Failed to Fetch Certificate " )
2019-05-29 15:50:53 -07:00
}
}
}
catch
{
2019-06-25 13:34:12 -07:00
finish ( error , title : " Failed to Register Device " )
2019-05-29 15:50:53 -07:00
}
}
}
catch
{
2019-06-25 13:34:12 -07:00
finish ( error , title : " Failed to Fetch Team " )
2019-05-29 15:50:53 -07:00
}
}
}
catch
{
2019-06-25 13:34:12 -07:00
finish ( error , title : " Failed to Authenticate " )
2019-05-29 15:50:53 -07:00
}
}
}
2019-06-26 17:05:52 -07:00
func downloadApp ( completionHandler : @ escaping ( Result < URL , Error > ) -> Void )
{
let appURL = URL ( string : " https://www.dropbox.com/s/w1gn9iztlqvltyp/AltStore.ipa?dl=1 " ) !
let downloadTask = URLSession . shared . downloadTask ( with : appURL ) { ( fileURL , response , error ) in
do
{
let ( fileURL , _ ) = try Result ( ( fileURL , response ) , error ) . get ( )
completionHandler ( . success ( fileURL ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
downloadTask . resume ( )
}
2019-07-01 15:19:22 -07:00
func authenticate ( appleID : String , password : String , completionHandler : @ escaping ( Result < ALTAccount , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-07-01 15:19:22 -07:00
ALTAppleAPI . shared . authenticate ( appleID : appleID , password : password ) { ( account , error ) in
2019-05-29 15:50:53 -07:00
let result = Result ( account , error )
completionHandler ( result )
}
}
func fetchTeam ( for account : ALTAccount , completionHandler : @ escaping ( Result < ALTTeam , Error > ) -> Void )
{
ALTAppleAPI . shared . fetchTeams ( for : account ) { ( teams , error ) in
do
{
let teams = try Result ( teams , error ) . get ( )
2019-09-13 14:26:21 -07:00
if let team = teams . first ( where : { $0 . type = = . free } )
{
return completionHandler ( . success ( team ) )
}
else if let team = teams . first ( where : { $0 . type = = . individual } )
{
return completionHandler ( . success ( team ) )
}
else if let team = teams . first
{
return completionHandler ( . success ( team ) )
}
else
{
throw InstallError . noTeam
}
2019-05-29 15:50:53 -07:00
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
func fetchCertificate ( for team : ALTTeam , completionHandler : @ escaping ( Result < ALTCertificate , Error > ) -> Void )
{
ALTAppleAPI . shared . fetchCertificates ( for : team ) { ( certificates , error ) in
do
{
let certificates = try Result ( certificates , error ) . get ( )
2019-09-14 11:28:57 -07:00
// C h e c k i f t h e r e i s a n o t h e r A l t S t o r e c e r t i f i c a t e , w h i c h m e a n s A l t S t o r e h a s b e e n i n s t a l l e d w i t h t h i s A p p l e I D b e f o r e .
if certificates . contains ( where : { $0 . machineName ? . starts ( with : " AltStore " ) = = true } )
{
var isCancelled = false
DispatchQueue . main . sync {
let alert = NSAlert ( )
alert . messageText = NSLocalizedString ( " AltStore already installed on another device. " , comment : " " )
alert . informativeText = NSLocalizedString ( " Apps installed with AltStore on your other devices will stop working. Are you sure you want to continue? " , comment : " " )
alert . addButton ( withTitle : NSLocalizedString ( " Continue " , comment : " " ) )
alert . addButton ( withTitle : NSLocalizedString ( " Cancel " , comment : " " ) )
NSRunningApplication . current . activate ( options : . activateIgnoringOtherApps )
let buttonIndex = alert . runModal ( )
if buttonIndex = = NSApplication . ModalResponse . alertSecondButtonReturn
{
isCancelled = true
}
}
if isCancelled
{
return completionHandler ( . failure ( InstallError . cancelled ) )
}
}
2019-05-29 15:50:53 -07:00
if let certificate = certificates . first
{
ALTAppleAPI . shared . revoke ( certificate , for : team ) { ( success , error ) in
do
{
try Result ( success , error ) . get ( )
self . fetchCertificate ( for : team , completionHandler : completionHandler )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
else
{
ALTAppleAPI . shared . addCertificate ( machineName : " AltStore " , to : team ) { ( certificate , error ) in
do
{
let certificate = try Result ( certificate , error ) . get ( )
guard let privateKey = certificate . privateKey else { throw InstallError . missingPrivateKey }
ALTAppleAPI . shared . fetchCertificates ( for : team ) { ( certificates , error ) in
do
{
let certificates = try Result ( certificates , error ) . get ( )
2019-06-18 17:40:30 -07:00
guard let certificate = certificates . first ( where : { $0 . serialNumber = = certificate . serialNumber } ) else {
2019-05-29 15:50:53 -07:00
throw InstallError . missingCertificate
}
certificate . privateKey = privateKey
completionHandler ( . success ( certificate ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
func registerAppID ( name appName : String , identifier : String , team : ALTTeam , completionHandler : @ escaping ( Result < ALTAppID , Error > ) -> Void )
{
2019-05-31 18:24:08 -07:00
let bundleID = " com. \( team . identifier ) . \( identifier ) "
2019-05-29 15:50:53 -07:00
ALTAppleAPI . shared . fetchAppIDs ( for : team ) { ( appIDs , error ) in
do
{
let appIDs = try Result ( appIDs , error ) . get ( )
if let appID = appIDs . first ( where : { $0 . bundleIdentifier = = bundleID } )
{
completionHandler ( . success ( appID ) )
}
else
{
ALTAppleAPI . shared . addAppID ( withName : appName , bundleIdentifier : bundleID , team : team ) { ( appID , error ) in
completionHandler ( Result ( appID , error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
2019-06-25 13:34:12 -07:00
func updateFeatures ( for appID : ALTAppID , app : ALTApplication , team : ALTTeam , completionHandler : @ escaping ( Result < ALTAppID , Error > ) -> Void )
{
let requiredFeatures = app . entitlements . compactMap { ( entitlement , value ) -> ( ALTFeature , Any ) ? in
guard let feature = ALTFeature ( entitlement ) else { return nil }
return ( feature , value )
}
var features = requiredFeatures . reduce ( into : [ ALTFeature : Any ] ( ) ) { $0 [ $1 . 0 ] = $1 . 1 }
if let applicationGroups = app . entitlements [ . appGroups ] as ? [ String ] , ! applicationGroups . isEmpty
{
features [ . appGroups ] = true
}
let appID = appID . copy ( ) as ! ALTAppID
appID . features = features
ALTAppleAPI . shared . update ( appID , team : team ) { ( appID , error ) in
completionHandler ( Result ( appID , error ) )
}
}
2019-05-29 15:50:53 -07:00
func register ( _ device : ALTDevice , team : ALTTeam , completionHandler : @ escaping ( Result < ALTDevice , Error > ) -> Void )
{
ALTAppleAPI . shared . fetchDevices ( for : team ) { ( devices , error ) in
do
{
let devices = try Result ( devices , error ) . get ( )
if let device = devices . first ( where : { $0 . identifier = = device . identifier } )
{
completionHandler ( . success ( device ) )
}
else
{
ALTAppleAPI . shared . registerDevice ( name : device . name , identifier : device . identifier , team : team ) { ( device , error ) in
completionHandler ( Result ( device , error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
func fetchProvisioningProfile ( for appID : ALTAppID , team : ALTTeam , completionHandler : @ escaping ( Result < ALTProvisioningProfile , Error > ) -> Void )
{
ALTAppleAPI . shared . fetchProvisioningProfile ( for : appID , team : team ) { ( profile , error ) in
completionHandler ( Result ( profile , error ) )
}
}
2019-06-25 13:34:12 -07:00
func install ( _ application : ALTApplication , to device : ALTDevice , team : ALTTeam , appID : ALTAppID , certificate : ALTCertificate , profile : ALTProvisioningProfile , completionHandler : @ escaping ( Result < Void , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-06-25 13:34:12 -07:00
DispatchQueue . global ( ) . async {
do
{
let infoPlistURL = application . fileURL . appendingPathComponent ( " Info.plist " )
guard var infoDictionary = NSDictionary ( contentsOf : infoPlistURL ) as ? [ String : Any ] else { throw ALTError ( . missingInfoPlist ) }
infoDictionary [ kCFBundleIdentifierKey as String ] = profile . bundleIdentifier
infoDictionary [ Bundle . Info . deviceID ] = device . identifier
2019-08-01 10:45:54 -07:00
infoDictionary [ Bundle . Info . serverID ] = UserDefaults . standard . serverID
2019-06-25 13:34:12 -07:00
try ( infoDictionary as NSDictionary ) . write ( to : infoPlistURL )
let resigner = ALTSigner ( team : team , certificate : certificate )
resigner . signApp ( at : application . fileURL , provisioningProfiles : [ profile ] ) { ( success , error ) in
do
{
try Result ( success , error ) . get ( )
ALTDeviceManager . shared . installApp ( at : application . fileURL , toDeviceWithUDID : device . identifier ) { ( success , error ) in
completionHandler ( Result ( success , error ) )
}
}
catch
{
print ( " Failed to install app " , error )
completionHandler ( . failure ( error ) )
2019-05-29 15:50:53 -07:00
}
}
}
2019-06-25 13:34:12 -07:00
catch
{
print ( " Failed to install AltStore " , error )
completionHandler ( . failure ( error ) )
}
2019-05-29 15:50:53 -07:00
}
2019-05-24 11:29:27 -07:00
}
}