diff --git a/RootHelper/main.m b/RootHelper/main.m index 9160b78..51e3adf 100644 --- a/RootHelper/main.m +++ b/RootHelper/main.m @@ -807,7 +807,13 @@ int signApp(NSString* appPath) } #endif -void applyPatchesToInfoDictionary(NSString* appPath) +NSString* randomStealthBundleIdentifierSuffix(void) +{ + uint64_t randomValue = ((uint64_t)arc4random() << 32) | arc4random(); + return [NSString stringWithFormat:@"TS_%016llX", randomValue]; +} + +void applyPatchesToInfoDictionary(NSString* appPath, BOOL stealthInstall, BOOL stripURLSchemesInStealth) { NSURL* appURL = [NSURL fileURLWithPath:appPath]; NSURL* infoPlistURL = [appURL URLByAppendingPathComponent:@"Info.plist"]; @@ -817,41 +823,84 @@ void applyPatchesToInfoDictionary(NSString* appPath) // Enable Notifications infoDictM[@"SBAppUsesLocalNotifications"] = @1; - // Remove system claimed URL schemes if existant - NSSet* appleSchemes = systemURLSchemes(); - NSArray* CFBundleURLTypes = infoDictM[@"CFBundleURLTypes"]; - if([CFBundleURLTypes isKindOfClass:[NSArray class]]) + if(stealthInstall) { - NSMutableArray* CFBundleURLTypesM = [NSMutableArray new]; - - for(NSDictionary* URLType in CFBundleURLTypes) + NSString* originalAppId = infoDictM[@"TSOriginalBundleIdentifier"]; + if(![originalAppId isKindOfClass:NSString.class] || originalAppId.length == 0) { - if(![URLType isKindOfClass:[NSDictionary class]]) continue; - - NSMutableDictionary* modifiedURLType = URLType.mutableCopy; - NSArray* URLSchemes = URLType[@"CFBundleURLSchemes"]; - if(URLSchemes) - { - NSMutableSet* URLSchemesSet = [NSMutableSet setWithArray:URLSchemes]; - for(NSString* existingURLScheme in [URLSchemesSet copy]) - { - if(![existingURLScheme isKindOfClass:[NSString class]]) - { - [URLSchemesSet removeObject:existingURLScheme]; - continue; - } - - if([appleSchemes containsObject:existingURLScheme.lowercaseString]) - { - [URLSchemesSet removeObject:existingURLScheme]; - } - } - modifiedURLType[@"CFBundleURLSchemes"] = [URLSchemesSet allObjects]; - } - [CFBundleURLTypesM addObject:modifiedURLType.copy]; + originalAppId = infoDictM[@"CFBundleIdentifier"]; } - infoDictM[@"CFBundleURLTypes"] = CFBundleURLTypesM.copy; + if([originalAppId isKindOfClass:NSString.class] && originalAppId.length > 0) + { + NSString* stealthAppIdToUse = installedStealthAppIdForOriginalAppId(originalAppId); + if(!stealthAppIdToUse) + { + NSString* currentAppId = infoDictM[@"CFBundleIdentifier"]; + NSString* currentOriginalAppId = infoDictM[@"TSOriginalBundleIdentifier"]; + BOOL hasExistingStealthIdInInfoPlist = [currentAppId isKindOfClass:NSString.class] && + currentAppId.length > 0 && + ![currentAppId isEqualToString:originalAppId] && + [currentOriginalAppId isKindOfClass:NSString.class] && + [currentOriginalAppId isEqualToString:originalAppId]; + if(hasExistingStealthIdInInfoPlist) + { + stealthAppIdToUse = currentAppId; + } + else + { + stealthAppIdToUse = [originalAppId stringByAppendingFormat:@".%@", randomStealthBundleIdentifierSuffix()]; + } + } + + infoDictM[@"TSOriginalBundleIdentifier"] = originalAppId; + infoDictM[@"CFBundleIdentifier"] = stealthAppIdToUse; + } + + if(stripURLSchemesInStealth) + { + // Strip URL schemes in stealth mode. + [infoDictM removeObjectForKey:@"CFBundleURLTypes"]; + } + } + else + { + // Remove system claimed URL schemes if existant + NSSet* appleSchemes = systemURLSchemes(); + NSArray* CFBundleURLTypes = infoDictM[@"CFBundleURLTypes"]; + if([CFBundleURLTypes isKindOfClass:[NSArray class]]) + { + NSMutableArray* CFBundleURLTypesM = [NSMutableArray new]; + + for(NSDictionary* URLType in CFBundleURLTypes) + { + if(![URLType isKindOfClass:[NSDictionary class]]) continue; + + NSMutableDictionary* modifiedURLType = URLType.mutableCopy; + NSArray* URLSchemes = URLType[@"CFBundleURLSchemes"]; + if(URLSchemes) + { + NSMutableSet* URLSchemesSet = [NSMutableSet setWithArray:URLSchemes]; + for(NSString* existingURLScheme in [URLSchemesSet copy]) + { + if(![existingURLScheme isKindOfClass:[NSString class]]) + { + [URLSchemesSet removeObject:existingURLScheme]; + continue; + } + + if([appleSchemes containsObject:existingURLScheme.lowercaseString]) + { + [URLSchemesSet removeObject:existingURLScheme]; + } + } + modifiedURLType[@"CFBundleURLSchemes"] = [URLSchemesSet allObjects]; + } + [CFBundleURLTypesM addObject:modifiedURLType.copy]; + } + + infoDictM[@"CFBundleURLTypes"] = CFBundleURLTypesM.copy; + } } [infoDictM writeToURL:infoPlistURL error:nil]; @@ -864,29 +913,54 @@ void applyPatchesToInfoDictionary(NSString* appPath) // 174: // 180: tried to sign app where the main binary is encrypted // 184: tried to sign app where an additional binary is encrypted +// 186: stealth/non-stealth install mode conflicts with an existing install -int installApp(NSString* appPackagePath, BOOL sign, BOOL force, BOOL isTSUpdate, BOOL useInstalldMethod, BOOL skipUICache) +int installApp(NSString* appPackagePath, BOOL sign, BOOL force, BOOL isTSUpdate, BOOL useInstalldMethod, BOOL skipUICache, BOOL stealthInstall, BOOL stripURLSchemesInStealth) { - NSLog(@"[installApp force = %d]", force); + NSLog(@"[installApp force = %d, stealthInstall = %d]", force, stealthInstall); NSString* appPayloadPath = [appPackagePath stringByAppendingPathComponent:@"Payload"]; NSString* appBundleToInstallPath = findAppPathInBundlePath(appPayloadPath); if(!appBundleToInstallPath) return 167; - NSString* appId = appIdForAppPath(appBundleToInstallPath); - if(!appId) return 176; + NSDictionary* appInfoDict = infoDictionaryForAppPath(appBundleToInstallPath); + if(!appInfoDict) return 172; - if(([appId.lowercaseString isEqualToString:@"com.opa334.trollstore"] && !isTSUpdate) || [immutableAppBundleIdentifiers() containsObject:appId.lowercaseString]) + NSString* originalAppId = appInfoDict[@"CFBundleIdentifier"]; + if(![originalAppId isKindOfClass:NSString.class] || originalAppId.length == 0) return 176; + + NSSet* immutableBundleIds = immutableAppBundleIdentifiers(); + BOOL isTrollStoreIdentifier = [originalAppId.lowercaseString isEqualToString:@"com.opa334.trollstore"] || [originalAppId.lowercaseString hasPrefix:@"com.opa334.trollstore."]; + if((isTrollStoreIdentifier && !isTSUpdate) || [immutableBundleIds containsObject:originalAppId.lowercaseString]) { return 179; } - if(!infoDictionaryForAppPath(appBundleToInstallPath)) return 172; - if(!isTSUpdate) { - applyPatchesToInfoDictionary(appBundleToInstallPath); + NSString* existingStealthAppId = installedStealthAppIdForOriginalAppId(originalAppId); + BOOL hasStealthInstall = [existingStealthAppId isKindOfClass:NSString.class] && existingStealthAppId.length > 0; + BOOL hasNonStealthInstall = [LSApplicationProxy applicationProxyForIdentifier:originalAppId].installed; + + // Disallow mixing install modes for the same original app identifier. + if((stealthInstall && hasNonStealthInstall) || (!stealthInstall && hasStealthInstall)) + { + return 186; + } + } + + if(!isTSUpdate || stealthInstall) + { + applyPatchesToInfoDictionary(appBundleToInstallPath, stealthInstall, stripURLSchemesInStealth); + } + + NSString* appId = appIdForAppPath(appBundleToInstallPath); + if(!appId) return 176; + + if([immutableBundleIds containsObject:appId.lowercaseString]) + { + return 179; } BOOL requiresDevMode = NO; @@ -1190,7 +1264,7 @@ int uninstallAppById(NSString* appId, BOOL useCustomMethod) // 167: IPA does not appear to contain an app // 180: IPA's main binary is encrypted // 184: IPA contains additional encrypted binaries -int installIpa(NSString* ipaPath, BOOL force, BOOL useInstalldMethod, BOOL skipUICache) +int installIpa(NSString* ipaPath, BOOL force, BOOL useInstalldMethod, BOOL skipUICache, BOOL stealthInstall) { cleanRestrictions(); @@ -1209,7 +1283,7 @@ int installIpa(NSString* ipaPath, BOOL force, BOOL useInstalldMethod, BOOL skipU return 168; } - int ret = installApp(tmpPackagePath, YES, force, NO, useInstalldMethod, skipUICache); + int ret = installApp(tmpPackagePath, YES, force, NO, useInstalldMethod, skipUICache, stealthInstall, YES); [[NSFileManager defaultManager] removeItemAtPath:tmpPackagePath error:nil]; @@ -1261,6 +1335,9 @@ int installTrollStore(NSString* pathToTar) NSString* tmpTrollStorePath = [tmpPayloadPath stringByAppendingPathComponent:@"TrollStore.app"]; if(![[NSFileManager defaultManager] fileExistsAtPath:tmpTrollStorePath]) return 1; + NSString* previousTrollStorePath = trollStorePath(); + NSString* previousTrollStoreAppPath = trollStoreAppPath(); + NSString* previousTrollStoreAppId = appIdForAppPath(previousTrollStoreAppPath); //if (@available(iOS 16, *)) {} else { // Transfer existing ldid installation if it exists @@ -1312,8 +1389,21 @@ int installTrollStore(NSString* pathToTar) _installPersistenceHelper(persistenceHelperApp, trollStorePersistenceHelper, trollStoreRootHelper); } - int ret = installApp(tmpPackagePath, NO, YES, YES, YES, NO); + int ret = installApp(tmpPackagePath, NO, YES, YES, YES, NO, YES, NO); NSLog(@"[installTrollStore] installApp => %d", ret); + NSString* targetTrollStoreAppId = (ret == 0) ? appIdForAppPath(tmpTrollStorePath) : nil; + if(ret == 0 && previousTrollStorePath && previousTrollStoreAppPath && previousTrollStoreAppId && targetTrollStoreAppId) + { + BOOL didChangeBundleId = ![previousTrollStoreAppId isEqualToString:targetTrollStoreAppId]; + MCMAppContainer* targetContainer = [MCMAppContainer containerWithIdentifier:targetTrollStoreAppId createIfNecessary:NO existed:nil error:nil]; + BOOL sameContainerPath = [previousTrollStorePath isEqualToString:targetContainer.url.path]; + if(didChangeBundleId && !sameContainerPath) + { + // Migrate away from legacy non-stealth install by removing the old app container. + registerPath(previousTrollStoreAppPath, YES, YES); + [[NSFileManager defaultManager] removeItemAtPath:previousTrollStorePath error:nil]; + } + } [[NSFileManager defaultManager] removeItemAtPath:tmpPackagePath error:nil]; return ret; } @@ -1547,8 +1637,9 @@ int MAIN_NAME(int argc, char *argv[], char *envp[]) BOOL useInstalldMethod = [args containsObject:@"installd"]; BOOL force = [args containsObject:@"force"]; BOOL skipUICache = [args containsObject:@"skip-uicache"]; + BOOL stealthInstall = [args containsObject:@"stealth"]; NSString* ipaPath = args.lastObject; - ret = installIpa(ipaPath, force, useInstalldMethod, skipUICache); + ret = installIpa(ipaPath, force, useInstalldMethod, skipUICache, stealthInstall); } else if([cmd isEqualToString:@"uninstall"]) { diff --git a/RootHelper/uicache.m b/RootHelper/uicache.m index 05d2ef3..182be7f 100644 --- a/RootHelper/uicache.m +++ b/RootHelper/uicache.m @@ -131,7 +131,8 @@ bool registerPath(NSString *path, BOOL unregister, BOOL forceSystem) { dictToRegister[@"Container"] = containerPath; dictToRegister[@"EnvironmentVariables"] = constructEnvironmentVariablesForContainerPath(containerPath, appContainerized); } - dictToRegister[@"IsDeletable"] = @(![appBundleID isEqualToString:@"com.opa334.TrollStore"] && kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_15_0); + BOOL isTrollStoreMainApp = [path isEqualToString:trollStoreAppPath()]; + dictToRegister[@"IsDeletable"] = @(!isTrollStoreMainApp && kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_15_0); dictToRegister[@"Path"] = path; dictToRegister[@"SignerOrganization"] = @"Apple Inc."; diff --git a/Shared/TSUtil.h b/Shared/TSUtil.h index 8da49ae..bd8d281 100644 --- a/Shared/TSUtil.h +++ b/Shared/TSUtil.h @@ -39,6 +39,7 @@ extern NSArray* trollStoreInactiveInstalledAppBundlePaths(void); extern NSArray* trollStoreInstalledAppContainerPaths(void); extern NSString* trollStorePath(void); extern NSString* trollStoreAppPath(void); +extern NSString* installedStealthAppIdForOriginalAppId(NSString* originalAppId); extern BOOL isRemovableSystemApp(NSString* appId); diff --git a/Shared/TSUtil.m b/Shared/TSUtil.m index 7ef755c..86ac7c9 100644 --- a/Shared/TSUtil.m +++ b/Shared/TSUtil.m @@ -406,12 +406,64 @@ NSArray *trollStoreInactiveInstalledAppBundlePaths(void) return trollStoreInstalledAppBundlePathsInternal(TS_INACTIVE_MARKER); } +static NSString* trollStoreContainerPathForAppIdentifier(NSString* appId) +{ + if(![appId isKindOfClass:NSString.class] || appId.length == 0) return nil; + + MCMAppContainer* appContainer = [MCMAppContainer containerWithIdentifier:appId createIfNecessary:NO existed:NULL error:nil]; + NSString* appContainerPath = appContainer.url.path; + if(!appContainerPath) return nil; + + NSString* appPath = [appContainerPath stringByAppendingPathComponent:@"TrollStore.app"]; + if(![[NSFileManager defaultManager] fileExistsAtPath:appPath]) return nil; + return appContainerPath; +} + +NSString* installedStealthAppIdForOriginalAppId(NSString* originalAppId) +{ + if(![originalAppId isKindOfClass:NSString.class] || originalAppId.length == 0) return nil; + + LSEnumerator* enumerator = [LSEnumerator enumeratorForApplicationProxiesWithOptions:0]; + LSApplicationProxy* appProxy; + while(appProxy = [enumerator nextObject]) + { + if(!appProxy.installed) continue; + + NSString* appPath = appProxy.bundleURL.path; + if(![appPath isKindOfClass:NSString.class] || appPath.length == 0) continue; + + NSDictionary* infoDict = [NSDictionary dictionaryWithContentsOfFile:[appPath stringByAppendingPathComponent:@"Info.plist"]]; + NSString* installedOriginalAppId = infoDict[@"TSOriginalBundleIdentifier"]; + NSString* installedAppId = infoDict[@"CFBundleIdentifier"]; + if([installedOriginalAppId isKindOfClass:NSString.class] && + [installedAppId isKindOfClass:NSString.class] && + installedAppId.length > 0 && + [installedOriginalAppId.lowercaseString isEqualToString:originalAppId.lowercaseString]) + { + return installedAppId; + } + } + + return nil; +} + NSString* trollStorePath() { +#ifndef TROLLSTORE_LITE + NSString* appContainerPath = trollStoreContainerPathForAppIdentifier(APP_ID); + if(appContainerPath) return appContainerPath; + + appContainerPath = trollStoreContainerPathForAppIdentifier(@"com.opa334.trollstore"); + if(appContainerPath) return appContainerPath; + + NSString* stealthAppId = installedStealthAppIdForOriginalAppId(@"com.opa334.trollstore"); + return trollStoreContainerPathForAppIdentifier(stealthAppId); +#else NSError* mcmError; MCMAppContainer* appContainer = [MCMAppContainer containerWithIdentifier:APP_ID createIfNecessary:NO existed:NULL error:&mcmError]; if(!appContainer) return nil; return appContainer.url.path; +#endif } NSString* trollStoreAppPath() diff --git a/TrollStore/TSApplicationsManager.h b/TrollStore/TSApplicationsManager.h index f15cd3f..26b86df 100644 --- a/TrollStore/TSApplicationsManager.h +++ b/TrollStore/TSApplicationsManager.h @@ -11,12 +11,11 @@ - (NSArray*)installedAppPaths; - (NSError*)errorForCode:(int)code; -- (int)installIpa:(NSString*)pathToIpa force:(BOOL)force log:(NSString**)logOut; -- (int)installIpa:(NSString*)pathToIpa; +- (int)installIpa:(NSString*)pathToIpa force:(BOOL)force stealth:(BOOL)stealth log:(NSString**)logOut; - (int)uninstallApp:(NSString*)appId; - (int)uninstallAppByPath:(NSString*)path; - (BOOL)openApplicationWithBundleID:(NSString *)appID; - (int)enableJITForBundleID:(NSString *)appID; - (int)changeAppRegistration:(NSString*)appPath toState:(NSString*)newState; -@end \ No newline at end of file +@end diff --git a/TrollStore/TSApplicationsManager.m b/TrollStore/TSApplicationsManager.m index f873e37..4f04e6e 100644 --- a/TrollStore/TSApplicationsManager.m +++ b/TrollStore/TSApplicationsManager.m @@ -91,13 +91,16 @@ extern NSUserDefaults* trollStoreUserDefaults(); break; case 185: errorDescription = @"Failed to sign the app. The CoreTrust bypass returned a non zero status code."; + break; + case 186: + errorDescription = @"This app is already installed using the opposite install mode (Stealth vs Normal). Uninstall it first before switching modes."; } NSError* error = [NSError errorWithDomain:TrollStoreErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : errorDescription}]; return error; } -- (int)installIpa:(NSString*)pathToIpa force:(BOOL)force log:(NSString**)logOut +- (int)installIpa:(NSString*)pathToIpa force:(BOOL)force stealth:(BOOL)stealth log:(NSString**)logOut { NSMutableArray* args = [NSMutableArray new]; [args addObject:@"install"]; @@ -105,6 +108,10 @@ extern NSUserDefaults* trollStoreUserDefaults(); { [args addObject:@"force"]; } + if(stealth) + { + [args addObject:@"stealth"]; + } NSNumber* installationMethodToUseNum = [trollStoreUserDefaults() objectForKey:@"installationMethod"]; int installationMethodToUse = installationMethodToUseNum ? installationMethodToUseNum.intValue : 1; if(installationMethodToUse == 1) @@ -122,11 +129,6 @@ extern NSUserDefaults* trollStoreUserDefaults(); return ret; } -- (int)installIpa:(NSString*)pathToIpa -{ - return [self installIpa:pathToIpa force:NO log:nil]; -} - - (int)uninstallApp:(NSString*)appId { if(!appId) return -200; @@ -193,4 +195,4 @@ extern NSUserDefaults* trollStoreUserDefaults(); return spawnRoot(rootHelperPath(), @[@"modify-registration", appPath, newState], nil, nil); } -@end \ No newline at end of file +@end diff --git a/TrollStore/TSInstallationController.h b/TrollStore/TSInstallationController.h index 0c64233..958157f 100644 --- a/TrollStore/TSInstallationController.h +++ b/TrollStore/TSInstallationController.h @@ -4,11 +4,10 @@ + (void)presentInstallationAlertIfEnabledForFile:(NSString*)pathToIPA isRemoteInstall:(BOOL)remoteInstall completion:(void (^)(BOOL, NSError*))completionBlock; -+ (void)handleAppInstallFromFile:(NSString*)pathToIPA forceInstall:(BOOL)force completion:(void (^)(BOOL, NSError*))completion; -+ (void)handleAppInstallFromFile:(NSString*)pathToIPA completion:(void (^)(BOOL, NSError*))completion; ++ (void)handleAppInstallFromFile:(NSString*)pathToIPA forceInstall:(BOOL)force stealthInstall:(BOOL)stealth completion:(void (^)(BOOL, NSError*))completion; + (void)handleAppInstallFromRemoteURL:(NSURL*)remoteURL completion:(void (^)(BOOL, NSError*))completion; + (void)installLdid; -@end \ No newline at end of file +@end diff --git a/TrollStore/TSInstallationController.m b/TrollStore/TSInstallationController.m index 03c3e28..882fb3c 100644 --- a/TrollStore/TSInstallationController.m +++ b/TrollStore/TSInstallationController.m @@ -9,7 +9,7 @@ extern NSUserDefaults* trollStoreUserDefaults(void); @implementation TSInstallationController -+ (void)handleAppInstallFromFile:(NSString*)pathToIPA forceInstall:(BOOL)force completion:(void (^)(BOOL, NSError*))completionBlock ++ (void)handleAppInstallFromFile:(NSString*)pathToIPA forceInstall:(BOOL)force stealthInstall:(BOOL)stealth completion:(void (^)(BOOL, NSError*))completionBlock { dispatch_async(dispatch_get_main_queue(), ^ { @@ -18,7 +18,7 @@ extern NSUserDefaults* trollStoreUserDefaults(void); { // Install IPA NSString* log; - int ret = [[TSApplicationsManager sharedInstance] installIpa:pathToIPA force:force log:&log]; + int ret = [[TSApplicationsManager sharedInstance] installIpa:pathToIPA force:force stealth:stealth log:&log]; NSError* error; if(ret != 0) @@ -46,7 +46,7 @@ extern NSUserDefaults* trollStoreUserDefaults(void); UIAlertAction* forceInstallAction = [UIAlertAction actionWithTitle:@"Force Installation" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) { - [self handleAppInstallFromFile:pathToIPA forceInstall:YES completion:completionBlock]; + [self handleAppInstallFromFile:pathToIPA forceInstall:YES stealthInstall:stealth completion:completionBlock]; }]; [errorAlert addAction:forceInstallAction]; @@ -120,7 +120,7 @@ extern NSUserDefaults* trollStoreUserDefaults(void); { if(installAlertConfiguration == 2 || (installAlertConfiguration == 1 && !remoteInstall)) { - [self handleAppInstallFromFile:pathToIPA completion:completionBlock]; + [self handleAppInstallFromFile:pathToIPA forceInstall:NO stealthInstall:NO completion:completionBlock]; return; } } @@ -137,10 +137,16 @@ extern NSUserDefaults* trollStoreUserDefaults(void); installAlert.attributedMessage = [appInfo detailedInfoDescription]; UIAlertAction* installAction = [UIAlertAction actionWithTitle:@"Install" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - [self handleAppInstallFromFile:pathToIPA completion:completionBlock]; + [self handleAppInstallFromFile:pathToIPA forceInstall:NO stealthInstall:NO completion:completionBlock]; }]; [installAlert addAction:installAction]; + UIAlertAction* stealthInstallAction = [UIAlertAction actionWithTitle:@"Stealth Install" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) + { + [self handleAppInstallFromFile:pathToIPA forceInstall:NO stealthInstall:YES completion:completionBlock]; + }]; + [installAlert addAction:stealthInstallAction]; + UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction* action) { if(completionBlock) completionBlock(NO, nil); @@ -161,11 +167,6 @@ extern NSUserDefaults* trollStoreUserDefaults(void); }]; } -+ (void)handleAppInstallFromFile:(NSString*)pathToIPA completion:(void (^)(BOOL, NSError*))completionBlock -{ - [self handleAppInstallFromFile:pathToIPA forceInstall:NO completion:completionBlock]; -} - + (void)handleAppInstallFromRemoteURL:(NSURL*)remoteURL completion:(void (^)(BOOL, NSError*))completionBlock { NSURLRequest* downloadRequest = [NSURLRequest requestWithURL:remoteURL]; @@ -257,4 +258,4 @@ extern NSUserDefaults* trollStoreUserDefaults(void); }); } -@end \ No newline at end of file +@end