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- (BOOL)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didLoadCachedUpdate:(EXUpdatesUpdate *)update 195{ 196 [self _setShouldShowRemoteUpdateStatus:update.manifest]; 197 // 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 198 if (update.manifest.isUsingDeveloperTool || [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager scopeKeyIsRecoveringFromError:update.manifest.scopeKey]) { 199 return NO; 200 } 201 return YES; 202} 203 204- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didStartLoadingUpdate:(EXUpdatesUpdate *)update 205{ 206 // expo-cli does not always respect our SDK version headers and respond with a compatible update or an error 207 // so we need to check the compatibility here 208 EXManifestResource *manifestResource = [[EXManifestResource alloc] initWithManifestUrl:_httpManifestUrl originalUrl:_manifestUrl]; 209 NSError *manifestCompatibilityError = [manifestResource verifyManifestSdkVersion:update.manifest]; 210 if (manifestCompatibilityError) { 211 _error = manifestCompatibilityError; 212 if (self.delegate) { 213 [self.delegate appLoader:self didFailWithError:_error]; 214 return; 215 } 216 } 217 218 _remoteUpdateStatus = kEXAppLoaderRemoteUpdateStatusDownloading; 219 [self _setShouldShowRemoteUpdateStatus:update.manifest]; 220 EXManifestsManifest *processedManifest = [self _processManifest:update.manifest]; 221 if (processedManifest == nil) { 222 return; 223 } 224 [self _setOptimisticManifest:processedManifest]; 225} 226 227- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id<EXUpdatesAppLauncher>)launcher isUpToDate:(BOOL)isUpToDate 228{ 229 if (_error) { 230 return; 231 } 232 233 if (!_optimisticManifest) { 234 EXManifestsManifest *processedManifest = [self _processManifest:launcher.launchedUpdate.manifest]; 235 if (processedManifest == nil) { 236 return; 237 } 238 [self _setOptimisticManifest:processedManifest]; 239 } 240 _isUpToDate = isUpToDate; 241 if (launcher.launchedUpdate.manifest.isUsingDeveloperTool) { 242 // in dev mode, we need to set an optimistic manifest but nothing else 243 return; 244 } 245 _confirmedManifest = [self _processManifest:launcher.launchedUpdate.manifest]; 246 if (_confirmedManifest == nil) { 247 return; 248 } 249 _bundle = [NSData dataWithContentsOfURL:launcher.launchAssetUrl]; 250 _appLauncher = launcher; 251 if (self.delegate) { 252 [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle]; 253 } 254} 255 256- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error 257{ 258 if ([EXEnvironment sharedEnvironment].isDetached) { 259 _isEmergencyLaunch = YES; 260 [self _launchWithNoDatabaseAndError:error]; 261 } else if (!_error) { 262 _error = error; 263 264 // if the error payload conforms to the error protocol, we can parse it and display 265 // a slightly nicer error message to the user 266 id errorJson = [NSJSONSerialization JSONObjectWithData:[error.localizedDescription dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; 267 if (errorJson && [errorJson isKindOfClass:[NSDictionary class]]) { 268 EXManifestResource *manifestResource = [[EXManifestResource alloc] initWithManifestUrl:_httpManifestUrl originalUrl:_manifestUrl]; 269 _error = [manifestResource formatError:[NSError errorWithDomain:EXNetworkErrorDomain code:error.code userInfo:errorJson]]; 270 } 271 272 if (self.delegate) { 273 [self.delegate appLoader:self didFailWithError:_error]; 274 } 275 } 276} 277 278- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishBackgroundUpdateWithStatus:(EXUpdatesBackgroundUpdateStatus)status update:(nullable EXUpdatesUpdate *)update error:(nullable NSError *)error 279{ 280 if (self.delegate) { 281 [self.delegate appLoader:self didResolveUpdatedBundleWithManifest:update.manifest isFromCache:(status == EXUpdatesBackgroundUpdateStatusNoUpdateAvailable) error:error]; 282 } 283} 284 285#pragma mark - internal 286 287+ (NSURL *)_httpUrlFromManifestUrl:(NSURL *)url 288{ 289 NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES]; 290 // if scheme is exps or https, use https. Else default to http 291 if (components.scheme && ([components.scheme isEqualToString:@"exps"] || [components.scheme isEqualToString:@"https"])){ 292 components.scheme = @"https"; 293 } else { 294 components.scheme = @"http"; 295 } 296 NSMutableString *path = [((components.path) ? components.path : @"") mutableCopy]; 297 path = [[EXKernelLinkingManager stringByRemovingDeepLink:path] mutableCopy]; 298 components.path = path; 299 return [components URL]; 300} 301 302- (BOOL)_initializeDatabase 303{ 304 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 305 BOOL success = updatesDatabaseManager.isDatabaseOpen; 306 if (!updatesDatabaseManager.isDatabaseOpen) { 307 success = [updatesDatabaseManager openDatabase]; 308 } 309 310 if (!success) { 311 _error = updatesDatabaseManager.error; 312 if (self.delegate) { 313 [self.delegate appLoader:self didFailWithError:_error]; 314 } 315 return NO; 316 } else { 317 return YES; 318 } 319} 320 321- (void)_beginRequest 322{ 323 if (![self _initializeDatabase]) { 324 return; 325 } 326 [self _startLoaderTask]; 327} 328 329- (void)_startLoaderTask 330{ 331 BOOL shouldCheckOnLaunch; 332 NSNumber *launchWaitMs; 333 if (_shouldUseCacheOnly) { 334 shouldCheckOnLaunch = NO; 335 launchWaitMs = @(0); 336 } else { 337 if ([EXEnvironment sharedEnvironment].isDetached) { 338 shouldCheckOnLaunch = [EXEnvironment sharedEnvironment].updatesCheckAutomatically; 339 launchWaitMs = [EXEnvironment sharedEnvironment].updatesFallbackToCacheTimeout; 340 } else { 341 shouldCheckOnLaunch = YES; 342 launchWaitMs = @(60000); 343 } 344 } 345 346 NSURL *httpManifestUrl = [[self class] _httpUrlFromManifestUrl:_manifestUrl]; 347 348 NSString *releaseChannel = [EXEnvironment sharedEnvironment].releaseChannel; 349 if (![EXEnvironment sharedEnvironment].isDetached) { 350 // in Expo Go, the release channel can change at runtime depending on the URL we load 351 NSURLComponents *manifestUrlComponents = [NSURLComponents componentsWithURL:httpManifestUrl resolvingAgainstBaseURL:YES]; 352 releaseChannel = [EXKernelLinkingManager releaseChannelWithUrlComponents:manifestUrlComponents]; 353 } 354 355 NSMutableDictionary *updatesConfig = [[NSMutableDictionary alloc] initWithDictionary:@{ 356 EXUpdatesConfig.EXUpdatesConfigUpdateUrlKey: httpManifestUrl.absoluteString, 357 EXUpdatesConfig.EXUpdatesConfigSDKVersionKey: [self _sdkVersions], 358 EXUpdatesConfig.EXUpdatesConfigScopeKeyKey: httpManifestUrl.absoluteString, 359 EXUpdatesConfig.EXUpdatesConfigReleaseChannelKey: releaseChannel, 360 EXUpdatesConfig.EXUpdatesConfigHasEmbeddedUpdateKey: @([EXEnvironment sharedEnvironment].isDetached), 361 EXUpdatesConfig.EXUpdatesConfigEnabledKey: @([EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled), 362 EXUpdatesConfig.EXUpdatesConfigLaunchWaitMsKey: launchWaitMs, 363 EXUpdatesConfig.EXUpdatesConfigCheckOnLaunchKey: shouldCheckOnLaunch ? EXUpdatesConfig.EXUpdatesConfigCheckOnLaunchValueAlways : EXUpdatesConfig.EXUpdatesConfigCheckOnLaunchValueNever, 364 EXUpdatesConfig.EXUpdatesConfigExpectsSignedManifestKey: @YES, 365 EXUpdatesConfig.EXUpdatesConfigRequestHeadersKey: [self _requestHeaders] 366 }]; 367 368 if (!EXEnvironment.sharedEnvironment.isDetached) { 369 // in Expo Go, embed the Expo Root Certificate and get the Expo Go intermediate certificate and development certificates 370 // from the multipart manifest response part 371 372 NSString *expoRootCertPath = [[NSBundle mainBundle] pathForResource:@"expo-root" ofType:@"pem"]; 373 if (!expoRootCertPath) { 374 @throw [NSException exceptionWithName:NSInternalInconsistencyException 375 reason:@"No expo-root certificate found in bundle" 376 userInfo:@{}]; 377 } 378 379 NSError *error; 380 NSString *expoRootCert = [NSString stringWithContentsOfFile:expoRootCertPath encoding:NSUTF8StringEncoding error:&error]; 381 if (error) { 382 expoRootCert = nil; 383 } 384 if (!expoRootCert) { 385 @throw [NSException exceptionWithName:NSInternalInconsistencyException 386 reason:@"Error reading expo-root certificate from bundle" 387 userInfo:@{ @"underlyingError": error.localizedDescription }]; 388 } 389 390 updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningCertificateKey] = expoRootCert; 391 updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningMetadataKey] = @{ 392 @"keyid": @"expo-root", 393 @"alg": @"rsa-v1_5-sha256", 394 }; 395 updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningIncludeManifestResponseCertificateChainKey] = @YES; 396 updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningAllowUnsignedManifestsKey] = @YES; 397 } 398 399 _config = [EXUpdatesConfig configFromDictionary:updatesConfig]; 400 401 if (![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) { 402 [self _launchWithNoDatabaseAndError:nil]; 403 return; 404 } 405 406 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 407 408 NSMutableArray *sdkVersions = [[EXVersions sharedInstance].versions[@"sdkVersions"] ?: @[[EXVersions sharedInstance].temporarySdkVersion] mutableCopy]; 409 [sdkVersions addObject:@"UNVERSIONED"]; 410 411 NSMutableArray *sdkVersionRuntimeVersions = [[NSMutableArray alloc] initWithCapacity:sdkVersions.count]; 412 for (NSString *sdkVersion in sdkVersions) { 413 [sdkVersionRuntimeVersions addObject:[NSString stringWithFormat:@"exposdk:%@", sdkVersion]]; 414 } 415 [sdkVersionRuntimeVersions addObject:@"exposdk:UNVERSIONED"]; 416 [sdkVersions addObjectsFromArray:sdkVersionRuntimeVersions]; 417 418 419 _selectionPolicy = [[EXUpdatesSelectionPolicy alloc] 420 initWithLauncherSelectionPolicy:[[EXUpdatesLauncherSelectionPolicyFilterAware alloc] initWithRuntimeVersions:sdkVersions] 421 loaderSelectionPolicy:[EXUpdatesLoaderSelectionPolicyFilterAware new] 422 reaperSelectionPolicy:[EXUpdatesReaperSelectionPolicyDevelopmentClient new]]; 423 424 EXUpdatesAppLoaderTask *loaderTask = [[EXUpdatesAppLoaderTask alloc] initWithConfig:_config 425 database:updatesDatabaseManager.database 426 directory:updatesDatabaseManager.updatesDirectory 427 selectionPolicy:_selectionPolicy 428 delegateQueue:_appLoaderQueue]; 429 loaderTask.delegate = self; 430 [loaderTask start]; 431} 432 433- (void)_launchWithNoDatabaseAndError:(nullable NSError *)error 434{ 435 EXUpdatesAppLauncherNoDatabase *appLauncher = [[EXUpdatesAppLauncherNoDatabase alloc] init]; 436 [appLauncher launchUpdateWithConfig:_config]; 437 438 _confirmedManifest = [self _processManifest:appLauncher.launchedUpdate.manifest]; 439 if (_confirmedManifest == nil) { 440 return; 441 } 442 _optimisticManifest = _confirmedManifest; 443 _bundle = [NSData dataWithContentsOfURL:appLauncher.launchAssetUrl]; 444 _appLauncher = appLauncher; 445 if (self.delegate) { 446 [self.delegate appLoader:self didLoadOptimisticManifest:_confirmedManifest]; 447 [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle]; 448 } 449 450 [[EXUpdatesErrorRecovery new] writeErrorOrExceptionToLog:error]; 451} 452 453- (void)_runReaper 454{ 455 if (_appLauncher.launchedUpdate) { 456 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 457 [EXUpdatesReaper reapUnusedUpdatesWithConfig:_config 458 database:updatesDatabaseManager.database 459 directory:updatesDatabaseManager.updatesDirectory 460 selectionPolicy:_selectionPolicy 461 launchedUpdate:_appLauncher.launchedUpdate]; 462 } 463} 464 465- (void)_setOptimisticManifest:(EXManifestsManifest *)manifest 466{ 467 _optimisticManifest = manifest; 468 if (self.delegate) { 469 [self.delegate appLoader:self didLoadOptimisticManifest:_optimisticManifest]; 470 } 471} 472 473- (void)_setShouldShowRemoteUpdateStatus:(EXManifestsManifest *)manifest 474{ 475 // we don't want to show the cached experience alert when Updates.reloadAsync() is called 476 if (_shouldUseCacheOnly) { 477 _shouldShowRemoteUpdateStatus = NO; 478 return; 479 } 480 481 if (manifest) { 482 if (manifest.isDevelopmentSilentLaunch) { 483 _shouldShowRemoteUpdateStatus = NO; 484 return; 485 } 486 } 487 _shouldShowRemoteUpdateStatus = YES; 488} 489 490- (void)_loadDevelopmentJavaScriptResource 491{ 492 _isLoadingDevelopmentJavaScriptResource = YES; 493 EXAppFetcher *appFetcher = [[EXAppFetcher alloc] initWithAppLoader:self]; 494 [appFetcher fetchJSBundleWithManifest:self.optimisticManifest cacheBehavior:EXCachedResourceNoCache timeoutInterval:kEXJSBundleTimeout progress:^(EXLoadingProgress *progress) { 495 if (self.delegate) { 496 [self.delegate appLoader:self didLoadBundleWithProgress:progress]; 497 } 498 } success:^(NSData *bundle) { 499 self.isUpToDate = YES; 500 self.bundle = bundle; 501 self.isLoadingDevelopmentJavaScriptResource = NO; 502 if (self.delegate) { 503 [self.delegate appLoader:self didFinishLoadingManifest:self.optimisticManifest bundle:self.bundle]; 504 } 505 } error:^(NSError *error) { 506 self.error = error; 507 self.isLoadingDevelopmentJavaScriptResource = NO; 508 if (self.delegate) { 509 [self.delegate appLoader:self didFailWithError:error]; 510 } 511 }]; 512} 513 514# pragma mark - manifest processing 515 516- (nullable EXManifestsManifest *)_processManifest:(EXManifestsManifest *)manifest 517{ 518 @try { 519 NSMutableDictionary *mutableManifest = [manifest.rawManifestJSON mutableCopy]; 520 521 // If legacy manifest is not yet verified, served by a third party, not standalone, and not an anonymous experience 522 // then scope it locally by using the manifest URL as a scopeKey (id) and consider it verified. 523 if (!mutableManifest[@"isVerified"] && 524 !EXEnvironment.sharedEnvironment.isDetached && 525 ![EXKernelLinkingManager isExpoHostedUrl:_httpManifestUrl] && 526 ![EXAppLoaderExpoUpdates _isAnonymousExperience:manifest] && 527 [manifest isKindOfClass:[EXManifestsLegacyManifest class]]) { 528 // the manifest id in a legacy manifest determines the namespace/experience id an app is sandboxed with 529 // if manifest is hosted by third parties, we sandbox it with the hostname to avoid clobbering exp.host namespaces 530 // for https urls, sandboxed id is of form quinlanj.github.io/myProj-myApp 531 // for http urls, sandboxed id is of form UNVERIFIED-quinlanj.github.io/myProj-myApp 532 NSString *securityPrefix = [_httpManifestUrl.scheme isEqualToString:@"https"] ? @"" : @"UNVERIFIED-"; 533 NSString *slugSuffix = manifest.slug ? [@"-" stringByAppendingString:manifest.slug]: @""; 534 mutableManifest[@"id"] = [NSString stringWithFormat:@"%@%@%@%@", securityPrefix, _httpManifestUrl.host, _httpManifestUrl.path ?: @"", slugSuffix]; 535 mutableManifest[@"isVerified"] = @(YES); 536 } 537 538 // set verified to false by default 539 if (!mutableManifest[@"isVerified"]) { 540 mutableManifest[@"isVerified"] = @(NO); 541 } 542 543 // if the app bypassed verification or the manifest is scoped to a random anonymous 544 // scope key, automatically verify it 545 if (![mutableManifest[@"isVerified"] boolValue] && (EXEnvironment.sharedEnvironment.isManifestVerificationBypassed || [EXAppLoaderExpoUpdates _isAnonymousExperience:manifest])) { 546 mutableManifest[@"isVerified"] = @(YES); 547 } 548 549 // when the manifest is not verified at this point, make the scope key a salted and hashed version of the claimed scope key 550 if (![mutableManifest[@"isVerified"] boolValue]) { 551 NSString *currentScopeKeyAndSaltToHash = [NSString stringWithFormat:@"unverified-%@", manifest.scopeKey]; 552 NSString *currentScopeKeyHash = [currentScopeKeyAndSaltToHash hexEncodedSHA256]; 553 NSString *newScopeKey = [NSString stringWithFormat:@"%@-%@", currentScopeKeyAndSaltToHash, currentScopeKeyHash]; 554 if ([manifest isKindOfClass:EXManifestsNewManifest.class]) { 555 NSDictionary *extra = mutableManifest[@"extra"] ?: @{}; 556 NSMutableDictionary *mutableExtra = [extra mutableCopy]; 557 mutableExtra[@"scopeKey"] = newScopeKey; 558 mutableManifest[@"extra"] = mutableExtra; 559 } else { 560 mutableManifest[@"scopeKey"] = newScopeKey; 561 mutableManifest[@"id"] = newScopeKey; 562 } 563 } 564 565 return [EXManifestsManifestFactory manifestForManifestJSON:[mutableManifest copy]]; 566 } 567 @catch (NSException *exception) { 568 // Catch parsing errors related to invalid or unexpected manifest properties. For example, if a manifest 569 // is missing the `id` property, it'll raise an exception which we want to forward to the user so they 570 // can adjust their manifest JSON accordingly. 571 _error = [NSError errorWithDomain:@"ExpoParsingManifest" 572 code:1025 573 userInfo:@{NSLocalizedDescriptionKey: [@"Failed to parse manifest JSON: " stringByAppendingString:exception.reason] }]; 574 if (self.delegate) { 575 [self.delegate appLoader:self didFailWithError:_error]; 576 } 577 } 578 return nil; 579} 580 581+ (BOOL)_isAnonymousExperience:(EXManifestsManifest *)manifest 582{ 583 return [manifest.scopeKey hasPrefix:@"@anonymous/"]; 584} 585 586#pragma mark - headers 587 588- (NSDictionary *)_requestHeaders 589{ 590 NSDictionary *requestHeaders = @{ 591 @"Exponent-SDK-Version": [self _sdkVersions], 592 @"Exponent-Accept-Signature": @"true", 593 @"Exponent-Platform": @"ios", 594 @"Exponent-Version": [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 595 @"Expo-Client-Environment": [self _clientEnvironment], 596 @"Expo-Updates-Environment": [self _clientEnvironment], 597 @"User-Agent": [self _userAgentString], 598 @"Expo-Client-Release-Type": [EXClientReleaseType clientReleaseType] 599 }; 600 601 NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret]; 602 if (sessionSecret) { 603 NSMutableDictionary *requestHeadersMutable = [requestHeaders mutableCopy]; 604 requestHeadersMutable[@"Expo-Session"] = sessionSecret; 605 requestHeaders = requestHeadersMutable; 606 } 607 608 return requestHeaders; 609} 610 611- (NSString *)_userAgentString 612{ 613 struct utsname systemInfo; 614 uname(&systemInfo); 615 NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; 616 return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)", 617 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 618 deviceModel, 619 [UIDevice currentDevice].systemName, 620 [UIDevice currentDevice].systemVersion, 621 [UIScreen mainScreen].scale, 622 [NSLocale autoupdatingCurrentLocale].localeIdentifier]; 623} 624 625- (NSString *)_clientEnvironment 626{ 627 if ([EXEnvironment sharedEnvironment].isDetached) { 628 return @"STANDALONE"; 629 } else { 630 return @"EXPO_DEVICE"; 631#if TARGET_IPHONE_SIMULATOR 632 return @"EXPO_SIMULATOR"; 633#endif 634 } 635} 636 637- (NSString *)_sdkVersions 638{ 639 NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"]; 640 if (versionsAvailable) { 641 return [versionsAvailable componentsJoinedByString:@","]; 642 } else { 643 return [EXVersions sharedInstance].temporarySdkVersion; 644 } 645} 646 647@end 648 649NS_ASSUME_NONNULL_END 650