Displays progress when downloading/refreshing apps

Refactors download/refresh steps into separate Operation subclasses
This commit is contained in:
Riley Testut
2019-06-10 15:03:47 -07:00
parent 4f372f959a
commit a932e0759e
19 changed files with 1330 additions and 962 deletions

View File

@@ -27,7 +27,8 @@ public struct ServerRequest: Codable
public struct ServerResponse: Codable public struct ServerResponse: Codable
{ {
public var success: Bool public var progress: Double
public var error: ALTServerError? { public var error: ALTServerError? {
get { get {
guard let code = self.errorCode else { return nil } guard let code = self.errorCode else { return nil }
@@ -37,12 +38,11 @@ public struct ServerResponse: Codable
self.errorCode = newValue?.code self.errorCode = newValue?.code
} }
} }
private var errorCode: ALTServerError.Code? private var errorCode: ALTServerError.Code?
public init(success: Bool, error: ALTServerError?) public init(progress: Double, error: ALTServerError?)
{ {
self.success = success self.progress = progress
self.error = error self.error = error
} }
} }

View File

@@ -184,8 +184,7 @@ private extension ConnectionManager
print("Processed request from \(connection.endpoint).") print("Processed request from \(connection.endpoint).")
} }
let success = (error == nil) let response = ServerResponse(progress: 1.0, error: error)
let response = ServerResponse(success: success, error: error)
self.send(response, to: connection) { (result) in self.send(response, to: connection) { (result) in
print("Sent response to \(connection.endpoint) with result:", result) print("Sent response to \(connection.endpoint) with result:", result)
@@ -209,12 +208,15 @@ private extension ConnectionManager
{ {
case .failure(let error): finish(error: error) case .failure(let error): finish(error: error)
case .success(let request, let fileURL): case .success(let request, let fileURL):
print("Installing to device..") print("Installing to device \(request.udid)...")
ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: request.udid) { (success, error) in self?.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in
print("Installed app with result:", result) print("Installed to device with result:", result)
let error = error.map { $0 as? ALTServerError ?? ALTServerError(.unknown) } switch result
finish(error: error) {
case .failure(let error): finish(error: error)
case .success: finish(error: nil)
}
} }
} }
} }
@@ -303,6 +305,46 @@ private extension ConnectionManager
} }
} }
} }
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
var isSending = false
var observation: NSKeyValueObservation?
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid) { (success, error) in
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
if let error = error.map({ $0 as? ALTServerError ?? ALTServerError(.unknown) })
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(()))
}
observation?.invalidate()
observation = nil
}
observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, change) in
serialQueue.async {
guard !isSending else { return }
isSending = true
print("Progress:", progress.fractionCompleted)
let response = ServerResponse(progress: progress.fractionCompleted, error: nil)
self.send(response, to: connection) { (result) in
serialQueue.async {
isSending = false
}
}
}
})
}
func send(_ response: ServerResponse, to connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void) func send(_ response: ServerResponse, to connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{ {

View File

@@ -25,6 +25,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, void (^)(void)> *installationCompletionHandlers; @property (nonatomic, readonly) NSMutableDictionary<NSUUID *, void (^)(void)> *installationCompletionHandlers;
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSProgress *> *installationProgress; @property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSProgress *> *installationProgress;
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSValue *> *installationClients; @property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSValue *> *installationClients;
@property (nonatomic, readonly) dispatch_queue_t installationQueue;
@end @end
@@ -49,6 +50,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
_installationCompletionHandlers = [NSMutableDictionary dictionary]; _installationCompletionHandlers = [NSMutableDictionary dictionary];
_installationProgress = [NSMutableDictionary dictionary]; _installationProgress = [NSMutableDictionary dictionary];
_installationClients = [NSMutableDictionary dictionary]; _installationClients = [NSMutableDictionary dictionary];
_installationQueue = dispatch_queue_create("com.rileytestut.AltServer.InstallationQueue", DISPATCH_QUEUE_SERIAL);
} }
return self; return self;
@@ -56,222 +59,239 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler - (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
{ {
NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:100]; NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:4];
NSUUID *UUID = [NSUUID UUID]; dispatch_async(self.installationQueue, ^{
__block char *uuidString = (char *)malloc(UUID.UUIDString.length + 1); NSUUID *UUID = [NSUUID UUID];
strncpy(uuidString, (const char *)UUID.UUIDString.UTF8String, UUID.UUIDString.length); __block char *uuidString = (char *)malloc(UUID.UUIDString.length + 1);
uuidString[UUID.UUIDString.length] = '\0'; strncpy(uuidString, (const char *)UUID.UUIDString.UTF8String, UUID.UUIDString.length);
uuidString[UUID.UUIDString.length] = '\0';
idevice_t device = NULL;
lockdownd_client_t client = NULL;
instproxy_client_t ipc = NULL;
np_client_t np = NULL;
afc_client_t afc = NULL;
lockdownd_service_descriptor_t service = NULL;
void (^finish)(NSError *error) = ^(NSError *error) {
np_client_free(np);
instproxy_client_free(ipc);
afc_client_free(afc);
lockdownd_client_free(client);
idevice_free(device);
lockdownd_service_descriptor_free(service);
free(uuidString); idevice_t device = NULL;
uuidString = NULL; lockdownd_client_t client = NULL;
instproxy_client_t ipc = NULL;
np_client_t np = NULL;
afc_client_t afc = NULL;
lockdownd_service_descriptor_t service = NULL;
if (error != nil) void (^finish)(NSError *error) = ^(NSError *error) {
np_client_free(np);
instproxy_client_free(ipc);
afc_client_free(afc);
lockdownd_client_free(client);
idevice_free(device);
lockdownd_service_descriptor_free(service);
free(uuidString);
uuidString = NULL;
if (error != nil)
{
completionHandler(NO, error);
}
else
{
completionHandler(YES, nil);
}
};
NSURL *appBundleURL = nil;
NSURL *temporaryDirectoryURL = nil;
if ([fileURL.pathExtension.lowercaseString isEqualToString:@"app"])
{ {
completionHandler(NO, error); appBundleURL = fileURL;
temporaryDirectoryURL = nil;
}
else if ([fileURL.pathExtension.lowercaseString isEqualToString:@"ipa"])
{
NSLog(@"Unzipping .ipa...");
temporaryDirectoryURL = [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:[[NSUUID UUID] UUIDString] isDirectory:YES];
NSError *error = nil;
if (![[NSFileManager defaultManager] createDirectoryAtURL:temporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error])
{
return finish(error);
}
appBundleURL = [[NSFileManager defaultManager] unzipAppBundleAtURL:fileURL toDirectory:temporaryDirectoryURL error:&error];
if (appBundleURL == nil)
{
return finish(error);
}
} }
else else
{ {
completionHandler(YES, nil); return finish([NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: fileURL}]);
} }
};
NSURL *appBundleURL = nil;
NSURL *temporaryDirectoryURL = nil;
if ([fileURL.pathExtension.lowercaseString isEqualToString:@"app"])
{
appBundleURL = fileURL;
temporaryDirectoryURL = nil;
}
else if ([fileURL.pathExtension.lowercaseString isEqualToString:@"ipa"])
{
NSLog(@"Unzipping .ipa...");
temporaryDirectoryURL = [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:[[NSUUID UUID] UUIDString] isDirectory:YES]; /* Find Device */
if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS)
NSError *error = nil;
if (![[NSFileManager defaultManager] createDirectoryAtURL:temporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error])
{ {
finish(error); return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
return progress;
} }
appBundleURL = [[NSFileManager defaultManager] unzipAppBundleAtURL:fileURL toDirectory:temporaryDirectoryURL error:&error]; /* Connect to Device */
if (appBundleURL == nil) if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS)
{ {
finish(error); return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
} }
}
else /* Connect to Notification Proxy */
{ if ((lockdownd_start_service(client, "com.apple.mobile.notification_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
finish([NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: fileURL}]); {
return progress; return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
} }
/* Find Device */ if (np_client_new(device, service, &np) != NP_E_SUCCESS)
if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS) {
{ return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]); }
return progress;
} np_set_notify_callback(np, ALTDeviceManagerDidFinishAppInstallation, uuidString);
/* Connect to Device */ const char *notifications[2] = { NP_APP_INSTALLED, NULL };
if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS) np_observe_notifications(np, notifications);
{
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); if (service)
return progress; {
} lockdownd_service_descriptor_free(service);
service = NULL;
/* Connect to Notification Proxy */ }
if ((lockdownd_start_service(client, "com.apple.mobile.notification_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
{ /* Connect to Installation Proxy */
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); if ((lockdownd_start_service(client, "com.apple.mobile.installation_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
return progress; {
} return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
if (np_client_new(device, service, &np) != NP_E_SUCCESS)
{ if (instproxy_client_new(device, service, &ipc) != INSTPROXY_E_SUCCESS)
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); {
return progress; return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
} }
np_set_notify_callback(np, ALTDeviceManagerDidFinishAppInstallation, uuidString); if (service)
{
const char *notifications[2] = { NP_APP_INSTALLED, NULL }; lockdownd_service_descriptor_free(service);
np_observe_notifications(np, notifications); service = NULL;
}
if (service)
{
lockdownd_service_descriptor_free(service); lockdownd_service_descriptor_free(service);
service = NULL; service = NULL;
}
/* Connect to Installation Proxy */
if ((lockdownd_start_service(client, "com.apple.mobile.installation_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
{
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
if (instproxy_client_new(device, service, &ipc) != INSTPROXY_E_SUCCESS)
{
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
if (service)
{
lockdownd_service_descriptor_free(service);
service = NULL;
}
lockdownd_service_descriptor_free(service);
service = NULL;
/* Connect to AFC service */
if ((lockdownd_start_service(client, "com.apple.afc", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
{
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
lockdownd_client_free(client);
client = NULL;
if (afc_client_new(device, service, &afc) != AFC_E_SUCCESS)
{
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
return progress;
}
NSURL *stagingURL = [NSURL fileURLWithPath:@"PublicStaging" isDirectory:YES];
/* Prepare for installation */
char **files = NULL;
if (afc_get_file_info(afc, stagingURL.relativePath.fileSystemRepresentation, &files) != AFC_E_SUCCESS)
{
if (afc_make_directory(afc, stagingURL.relativePath.fileSystemRepresentation) != AFC_E_SUCCESS)
{
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceWriteFailed userInfo:nil]);
return progress;
}
}
if (files)
{
int i = 0;
while (files[i]) /* Connect to AFC service */
if ((lockdownd_start_service(client, "com.apple.afc", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
{ {
free(files[i]); return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
i++;
} }
free(files); lockdownd_client_free(client);
} client = NULL;
NSLog(@"Writing to device...");
plist_t options = instproxy_client_options_new();
instproxy_client_options_add(options, "PackageType", "Developer", NULL);
NSURL *destinationURL = [stagingURL URLByAppendingPathComponent:appBundleURL.lastPathComponent];
NSError *writeError = nil;
if (![self writeDirectory:appBundleURL toDestinationURL:destinationURL client:afc error:&writeError])
{
finish(writeError);
return progress;
}
NSLog(@"Finished writing to device.");
NSValue *value = [NSValue valueWithPointer:(const void *)np];
self.installationClients[UUID] = value;
self.installationProgress[UUID] = progress;
self.installationCompletionHandlers[UUID] = ^{
finish(nil);
if (temporaryDirectoryURL != nil) if (afc_client_new(device, service, &afc) != AFC_E_SUCCESS)
{ {
NSError *error = nil; return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
if (![[NSFileManager defaultManager] removeItemAtURL:temporaryDirectoryURL error:&error]) }
NSURL *stagingURL = [NSURL fileURLWithPath:@"PublicStaging" isDirectory:YES];
/* Prepare for installation */
char **files = NULL;
if (afc_get_file_info(afc, stagingURL.relativePath.fileSystemRepresentation, &files) != AFC_E_SUCCESS)
{
if (afc_make_directory(afc, stagingURL.relativePath.fileSystemRepresentation) != AFC_E_SUCCESS)
{ {
NSLog(@"Error removing temporary directory. %@", error); return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceWriteFailed userInfo:nil]);
} }
} }
};
if (files)
NSLog(@"Installing to device %@...", udid); {
int i = 0;
instproxy_install(ipc, destinationURL.relativePath.fileSystemRepresentation, options, ALTDeviceManagerUpdateStatus, uuidString);
instproxy_client_options_free(options); while (files[i])
{
free(files[i]);
i++;
}
free(files);
}
NSLog(@"Writing to device...");
plist_t options = instproxy_client_options_new();
instproxy_client_options_add(options, "PackageType", "Developer", NULL);
NSURL *destinationURL = [stagingURL URLByAppendingPathComponent:appBundleURL.lastPathComponent];
// Writing files to device should be worth 3/4 of total work.
[progress becomeCurrentWithPendingUnitCount:3];
NSError *writeError = nil;
if (![self writeDirectory:appBundleURL toDestinationURL:destinationURL client:afc progress:nil error:&writeError])
{
return finish(writeError);
}
NSLog(@"Finished writing to device.");
NSProgress *installationProgress = [NSProgress progressWithTotalUnitCount:100 parent:progress pendingUnitCount:1];
NSValue *value = [NSValue valueWithPointer:(const void *)np];
self.installationClients[UUID] = value;
self.installationProgress[UUID] = installationProgress;
self.installationCompletionHandlers[UUID] = ^{
finish(nil);
if (temporaryDirectoryURL != nil)
{
NSError *error = nil;
if (![[NSFileManager defaultManager] removeItemAtURL:temporaryDirectoryURL error:&error])
{
NSLog(@"Error removing temporary directory. %@", error);
}
}
};
NSLog(@"Installing to device %@...", udid);
instproxy_install(ipc, destinationURL.relativePath.fileSystemRepresentation, options, ALTDeviceManagerUpdateStatus, uuidString);
instproxy_client_options_free(options);
});
return progress; return progress;
} }
- (BOOL)writeDirectory:(NSURL *)directoryURL toDestinationURL:(NSURL *)destinationURL client:(afc_client_t)afc error:(NSError **)error - (BOOL)writeDirectory:(NSURL *)directoryURL toDestinationURL:(NSURL *)destinationURL client:(afc_client_t)afc progress:(NSProgress *)progress error:(NSError **)error
{ {
afc_make_directory(afc, destinationURL.relativePath.fileSystemRepresentation); afc_make_directory(afc, destinationURL.relativePath.fileSystemRepresentation);
if (progress == nil)
{
NSDirectoryEnumerator *countEnumerator = [[NSFileManager defaultManager] enumeratorAtURL:directoryURL
includingPropertiesForKeys:@[]
options:0
errorHandler:^BOOL(NSURL * _Nonnull url, NSError * _Nonnull error) {
if (error) {
NSLog(@"[Error] %@ (%@)", error, url);
return NO;
}
return YES;
}];
NSInteger totalCount = 0;
for (NSURL *__unused fileURL in countEnumerator)
{
totalCount++;
}
progress = [NSProgress progressWithTotalUnitCount:totalCount];
}
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtURL:directoryURL NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtURL:directoryURL
includingPropertiesForKeys:@[NSURLIsDirectoryKey] includingPropertiesForKeys:@[NSURLIsDirectoryKey]
options:NSDirectoryEnumerationSkipsSubdirectoryDescendants options:NSDirectoryEnumerationSkipsSubdirectoryDescendants
@@ -295,7 +315,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
if ([isDirectory boolValue]) if ([isDirectory boolValue])
{ {
NSURL *destinationDirectoryURL = [destinationURL URLByAppendingPathComponent:fileURL.lastPathComponent isDirectory:YES]; NSURL *destinationDirectoryURL = [destinationURL URLByAppendingPathComponent:fileURL.lastPathComponent isDirectory:YES];
if (![self writeDirectory:fileURL toDestinationURL:destinationDirectoryURL client:afc error:error]) if (![self writeDirectory:fileURL toDestinationURL:destinationDirectoryURL client:afc progress:progress error:error])
{ {
return NO; return NO;
} }
@@ -308,6 +328,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
return NO; return NO;
} }
} }
progress.completedUnitCount += 1;
} }
return YES; return YES;
@@ -315,12 +337,19 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
- (BOOL)writeFile:(NSURL *)fileURL toDestinationURL:(NSURL *)destinationURL client:(afc_client_t)afc error:(NSError **)error - (BOOL)writeFile:(NSURL *)fileURL toDestinationURL:(NSURL *)destinationURL client:(afc_client_t)afc error:(NSError **)error
{ {
NSData *data = [NSData dataWithContentsOfURL:fileURL options:0 error:error]; NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:fileURL.path];
if (data == nil) if (fileHandle == nil)
{ {
if (error)
{
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{NSURLErrorKey: fileURL}];
}
return NO; return NO;
} }
NSData *data = [fileHandle readDataToEndOfFile];
uint64_t af = 0; uint64_t af = 0;
if ((afc_file_open(afc, destinationURL.relativePath.fileSystemRepresentation, AFC_FOPEN_WRONLY, &af) != AFC_E_SUCCESS) || af == 0) if ((afc_file_open(afc, destinationURL.relativePath.fileSystemRepresentation, AFC_FOPEN_WRONLY, &af) != AFC_E_SUCCESS) || af == 0)
{ {
@@ -334,11 +363,12 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
BOOL success = YES; BOOL success = YES;
uint32_t bytesWritten = 0; uint32_t bytesWritten = 0;
while (bytesWritten < data.length) while (bytesWritten < data.length)
{ {
uint32_t count = 0; uint32_t count = 0;
if (afc_file_write(afc, af, (const char *)data.bytes, (uint32_t)data.length, &bytesWritten) != AFC_E_SUCCESS)
if (afc_file_write(afc, af, (const char *)data.bytes + bytesWritten, (uint32_t)data.length - bytesWritten, &count) != AFC_E_SUCCESS)
{ {
if (error) if (error)
{ {

View File

@@ -98,6 +98,7 @@
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */; }; BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */; };
BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.swift */; }; BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.swift */; };
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; }; BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; };
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */; };
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; }; BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; };
BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476F2284B9A500981D42 /* AppsViewController.swift */; }; BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476F2284B9A500981D42 /* AppsViewController.swift */; };
BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD247712284B9A500981D42 /* MyAppsViewController.swift */; }; BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD247712284B9A500981D42 /* MyAppsViewController.swift */; };
@@ -112,7 +113,6 @@
BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479B2284E19A00981D42 /* AppDetailViewController.swift */; }; BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479B2284E19A00981D42 /* AppDetailViewController.swift */; };
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */; }; BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */; };
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BD322A0800A000B7ED1 /* ServerManager.swift */; }; BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BD322A0800A000B7ED1 /* ServerManager.swift */; };
BFD52BD622A08A85000B7ED1 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BD522A08A85000B7ED1 /* Server.swift */; };
BFD52C0122A1A9CB000B7ED1 /* ptrarray.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */; }; BFD52C0122A1A9CB000B7ED1 /* ptrarray.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */; };
BFD52C0222A1A9CB000B7ED1 /* base64.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE622A1A9CA000B7ED1 /* base64.c */; }; BFD52C0222A1A9CB000B7ED1 /* base64.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE622A1A9CA000B7ED1 /* base64.c */; };
BFD52C0322A1A9CB000B7ED1 /* hashtable.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE722A1A9CA000B7ED1 /* hashtable.c */; }; BFD52C0322A1A9CB000B7ED1 /* hashtable.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE722A1A9CA000B7ED1 /* hashtable.c */; };
@@ -146,6 +146,10 @@
BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; }; BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; };
BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */; }; BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */; };
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */; }; BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */; };
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */; };
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */; };
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */; };
BFDB6A0F22AB2776007EA6D6 /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0E22AB2776007EA6D6 /* InstallAppOperation.swift */; };
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; }; BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; };
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */; }; BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */; };
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */; }; BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */; };
@@ -315,6 +319,7 @@
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AltStore.xcdatamodel; sourceTree = "<group>"; }; BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AltStore.xcdatamodel; sourceTree = "<group>"; };
BFBBE2DE22931F73002097FA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; BFBBE2DE22931F73002097FA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
BFBBE2E022931F81002097FA /* InstalledApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledApp.swift; sourceTree = "<group>"; }; BFBBE2E022931F81002097FA /* InstalledApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledApp.swift; sourceTree = "<group>"; };
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAppOperation.swift; sourceTree = "<group>"; };
BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; }; BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; };
BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
BFD2476F2284B9A500981D42 /* AppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsViewController.swift; sourceTree = "<group>"; }; BFD2476F2284B9A500981D42 /* AppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsViewController.swift; sourceTree = "<group>"; };
@@ -331,7 +336,6 @@
BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = "<group>"; }; BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = "<group>"; };
BFD52BD222A06EFB000B7ED1 /* AltKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltKit.h; sourceTree = "<group>"; }; BFD52BD222A06EFB000B7ED1 /* AltKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltKit.h; sourceTree = "<group>"; };
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = "<group>"; }; BFD52BD322A0800A000B7ED1 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = "<group>"; };
BFD52BD522A08A85000B7ED1 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = ptrarray.c; path = Dependencies/libplist/src/ptrarray.c; sourceTree = SOURCE_ROOT; }; BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = ptrarray.c; path = Dependencies/libplist/src/ptrarray.c; sourceTree = SOURCE_ROOT; };
BFD52BE622A1A9CA000B7ED1 /* base64.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = base64.c; path = Dependencies/libplist/src/base64.c; sourceTree = SOURCE_ROOT; }; BFD52BE622A1A9CA000B7ED1 /* base64.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = base64.c; path = Dependencies/libplist/src/base64.c; sourceTree = SOURCE_ROOT; };
BFD52BE722A1A9CA000B7ED1 /* hashtable.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = hashtable.c; path = Dependencies/libplist/src/hashtable.c; sourceTree = SOURCE_ROOT; }; BFD52BE722A1A9CA000B7ED1 /* hashtable.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = hashtable.c; path = Dependencies/libplist/src/hashtable.c; sourceTree = SOURCE_ROOT; };
@@ -365,6 +369,10 @@
BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; }; BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; };
BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = "<group>"; }; BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = "<group>"; };
BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = "<group>"; }; BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = "<group>"; };
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResignAppOperation.swift; sourceTree = "<group>"; };
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; };
BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = "<group>"; };
BFDB6A0E22AB2776007EA6D6 /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = "<group>"; };
BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = "<group>"; }; BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = "<group>"; };
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = "<group>"; }; BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = "<group>"; };
BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; }; BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; };
@@ -641,7 +649,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */, BFD52BD322A0800A000B7ED1 /* ServerManager.swift */,
BFD52BD522A08A85000B7ED1 /* Server.swift */,
); );
path = Server; path = Server;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -682,6 +689,7 @@
BFDB69FB22A9A7A6007EA6D6 /* Account */, BFDB69FB22A9A7A6007EA6D6 /* Account */,
BFC51D7922972F1F00388324 /* Server */, BFC51D7922972F1F00388324 /* Server */,
BFD247982284D7FC00981D42 /* Model */, BFD247982284D7FC00981D42 /* Model */,
BFDB6A0922AAEDA1007EA6D6 /* Operations */,
BFD2478D2284C4C700981D42 /* Components */, BFD2478D2284C4C700981D42 /* Components */,
BFDB6A0622A9B114007EA6D6 /* Protocols */, BFDB6A0622A9B114007EA6D6 /* Protocols */,
BFD2479D2284FBBD00981D42 /* Extensions */, BFD2479D2284FBBD00981D42 /* Extensions */,
@@ -793,6 +801,19 @@
path = Protocols; path = Protocols;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
BFDB6A0922AAEDA1007EA6D6 /* Operations */ = {
isa = PBXGroup;
children = (
BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */,
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */,
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* InstallAppOperation.swift */,
);
path = Operations;
sourceTree = "<group>";
};
BFE6325822A83BA800F30809 /* Authentication */ = { BFE6325822A83BA800F30809 /* Authentication */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -800,7 +821,6 @@
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */, BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */,
BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */, BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */,
BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */, BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */,
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
); );
path = Authentication; path = Authentication;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1148,12 +1168,15 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
BFDB6A0F22AB2776007EA6D6 /* InstallAppOperation.swift in Sources */,
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */, BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */, BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */,
BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */, BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */,
BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */, BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */,
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */, BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */, BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
BFBBE2DF22931F73002097FA /* App.swift in Sources */, BFBBE2DF22931F73002097FA /* App.swift in Sources */,
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */, BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
@@ -1163,14 +1186,15 @@
BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */, BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */, BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */,
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
BFB116A022933DEB00BB457C /* UpdatesViewController.swift in Sources */, BFB116A022933DEB00BB457C /* UpdatesViewController.swift in Sources */,
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */, BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */, BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */,
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */, BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
BFD247932284D4B700981D42 /* AppTableViewCell.swift in Sources */, BFD247932284D4B700981D42 /* AppTableViewCell.swift in Sources */,
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */, BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
BFD52BD622A08A85000B7ED1 /* Server.swift in Sources */,
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */, BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */, BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */, BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */,

