1// Copyright 2020-present 650 Industries. All rights reserved. 2 3#import "EXAppFetcher.h" 4#import "EXAppLoaderExpoUpdates.h" 5#import "EXClientReleaseType.h" 6#import "EXEnvironment.h" 7#import "EXErrorRecoveryManager.h" 8#import "EXFileDownloader.h" 9#import "EXKernel.h" 10#import "EXKernelLinkingManager.h" 11#import "EXManifestResource.h" 12#import "EXSession.h" 13#import "EXUpdatesDatabaseManager.h" 14#import "EXVersions.h" 15 16#if defined(EX_DETACHED) 17#import "ExpoKit-Swift.h" 18#else 19#import "Expo_Go-Swift.h" 20#endif // defined(EX_DETACHED) 21 22#import <React/RCTUtils.h> 23#import <sys/utsname.h> 24 25@import EXManifests; 26@import EXUpdates; 27 28NS_ASSUME_NONNULL_BEGIN 29 30@interface EXAppLoaderExpoUpdates () 31 32@property (nonatomic, strong, nullable) NSURL *manifestUrl; 33@property (nonatomic, strong, nullable) NSURL *httpManifestUrl; 34 35@property (nonatomic, strong, nullable) EXManifestsManifest *confirmedManifest; 36@property (nonatomic, strong, nullable) EXManifestsManifest *optimisticManifest; 37@property (nonatomic, strong, nullable) NSData *bundle; 38@property (nonatomic, assign) EXAppLoaderRemoteUpdateStatus remoteUpdateStatus; 39@property (nonatomic, assign) BOOL shouldShowRemoteUpdateStatus; 40@property (nonatomic, assign) BOOL isUpToDate; 41 42/** 43 * Stateful variable to let us prevent multiple simultaneous fetches from the development server. 44 * This can happen when reloading a bundle with remote debugging enabled; 45 * RN requests the bundle multiple times for some reason. 46 */ 47@property (nonatomic, assign) BOOL isLoadingDevelopmentJavaScriptResource; 48 49@property (nonatomic, strong, nullable) NSError *error; 50 51@property (nonatomic, assign) BOOL shouldUseCacheOnly; 52 53@property (nonatomic, strong) dispatch_queue_t appLoaderQueue; 54 55@property (nonatomic, nullable) EXUpdatesConfig *config; 56@property (nonatomic, nullable) EXUpdatesSelectionPolicy *selectionPolicy; 57@property (nonatomic, nullable) id<EXUpdatesAppLauncher> appLauncher; 58@property (nonatomic, assign) BOOL isEmergencyLaunch; 59 60@end 61 62/** 63 * Entry point to expo-updates in Expo Go and legacy standalone builds. Fulfills many of the 64 * purposes of EXUpdatesAppController along with serving as an interface to the rest of the ExpoKit 65 * kernel. 66 * 67 * Dynamically generates a configuration object with the correct scope key, and then, like 68 * EXUpdatesAppController, delegates to an instance of EXUpdatesAppLoaderTask to start the process 69 * of loading and launching an update, and responds appropriately depending on the callbacks that 70 * are invoked. 71 * 72 * Multiple instances of EXAppLoaderExpoUpdates can exist at a time; instances are retained by 73 * EXKernelAppRegistry (through EXKernelAppRecord). 74 */ 75@implementation EXAppLoaderExpoUpdates 76 77@synthesize manifestUrl = _manifestUrl; 78@synthesize bundle = _bundle; 79@synthesize remoteUpdateStatus = _remoteUpdateStatus; 80@synthesize shouldShowRemoteUpdateStatus = _shouldShowRemoteUpdateStatus; 81@synthesize config = _config; 82@synthesize selectionPolicy = _selectionPolicy; 83@synthesize appLauncher = _appLauncher; 84@synthesize isEmergencyLaunch = _isEmergencyLaunch; 85@synthesize isUpToDate = _isUpToDate; 86 87- (instancetype)initWithManifestUrl:(NSURL *)url 88{ 89 if (self = [super init]) { 90 _manifestUrl = url; 91 _httpManifestUrl = [EXAppLoaderExpoUpdates _httpUrlFromManifestUrl:_manifestUrl]; 92 _appLoaderQueue = dispatch_queue_create("host.exp.exponent.LoaderQueue", DISPATCH_QUEUE_SERIAL); 93 } 94 return self; 95} 96 97#pragma mark - getters and lifecycle 98 99- (void)_reset 100{ 101 _confirmedManifest = nil; 102 _optimisticManifest = nil; 103 _bundle = nil; 104 _config = nil; 105 _selectionPolicy = nil; 106 _appLauncher = nil; 107 _error = nil; 108 _shouldUseCacheOnly = NO; 109 _isEmergencyLaunch = NO; 110 _remoteUpdateStatus = kEXAppLoaderRemoteUpdateStatusChecking; 111 _shouldShowRemoteUpdateStatus = YES; 112 _isUpToDate = NO; 113 _isLoadingDevelopmentJavaScriptResource = NO; 114} 115 116- (EXAppLoaderStatus)status 117{ 118 if (_error) { 119 return kEXAppLoaderStatusError; 120 } else if (_bundle) { 121 return kEXAppLoaderStatusHasManifestAndBundle; 122 } else if (_optimisticManifest) { 123 return kEXAppLoaderStatusHasManifest; 124 } 125 return kEXAppLoaderStatusNew; 126} 127 128- (nullable EXManifestsManifest *)manifest 129{ 130 if (_confirmedManifest) { 131 return _confirmedManifest; 132 } 133 if (_optimisticManifest) { 134 return _optimisticManifest; 135 } 136 return nil; 137} 138 139- (nullable NSData *)bundle 140{ 141 if (_bundle) { 142 return _bundle; 143 } 144 return nil; 145} 146 147- (void)forceBundleReload 148{ 149 if (self.status == kEXAppLoaderStatusNew) { 150 @throw [NSException exceptionWithName:NSInternalInconsistencyException 151 reason:@"Tried to load a bundle from an AppLoader with no manifest." 152 userInfo:@{}]; 153 } 154 NSAssert([self supportsBundleReload], @"Tried to force a bundle reload on a non-development bundle"); 155 if (self.isLoadingDevelopmentJavaScriptResource) { 156 // prevent multiple simultaneous fetches from the development server. 157 // this can happen when reloading a bundle with remote debugging enabled; 158 // RN requests the bundle multiple times for some reason. 159 // TODO: fix inside of RN 160 return; 161 } 162 [self _loadDevelopmentJavaScriptResource]; 163} 164 165- (BOOL)supportsBundleReload 166{ 167 if (_optimisticManifest) { 168 return _optimisticManifest.isUsingDeveloperTool; 169 } 170 return NO; 171} 172 173#pragma mark - public 174 175- (void)request 176{ 177 [self _reset]; 178 if (_manifestUrl) { 179 [self _beginRequest]; 180 } 181} 182 183- (void)requestFromCache 184{ 185 [self _reset]; 186 _shouldUseCacheOnly = YES; 187 if (_manifestUrl) { 188 [self _beginRequest]; 189 } 190} 191 192#pragma mark - EXUpdatesAppLoaderTaskDelegate 193 194// Implement empty stubs 195- (void)didStartCheckingForRemoteUpdate {} 196- (void)didFinishCheckingForRemoteUpdate:(NSDictionary<NSString *,id> *)body {} 197- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)_ didLoadAsset:(EXUpdatesAsset *)asset successfulAssetCount:(NSInteger)successfulAssetCount failedAssetCount:(NSInteger)failedAssetCount totalAssetCount:(NSInteger)totalAssetCount {} 198 199- (BOOL)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didLoadCachedUpdate:(EXUpdatesUpdate *)update 200{ 201 [self _setShouldShowRemoteUpdateStatus:update.manifest]; 202 // if cached manifest was dev mode, or a previous run of this app failed due to a loading error, we want to make sure to check for remote updates 203 if (update.manifest.isUsingDeveloperTool || [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager scopeKeyIsRecoveringFromError:update.manifest.scopeKey]) { 204 return NO; 205 } 206 return YES; 207} 208 209- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didStartLoadingUpdate:(nullable EXUpdatesUpdate *)update 210{ 211 // expo-cli does not always respect our SDK version headers and respond with a compatible update or an error 212 // so we need to check the compatibility here 213 EXManifestResource *manifestResource = [[EXManifestResource alloc] initWithManifestUrl:_httpManifestUrl originalUrl:_manifestUrl]; 214 NSError *manifestCompatibilityError = [manifestResource verifyManifestSdkVersion:update.manifest]; 215 if (manifestCompatibilityError) { 216 _error = manifestCompatibilityError; 217 if (self.delegate) { 218 [self.delegate appLoader:self didFailWithError:_error]; 219 return; 220 } 221 } 222 223 _remoteUpdateStatus = kEXAppLoaderRemoteUpdateStatusDownloading; 224 [self _setShouldShowRemoteUpdateStatus:update.manifest]; 225 EXManifestsManifest *processedManifest = [self _processManifest:update.manifest]; 226 if (processedManifest == nil) { 227 return; 228 } 229 [self _setOptimisticManifest:processedManifest]; 230} 231 232- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id<EXUpdatesAppLauncher>)launcher isUpToDate:(BOOL)isUpToDate 233{ 234 if (_error) { 235 return; 236 } 237 238 if (!_optimisticManifest) { 239 EXManifestsManifest *processedManifest = [self _processManifest:launcher.launchedUpdate.manifest]; 240 if (processedManifest == nil) { 241 return; 242 } 243 [self _setOptimisticManifest:processedManifest]; 244 } 245 _isUpToDate = isUpToDate; 246 if (launcher.launchedUpdate.manifest.isUsingDeveloperTool) { 247 // in dev mode, we need to set an optimistic manifest but nothing else 248 return; 249 } 250 _confirmedManifest = [self _processManifest:launcher.launchedUpdate.manifest]; 251 if (_confirmedManifest == nil) { 252 return; 253 } 254 _bundle = [NSData dataWithContentsOfURL:launcher.launchAssetUrl]; 255 _appLauncher = launcher; 256 if (self.delegate) { 257 [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle]; 258 } 259} 260 261- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error 262{ 263 if ([EXEnvironment sharedEnvironment].isDetached) { 264 _isEmergencyLaunch = YES; 265 [self _launchWithNoDatabaseAndError:error]; 266 } else if (!_error) { 267 _error = error; 268 269 // if the error payload conforms to the error protocol, we can parse it and display 270 // a slightly nicer error message to the user 271 id errorJson = [NSJSONSerialization JSONObjectWithData:[error.localizedDescription dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; 272 if (errorJson && [errorJson isKindOfClass:[NSDictionary class]]) { 273 EXManifestResource *manifestResource = [[EXManifestResource alloc] initWithManifestUrl:_httpManifestUrl originalUrl:_manifestUrl]; 274 _error = [manifestResource formatError:[NSError errorWithDomain:EXNetworkErrorDomain code:error.code userInfo:errorJson]]; 275 } 276 277 if (self.delegate) { 278 [self.delegate appLoader:self didFailWithError:_error]; 279 } 280 } 281} 282 283- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishBackgroundUpdateWithStatus:(EXUpdatesBackgroundUpdateStatus)status update:(nullable EXUpdatesUpdate *)update error:(nullable NSError *)error 284{ 285 if (self.delegate) { 286 [self.delegate appLoader:self didResolveUpdatedBundleWithManifest:update.manifest isFromCache:(status == EXUpdatesBackgroundUpdateStatusNoUpdateAvailable) error:error]; 287 } 288} 289 290#pragma mark - internal 291 292+ (NSURL *)_httpUrlFromManifestUrl:(NSURL *)url 293{ 294 NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES]; 295 // if scheme is exps or https, use https. Else default to http 296 if (components.scheme && ([components.scheme isEqualToString:@"exps"] || [components.scheme isEqualToString:@"https"])){ 297 components.scheme = @"https"; 298 } else { 299 components.scheme = @"http"; 300 } 301 NSMutableString *path = [((components.path) ? components.path : @"") mutableCopy]; 302 path = [[EXKernelLinkingManager stringByRemovingDeepLink:path] mutableCopy]; 303 components.path = path; 304 return [components URL]; 305} 306 307- (BOOL)_initializeDatabase 308{ 309 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 310 BOOL success = updatesDatabaseManager.isDatabaseOpen; 311 if (!updatesDatabaseManager.isDatabaseOpen) { 312 success = [updatesDatabaseManager openDatabase]; 313 } 314 315 if (!success) { 316 _error = updatesDatabaseManager.error; 317 if (self.delegate) { 318 [self.delegate appLoader:self didFailWithError:_error]; 319 } 320 return NO; 321 } else { 322 return YES; 323 } 324} 325 326- (void)_beginRequest 327{ 328 if (![self _initializeDatabase]) { 329 return; 330 } 331 [self _startLoaderTask]; 332} 333 334- (void)_startLoaderTask 335{ 336 BOOL shouldCheckOnLaunch; 337 NSNumber *launchWaitMs; 338 if (_shouldUseCacheOnly) { 339 shouldCheckOnLaunch = NO; 340 launchWaitMs = @(0); 341 } else { 342 if ([EXEnvironment sharedEnvironment].isDetached) { 343 shouldCheckOnLaunch = [EXEnvironment sharedEnvironment].updatesCheckAutomatically; 344 launchWaitMs = [EXEnvironment sharedEnvironment].updatesFallbackToCacheTimeout; 345 } else { 346 shouldCheckOnLaunch = YES; 347 launchWaitMs = @(60000); 348 } 349 } 350 351 NSURL *httpManifestUrl = [[self class] _httpUrlFromManifestUrl:_manifestUrl]; 352 353 NSString *releaseChannel = [EXEnvironment sharedEnvironment].releaseChannel; 354 if (![EXEnvironment sharedEnvironment].isDetached) { 355 // in Expo Go, the release channel can change at runtime depending on the URL we load 356 NSURLComponents *manifestUrlComponents = [NSURLComponents componentsWithURL:httpManifestUrl resolvingAgainstBaseURL:YES]; 357 releaseChannel = [EXKernelLinkingManager releaseChannelWithUrlComponents:manifestUrlComponents]; 358 } 359 360 NSMutableDictionary *updatesConfig = [[NSMutableDictionary alloc] initWithDictionary:@{ 361 EXUpdatesConfig.EXUpdatesConfigUpdateUrlKey: httpManifestUrl.absoluteString, 362 EXUpdatesConfig.EXUpdatesConfigSDKVersionKey: [self _sdkVersions], 363 EXUpdatesConfig.EXUpdatesConfigScopeKeyKey: httpManifestUrl.absoluteString, 364 EXUpdatesConfig.EXUpdatesConfigReleaseChannelKey: releaseChannel, 365 EXUpdatesConfig.EXUpdatesConfigHasEmbeddedUpdateKey: @([EXEnvironment sharedEnvironment].isDetached), 366 EXUpdatesConfig.EXUpdatesConfigEnabledKey: @([EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled), 367 EXUpdatesConfig.EXUpdatesConfigLaunchWaitMsKey: launchWaitMs, 368 EXUpdatesConfig.EXUpdatesConfigCheckOnLaunchKey: shouldCheckOnLaunch ? EXUpdatesConfig.EXUpdatesConfigCheckOnLaunchValueAlways : EXUpdatesConfig.EXUpdatesConfigCheckOnLaunchValueNever, 369 EXUpdatesConfig.EXUpdatesConfigExpectsSignedManifestKey: @YES, 370 EXUpdatesConfig.EXUpdatesConfigRequestHeadersKey: [self _requestHeaders] 371 }]; 372 373 if (!EXEnvironment.sharedEnvironment.isDetached) { 374 // in Expo Go, embed the Expo Root Certificate and get the Expo Go intermediate certificate and development certificates 375 // from the multipart manifest response part 376 377 NSString *expoRootCertPath = [[NSBundle mainBundle] pathForResource:@"expo-root" ofType:@"pem"]; 378 if (!expoRootCertPath) { 379 @throw [NSException exceptionWithName:NSInternalInconsistencyException 380 reason:@"No expo-root certificate found in bundle" 381 userInfo:@{}]; 382 } 383 384 NSError *error; 385 NSString *expoRootCert = [NSString stringWithContentsOfFile:expoRootCertPath encoding:NSUTF8StringEncoding error:&error]; 386 if (error) { 387 expoRootCert = nil; 388 } 389 if (!expoRootCert) { 390 @throw [NSException exceptionWithName:NSInternalInconsistencyException 391 reason:@"Error reading expo-root certificate from bundle" 392 userInfo:@{ @"underlyingError": error.localizedDescription }]; 393 } 394 395 updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningCertificateKey] = expoRootCert; 396 updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningMetadataKey] = @{ 397 @"keyid": @"expo-root", 398 @"alg": @"rsa-v1_5-sha256", 399 }; 400 updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningIncludeManifestResponseCertificateChainKey] = @YES; 401 updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningAllowUnsignedManifestsKey] = @YES; 402 403 // in Expo Go, ignore directives in manifest responses and require a manifest. the current directives 404 // (no update available, roll back) don't have any practical use outside of standalone apps 405 updatesConfig[EXUpdatesConfig.EXUpdatesConfigEnableExpoUpdatesProtocolV0CompatibilityModeKey] = @YES; 406 } 407 408 _config = [EXUpdatesConfig configFromDictionary:updatesConfig]; 409 410 if (![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) { 411 [self _launchWithNoDatabaseAndError:nil]; 412 return; 413 } 414 415 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 416 417 NSMutableArray *sdkVersions = [[EXVersions sharedInstance].versions[@"sdkVersions"] ?: @[[EXVersions sharedInstance].temporarySdkVersion] mutableCopy]; 418 [sdkVersions addObject:@"UNVERSIONED"]; 419 420 NSMutableArray *sdkVersionRuntimeVersions = [[NSMutableArray alloc] initWithCapacity:sdkVersions.count]; 421 for (NSString *sdkVersion in sdkVersions) { 422 [sdkVersionRuntimeVersions addObject:[NSString stringWithFormat:@"exposdk:%@", sdkVersion]]; 423 } 424 [sdkVersionRuntimeVersions addObject:@"exposdk:UNVERSIONED"]; 425 [sdkVersions addObjectsFromArray:sdkVersionRuntimeVersions]; 426 427 _selectionPolicy = [[EXUpdatesSelectionPolicy alloc] 428 initWithLauncherSelectionPolicy:[[EXExpoGoLauncherSelectionPolicyFilterAware alloc] initWithSdkVersions:sdkVersions] 429 loaderSelectionPolicy:[EXUpdatesLoaderSelectionPolicyFilterAware new] 430 reaperSelectionPolicy:[EXUpdatesReaperSelectionPolicyDevelopmentClient new]]; 431 432 EXUpdatesAppLoaderTask *loaderTask = [[EXUpdatesAppLoaderTask alloc] initWithConfig:_config 433 database:updatesDatabaseManager.database 434 directory:updatesDatabaseManager.updatesDirectory 435 selectionPolicy:_selectionPolicy 436 delegateQueue:_appLoaderQueue]; 437 loaderTask.delegate = self; 438 [loaderTask start]; 439} 440 441- (void)_launchWithNoDatabaseAndError:(nullable NSError *)error 442{ 443 EXUpdatesAppLauncherNoDatabase *appLauncher = [[EXUpdatesAppLauncherNoDatabase alloc] init]; 444 [appLauncher launchUpdateWithConfig:_config]; 445 446 _confirmedManifest = [self _processManifest:appLauncher.launchedUpdate.manifest]; 447 if (_confirmedManifest == nil) { 448 return; 449 } 450 _optimisticManifest = _confirmedManifest; 451 _bundle = [NSData dataWithContentsOfURL:appLauncher.launchAssetUrl]; 452 _appLauncher = appLauncher; 453 if (self.delegate) { 454 [self.delegate appLoader:self didLoadOptimisticManifest:_confirmedManifest]; 455 [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle]; 456 } 457 458 [[EXUpdatesErrorRecovery new] writeErrorOrExceptionToLog:error]; 459} 460 461- (void)_runReaper 462{ 463 if (_appLauncher.launchedUpdate) { 464 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 465 [EXUpdatesReaper reapUnusedUpdatesWithConfig:_config 466 database:updatesDatabaseManager.database 467 directory:updatesDatabaseManager.updatesDirectory 468 selectionPolicy:_selectionPolicy 469 launchedUpdate:_appLauncher.launchedUpdate]; 470 } 471} 472 473- (void)_setOptimisticManifest:(EXManifestsManifest *)manifest 474{ 475 _optimisticManifest = manifest; 476 if (self.delegate) { 477 [self.delegate appLoader:self didLoadOptimisticManifest:_optimisticManifest]; 478 } 479} 480 481- (void)_setShouldShowRemoteUpdateStatus:(EXManifestsManifest *)manifest 482{ 483 // we don't want to show the cached experience alert when Updates.reloadAsync() is called 484 if (_shouldUseCacheOnly) { 485 _shouldShowRemoteUpdateStatus = NO; 486 return; 487 } 488 489 if (manifest) { 490 if (manifest.isDevelopmentSilentLaunch) { 491 _shouldShowRemoteUpdateStatus = NO; 492 return; 493 } 494 } 495 _shouldShowRemoteUpdateStatus = YES; 496} 497 498- (void)_loadDevelopmentJavaScriptResource 499{ 500 _isLoadingDevelopmentJavaScriptResource = YES; 501 EXAppFetcher *appFetcher = [[EXAppFetcher alloc] initWithAppLoader:self]; 502 [appFetcher fetchJSBundleWithManifest:self.optimisticManifest cacheBehavior:EXCachedResourceNoCache timeoutInterval:kEXJSBundleTimeout progress:^(EXLoadingProgress *progress) { 503 if (self.delegate) { 504 [self.delegate appLoader:self didLoadBundleWithProgress:progress]; 505 } 506 } success:^(NSData *bundle) { 507 self.isUpToDate = YES; 508 self.bundle = bundle; 509 self.isLoadingDevelopmentJavaScriptResource = NO; 510 if (self.delegate) { 511 [self.delegate appLoader:self didFinishLoadingManifest:self.optimisticManifest bundle:self.bundle]; 512 } 513 } error:^(NSError *error) { 514 self.error = error; 515 self.isLoadingDevelopmentJavaScriptResource = NO; 516 if (self.delegate) { 517 [self.delegate appLoader:self didFailWithError:error]; 518 } 519 }]; 520} 521 522# pragma mark - manifest processing 523 524- (nullable EXManifestsManifest *)_processManifest:(EXManifestsManifest *)manifest 525{ 526 @try { 527 NSMutableDictionary *mutableManifest = [manifest.rawManifestJSON mutableCopy]; 528 529 // If legacy manifest is not yet verified, served by a third party, not standalone, and not an anonymous experience 530 // then scope it locally by using the manifest URL as a scopeKey (id) and consider it verified. 531 if (!mutableManifest[@"isVerified"] && 532 !EXEnvironment.sharedEnvironment.isDetached && 533 ![EXKernelLinkingManager isExpoHostedUrl:_httpManifestUrl] && 534 ![EXAppLoaderExpoUpdates _isAnonymousExperience:manifest] && 535 [manifest isKindOfClass:[EXManifestsLegacyManifest class]]) { 536 // the manifest id in a legacy manifest determines the namespace/experience id an app is sandboxed with 537 // if manifest is hosted by third parties, we sandbox it with the hostname to avoid clobbering exp.host namespaces 538 // for https urls, sandboxed id is of form quinlanj.github.io/myProj-myApp 539 // for http urls, sandboxed id is of form UNVERIFIED-quinlanj.github.io/myProj-myApp 540 NSString *securityPrefix = [_httpManifestUrl.scheme isEqualToString:@"https"] ? @"" : @"UNVERIFIED-"; 541 NSString *slugSuffix = manifest.slug ? [@"-" stringByAppendingString:manifest.slug]: @""; 542 mutableManifest[@"id"] = [NSString stringWithFormat:@"%@%@%@%@", securityPrefix, _httpManifestUrl.host, _httpManifestUrl.path ?: @"", slugSuffix]; 543 mutableManifest[@"isVerified"] = @(YES); 544 } 545 546 // set verified to false by default 547 if (!mutableManifest[@"isVerified"]) { 548 mutableManifest[@"isVerified"] = @(NO); 549 } 550 551 // if the app bypassed verification or the manifest is scoped to a random anonymous 552 // scope key, automatically verify it 553 if (![mutableManifest[@"isVerified"] boolValue] && (EXEnvironment.sharedEnvironment.isManifestVerificationBypassed || [EXAppLoaderExpoUpdates _isAnonymousExperience:manifest])) { 554 mutableManifest[@"isVerified"] = @(YES); 555 } 556 557 // when the manifest is not verified at this point, make the scope key a salted and hashed version of the claimed scope key 558 if (![mutableManifest[@"isVerified"] boolValue]) { 559 NSString *currentScopeKeyAndSaltToHash = [NSString stringWithFormat:@"unverified-%@", manifest.scopeKey]; 560 NSString *currentScopeKeyHash = [currentScopeKeyAndSaltToHash hexEncodedSHA256]; 561 NSString *newScopeKey = [NSString stringWithFormat:@"%@-%@", currentScopeKeyAndSaltToHash, currentScopeKeyHash]; 562 if ([manifest isKindOfClass:EXManifestsNewManifest.class]) { 563 NSDictionary *extra = mutableManifest[@"extra"] ?: @{}; 564 NSMutableDictionary *mutableExtra = [extra mutableCopy]; 565 mutableExtra[@"scopeKey"] = newScopeKey; 566 mutableManifest[@"extra"] = mutableExtra; 567 } else { 568 mutableManifest[@"scopeKey"] = newScopeKey; 569 mutableManifest[@"id"] = newScopeKey; 570 } 571 } 572 573 return [EXManifestsManifestFactory manifestForManifestJSON:[mutableManifest copy]]; 574 } 575 @catch (NSException *exception) { 576 // Catch parsing errors related to invalid or unexpected manifest properties. For example, if a manifest 577 // is missing the `id` property, it'll raise an exception which we want to forward to the user so they 578 // can adjust their manifest JSON accordingly. 579 _error = [NSError errorWithDomain:@"ExpoParsingManifest" 580 code:1025 581 userInfo:@{NSLocalizedDescriptionKey: [@"Failed to parse manifest JSON: " stringByAppendingString:exception.reason] }]; 582 if (self.delegate) { 583 [self.delegate appLoader:self didFailWithError:_error]; 584 } 585 } 586 return nil; 587} 588 589+ (BOOL)_isAnonymousExperience:(EXManifestsManifest *)manifest 590{ 591 return [manifest.scopeKey hasPrefix:@"@anonymous/"]; 592} 593 594#pragma mark - headers 595 596- (NSDictionary *)_requestHeaders 597{ 598 NSDictionary *requestHeaders = @{ 599 @"Exponent-SDK-Version": [self _sdkVersions], 600 @"Exponent-Accept-Signature": @"true", 601 @"Exponent-Platform": @"ios", 602 @"Exponent-Version": [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 603 @"Expo-Client-Environment": [self _clientEnvironment], 604 @"Expo-Updates-Environment": [self _clientEnvironment], 605 @"User-Agent": [self _userAgentString], 606 @"Expo-Client-Release-Type": [EXClientReleaseType clientReleaseType] 607 }; 608 609 NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret]; 610 if (sessionSecret) { 611 NSMutableDictionary *requestHeadersMutable = [requestHeaders mutableCopy]; 612 requestHeadersMutable[@"Expo-Session"] = sessionSecret; 613 requestHeaders = requestHeadersMutable; 614 } 615 616 return requestHeaders; 617} 618 619- (NSString *)_userAgentString 620{ 621 struct utsname systemInfo; 622 uname(&systemInfo); 623 NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; 624 return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)", 625 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 626 deviceModel, 627 [UIDevice currentDevice].systemName, 628 [UIDevice currentDevice].systemVersion, 629 [UIScreen mainScreen].scale, 630 [NSLocale autoupdatingCurrentLocale].localeIdentifier]; 631} 632 633- (NSString *)_clientEnvironment 634{ 635 if ([EXEnvironment sharedEnvironment].isDetached) { 636 return @"STANDALONE"; 637 } else { 638 return @"EXPO_DEVICE"; 639#if TARGET_IPHONE_SIMULATOR 640 return @"EXPO_SIMULATOR"; 641#endif 642 } 643} 644 645- (NSString *)_sdkVersions 646{ 647 NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"]; 648 if (versionsAvailable) { 649 return [versionsAvailable componentsJoinedByString:@","]; 650 } else { 651 return [EXVersions sharedInstance].temporarySdkVersion; 652 } 653} 654 655@end 656 657NS_ASSUME_NONNULL_END 658