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