View File

@@ -62,7 +62,7 @@ extension AppDelegate
private func prepareForBackgroundFetch() private func prepareForBackgroundFetch()
{ {
// Fetch every 6 hours. // Fetch every 6 hours.
UIApplication.shared.setMinimumBackgroundFetchInterval(60 * 60 * 6) UIApplication.shared.setMinimumBackgroundFetchInterval(60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
} }
@@ -72,8 +72,10 @@ extension AppDelegate
{ {
ServerManager.shared.startDiscovering() ServerManager.shared.startDiscovering()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
AppManager.shared.refreshAllApps(presentingViewController: nil) { (result) in let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
_ = AppManager.shared.refresh(installedApps, presentingViewController: nil) { (result) in
ServerManager.shared.stopDiscovering() ServerManager.shared.stopDiscovering()
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
@@ -87,9 +89,7 @@ extension AppDelegate
guard case let .failure(error) = result else { continue } guard case let .failure(error) = result else { continue }
throw error throw error
} }
print(results)
content.title = "Refreshed Apps!" content.title = "Refreshed Apps!"
content.body = "Successfully refreshed all apps." content.body = "Successfully refreshed all apps."

View File

@@ -124,7 +124,11 @@ private extension AppDetailViewController
sender.isIndicatingActivity = true sender.isIndicatingActivity = true
AppManager.shared.install(self.app, presentingViewController: self) { (result) in let progressView = UIProgressView(progressViewStyle: .bar)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.progress = 0.0
let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
do do
{ {
let installedApp = try result.get() let installedApp = try result.get()
@@ -138,7 +142,7 @@ private extension AppDetailViewController
toastView.show(in: self.navigationController!.view, duration: 2) toastView.show(in: self.navigationController!.view, duration: 2)
} }
} }
catch AppManager.AppError.authentication(AuthenticationOperation.Error.cancelled) catch OperationError.cancelled
{ {
// Ignore // Ignore
} }
@@ -150,12 +154,28 @@ private extension AppDetailViewController
toastView.show(in: self.navigationController!.view, duration: 2) toastView.show(in: self.navigationController!.view, duration: 2)
} }
} }
DispatchQueue.main.async { DispatchQueue.main.async {
UIView.animate(withDuration: 0.4, animations: {
progressView.alpha = 0.0
}) { _ in
progressView.removeFromSuperview()
}
sender.isIndicatingActivity = false sender.isIndicatingActivity = false
self.update() self.update()
} }
} }
progressView.observedProgress = progress
if let navigationBar = self.navigationController?.navigationBar
{
navigationBar.addSubview(progressView)
NSLayoutConstraint.activate([progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor),
progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
}
} }
} }

