From 27bce4e456464067b652a0c6d941cc8109b0ea1a Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 5 Mar 2020 14:49:21 -0800 Subject: [PATCH] [AltServer] Supports Install/Remove provisioning profiles requests Stuff I shoulda committed --- AltKit/Bundle+AltStore.swift | 5 + AltKit/CodableServerError.swift | 58 +++ AltKit/NSError+ALTServerError.h | 9 +- AltKit/NSError+ALTServerError.m | 37 ++ AltKit/ServerProtocol.swift | 162 +++++++- AltServer/Connections/ConnectionManager.swift | 65 ++- AltServer/Devices/ALTDeviceManager.h | 3 + AltServer/Devices/ALTDeviceManager.mm | 378 +++++++++++++++--- AltStore.xcodeproj/project.pbxproj | 4 + 9 files changed, 637 insertions(+), 84 deletions(-) create mode 100644 AltKit/CodableServerError.swift diff --git a/AltKit/Bundle+AltStore.swift b/AltKit/Bundle+AltStore.swift index 90f065b7..110b20f5 100644 --- a/AltKit/Bundle+AltStore.swift +++ b/AltKit/Bundle+AltStore.swift @@ -29,6 +29,11 @@ public extension Bundle return infoPlistURL } + var provisioningProfileURL: URL { + let infoPlistURL = self.bundleURL.appendingPathComponent("embedded.mobileprovision") + return infoPlistURL + } + var certificateURL: URL { let infoPlistURL = self.bundleURL.appendingPathComponent("ALTCertificate.p12") return infoPlistURL diff --git a/AltKit/CodableServerError.swift b/AltKit/CodableServerError.swift new file mode 100644 index 00000000..5dff7787 --- /dev/null +++ b/AltKit/CodableServerError.swift @@ -0,0 +1,58 @@ +// +// CodableServerError.swift +// AltKit +// +// Created by Riley Testut on 3/5/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself +extension ALTServerError.Code: Codable {} + +struct CodableServerError: Codable +{ + var error: ALTServerError { + return ALTServerError(self.errorCode, userInfo: self.userInfo ?? [:]) + } + + private var errorCode: ALTServerError.Code + private var userInfo: [String: String]? + + private enum CodingKeys: String, CodingKey + { + case errorCode + case userInfo + } + + init(error: ALTServerError) + { + self.errorCode = error.code + + let userInfo = error.userInfo.compactMapValues { $0 as? String } + if !userInfo.isEmpty + { + self.userInfo = userInfo + } + } + + init(from decoder: Decoder) throws + { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let errorCode = try container.decode(Int.self, forKey: .errorCode) + self.errorCode = ALTServerError.Code(rawValue: errorCode) ?? .unknown + + let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo) + self.userInfo = userInfo + } + + func encode(to encoder: Encoder) throws + { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.error.code.rawValue, forKey: .errorCode) + try container.encodeIfPresent(self.userInfo, forKey: .userInfo) + } +} + diff --git a/AltKit/NSError+ALTServerError.h b/AltKit/NSError+ALTServerError.h index bb9176ae..0a9e884a 100644 --- a/AltKit/NSError+ALTServerError.h +++ b/AltKit/NSError+ALTServerError.h @@ -11,6 +11,9 @@ extern NSErrorDomain const AltServerErrorDomain; extern NSErrorDomain const AltServerInstallationErrorDomain; +extern NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey; +extern NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey; + typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError) { ALTServerErrorUnknown = 0, @@ -32,7 +35,11 @@ typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError) ALTServerErrorUnknownResponse = 12, ALTServerErrorInvalidAnisetteData = 13, - ALTServerErrorPluginNotFound = 14 + ALTServerErrorPluginNotFound = 14, + + ALTServerErrorProfileInstallFailed = 15, + ALTServerErrorProfileCopyFailed = 16, + ALTServerErrorProfileRemoveFailed = 17, }; NS_ASSUME_NONNULL_BEGIN diff --git a/AltKit/NSError+ALTServerError.m b/AltKit/NSError+ALTServerError.m index 463e2fe4..2349152d 100644 --- a/AltKit/NSError+ALTServerError.m +++ b/AltKit/NSError+ALTServerError.m @@ -11,6 +11,9 @@ NSErrorDomain const AltServerErrorDomain = @"com.rileytestut.AltServer"; NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServer.Installation"; +NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode"; +NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdentifier"; + @implementation NSError (ALTServerError) + (void)load @@ -73,7 +76,41 @@ NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServ case ALTServerErrorPluginNotFound: return NSLocalizedString(@"Could not connect to Mail plug-in. Please make sure Mail is running and that you've enabled the plug-in in Mail's preferences, then try again.", @""); + + case ALTServerErrorProfileInstallFailed: + return [self underlyingProfileErrorLocalizedDescriptionWithBaseDescription:NSLocalizedString(@"Could not install profile", "")]; + + case ALTServerErrorProfileCopyFailed: + return [self underlyingProfileErrorLocalizedDescriptionWithBaseDescription:NSLocalizedString(@"Could not copy provisioning profiles", "")]; + + case ALTServerErrorProfileRemoveFailed: + return [self underlyingProfileErrorLocalizedDescriptionWithBaseDescription:NSLocalizedString(@"Could not remove profile", "")]; } } + +- (NSString *)underlyingProfileErrorLocalizedDescriptionWithBaseDescription:(NSString *)baseDescription +{ + NSMutableString *localizedDescription = [NSMutableString string]; + + NSString *bundleID = self.userInfo[ALTProvisioningProfileBundleIDErrorKey]; + if (bundleID) + { + [localizedDescription appendFormat:NSLocalizedString(@"%@ “%@”", @""), baseDescription, bundleID]; + } + else + { + [localizedDescription appendString:baseDescription]; + } + + NSString *underlyingErrorCode = self.userInfo[ALTUnderlyingErrorCodeErrorKey]; + if (underlyingErrorCode) + { + [localizedDescription appendFormat:@" (%@)", underlyingErrorCode]; + } + + [localizedDescription appendString:@"."]; + + return localizedDescription; +} @end diff --git a/AltKit/ServerProtocol.swift b/AltKit/ServerProtocol.swift index 1132b3ce..b3530f07 100644 --- a/AltKit/ServerProtocol.swift +++ b/AltKit/ServerProtocol.swift @@ -11,9 +11,6 @@ import AltSign public let ALTServerServiceType = "_altserver._tcp" -// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself -extension ALTServerError.Code: Codable {} - protocol ServerMessageProtocol: Codable { var version: Int { get } @@ -25,6 +22,8 @@ public enum ServerRequest: Decodable case anisetteData(AnisetteDataRequest) case prepareApp(PrepareAppRequest) case beginInstallation(BeginInstallationRequest) + case installProvisioningProfiles(InstallProvisioningProfilesRequest) + case removeProvisioningProfiles(RemoveProvisioningProfilesRequest) case unknown(identifier: String, version: Int) var identifier: String { @@ -33,6 +32,8 @@ public enum ServerRequest: Decodable case .anisetteData(let request): return request.identifier case .prepareApp(let request): return request.identifier case .beginInstallation(let request): return request.identifier + case .installProvisioningProfiles(let request): return request.identifier + case .removeProvisioningProfiles(let request): return request.identifier case .unknown(let identifier, _): return identifier } } @@ -43,6 +44,8 @@ public enum ServerRequest: Decodable case .anisetteData(let request): return request.version case .prepareApp(let request): return request.version case .beginInstallation(let request): return request.version + case .installProvisioningProfiles(let request): return request.version + case .removeProvisioningProfiles(let request): return request.version case .unknown(_, let version): return version } } @@ -73,6 +76,14 @@ public enum ServerRequest: Decodable case "BeginInstallationRequest": let request = try BeginInstallationRequest(from: decoder) self = .beginInstallation(request) + + case "InstallProvisioningProfilesRequest": + let request = try InstallProvisioningProfilesRequest(from: decoder) + self = .installProvisioningProfiles(request) + + case "RemoveProvisioningProfilesRequest": + let request = try RemoveProvisioningProfilesRequest(from: decoder) + self = .removeProvisioningProfiles(request) default: self = .unknown(identifier: identifier, version: version) @@ -84,6 +95,8 @@ public enum ServerResponse: Decodable { case anisetteData(AnisetteDataResponse) case installationProgress(InstallationProgressResponse) + case installProvisioningProfiles(InstallProvisioningProfilesResponse) + case removeProvisioningProfiles(RemoveProvisioningProfilesResponse) case error(ErrorResponse) case unknown(identifier: String, version: Int) @@ -92,6 +105,8 @@ public enum ServerResponse: Decodable { case .anisetteData(let response): return response.identifier case .installationProgress(let response): return response.identifier + case .installProvisioningProfiles(let response): return response.identifier + case .removeProvisioningProfiles(let response): return response.identifier case .error(let response): return response.identifier case .unknown(let identifier, _): return identifier } @@ -102,6 +117,8 @@ public enum ServerResponse: Decodable { case .anisetteData(let response): return response.version case .installationProgress(let response): return response.version + case .installProvisioningProfiles(let response): return response.version + case .removeProvisioningProfiles(let response): return response.version case .error(let response): return response.version case .unknown(_, let version): return version } @@ -129,6 +146,14 @@ public enum ServerResponse: Decodable case "InstallationProgressResponse": let response = try InstallationProgressResponse(from: decoder) self = .installationProgress(response) + + case "InstallProvisioningProfilesResponse": + let response = try InstallProvisioningProfilesResponse(from: decoder) + self = .installProvisioningProfiles(response) + + case "RemoveProvisioningProfilesResponse": + let response = try RemoveProvisioningProfilesResponse(from: decoder) + self = .removeProvisioningProfiles(response) case "ErrorResponse": let response = try ErrorResponse(from: decoder) @@ -140,6 +165,28 @@ public enum ServerResponse: Decodable } } +// _Don't_ provide generic SuccessResponse, as that would prevent us +// from easily changing response format for a request in the future. +public struct ErrorResponse: ServerMessageProtocol +{ + public var version = 2 + public var identifier = "ErrorResponse" + + public var error: ALTServerError { + return self.serverError?.error ?? ALTServerError(self.errorCode) + } + private var serverError: CodableServerError? + + // Legacy (v1) + private var errorCode: ALTServerError.Code + + public init(error: ALTServerError) + { + self.serverError = CodableServerError(error: error) + self.errorCode = error.code + } +} + public struct AnisetteDataRequest: ServerMessageProtocol { public var version = 1 @@ -223,22 +270,6 @@ public struct BeginInstallationRequest: ServerMessageProtocol } } -public struct ErrorResponse: ServerMessageProtocol -{ - public var version = 1 - public var identifier = "ErrorResponse" - - public var error: ALTServerError { - return ALTServerError(self.errorCode) - } - private var errorCode: ALTServerError.Code - - public init(error: ALTServerError) - { - self.errorCode = error.code - } -} - public struct InstallationProgressResponse: ServerMessageProtocol { public var version = 1 @@ -251,3 +282,96 @@ public struct InstallationProgressResponse: ServerMessageProtocol self.progress = progress } } + +public struct InstallProvisioningProfilesRequest: ServerMessageProtocol +{ + public var version = 1 + public var identifier = "InstallProvisioningProfilesRequest" + + public var udid: String + public var provisioningProfiles: Set + + // If activeProfiles is non-nil, then AltServer should remove all profiles except active ones. + public var activeProfiles: Set? + + private enum CodingKeys: String, CodingKey + { + case identifier + case version + case udid + case provisioningProfiles + case activeProfiles + } + + public init(udid: String, provisioningProfiles: Set, activeProfiles: Set?) + { + self.udid = udid + self.provisioningProfiles = provisioningProfiles + self.activeProfiles = activeProfiles + } + + public init(from decoder: Decoder) throws + { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.version = try container.decode(Int.self, forKey: .version) + self.identifier = try container.decode(String.self, forKey: .identifier) + self.udid = try container.decode(String.self, forKey: .udid) + + let rawProvisioningProfiles = try container.decode([Data].self, forKey: .provisioningProfiles) + let provisioningProfiles = try rawProvisioningProfiles.map { (data) -> ALTProvisioningProfile in + guard let profile = ALTProvisioningProfile(data: data) else { + throw DecodingError.dataCorruptedError(forKey: CodingKeys.provisioningProfiles, in: container, debugDescription: "Could not parse provisioning profile from data.") + } + return profile + } + + self.provisioningProfiles = Set(provisioningProfiles) + self.activeProfiles = try container.decodeIfPresent(Set.self, forKey: .activeProfiles) + } + + public func encode(to encoder: Encoder) throws + { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.version, forKey: .version) + try container.encode(self.identifier, forKey: .identifier) + try container.encode(self.udid, forKey: .udid) + + try container.encode(self.provisioningProfiles.map { $0.data }, forKey: .provisioningProfiles) + try container.encodeIfPresent(self.activeProfiles, forKey: .activeProfiles) + } +} + +public struct InstallProvisioningProfilesResponse: ServerMessageProtocol +{ + public var version = 1 + public var identifier = "InstallProvisioningProfilesResponse" + + public init() + { + } +} + +public struct RemoveProvisioningProfilesRequest: ServerMessageProtocol +{ + public var version = 1 + public var identifier = "RemoveProvisioningProfilesRequest" + + public var udid: String + public var bundleIdentifiers: Set + + public init(udid: String, bundleIdentifiers: Set) + { + self.udid = udid + self.bundleIdentifiers = bundleIdentifiers + } +} + +public struct RemoveProvisioningProfilesResponse: ServerMessageProtocol +{ + public var version = 1 + public var identifier = "RemoveProvisioningProfilesResponse" + + public init() + { + } +} diff --git a/AltServer/Connections/ConnectionManager.swift b/AltServer/Connections/ConnectionManager.swift index 94cef17a..519b2e2f 100644 --- a/AltServer/Connections/ConnectionManager.swift +++ b/AltServer/Connections/ConnectionManager.swift @@ -21,9 +21,8 @@ extension ALTServerError case let error as ALTServerError: self = error case is DecodingError: self = ALTServerError(.invalidRequest) case is EncodingError: self = ALTServerError(.invalidResponse) - default: - assertionFailure("Caught unknown error type") - self = ALTServerError(.unknown) + case let error as NSError: + self = ALTServerError(.unknown, userInfo: error.userInfo) } } } @@ -266,7 +265,15 @@ private extension ConnectionManager case .success(.prepareApp(let request)): self.handlePrepareAppRequest(request, for: connection) - case .success: + case .success(.beginInstallation): break + + case .success(.installProvisioningProfiles(let request)): + self.handleInstallProvisioningProfilesRequest(request, for: connection) + + case .success(.removeProvisioningProfiles(let request)): + self.handleRemoveProvisioningProfilesRequest(request, for: connection) + + case .success(.unknown): let response = ErrorResponse(error: ALTServerError(.unknownRequest)) connection.send(response, shouldDisconnect: true) { (result) in print("Sent unknown request response with result:", result) @@ -435,6 +442,56 @@ private extension ConnectionManager } }) } + + func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: ClientConnection) + { + let removeInactiveProfiles = (request.activeProfiles != nil) + ALTDeviceManager.shared.installProvisioningProfiles(request.provisioningProfiles, toDeviceWithUDID: request.udid, activeProvisioningProfiles: request.activeProfiles, removeInactiveProvisioningProfiles: removeInactiveProfiles) { (errors) in + + if let error = errors.values.first + { + print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", errors) + + let errorResponse = ErrorResponse(error: ALTServerError(error)) + connection.send(errorResponse, shouldDisconnect: true) { (result) in + print("Sent install profiles error response with result:", result) + } + } + else + { + print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier }) + + let response = InstallProvisioningProfilesResponse() + connection.send(response, shouldDisconnect: true) { (result) in + print("Sent install profiles response to \(connection) with result:", result) + } + } + } + } + + func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: ClientConnection) + { + ALTDeviceManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers, fromDeviceWithUDID: request.udid) { (errors) in + if let error = errors.values.first + { + print("Failed to remove profiles \(request.bundleIdentifiers):", errors) + + let errorResponse = ErrorResponse(error: ALTServerError(error)) + connection.send(errorResponse, shouldDisconnect: true) { (result) in + print("Sent remove profiles error response with result:", result) + } + } + else + { + print("Removed profiles:", request.bundleIdentifiers) + + let response = RemoveProvisioningProfilesResponse() + connection.send(response, shouldDisconnect: true) { (result) in + print("Sent remove profiles error response to \(connection) with result:", result) + } + } + } + } } private extension ConnectionManager diff --git a/AltServer/Devices/ALTDeviceManager.h b/AltServer/Devices/ALTDeviceManager.h index f70af7a3..39a46ebd 100644 --- a/AltServer/Devices/ALTDeviceManager.h +++ b/AltServer/Devices/ALTDeviceManager.h @@ -29,6 +29,9 @@ extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification /* App Installation */ - (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; +- (void)installProvisioningProfiles:(NSSet *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet *)activeProvisioningProfiles removeInactiveProvisioningProfiles:(BOOL)removeInactiveProvisioningProfiles completionHandler:(void (^)(NSDictionary *errors))completionHandler; +- (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(NSDictionary *errors))completionHandler; + /* Connections */ - (void)startWiredConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTWiredConnection *_Nullable connection, NSError *_Nullable error))completionHandler; - (void)startNotificationConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTNotificationConnection *_Nullable connection, NSError *_Nullable error))completionHandler; diff --git a/AltServer/Devices/ALTDeviceManager.mm b/AltServer/Devices/ALTDeviceManager.mm index 24420458..9b1d4bd8 100644 --- a/AltServer/Devices/ALTDeviceManager.mm +++ b/AltServer/Devices/ALTDeviceManager.mm @@ -114,19 +114,11 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT continue; } - plist_t pdata = plist_new_data((const char *)provisioningProfile.data.bytes, provisioningProfile.data.length); - - if (misagent_install(mis, pdata) == MISAGENT_E_SUCCESS) + NSError *installError = nil; + if (![self installProvisioningProfile:preferredProfile misagent:mis error:&installError]) { - NSLog(@"Reinstalled profile: %@", provisioningProfile.UUID); + error = installError; } - else - { - int code = misagent_get_status_code(mis); - NSLog(@"Failed to reinstall provisioning profile %@. (%@)", provisioningProfile.UUID, @(code)); - } - - plist_free(pdata); } [[NSFileManager defaultManager] removeItemAtURL:removedProfilesDirectoryURL error:nil]; @@ -296,50 +288,17 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT { return finish(error); } - - plist_t rawProfiles = NULL; - if (misagent_copy_all(mis, &rawProfiles) != MISAGENT_E_SUCCESS) + NSArray *provisioningProfiles = [self copyProvisioningProfilesWithClient:mis error:&error]; + if (provisioningProfiles == nil) { - return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + return finish(error); } - - // For some reason, libplist now fails to parse `rawProfiles` correctly. - // Specifically, it no longer recognizes the nodes in the plist array as "data" nodes. - // However, if we encode it as XML then decode it again, it'll work ¯\_(ツ)_/¯ - char *plistXML = nullptr; - uint32_t plistLength = 0; - plist_to_xml(rawProfiles, &plistXML, &plistLength); - - plist_t profiles = NULL; - plist_from_xml(plistXML, plistLength, &profiles); - - free(plistXML); - - uint32_t profileCount = plist_array_get_size(profiles); - for (int i = 0; i < profileCount; i++) + + for (ALTProvisioningProfile *provisioningProfile in provisioningProfiles) { - plist_t profile = plist_array_get_item(profiles, i); - if (plist_get_node_type(profile) != PLIST_DATA) - { - continue; - } - - char *bytes = NULL; - uint64_t length = 0; - - plist_get_data_val(profile, &bytes, &length); - if (bytes == NULL) - { - continue; - } - - NSData *data = [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:YES]; - ALTProvisioningProfile *provisioningProfile = [[ALTProvisioningProfile alloc] initWithData:data]; - if (![provisioningProfile isFreeProvisioningProfile]) { - NSLog(@"Ignoring: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier); continue; } @@ -365,20 +324,12 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT NSLog(@"Failed to copy profile to temporary URL. %@", copyError); continue; } - - if (misagent_remove(mis, provisioningProfile.UUID.UUIDString.lowercaseString.UTF8String) == MISAGENT_E_SUCCESS) + + if (![self removeProvisioningProfile:provisioningProfile misagent:mis error:&error]) { - NSLog(@"Removed provisioning profile: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier); - } - else - { - int code = misagent_get_status_code(mis); - NSLog(@"Failed to remove provisioning profile %@ (Team: %@). Error Code: %@", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier, @(code)); + return finish(error); } } - - plist_free(rawProfiles); - plist_free(profiles); lockdownd_client_free(client); client = NULL; @@ -547,6 +498,313 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT return success; } +#pragma mark - Provisioning Profiles - + +- (void)installProvisioningProfiles:(NSSet *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(NSSet *)activeProvisioningProfiles removeInactiveProvisioningProfiles:(BOOL)removeInactiveProvisioningProfiles completionHandler:(void (^)(NSDictionary *errors))completionHandler; +{ + dispatch_async(self.installationQueue, ^{ + __block idevice_t device = NULL; + __block lockdownd_client_t client = NULL; + __block afc_client_t afc = NULL; + __block misagent_client_t mis = NULL; + __block lockdownd_service_descriptor_t service = NULL; + + void (^finish)(NSDictionary *, NSError *) = ^(NSDictionary *installationErrors, NSError *error) { + lockdownd_service_descriptor_free(service); + misagent_client_free(mis); + afc_client_free(afc); + lockdownd_client_free(client); + idevice_free(device); + + if (installationErrors) + { + completionHandler(installationErrors); + } + else + { + NSMutableDictionary *installationErrors = [NSMutableDictionary dictionary]; + for (ALTProvisioningProfile *profile in provisioningProfiles) + { + installationErrors[profile] = error; + } + + completionHandler(installationErrors); + } + }; + + /* Find Device */ + if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]); + } + + /* Connect to Device */ + if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + /* Connect to Misagent */ + if (lockdownd_start_service(client, "com.apple.misagent", &service) != LOCKDOWN_E_SUCCESS || service == NULL) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + if (misagent_client_new(device, service, &mis) != MISAGENT_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + NSError *error = nil; + NSArray *installedProvisioningProfiles = [self copyProvisioningProfilesWithClient:mis error:&error]; + if (provisioningProfiles == nil) + { + return finish(nil, error); + } + + for (ALTProvisioningProfile *provisioningProfile in installedProvisioningProfiles) + { + if (![provisioningProfile isFreeProvisioningProfile]) + { + continue; + } + + BOOL removeProfile = NO; + + for (ALTProvisioningProfile *profile in provisioningProfiles) + { + if ([profile.bundleIdentifier isEqualToString:provisioningProfile.bundleIdentifier]) + { + // Remove previous provisioning profile before installing new one. + removeProfile = YES; + break; + } + } + + if (removeInactiveProvisioningProfiles && ![activeProvisioningProfiles containsObject:provisioningProfile.bundleIdentifier]) + { + // Remove all non-active provisioning profiles to remain under 3 app limit for free developer accounts. + removeProfile = YES; + } + + if (removeProfile) + { + if (![self removeProvisioningProfile:provisioningProfile misagent:mis error:&error]) + { + return finish(nil, error); + } + } + } + + NSMutableDictionary *profileErrors = [NSMutableDictionary dictionary]; + + for (ALTProvisioningProfile *provisioningProfile in provisioningProfiles) + { + NSError *error = nil; + if (![self installProvisioningProfile:provisioningProfile misagent:mis error:&error]) + { + profileErrors[provisioningProfile] = error; + } + } + + finish(profileErrors, nil); + }); +} + +- (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(NSDictionary *errors))completionHandler +{ + dispatch_async(self.installationQueue, ^{ + __block idevice_t device = NULL; + __block lockdownd_client_t client = NULL; + __block afc_client_t afc = NULL; + __block misagent_client_t mis = NULL; + __block lockdownd_service_descriptor_t service = NULL; + + void (^finish)(NSDictionary *, NSError *) = ^(NSDictionary *installationErrors, NSError *error) { + lockdownd_service_descriptor_free(service); + misagent_client_free(mis); + afc_client_free(afc); + lockdownd_client_free(client); + idevice_free(device); + + if (installationErrors) + { + completionHandler(installationErrors); + } + else + { + NSMutableDictionary *installationErrors = [NSMutableDictionary dictionary]; + for (NSString *bundleID in bundleIdentifiers) + { + installationErrors[bundleID] = error; + } + + completionHandler(installationErrors); + } + }; + + /* Find Device */ + if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]); + } + + /* Connect to Device */ + if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + /* Connect to Misagent */ + if (lockdownd_start_service(client, "com.apple.misagent", &service) != LOCKDOWN_E_SUCCESS || service == NULL) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + if (misagent_client_new(device, service, &mis) != MISAGENT_E_SUCCESS) + { + return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + NSError *error = nil; + NSArray *provisioningProfiles = [self copyProvisioningProfilesWithClient:mis error:&error]; + if (provisioningProfiles == nil) + { + return finish(nil, error); + } + + NSMutableDictionary *profileErrors = [NSMutableDictionary dictionary]; + + /* Remove provisioning profiles */ + for (ALTProvisioningProfile *provisioningProfile in provisioningProfiles) + { + if (![bundleIdentifiers containsObject:provisioningProfile.bundleIdentifier]) + { + continue; + } + + if (![self removeProvisioningProfile:provisioningProfile misagent:mis error:&error]) + { + profileErrors[provisioningProfile.bundleIdentifier] = error; + } + } + + finish(profileErrors, nil); + }); +} + +- (BOOL)installProvisioningProfile:(ALTProvisioningProfile *)provisioningProfile misagent:(misagent_client_t)mis error:(NSError **)error +{ + plist_t pdata = plist_new_data((const char *)provisioningProfile.data.bytes, provisioningProfile.data.length); + + misagent_error_t result = misagent_install(mis, pdata); + plist_free(pdata); + + if (result == MISAGENT_E_SUCCESS) + { + NSLog(@"Installed profile: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier); + return YES; + } + else + { + NSLog(@"Failed to install provisioning profile %@ (Team: %@). Error Code: %@", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier, @(result)); + + if (error) + { + *error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorProfileInstallFailed userInfo:@{ALTUnderlyingErrorCodeErrorKey: [@(result) description], ALTProvisioningProfileBundleIDErrorKey: provisioningProfile.bundleIdentifier}]; + } + + return NO; + } + + return NO; +} + +- (BOOL)removeProvisioningProfile:(ALTProvisioningProfile *)provisioningProfile misagent:(misagent_client_t)mis error:(NSError **)error +{ + misagent_error_t result = misagent_remove(mis, provisioningProfile.UUID.UUIDString.lowercaseString.UTF8String); + if (result == MISAGENT_E_SUCCESS) + { + NSLog(@"Removed provisioning profile: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier); + return YES; + } + else + { + NSLog(@"Failed to remove provisioning profile %@ (Team: %@). Error Code: %@", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier, @(result)); + + if (error) + { + *error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorProfileRemoveFailed userInfo:@{ALTUnderlyingErrorCodeErrorKey: [@(result) description], ALTProvisioningProfileBundleIDErrorKey: provisioningProfile.bundleIdentifier}]; + } + + return NO; + } +} + +- (nullable NSArray *)copyProvisioningProfilesWithClient:(misagent_client_t)mis error:(NSError **)error +{ + plist_t rawProfiles = NULL; + misagent_error_t result = misagent_copy_all(mis, &rawProfiles); + if (result != MISAGENT_E_SUCCESS) + { + if (error) + { + *error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorProfileCopyFailed userInfo:@{ALTUnderlyingErrorCodeErrorKey: [@(result) description]}]; + } + + return nil; + } + + /* Copy all provisioning profiles */ + + // For some reason, libplist now fails to parse `rawProfiles` correctly. + // Specifically, it no longer recognizes the nodes in the plist array as "data" nodes. + // However, if we encode it as XML then decode it again, it'll work ¯\_(ツ)_/¯ + char *plistXML = nullptr; + uint32_t plistLength = 0; + plist_to_xml(rawProfiles, &plistXML, &plistLength); + + plist_t profiles = NULL; + plist_from_xml(plistXML, plistLength, &profiles); + + free(plistXML); + + NSMutableArray *provisioningProfiles = [NSMutableArray array]; + + uint32_t profileCount = plist_array_get_size(profiles); + for (int i = 0; i < profileCount; i++) + { + plist_t profile = plist_array_get_item(profiles, i); + if (plist_get_node_type(profile) != PLIST_DATA) + { + continue; + } + + char *bytes = NULL; + uint64_t length = 0; + + plist_get_data_val(profile, &bytes, &length); + if (bytes == NULL) + { + continue; + } + + NSData *data = [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:YES]; + ALTProvisioningProfile *provisioningProfile = [[ALTProvisioningProfile alloc] initWithData:data]; + if (provisioningProfile == nil) + { + continue; + } + + [provisioningProfiles addObject:provisioningProfile]; + } + + plist_free(rawProfiles); + plist_free(profiles); + + return provisioningProfiles; +} + #pragma mark - Connections - - (void)startWiredConnectionToDevice:(ALTDevice *)altDevice completionHandler:(void (^)(ALTWiredConnection * _Nullable, NSError * _Nullable))completionHandler diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 6b2aebfb..5dfd2871 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -167,6 +167,7 @@ BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478B2284C4C300981D42 /* AppIconImageView.swift */; }; BFD2478F2284C8F900981D42 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478E2284C8F900981D42 /* Button.swift */; }; BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */; }; + BFD44606241188C400EAB90A /* CodableServerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableServerError.swift */; }; BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BD322A0800A000B7ED1 /* ServerManager.swift */; }; BFD52C0122A1A9CB000B7ED1 /* ptrarray.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */; }; BFD52C0222A1A9CB000B7ED1 /* base64.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE622A1A9CA000B7ED1 /* base64.c */; }; @@ -490,6 +491,7 @@ BFD2478B2284C4C300981D42 /* AppIconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconImageView.swift; sourceTree = ""; }; BFD2478E2284C8F900981D42 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = ""; }; + BFD44605241188C300EAB90A /* CodableServerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableServerError.swift; sourceTree = ""; }; BFD52BD222A06EFB000B7ED1 /* AltKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltKit.h; sourceTree = ""; }; BFD52BD322A0800A000B7ED1 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = ""; }; BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = ptrarray.c; path = Dependencies/libplist/src/ptrarray.c; sourceTree = SOURCE_ROOT; }; @@ -667,6 +669,7 @@ BFBAC8852295C90300587369 /* Result+Conveniences.swift */, BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */, BF1E3128229F474900370A3C /* ServerProtocol.swift */, + BFD44605241188C300EAB90A /* CodableServerError.swift */, BF1E314822A060F400370A3C /* NSError+ALTServerError.h */, BF1E314922A060F400370A3C /* NSError+ALTServerError.m */, BF718BC723C919CC00A89F2D /* CFNotificationName+AltStore.h */, @@ -1566,6 +1569,7 @@ BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */, BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */, BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */, + BFD44606241188C400EAB90A /* CodableServerError.swift in Sources */, BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0;