2019-05-31 18:24:08 -07:00
//
// A p p M a n a g e r . s w i f t
// A l t S t o r e
//
// C r e a t e d b y R i l e y T e s t u t o n 5 / 2 9 / 1 9 .
// 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 Foundation
import UIKit
import AltSign
import AltKit
import Roxas
extension AppManager
{
enum AppError : LocalizedError
{
case unknown
case missingUDID
case noServersFound
case missingPrivateKey
case missingCertificate
2019-06-04 18:29:50 -07:00
case notAuthenticated
case multipleCertificates
case multipleTeams
2019-05-31 18:24:08 -07:00
case download ( URLError )
case authentication ( Error )
case fetchingSigningResources ( Error )
2019-06-04 13:53:21 -07:00
case prepare ( Error )
2019-05-31 18:24:08 -07:00
case install ( Error )
var errorDescription : String ? {
switch self
{
case . unknown : return " An unknown error occured. "
case . missingUDID : return " The UDID for this device is unknown. "
case . noServersFound : return " An active AltServer could not be found. "
case . missingPrivateKey : return " A valid private key must be provided. "
case . missingCertificate : return " A valid certificate must be provided. "
2019-06-04 18:29:50 -07:00
case . notAuthenticated : return " You must be logged in with your Apple ID to install apps. "
case . multipleCertificates : return " You must select a certificate to use to install apps. "
case . multipleTeams : return " You must select a team to use to install apps. "
2019-05-31 18:24:08 -07:00
case . download ( let error ) : return error . localizedDescription
case . authentication ( let error ) : return error . localizedDescription
case . fetchingSigningResources ( let error ) : return error . localizedDescription
2019-06-04 13:53:21 -07:00
case . prepare ( let error ) : return error . localizedDescription
2019-05-31 18:24:08 -07:00
case . install ( let error ) : return error . localizedDescription
}
}
}
}
class AppManager
{
static let shared = AppManager ( )
private let session = URLSession ( configuration : . default )
private init ( )
{
}
}
2019-06-04 13:53:21 -07:00
extension AppManager
{
2019-06-04 18:29:50 -07:00
func update ( )
2019-06-04 13:53:21 -07:00
{
let context = DatabaseManager . shared . persistentContainer . newBackgroundSavingViewContext ( )
let fetchRequest = InstalledApp . fetchRequest ( ) as NSFetchRequest < InstalledApp >
fetchRequest . relationshipKeyPathsForPrefetching = [ # keyPath ( InstalledApp . app ) ]
do
{
let installedApps = try context . fetch ( fetchRequest )
for app in installedApps
{
if UIApplication . shared . canOpenURL ( app . openAppURL )
{
// A p p i s s t i l l i n s t a l l e d , g o o d !
}
else
{
context . delete ( app )
}
}
try context . save ( )
}
catch
{
print ( " Error while fetching installed apps " )
}
}
}
2019-05-31 18:24:08 -07:00
extension AppManager
{
func install ( _ app : App , presentingViewController : UIViewController , completionHandler : @ escaping ( Result < InstalledApp , AppError > ) -> Void )
{
2019-06-04 13:53:21 -07:00
let ipaURL = InstalledApp . ipaURL ( for : app )
2019-05-31 18:24:08 -07:00
let backgroundTaskID = RSTBeginBackgroundTask ( " com.rileytestut.AltStore.InstallApp " )
func finish ( _ result : Result < InstalledApp , AppError > )
{
completionHandler ( result )
RSTEndBackgroundTask ( backgroundTaskID )
}
// D o w n l o a d a p p
self . downloadApp ( from : app . downloadURL ) { ( result ) in
let result = result . flatMap { ( fileURL ) -> Result < Void , URLError > in
// C o p y d o w n l o a d e d a p p t o p r o p e r l o c a t i o n
let result = Result { try FileManager . default . copyItem ( at : fileURL , to : ipaURL , shouldReplace : true ) }
return result . mapError { _ in URLError ( . cannotWriteToFile ) }
}
switch result
{
case . failure ( let error ) : finish ( . failure ( . download ( error ) ) )
case . success :
// A u t h e n t i c a t e
self . authenticate ( presentingViewController : presentingViewController ) { ( result ) in
switch result
{
case . failure ( let error ) : finish ( . failure ( . authentication ( error ) ) )
case . success ( let team ) :
// F e t c h s i g n i n g r e s o u r c e s
self . fetchSigningResources ( for : app , team : team , presentingViewController : presentingViewController ) { ( result ) in
switch result
{
case . failure ( let error ) : finish ( . failure ( . fetchingSigningResources ( error ) ) )
case . success ( let certificate , let profile ) :
2019-06-04 13:53:21 -07:00
// P r e p a r e a p p
DatabaseManager . shared . persistentContainer . performBackgroundTask { ( context ) in
let app = context . object ( with : app . objectID ) as ! App
let installedApp = InstalledApp ( app : app ,
bundleIdentifier : profile . appID . bundleIdentifier ,
2019-06-04 18:50:55 -07:00
expirationDate : profile . expirationDate ,
2019-06-04 13:53:21 -07:00
context : context )
2019-06-04 18:29:50 -07:00
let signer = ALTSigner ( team : team , certificate : certificate )
self . prepare ( installedApp , provisioningProfile : profile , signer : signer ) { ( result ) in
2019-05-31 18:24:08 -07:00
switch result
{
2019-06-04 13:53:21 -07:00
case . failure ( let error ) : finish ( . failure ( . prepare ( error ) ) )
2019-05-31 18:24:08 -07:00
case . success ( let resignedURL ) :
// S e n d a p p t o s e r v e r
2019-06-04 13:53:21 -07:00
context . perform {
self . sendAppToServer ( fileURL : resignedURL , identifier : installedApp . bundleIdentifier ) { ( result ) in
2019-05-31 18:24:08 -07:00
switch result
{
case . failure ( let error ) : finish ( . failure ( . install ( error ) ) )
case . success :
2019-06-04 13:53:21 -07:00
context . perform {
2019-05-31 18:24:08 -07:00
finish ( . success ( installedApp ) )
}
}
}
}
}
}
}
}
}
}
}
}
}
}
2019-06-04 18:29:50 -07:00
2019-06-05 11:03:49 -07:00
func refresh ( _ app : InstalledApp , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void )
{
self . refresh ( [ app ] ) { ( result ) in
do
{
guard let ( _ , result ) = try result . get ( ) . first else { throw AppError . unknown }
completionHandler ( result )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
func refreshAllApps ( completionHandler : @ escaping ( Result < [ String : Result < InstalledApp , Error > ] , AppError > ) -> Void )
{
DatabaseManager . shared . persistentContainer . performBackgroundTask { ( context ) in
do
{
let fetchRequest = InstalledApp . fetchRequest ( ) as NSFetchRequest < InstalledApp >
fetchRequest . relationshipKeyPathsForPrefetching = [ # keyPath ( InstalledApp . app ) ]
let installedApps = try context . fetch ( fetchRequest )
self . refresh ( installedApps ) { ( result ) in
context . perform { // k e e p c o n t e x t a l i v e
completionHandler ( result )
}
}
}
catch
{
completionHandler ( . failure ( . prepare ( error ) ) )
}
}
}
private func refresh < T : Collection > ( _ installedApps : T , completionHandler : @ escaping ( Result < [ String : Result < InstalledApp , Error > ] , AppError > ) -> Void ) where T . Element = = InstalledApp
2019-06-04 18:29:50 -07:00
{
let backgroundTaskID = RSTBeginBackgroundTask ( " com.rileytestut.AltStore.RefreshApps " )
2019-06-05 11:03:49 -07:00
func finish ( _ result : Result < [ String : Result < InstalledApp , Error > ] , AppError > )
2019-06-04 18:29:50 -07:00
{
completionHandler ( result )
RSTEndBackgroundTask ( backgroundTaskID )
}
// A u t h e n t i c a t e
self . authenticate ( presentingViewController : nil ) { ( result ) in
switch result
{
case . failure ( let error ) : finish ( . failure ( . authentication ( error ) ) )
case . success ( let team ) :
// F e t c h C e r t i f i c a t e
self . fetchCertificate ( for : team , presentingViewController : nil ) { ( result ) in
switch result
{
case . failure ( let error ) : finish ( . failure ( . fetchingSigningResources ( error ) ) )
case . success ( let certificate ) :
let signer = ALTSigner ( team : team , certificate : certificate )
2019-06-05 11:03:49 -07:00
let dispatchGroup = DispatchGroup ( )
var results = [ String : Result < InstalledApp , Error > ] ( )
let context = DatabaseManager . shared . persistentContainer . newBackgroundContext ( )
for app in installedApps
{
dispatchGroup . enter ( )
app . managedObjectContext ? . perform {
let bundleIdentifier = app . bundleIdentifier
print ( " Refreshing App: " , bundleIdentifier )
2019-06-04 18:29:50 -07:00
2019-06-05 11:03:49 -07:00
self . refresh ( app , signer : signer , context : context ) { ( result ) in
print ( " Refreshed App: \( bundleIdentifier ) . " , result )
results [ bundleIdentifier ] = result
dispatchGroup . leave ( )
2019-06-04 18:29:50 -07:00
}
}
2019-06-05 11:03:49 -07:00
}
dispatchGroup . notify ( queue : . global ( ) ) {
context . perform {
finish ( . success ( results ) )
2019-06-04 18:29:50 -07:00
}
}
}
}
}
}
}
2019-05-31 18:24:08 -07:00
}
private extension AppManager
{
func downloadApp ( from url : URL , completionHandler : @ escaping ( Result < URL , URLError > ) -> Void )
{
let downloadTask = self . session . downloadTask ( with : url ) { ( fileURL , response , error ) in
do
{
let ( fileURL , _ ) = try Result ( ( fileURL , response ) , error ) . get ( )
completionHandler ( . success ( fileURL ) )
}
catch let error as URLError
{
completionHandler ( . failure ( error ) )
}
catch
{
completionHandler ( . failure ( URLError ( . unknown ) ) )
}
}
downloadTask . resume ( )
}
2019-06-04 18:29:50 -07:00
func authenticate ( presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < ALTTeam , Error > ) -> Void )
2019-05-31 18:24:08 -07:00
{
2019-06-04 18:29:50 -07:00
func authenticate ( emailAddress : String , password : String )
{
ALTAppleAPI . shared . authenticate ( appleID : emailAddress , password : password ) { ( account , error ) in
do
{
let account = try Result ( account , error ) . get ( )
Keychain . shared . appleIDEmailAddress = emailAddress
Keychain . shared . appleIDPassword = password
self . fetchTeam ( for : account , presentingViewController : presentingViewController , completionHandler : completionHandler )
}
catch
{
completionHandler ( . failure ( error ) )
}
2019-06-04 11:32:08 -07:00
}
2019-06-04 18:29:50 -07:00
}
if let emailAddress = Keychain . shared . appleIDEmailAddress , let password = Keychain . shared . appleIDPassword
{
authenticate ( emailAddress : emailAddress , password : password )
}
else if let presentingViewController = presentingViewController
{
DispatchQueue . main . async {
let alertController = UIAlertController ( title : " Enter Apple ID + Password " , message : " " , preferredStyle : . alert )
alertController . addTextField { ( textField ) in
textField . placeholder = " Apple ID "
textField . textContentType = . emailAddress
}
alertController . addTextField { ( textField ) in
textField . placeholder = " Password "
textField . textContentType = . password
}
alertController . addAction ( . cancel )
alertController . addAction ( UIAlertAction ( title : " Sign In " , style : . default ) { [ unowned alertController ] ( action ) in
guard
let emailAddress = alertController . textFields ! [ 0 ] . text ,
let password = alertController . textFields ! [ 1 ] . text ,
! emailAddress . isEmpty , ! password . isEmpty
2019-06-04 11:32:08 -07:00
else { return completionHandler ( . failure ( ALTAppleAPIError ( . incorrectCredentials ) ) ) }
2019-06-04 18:29:50 -07:00
authenticate ( emailAddress : emailAddress , password : password )
} )
2019-06-04 11:32:08 -07:00
2019-06-04 18:29:50 -07:00
presentingViewController . present ( alertController , animated : true , completion : nil )
}
}
else
{
completionHandler ( . failure ( AppError . notAuthenticated ) )
2019-06-04 11:32:08 -07:00
}
2019-05-31 18:24:08 -07:00
}
2019-06-04 18:29:50 -07:00
func prepareProvisioningProfile ( for app : App , team : ALTTeam , completionHandler : @ escaping ( Result < ALTProvisioningProfile , Error > ) -> Void )
2019-05-31 18:24:08 -07:00
{
guard let udid = Bundle . main . object ( forInfoDictionaryKey : Bundle . Info . deviceID ) as ? String else { return completionHandler ( . failure ( AppError . missingUDID ) ) }
let device = ALTDevice ( name : UIDevice . current . name , identifier : udid )
self . register ( device , team : team ) { ( result ) in
do
{
_ = try result . get ( )
2019-06-04 18:29:50 -07:00
app . managedObjectContext ? . perform {
self . register ( app , with : team ) { ( result ) in
do
{
let appID = try result . get ( )
self . fetchProvisioningProfile ( for : appID , team : team ) { ( result ) in
2019-05-31 18:24:08 -07:00
do
{
2019-06-04 18:29:50 -07:00
let provisioningProfile = try result . get ( )
completionHandler ( . success ( provisioningProfile ) )
2019-05-31 18:24:08 -07:00
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
2019-06-04 18:29:50 -07:00
catch
{
completionHandler ( . failure ( error ) )
}
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
func fetchSigningResources ( for app : App , team : ALTTeam , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < ( ALTCertificate , ALTProvisioningProfile ) , Error > ) -> Void )
{
self . fetchCertificate ( for : team , presentingViewController : presentingViewController ) { ( result ) in
do
{
let certificate = try result . get ( )
self . prepareProvisioningProfile ( for : app , team : team ) { ( result ) in
do
{
let provisioningProfile = try result . get ( )
completionHandler ( . success ( ( certificate , provisioningProfile ) ) )
2019-05-31 18:24:08 -07:00
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
2019-06-04 18:29:50 -07:00
func prepare ( _ installedApp : InstalledApp , provisioningProfile : ALTProvisioningProfile , signer : ALTSigner , completionHandler : @ escaping ( Result < URL , Error > ) -> Void )
2019-05-31 18:24:08 -07:00
{
2019-06-04 13:53:21 -07:00
do
{
let refreshedAppDirectory = installedApp . directoryURL . appendingPathComponent ( " Refreshed " , isDirectory : true )
if FileManager . default . fileExists ( atPath : refreshedAppDirectory . path )
{
try FileManager . default . removeItem ( at : refreshedAppDirectory )
}
try FileManager . default . createDirectory ( at : refreshedAppDirectory , withIntermediateDirectories : true , attributes : nil )
let appBundleURL = try FileManager . default . unzipAppBundle ( at : installedApp . ipaURL , toDirectory : refreshedAppDirectory )
guard let bundle = Bundle ( url : appBundleURL ) else { throw ALTError ( . missingAppBundle ) }
guard var infoDictionary = NSDictionary ( contentsOf : bundle . infoPlistURL ) as ? [ String : Any ] else { throw ALTError ( . missingInfoPlist ) }
var allURLSchemes = infoDictionary [ Bundle . Info . urlTypes ] as ? [ [ String : Any ] ] ? ? [ ]
let altstoreURLScheme = [ " CFBundleTypeRole " : " Editor " ,
" CFBundleURLName " : installedApp . bundleIdentifier ,
" CFBundleURLSchemes " : [ installedApp . openAppURL . scheme ! ] ] as [ String : Any ]
allURLSchemes . append ( altstoreURLScheme )
infoDictionary [ Bundle . Info . urlTypes ] = allURLSchemes
try ( infoDictionary as NSDictionary ) . write ( to : bundle . infoPlistURL )
signer . signApp ( at : appBundleURL , provisioningProfile : provisioningProfile ) { ( success , error ) in
do
{
try Result ( success , error ) . get ( )
let resignedURL = try FileManager . default . zipAppBundle ( at : appBundleURL )
completionHandler ( . success ( resignedURL ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
2019-05-31 18:24:08 -07:00
}
}
func sendAppToServer ( fileURL : URL , identifier : String , completionHandler : @ escaping ( Result < Void , Error > ) -> Void )
{
guard let server = ServerManager . shared . discoveredServers . first else { return completionHandler ( . failure ( AppError . noServersFound ) ) }
server . installApp ( at : fileURL , identifier : identifier ) { ( result ) in
let result = result . mapError { $0 as Error }
completionHandler ( result )
}
}
}
private extension AppManager
{
2019-06-04 18:29:50 -07:00
func fetchTeam ( for account : ALTAccount , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < ALTTeam , Error > ) -> Void )
2019-05-31 18:24:08 -07:00
{
ALTAppleAPI . shared . fetchTeams ( for : account ) { ( teams , error ) in
do
{
let teams = try Result ( teams , error ) . get ( )
guard teams . count > 0 else { throw ALTAppleAPIError ( . noTeams ) }
if let team = teams . first , teams . count = = 1
{
completionHandler ( . success ( team ) )
}
else
{
DispatchQueue . main . async {
let alertController = UIAlertController ( title : " Select Team " , message : " " , preferredStyle : . actionSheet )
alertController . addAction ( . cancel )
for team in teams
{
alertController . addAction ( UIAlertAction ( title : team . name , style : . default ) { ( action ) in
completionHandler ( . success ( team ) )
} )
}
2019-06-04 18:29:50 -07:00
presentingViewController ? . present ( alertController , animated : true , completion : nil )
2019-05-31 18:24:08 -07:00
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
2019-06-04 18:29:50 -07:00
func fetchCertificate ( for team : ALTTeam , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < ALTCertificate , Error > ) -> Void )
2019-05-31 18:24:08 -07:00
{
ALTAppleAPI . shared . fetchCertificates ( for : team ) { ( certificates , error ) in
do
{
let certificates = try Result ( certificates , error ) . get ( )
2019-06-04 18:29:50 -07:00
if
let identifier = UserDefaults . standard . signingCertificateIdentifier ,
let privateKey = Keychain . shared . signingCertificatePrivateKey ,
let certificate = certificates . first ( where : { $0 . identifier = = identifier } )
{
certificate . privateKey = privateKey
completionHandler ( . success ( certificate ) )
}
else if certificates . count < 1
2019-05-31 18:24:08 -07:00
{
let machineName = " AltStore - " + UIDevice . current . name
ALTAppleAPI . shared . addCertificate ( machineName : machineName , to : team ) { ( certificate , error ) in
do
{
let certificate = try Result ( certificate , error ) . get ( )
guard let privateKey = certificate . privateKey else { throw AppError . missingPrivateKey }
ALTAppleAPI . shared . fetchCertificates ( for : team ) { ( certificates , error ) in
do
{
let certificates = try Result ( certificates , error ) . get ( )
guard let certificate = certificates . first ( where : { $0 . identifier = = certificate . identifier } ) else {
throw AppError . missingCertificate
}
certificate . privateKey = privateKey
2019-06-04 18:29:50 -07:00
UserDefaults . standard . signingCertificateIdentifier = certificate . identifier
Keychain . shared . signingCertificatePrivateKey = privateKey
2019-05-31 18:24:08 -07:00
completionHandler ( . success ( certificate ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
2019-06-04 18:29:50 -07:00
else if let presentingViewController = presentingViewController
2019-05-31 18:24:08 -07:00
{
DispatchQueue . main . async {
let alertController = UIAlertController ( title : " Too Many Certificates " , message : " Please select the certificate you would like to revoke. " , preferredStyle : . actionSheet )
alertController . addAction ( . cancel )
for certificate in certificates
{
alertController . addAction ( UIAlertAction ( title : certificate . name , style : . default ) { ( action ) in
ALTAppleAPI . shared . revoke ( certificate , for : team ) { ( success , error ) in
do
{
try Result ( success , error ) . get ( )
self . fetchCertificate ( for : team , presentingViewController : presentingViewController , completionHandler : completionHandler )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
} )
}
presentingViewController . present ( alertController , animated : true , completion : nil )
}
}
2019-06-04 18:29:50 -07:00
else
{
completionHandler ( . failure ( AppError . multipleCertificates ) )
}
2019-05-31 18:24:08 -07:00
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
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 register ( _ app : App , with team : ALTTeam , completionHandler : @ escaping ( Result < ALTAppID , Error > ) -> Void )
{
let appName = app . name
let bundleID = " com. \( team . identifier ) . \( app . identifier ) "
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 ) )
}
}
}
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-04 18:29:50 -07:00
2019-06-05 11:03:49 -07:00
func refresh ( _ installedApp : InstalledApp , signer : ALTSigner , context : NSManagedObjectContext , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void )
2019-06-04 18:29:50 -07:00
{
self . prepareProvisioningProfile ( for : installedApp . app , team : signer . team ) { ( result ) in
switch result
{
case . failure ( let error ) : completionHandler ( . failure ( error ) )
case . success ( let profile ) :
installedApp . managedObjectContext ? . perform {
self . prepare ( installedApp , provisioningProfile : profile , signer : signer ) { ( result ) in
switch result
{
case . failure ( let error ) : completionHandler ( . failure ( error ) )
case . success ( let resignedURL ) :
// S e n d a p p t o s e r v e r
installedApp . managedObjectContext ? . perform {
2019-06-05 11:03:49 -07:00
self . sendAppToServer ( fileURL : resignedURL , identifier : installedApp . bundleIdentifier ) { ( result ) in
context . perform {
switch result
{
case . success :
let installedApp = context . object ( with : installedApp . objectID ) as ! InstalledApp
installedApp . expirationDate = profile . expirationDate
completionHandler ( . success ( installedApp ) )
case . failure ( let error ) :
completionHandler ( . failure ( error ) )
}
}
}
2019-06-04 18:29:50 -07:00
}
}
}
}
}
}
}
2019-05-31 18:24:08 -07:00
}