View File

@@ -14,52 +14,10 @@ import AltKit
import Roxas import Roxas
extension AppManager
{
enum AppError: LocalizedError
{
case unknown
case missingUDID
case noServersFound
case missingPrivateKey
case missingCertificate
case notAuthenticated
case multipleCertificates
case multipleTeams
case download(URLError)
case authentication(Error)
case fetchingSigningResources(Error)
case prepare(Error)
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."
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."
case .download(let error): return error.localizedDescription
case .authentication(let error): return error.localizedDescription
case .fetchingSigningResources(let error): return error.localizedDescription
case .prepare(let error): return error.localizedDescription
case .install(let error): return error.localizedDescription
}
}
}
}
class AppManager class AppManager
{ {
static let shared = AppManager() static let shared = AppManager()
private let session = URLSession(configuration: .default)
private let operationQueue = OperationQueue() private let operationQueue = OperationQueue()
private init() private init()
@@ -107,7 +65,7 @@ extension AppManager
#endif #endif
} }
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTTeam, ALTCertificate), Error>) -> Void) func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTSigner, Error>) -> Void)
{ {
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController) let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in authenticationOperation.resultHandler = { (result) in
@@ -119,91 +77,60 @@ extension AppManager
extension AppManager extension AppManager
{ {
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, AppError>) -> Void) func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{ {
let ipaURL = InstalledApp.ipaURL(for: app) let progress = Progress.discreteProgress(totalUnitCount: 100)
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.InstallApp") // Authenticate
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
func finish(_ result: Result<InstalledApp, AppError>) authenticationOperation.resultHandler = { (result) in
{
completionHandler(result)
RSTEndBackgroundTask(backgroundTaskID)
}
// Download app
self.downloadApp(from: app.downloadURL) { (result) in
let result = result.flatMap { (fileURL) -> Result<Void, URLError> in
// Copy downloaded app to proper location
let result = Result { try FileManager.default.copyItem(at: fileURL, to: ipaURL, shouldReplace: true) }
return result.mapError { _ in URLError(.cannotWriteToFile) }
}
switch result switch result
{ {
case .failure(let error): finish(.failure(.download(error))) case .failure(let error): completionHandler(.failure(error))
case .success: case .success(let signer):
// Authenticate
self.authenticate(presentingViewController: presentingViewController) { (result) in // Download
switch result app.managedObjectContext?.perform {
{ let downloadAppOperation = DownloadAppOperation(app: app)
case .failure(let error): finish(.failure(.authentication(error))) downloadAppOperation.resultHandler = { (result) in
case .success(let team, let certificate): switch result
{
// Fetch provisioning profile case .failure(let error): completionHandler(.failure(error))
self.prepareProvisioningProfile(for: app, team: team) { (result) in case .success(let installedApp):
switch result let context = installedApp.managedObjectContext
{
case .failure(let error): finish(.failure(.fetchingSigningResources(error))) // Refresh/Install
case .success(let profile): let (resignProgress, installProgress) = self.refresh(installedApp, signer: signer, presentingViewController: presentingViewController) { (result) in
switch result
// Prepare app {
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in case .failure(let error): completionHandler(.failure(error))
let app = context.object(with: app.objectID) as! App case .success:
context?.perform {
let installedApp = InstalledApp(app: app, completionHandler(.success(installedApp))
bundleIdentifier: profile.appID.bundleIdentifier,
expirationDate: profile.expirationDate,
context: context)
let signer = ALTSigner(team: team, certificate: certificate)
self.prepare(installedApp, provisioningProfile: profile, signer: signer) { (result) in
switch result
{
case .failure(let error): finish(.failure(.prepare(error)))
case .success(let resignedURL):
// Send app to server
context.perform {
self.sendAppToServer(fileURL: resignedURL, identifier: installedApp.bundleIdentifier) { (result) in
switch result
{
case .failure(let error): finish(.failure(.install(error)))
case .success:
context.perform {
finish(.success(installedApp))
}
}
}
}
}
} }
} }
} }
progress.addChild(resignProgress, withPendingUnitCount: 10)
progress.addChild(installProgress, withPendingUnitCount: 45)
} }
} }
progress.addChild(downloadAppOperation.progress, withPendingUnitCount: 40)
self.operationQueue.addOperation(downloadAppOperation)
} }
} }
} }
progress.addChild(authenticationOperation.progress, withPendingUnitCount: 5)
self.operationQueue.addOperation(authenticationOperation)
return progress
} }
func refresh(_ app: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) func refresh(_ app: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{ {
self.refresh([app], presentingViewController: presentingViewController) { (result) in return self.refresh([app], presentingViewController: presentingViewController) { (result) in
do do
{ {
guard let (_, result) = try result.get().first else { throw AppError.unknown } guard let (_, result) = try result.get().first else { throw OperationError.unknown }
completionHandler(result) completionHandler(result)
} }
catch catch
@@ -213,303 +140,101 @@ extension AppManager
} }
} }
func refreshAllApps(presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], AppError>) -> Void) @discardableResult func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> Progress
{ {
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in let progress = Progress.discreteProgress(totalUnitCount: Int64(installedApps.count))
do
{
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)]
let installedApps = try context.fetch(fetchRequest)
self.refresh(installedApps, presentingViewController: presentingViewController) { (result) in
context.perform { // keep context alive
completionHandler(result)
}
}
}
catch
{
completionHandler(.failure(.prepare(error)))
}
}
}
private func refresh<T: Collection>(_ installedApps: T, presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], AppError>) -> Void) where T.Element == InstalledApp
{
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.RefreshApps")
func finish(_ result: Result<[String: Result<InstalledApp, Error>], AppError>) guard let context = installedApps.first?.managedObjectContext else {
{ completionHandler(.success([:]))
completionHandler(result) return progress
RSTEndBackgroundTask(backgroundTaskID)
} }
guard !ServerManager.shared.discoveredServers.isEmpty else { return finish(.failure(.noServersFound)) }
// Authenticate // Authenticate
self.authenticate(presentingViewController: presentingViewController) { (result) in let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in
switch result switch result
{ {
case .failure(let error): finish(.failure(.authentication(error))) case .failure(let error): completionHandler(.failure(error))
case .success(let team, let certificate): case .success(let signer):
// Sign // Refresh
let signer = ALTSigner(team: team, certificate: certificate) context.perform {
let dispatchGroup = DispatchGroup()
let dispatchGroup = DispatchGroup() var results = [String: Result<InstalledApp, Error>]()
var results = [String: Result<InstalledApp, Error>]()
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
for app in installedApps
{
dispatchGroup.enter()
app.managedObjectContext?.perform { for installedApp in installedApps
let bundleIdentifier = app.bundleIdentifier {
let bundleIdentifier = installedApp.bundleIdentifier
print("Refreshing App:", bundleIdentifier) print("Refreshing App:", bundleIdentifier)
self.refresh(app, signer: signer, context: context) { (result) in dispatchGroup.enter()
let (resignProgress, installProgress) = self.refresh(installedApp, signer: signer, presentingViewController: presentingViewController) { (result) in
print("Refreshed App: \(bundleIdentifier).", result) print("Refreshed App: \(bundleIdentifier).", result)
results[bundleIdentifier] = result results[bundleIdentifier] = result
dispatchGroup.leave() dispatchGroup.leave()
} }
let refreshProgress = Progress(totalUnitCount: 100)
refreshProgress.addChild(resignProgress, withPendingUnitCount: 20)
refreshProgress.addChild(installProgress, withPendingUnitCount: 80)
progress.addChild(refreshProgress, withPendingUnitCount: 1)
} }
}
dispatchGroup.notify(queue: .global()) {
context.perform {
finish(.success(results))
}
}
}
}
}
}
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()
}
func prepareProvisioningProfile(for app: App, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
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()
app.managedObjectContext?.perform {
self.register(app, with: team) { (result) in
do
{
let appID = try result.get()
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
do
{
let provisioningProfile = try result.get()
completionHandler(.success(provisioningProfile))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func prepare(_ installedApp: InstalledApp, provisioningProfile: ALTProvisioningProfile, signer: ALTSigner, completionHandler: @escaping (Result<URL, Error>) -> Void)
{
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) dispatchGroup.notify(queue: .global()) {
completionHandler(.success(resignedURL)) context.perform {
} completionHandler(.success(results))
catch }
{ }
completionHandler(.failure(error))
} }
} }
} }
catch
{
completionHandler(.failure(error))
}
}
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 self.operationQueue.addOperation(authenticationOperation)
let result = result.mapError { $0 as Error }
completionHandler(result) return progress
}
} }
} }
private extension AppManager private extension AppManager
{ {
func register(_ device: ALTDevice, team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void) func refresh(_ installedApp: InstalledApp, signer: ALTSigner, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> (Progress, Progress)
{ {
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in let context = installedApp.managedObjectContext
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 let resignAppOperation = ResignAppOperation(installedApp: installedApp)
do let installAppOperation = InstallAppOperation()
{
let appIDs = try Result(appIDs, error).get() // Resign
resignAppOperation.signer = signer
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleID }) resignAppOperation.resultHandler = { (result) in
{ switch result
completionHandler(.success(appID))
}
else
{
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
completionHandler(Result(appID, error))
}
}
}
catch
{ {
case .failure(let error):
installAppOperation.cancel()
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success(let resignedURL):
installAppOperation.fileURL = resignedURL
} }
} }
}
// Install
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void) installAppOperation.addDependency(resignAppOperation)
{ installAppOperation.resultHandler = { (result) in
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
completionHandler(Result(profile, error))
}
}
func refresh(_ installedApp: InstalledApp, signer: ALTSigner, context: NSManagedObjectContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
{
self.prepareProvisioningProfile(for: installedApp.app, team: signer.team) { (result) in
switch result switch result
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
case .success(let profile): case .success:
context?.perform {
installedApp.managedObjectContext?.perform { completionHandler(.success(installedApp))
self.prepare(installedApp, provisioningProfile: profile, signer: signer) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let resignedURL):
// Send app to server
installedApp.managedObjectContext?.perform {
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))
}
}
}
}
}
}
} }
} }
} }
self.operationQueue.addOperations([resignAppOperation, installAppOperation], waitUntilFinished: false)
return (resignAppOperation.progress, installAppOperation.progress)
} }
} }

