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