View File

@@ -76,12 +76,19 @@
<navigationItem key="navigationItem" title="My Apps" id="dz9-0e-LKa"> <navigationItem key="navigationItem" title="My Apps" id="dz9-0e-LKa">
<barButtonItem key="rightBarButtonItem" title="Refresh All" id="0Ke-yl-tAg"> <barButtonItem key="rightBarButtonItem" title="Refresh All" id="0Ke-yl-tAg">
<connections> <connections>
<action selector="refreshAllApps:" destination="Xf1-LZ-1vU" id="6op-Mf-HSD"/> <action selector="refreshAllApps:" destination="Xf1-LZ-1vU" id="GOS-gx-qKD"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
</navigationItem> </navigationItem>
<connections>
<outlet property="progressView" destination="CuF-K7-fn8" id="SPP-VP-a9e"/>
</connections>
</tableViewController> </tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="nb5-5T-hHT" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="nb5-5T-hHT" userLabel="First Responder" sceneMemberID="firstResponder"/>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progressViewStyle="bar" id="CuF-K7-fn8">
<rect key="frame" x="0.0" y="0.0" width="150" height="2.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</progressView>
</objects> </objects>
<point key="canvasLocation" x="1518" y="420"/> <point key="canvasLocation" x="1518" y="420"/>
</scene> </scene>
@@ -629,7 +636,7 @@
<image name="second" width="30" height="30"/> <image name="second" width="30" height="30"/>
</resources> </resources>
<inferredMetricsTieBreakers> <inferredMetricsTieBreakers>
<segue reference="miz-8X-gFg"/> <segue reference="8jj-zE-2hk"/>
</inferredMetricsTieBreakers> </inferredMetricsTieBreakers>
<color key="tintColor" name="Purple"/> <color key="tintColor" name="Purple"/>
</document> </document>

View File

@@ -10,7 +10,7 @@ import Foundation
import CoreData import CoreData
@objc(InstalledApp) @objc(InstalledApp)
class InstalledApp: NSManagedObject class InstalledApp: NSManagedObject, Fetchable
{ {
/* Properties */ /* Properties */
@NSManaged var bundleIdentifier: String @NSManaged var bundleIdentifier: String
@@ -75,6 +75,13 @@ extension InstalledApp
return ipaURL return ipaURL
} }
class func refreshedIPAURL(for app: App) -> URL
{
let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
return ipaURL
}
class func directoryURL(for app: App) -> URL class func directoryURL(for app: App) -> URL
{ {
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.identifier) let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.identifier)
@@ -92,4 +99,8 @@ extension InstalledApp
var ipaURL: URL { var ipaURL: URL {
return InstalledApp.ipaURL(for: self.app) return InstalledApp.ipaURL(for: self.app)
} }
var refreshedIPAURL: URL {
return InstalledApp.refreshedIPAURL(for: self.app)
}
} }

View File

@@ -9,6 +9,8 @@
import UIKit import UIKit
import Roxas import Roxas
import AltSign
class MyAppsViewController: UITableViewController class MyAppsViewController: UITableViewController
{ {
private var refreshErrors = [String: Error]() private var refreshErrors = [String: Error]()
@@ -22,12 +24,23 @@ class MyAppsViewController: UITableViewController
return dateFormatter return dateFormatter
}() }()
@IBOutlet private var progressView: UIProgressView!
override func viewDidLoad() override func viewDidLoad()
{ {
super.viewDidLoad() super.viewDidLoad()
self.tableView.dataSource = self.dataSource self.tableView.dataSource = self.dataSource
if let navigationBar = self.navigationController?.navigationBar
{
self.progressView.translatesAutoresizingMaskIntoConstraints = false
navigationBar.addSubview(self.progressView)
NSLayoutConstraint.activate([self.progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor),
self.progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
}
self.update() self.update()
} }
@@ -106,7 +119,9 @@ private extension MyAppsViewController
{ {
sender.isIndicatingActivity = true sender.isIndicatingActivity = true
AppManager.shared.refreshAllApps(presentingViewController: self) { (result) in let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
let progress = AppManager.shared.refresh(installedApps, presentingViewController: self) { (result) in
DispatchQueue.main.async { DispatchQueue.main.async {
switch result switch result
{ {
@@ -146,10 +161,49 @@ private extension MyAppsViewController
self.refreshErrors = failures self.refreshErrors = failures
} }
self.progressView.observedProgress = nil
self.progressView.progress = 0.0
sender.isIndicatingActivity = false sender.isIndicatingActivity = false
self.update() self.update()
} }
} }
self.progressView.observedProgress = progress
}
func refresh(_ installedApp: InstalledApp)
{
let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
do
{
let app = try result.get()
try app.managedObjectContext?.save()
DispatchQueue.main.async {
let toastView = RSTToastView(text: "Refreshed \(installedApp.app.name)!", detailText: nil)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
self.update()
}
}
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: "Failed to refresh \(installedApp.app.name)", detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
}
}
DispatchQueue.main.async {
self.progressView.observedProgress = nil
self.progressView.progress = 0.0
}
}
self.progressView.observedProgress = progress
} }
} }
@@ -183,7 +237,7 @@ extension MyAppsViewController
toastView.activityIndicatorView.startAnimating() toastView.activityIndicatorView.startAnimating()
toastView.show(in: self.navigationController?.view ?? self.view) toastView.show(in: self.navigationController?.view ?? self.view)
AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
do do
{ {
let app = try result.get() let app = try result.get()
@@ -203,11 +257,16 @@ extension MyAppsViewController
let toastView = RSTToastView(text: "Failed to refresh \(installedApp.app.name)", detailText: error.localizedDescription) let toastView = RSTToastView(text: "Failed to refresh \(installedApp.app.name)", detailText: error.localizedDescription)
toastView.tintColor = .altPurple toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2) toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
self.update()
} }
} }
DispatchQueue.main.async {
self.progressView.observedProgress = nil
self.progressView.progress = 0.0
}
} }
self.progressView.observedProgress = progress
} }
return [deleteAction, refreshAction] return [deleteAction, refreshAction]

View File

@@ -11,36 +11,27 @@ import Roxas
import AltSign import AltSign
extension AuthenticationOperation enum AuthenticationError: LocalizedError
{ {
enum Error: LocalizedError case noTeam
{ case noCertificate
case cancelled
case missingPrivateKey
case notAuthenticated case missingCertificate
case noTeam
case noCertificate var errorDescription: String? {
switch self {
case missingPrivateKey case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
case missingCertificate case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
var errorDescription: String? { case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
switch self {
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
}
} }
} }
} }
class AuthenticationOperation: RSTOperation @objc(AuthenticationOperation)
class AuthenticationOperation: ResultOperation<ALTSigner>
{ {
var resultHandler: ((Result<(ALTTeam, ALTCertificate), Swift.Error>) -> Void)?
private weak var presentingViewController: UIViewController? private weak var presentingViewController: UIViewController?
private lazy var navigationController = UINavigationController() private lazy var navigationController = UINavigationController()
@@ -48,106 +39,51 @@ class AuthenticationOperation: RSTOperation
private var appleIDPassword: String? private var appleIDPassword: String?
override var isAsynchronous: Bool {
return true
}
init(presentingViewController: UIViewController?) init(presentingViewController: UIViewController?)
{ {
self.presentingViewController = presentingViewController self.presentingViewController = presentingViewController
super.init() super.init()
self.progress.totalUnitCount = 3
} }
override func main() override func main()
{ {
super.main() super.main()
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.Authenticate")
func finish(_ result: Result<(ALTTeam, ALTCertificate), Swift.Error>)
{
print("Finished authenticating with result:", result)
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
do
{
let (altTeam, altCertificate) = try result.get()
let altAccount = altTeam.account
// Account
let account = Account(altAccount, context: context)
account.isActiveAccount = true
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
for account in otherAccounts
{
account.isActiveAccount = false
}
// Team
let team = Team(altTeam, account: account, context: context)
team.isActiveTeam = true
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
let otherTeams = try context.fetch(otherTeamsFetchRequest)
for team in otherTeams
{
team.isActiveTeam = false
}
// Save
try context.save()
// Update keychain
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
Keychain.shared.appleIDPassword = self.appleIDPassword
Keychain.shared.signingCertificateIdentifier = altCertificate.identifier
Keychain.shared.signingCertificatePrivateKey = altCertificate.privateKey
self.resultHandler?(.success((altTeam, altCertificate)))
}
catch
{
self.resultHandler?(.failure(error))
}
self.finish()
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
}
RSTEndBackgroundTask(backgroundTaskID)
}
}
// Sign In // Sign In
self.signIn { (result) in self.signIn { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result switch result
{ {
case .failure(let error): finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success(let account): case .success(let account):
self.progress.completedUnitCount += 1
// Fetch Team // Fetch Team
self.fetchTeam(for: account) { (result) in self.fetchTeam(for: account) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result switch result
{ {
case .failure(let error): finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success(let team): case .success(let team):
self.progress.completedUnitCount += 1
// Fetch Certificate // Fetch Certificate
self.fetchCertificate(for: team) { (result) in self.fetchCertificate(for: team) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result switch result
{ {
case .failure(let error): finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success(let certificate): finish(.success((team, certificate))) case .success(let certificate):
self.progress.completedUnitCount += 1
let signer = ALTSigner(team: team, certificate: certificate)
self.finish(.success(signer))
} }
} }
} }
@@ -155,6 +91,68 @@ class AuthenticationOperation: RSTOperation
} }
} }
} }
override func finish(_ result: Result<ALTSigner, Error>)
{
guard !self.isFinished else { return }
print("Finished authenticating with result:", result)
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
do
{
let signer = try result.get()
let altAccount = signer.team.account
// Account
let account = Account(altAccount, context: context)
account.isActiveAccount = true
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
for account in otherAccounts
{
account.isActiveAccount = false
}
// Team
let team = Team(signer.team, account: account, context: context)
team.isActiveTeam = true
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
let otherTeams = try context.fetch(otherTeamsFetchRequest)
for team in otherTeams
{
team.isActiveTeam = false
}
// Save
try context.save()
// Update keychain
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
Keychain.shared.appleIDPassword = self.appleIDPassword
Keychain.shared.signingCertificateIdentifier = signer.certificate.identifier
Keychain.shared.signingCertificatePrivateKey = signer.certificate.privateKey
super.finish(.success(signer))
}
catch
{
super.finish(.failure(error))
}
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
}
}
}
} }
private extension AuthenticationOperation private extension AuthenticationOperation
@@ -199,13 +197,13 @@ private extension AuthenticationOperation
} }
else else
{ {
completionHandler(.failure(Error.cancelled)) completionHandler(.failure(OperationError.cancelled))
} }
} }
if !self.present(authenticationViewController) if !self.present(authenticationViewController)
{ {
completionHandler(.failure(Error.notAuthenticated)) completionHandler(.failure(OperationError.notAuthenticated))
} }
} }
} }
@@ -254,13 +252,13 @@ private extension AuthenticationOperation
} }
else else
{ {
completionHandler(.failure(Error.cancelled)) completionHandler(.failure(OperationError.cancelled))
} }
} }
if !self.present(selectTeamViewController) if !self.present(selectTeamViewController)
{ {
completionHandler(.failure(Error.noTeam)) completionHandler(.failure(AuthenticationError.noTeam))
} }
} }
} }
@@ -294,7 +292,7 @@ private extension AuthenticationOperation
do do
{ {
let certificate = try Result(certificate, error).get() let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw Error.missingPrivateKey } guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
do do
@@ -302,7 +300,7 @@ private extension AuthenticationOperation
let certificates = try Result(certificates, error).get() let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else { guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else {
throw Error.missingCertificate throw AuthenticationError.missingCertificate
} }
certificate.privateKey = privateKey certificate.privateKey = privateKey
@@ -334,13 +332,13 @@ private extension AuthenticationOperation
} }
else else
{ {
completionHandler(.failure(Error.cancelled)) completionHandler(.failure(OperationError.cancelled))
} }
} }
if !self.present(replaceCertificateViewController) if !self.present(replaceCertificateViewController)
{ {
completionHandler(.failure(Error.noCertificate)) completionHandler(.failure(AuthenticationError.noCertificate))
} }
} }
} }

View File

@@ -0,0 +1,65 @@
//
// DownloadAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/10/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltSign
@objc(DownloadAppOperation)
class DownloadAppOperation: ResultOperation<InstalledApp>
{
let app: App
private let downloadURL: URL
private let ipaURL: URL
private let session = URLSession(configuration: .default)
init(app: App)
{
self.app = app
self.downloadURL = app.downloadURL
self.ipaURL = InstalledApp.ipaURL(for: app)
super.init()
self.progress.totalUnitCount = 1
}
override func main()
{
super.main()
let downloadTask = self.session.downloadTask(with: self.downloadURL) { (fileURL, response, error) in
do
{
let (fileURL, _) = try Result((fileURL, response), error).get()
try FileManager.default.copyItem(at: fileURL, to: self.ipaURL, shouldReplace: true)
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let app = context.object(with: self.app.objectID) as! App
let installedApp = InstalledApp(app: app,
bundleIdentifier: app.identifier,
expirationDate: Date(),
context: context)
self.finish(.success(installedApp))
}
}
catch let error
{
self.finish(.failure(error))
}
}
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
downloadTask.resume()
}
}

View File

@@ -0,0 +1,286 @@
//
// InstallAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Network
import AltKit
extension ALTServerError
{
init<E: Error>(_ error: E)
{
switch error
{
case let error as ALTServerError: self = error
case is DecodingError: self = ALTServerError(.invalidResponse)
case is EncodingError: self = ALTServerError(.invalidRequest)
default:
assertionFailure("Caught unknown error type")
self = ALTServerError(.unknown)
}
}
}
enum InstallationError: LocalizedError
{
case serverNotFound
case connectionFailed
case connectionDropped
case invalidApp
var errorDescription: String? {
switch self
{
case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "")
case .connectionFailed: return NSLocalizedString("Could not connect to AltServer.", comment: "")
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
}
}
}
@objc(InstallAppOperation)
class InstallAppOperation: ResultOperation<Void>
{
var fileURL: URL?
private let dispatchQueue = DispatchQueue(label: "com.altstore.InstallAppOperation")
private var connection: NWConnection?
override init()
{
super.init()
self.progress.totalUnitCount = 4
}
override func main()
{
super.main()
guard let fileURL = self.fileURL else { return self.finish(.failure(OperationError.appNotFound)) }
// Connect to server.
self.connect { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
self.connection = connection
// Send app to server.
self.sendApp(at: fileURL, via: connection) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
self.progress.completedUnitCount += 1
// Receive response from server.
let progress = self.receiveResponse(from: connection) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success: self.finish(.success(()))
}
}
self.progress.addChild(progress, withPendingUnitCount: 3)
}
}
}
}
}
override func finish(_ result: Result<Void, Error>)
{
super.finish(result)
if let connection = self.connection
{
connection.cancel()
}
}
}
private extension InstallAppOperation
{
func connect(completionHandler: @escaping (Result<NWConnection, Error>) -> Void)
{
guard let server = ServerManager.shared.discoveredServers.first else { return completionHandler(.failure(InstallationError.serverNotFound)) }
let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp)
connection.stateUpdateHandler = { [unowned connection] (state) in
switch state
{
case .failed(let error):
print("Failed to connect to service \(server.service.name).", error)
completionHandler(.failure(InstallationError.connectionFailed))
case .cancelled:
completionHandler(.failure(OperationError.cancelled))
case .ready:
completionHandler(.success(connection))
case .waiting: break
case .setup: break
case .preparing: break
@unknown default: break
}
}
connection.start(queue: self.dispatchQueue)
}
func sendApp(at fileURL: URL, via connection: NWConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
guard let appData = try? Data(contentsOf: fileURL) else { throw InstallationError.invalidApp }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
let request = ServerRequest(udid: udid, contentSize: appData.count)
let requestData: Data
do {
requestData = try JSONEncoder().encode(request)
}
catch {
print("Invalid request.", error)
throw ALTServerError(.invalidRequest)
}
let requestSize = Int32(requestData.count)
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
func process(_ error: Error?) -> Bool
{
if error != nil
{
completionHandler(.failure(InstallationError.connectionDropped))
return false
}
else
{
return true
}
}
// Send request data size.
print("Sending request data size \(requestSize)")
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
guard process(error) else { return }
// Send request.
print("Sending request \(request)")
connection.send(content: requestData, completion: .contentProcessed { (error) in
guard process(error) else { return }
// Send app data.
print("Sending app data (Size: \(appData.count))")
connection.send(content: appData, completion: .contentProcessed { (error) in
print("Sent app data!")
guard process(error) else { return }
completionHandler(.success(()))
})
})
})
}
catch
{
completionHandler(.failure(error))
}
}
func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result<Void, Error>) -> Void) -> Progress
{
func receive(from connection: NWConnection, progress: Progress, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let size = MemoryLayout<Int32>.size
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
print(response)
if let error = response.error
{
completionHandler(.failure(error))
}
else if response.progress == 1.0
{
completionHandler(.success(()))
}
else
{
progress.completedUnitCount = Int64(response.progress * 100)
receive(from: connection, progress: progress, completionHandler: completionHandler)
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
let progress = Progress.discreteProgress(totalUnitCount: 100)
receive(from: connection, progress: progress, completionHandler: completionHandler)
return progress
}
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
}
}

View File

@@ -0,0 +1,84 @@
//
// Operation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
class ResultOperation<ResultType>: Operation
{
var resultHandler: ((Result<ResultType, Error>) -> Void)?
@available(*, unavailable)
override func finish()
{
super.finish()
}
func finish(_ result: Result<ResultType, Error>)
{
guard !self.isFinished else { return }
super.finish()
self.resultHandler?(result)
}
}
class Operation: RSTOperation, ProgressReporting
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
private var backgroundTaskID: UIBackgroundTaskIdentifier?
override var isAsynchronous: Bool {
return true
}
override init()
{
super.init()
self.progress.cancellationHandler = { [weak self] in self?.cancel() }
}
override func cancel()
{
super.cancel()
if !self.progress.isCancelled
{
self.progress.cancel()
}
}
override func main()
{
super.main()
let name = "com.altstore." + NSStringFromClass(type(of: self))
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: name) { [weak self] in
guard let backgroundTask = self?.backgroundTaskID else { return }
self?.cancel()
UIApplication.shared.endBackgroundTask(backgroundTask)
self?.backgroundTaskID = .invalid
}
}
override func finish()
{
super.finish()
if let backgroundTaskID = self.backgroundTaskID
{
UIApplication.shared.endBackgroundTask(backgroundTaskID)
self.backgroundTaskID = .invalid
}
}
}

View File

@@ -0,0 +1,32 @@
//
// OperationError.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
enum OperationError: LocalizedError
{
case unknown
case unknownResult
case cancelled
case notAuthenticated
case appNotFound
case unknownUDID
var errorDescription: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
}
}
}

View File

@@ -0,0 +1,244 @@
//
// ResignAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltSign
@objc(ResignAppOperation)
class ResignAppOperation: ResultOperation<URL>
{
let installedApp: InstalledApp
private var context: NSManagedObjectContext?
var signer: ALTSigner?
init(installedApp: InstalledApp)
{
self.installedApp = installedApp
self.context = installedApp.managedObjectContext
super.init()
self.progress.totalUnitCount = 3
}
override func main()
{
super.main()
guard let context = self.context else { return self.finish(.failure(OperationError.appNotFound)) }
guard let signer = self.signer else { return self.finish(.failure(OperationError.notAuthenticated)) }
context.perform {
// Register Device
self.registerCurrentDevice(for: signer.team) { (result) in
guard let _ = self.process(result) else { return }
// Register App
context.perform {
self.register(self.installedApp.app, team: signer.team) { (result) in
guard let appID = self.process(result) else { return }
// Fetch Provisioning Profile
self.fetchProvisioningProfile(for: appID, team: signer.team) { (result) in
guard let profile = self.process(result) else { return }
// Prepare app bundle
context.perform {
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
let prepareAppBundleProgress = self.prepareAppBundle(for: self.installedApp) { (result) in
guard let appBundleURL = self.process(result) else { return }
// Resign app bundle
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profile: profile) { (result) in
guard let resignedURL = self.process(result) else { return }
// Finish
context.perform {
do
{
try FileManager.default.copyItem(at: resignedURL, to: self.installedApp.refreshedIPAURL, shouldReplace: true)
let refreshedDirectory = resignedURL.deletingLastPathComponent()
try? FileManager.default.removeItem(at: refreshedDirectory)
self.finish(.success(self.installedApp.refreshedIPAURL))
}
catch
{
self.finish(.failure(error))
}
}
}
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
}
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
}
}
}
}
}
}
}
func process<T>(_ result: Result<T, Error>) -> T?
{
switch result
{
case .failure(let error):
self.finish(.failure(error))
return nil
case .success(let value):
guard !self.isCancelled else {
self.finish(.failure(OperationError.cancelled))
return nil
}
return value
}
}
}
private extension ResignAppOperation
{
func registerCurrentDevice(for team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
return completionHandler(.failure(OperationError.unknownUDID))
}
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
if let device = devices.first(where: { $0.identifier == udid })
{
completionHandler(.success(device))
}
else
{
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team) { (device, error) in
completionHandler(Result(device, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func register(_ app: App, 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))
}
}
func prepareAppBundle(for installedApp: InstalledApp, completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
let refreshedAppDirectory = installedApp.directoryURL.appendingPathComponent("Refreshed", isDirectory: true)
let ipaURL = installedApp.ipaURL
let bundleIdentifier = installedApp.bundleIdentifier
let openURL = installedApp.openAppURL
DispatchQueue.global().async {
do
{
if FileManager.default.fileExists(atPath: refreshedAppDirectory.path)
{
try FileManager.default.removeItem(at: refreshedAppDirectory)
}
try FileManager.default.createDirectory(at: refreshedAppDirectory, withIntermediateDirectories: true, attributes: nil)
// Become current so we can observe progress from unzipAppBundle().
progress.becomeCurrent(withPendingUnitCount: 1)
let appBundleURL = try FileManager.default.unzipAppBundle(at: 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": bundleIdentifier,
"CFBundleURLSchemes": [openURL.scheme!]] as [String : Any]
allURLSchemes.append(altstoreURLScheme)
infoDictionary[Bundle.Info.urlTypes] = allURLSchemes
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
completionHandler(.success(appBundleURL))
}
catch
{
completionHandler(.failure(error))
}
}
return progress
}
func resignAppBundle(at fileURL: URL, signer: ALTSigner, profile: ALTProvisioningProfile, completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let progress = signer.signApp(at: fileURL, provisioningProfile: profile) { (success, error) in
do
{
try Result(success, error).get()
let ipaURL = try FileManager.default.zipAppBundle(at: fileURL)
completionHandler(.success(ipaURL))
}
catch
{
completionHandler(.failure(error))
}
}
return progress
}
}

View File

@@ -1,264 +0,0 @@
//
// Server.swift
// AltStore
//
// Created by Riley Testut on 5/30/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Network
import AltKit
extension ALTServerError
{
init<E: Error>(_ error: E)
{
switch error
{
case let error as ALTServerError: self = error
case is DecodingError: self = ALTServerError(.invalidResponse)
case is EncodingError: self = ALTServerError(.invalidRequest)
default:
assertionFailure("Caught unknown error type")
self = ALTServerError(.unknown)
}
}
}
enum InstallError: LocalizedError
{
case unknown
case cancelled
case invalidApp
case noUDID
case server(ALTServerError)
var errorDescription: String? {
switch self
{
case .server(let error): return error.localizedDescription
default: return nil
}
}
}
struct Server: Equatable
{
var service: NetService
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltStore.server", qos: .utility)
func installApp(at fileURL: URL, identifier: String, completionHandler: @escaping (Result<Void, InstallError>) -> Void)
{
var isFinished = false
var serverConnection: NWConnection?
func finish(error: InstallError?)
{
// Prevent duplicate callbacks if connection is lost.
guard !isFinished else { return }
isFinished = true
if let connection = serverConnection
{
connection.cancel()
}
if let error = error
{
print("Failed to install \(identifier).", error)
completionHandler(.failure(error))
}
else
{
print("Installed \(identifier)!")
completionHandler(.success(()))
}
}
self.connect { (result) in
switch result
{
case .failure(let error): finish(error: error)
case .success(let connection):
serverConnection = connection
self.sendApp(at: fileURL, via: connection) { (result) in
switch result
{
case .failure(let error): finish(error: error)
case .success:
self.receiveResponse(from: connection) { (result) in
switch result
{
case .success: finish(error: nil)
case .failure(let error): finish(error: .server(error))
}
}
}
}
}
}
}
}
private extension Server
{
func connect(completionHandler: @escaping (Result<NWConnection, InstallError>) -> Void)
{
let connection = NWConnection(to: .service(name: self.service.name, type: self.service.type, domain: self.service.domain, interface: nil), using: .tcp)
connection.stateUpdateHandler = { [weak service, unowned connection] (state) in
switch state
{
case .ready: completionHandler(.success(connection))
case .cancelled: completionHandler(.failure(.cancelled))
case .failed(let error):
print("Failed to connect to service \(service?.name ?? "").", error)
completionHandler(.failure(.server(.init(.connectionFailed))))
case .waiting: break
case .setup: break
case .preparing: break
@unknown default: break
}
}
connection.start(queue: self.dispatchQueue)
}
func sendApp(at fileURL: URL, via connection: NWConnection, completionHandler: @escaping (Result<Void, InstallError>) -> Void)
{
do
{
guard let appData = try? Data(contentsOf: fileURL) else { throw InstallError.invalidApp }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw InstallError.noUDID }
let request = ServerRequest(udid: udid, contentSize: appData.count)
let requestData = try JSONEncoder().encode(request)
let requestSize = Int32(requestData.count)
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
// Send request data size.
print("Sending request data size \(requestSize)")
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
if error != nil
{
completionHandler(.failure(.server(.init(.lostConnection))))
}
else
{
// Send request.
print("Sending request \(request)")
connection.send(content: requestData, completion: .contentProcessed { (error) in
if error != nil
{
completionHandler(.failure(.server(.init(.lostConnection))))
}
else
{
// Send app data.
print("Sending app data (Size: \(appData.count))")
connection.send(content: appData, completion: .contentProcessed { (error) in
if error != nil
{
completionHandler(.failure(.server(.init(.lostConnection))))
}
else
{
completionHandler(.success(()))
}
})
}
})
}
})
}
catch is EncodingError
{
completionHandler(.failure(.server(.init(.invalidRequest))))
}
catch let error as InstallError
{
completionHandler(.failure(error))
}
catch
{
assertionFailure("Unknown error type. \(error)")
completionHandler(.failure(.unknown))
}
}
func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
let size = MemoryLayout<Int32>.size
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
if let error = response.error
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(()))
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
}
}

View File

@@ -11,6 +11,11 @@ import Network
import AltKit import AltKit
struct Server: Equatable
{
var service: NetService
}
class ServerManager: NSObject class ServerManager: NSObject
{ {
static let shared = ServerManager() static let shared = ServerManager()