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 // in Expo Go, ignore directives in manifest responses and require a manifest. the current directives 399 // (no update available, roll back) don't have any practical use outside of standalone apps 400 updatesConfig[EXUpdatesConfig.EXUpdatesConfigEnableExpoUpdatesProtocolV0CompatibilityModeKey] = @YES; 401 } 402 403 _config = [EXUpdatesConfig configFromDictionary:updatesConfig]; 404 405 if (![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) { 406 [self _launchWithNoDatabaseAndError:nil]; 407 return; 408 } 409 410 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 411 412 NSMutableArray *sdkVersions = [[EXVersions sharedInstance].versions[@"sdkVersions"] ?: @[[EXVersions sharedInstance].temporarySdkVersion] mutableCopy]; 413 [sdkVersions addObject:@"UNVERSIONED"]; 414 415 NSMutableArray *sdkVersionRuntimeVersions = [[NSMutableArray alloc] initWithCapacity:sdkVersions.count]; 416 for (NSString *sdkVersion in sdkVersions) { 417 [sdkVersionRuntimeVersions addObject:[NSString stringWithFormat:@"exposdk:%@", sdkVersion]]; 418 } 419 [sdkVersionRuntimeVersions addObject:@"exposdk:UNVERSIONED"]; 420 [sdkVersions addObjectsFromArray:sdkVersionRuntimeVersions]; 421 422 423 _selectionPolicy = [[EXUpdatesSelectionPolicy alloc] 424 initWithLauncherSelectionPolicy:[[EXUpdatesLauncherSelectionPolicyFilterAware alloc] initWithRuntimeVersions:sdkVersions] 425 loaderSelectionPolicy:[EXUpdatesLoaderSelectionPolicyFilterAware new] 426 reaperSelectionPolicy:[EXUpdatesReaperSelectionPolicyDevelopmentClient new]]; 427 428 EXUpdatesAppLoaderTask *loaderTask = [[EXUpdatesAppLoaderTask alloc] initWithConfig:_config 429 database:updatesDatabaseManager.database 430 directory:updatesDatabaseManager.updatesDirectory 431 selectionPolicy:_selectionPolicy 432 delegateQueue:_appLoaderQueue]; 433 loaderTask.delegate = self; 434 [loaderTask start]; 435} 436 437- (void)_launchWithNoDatabaseAndError:(nullable NSError *)error 438{ 439 EXUpdatesAppLauncherNoDatabase *appLauncher = [[EXUpdatesAppLauncherNoDatabase alloc] init]; 440 [appLauncher launchUpdateWithConfig:_config]; 441 442 _confirmedManifest = [self _processManifest:appLauncher.launchedUpdate.manifest]; 443 if (_confirmedManifest == nil) { 444 return; 445 } 446 _optimisticManifest = _confirmedManifest; 447 _bundle = [NSData dataWithContentsOfURL:appLauncher.launchAssetUrl]; 448 _appLauncher = appLauncher; 449 if (self.delegate) { 450 [self.delegate appLoader:self didLoadOptimisticManifest:_confirmedManifest]; 451 [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle]; 452 } 453 454 [[EXUpdatesErrorRecovery new] writeErrorOrExceptionToLog:error]; 455} 456 457- (void)_runReaper 458{ 459 if (_appLauncher.launchedUpdate) { 460 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 461 [EXUpdatesReaper reapUnusedUpdatesWithConfig:_config 462 database:updatesDatabaseManager.database 463 directory:updatesDatabaseManager.updatesDirectory 464 selectionPolicy:_selectionPolicy 465 launchedUpdate:_appLauncher.launchedUpdate]; 466 } 467} 468 469- (void)_setOptimisticManifest:(EXManifestsManifest *)manifest 470{ 471 _optimisticManifest = manifest; 472 if (self.delegate) { 473 [self.delegate appLoader:self didLoadOptimisticManifest:_optimisticManifest]; 474 } 475} 476 477- (void)_setShouldShowRemoteUpdateStatus:(EXManifestsManifest *)manifest 478{ 479 // we don't want to show the cached experience alert when Updates.reloadAsync() is called 480 if (_shouldUseCacheOnly) { 481 _shouldShowRemoteUpdateStatus = NO; 482 return; 483 } 484 485 if (manifest) { 486 if (manifest.isDevelopmentSilentLaunch) { 487 _shouldShowRemoteUpdateStatus = NO; 488 return; 489 } 490 } 491 _shouldShowRemoteUpdateStatus = YES; 492} 493 494- (void)_loadDevelopmentJavaScriptResource 495{ 496 _isLoadingDevelopmentJavaScriptResource = YES; 497 EXAppFetcher *appFetcher = [[EXAppFetcher alloc] initWithAppLoader:self]; 498 [appFetcher fetchJSBundleWithManifest:self.optimisticManifest cacheBehavior:EXCachedResourceNoCache timeoutInterval:kEXJSBundleTimeout progress:^(EXLoadingProgress *progress) { 499 if (self.delegate) { 500 [self.delegate appLoader:self didLoadBundleWithProgress:progress]; 501 } 502 } success:^(NSData *bundle) { 503 self.isUpToDate = YES; 504 self.bundle = bundle; 505 self.isLoadingDevelopmentJavaScriptResource = NO; 506 if (self.delegate) { 507 [self.delegate appLoader:self didFinishLoadingManifest:self.optimisticManifest bundle:self.bundle]; 508 } 509 } error:^(NSError *error) { 510 self.error = error; 511 self.isLoadingDevelopmentJavaScriptResource = NO; 512 if (self.delegate) { 513 [self.delegate appLoader:self didFailWithError:error]; 514 } 515 }]; 516} 517 518# pragma mark - manifest processing 519 520- (nullable EXManifestsManifest *)_processManifest:(EXManifestsManifest *)manifest 521{ 522 @try { 523 NSMutableDictionary *mutableManifest = [manifest.rawManifestJSON mutableCopy]; 524 525 // If legacy manifest is not yet verified, served by a third party, not standalone, and not an anonymous experience 526 // then scope it locally by using the manifest URL as a scopeKey (id) and consider it verified. 527 if (!mutableManifest[@"isVerified"] && 528 !EXEnvironment.sharedEnvironment.isDetached && 529 ![EXKernelLinkingManager isExpoHostedUrl:_httpManifestUrl] && 530 ![EXAppLoaderExpoUpdates _isAnonymousExperience:manifest] && 531 [manifest isKindOfClass:[EXManifestsLegacyManifest class]]) { 532 // the manifest id in a legacy manifest determines the namespace/experience id an app is sandboxed with 533 // if manifest is hosted by third parties, we sandbox it with the hostname to avoid clobbering exp.host namespaces 534 // for https urls, sandboxed id is of form quinlanj.github.io/myProj-myApp 535 // for http urls, sandboxed id is of form UNVERIFIED-quinlanj.github.io/myProj-myApp 536 NSString *securityPrefix = [_httpManifestUrl.scheme isEqualToString:@"https"] ? @"" : @"UNVERIFIED-"; 537 NSString *slugSuffix = manifest.slug ? [@"-" stringByAppendingString:manifest.slug]: @""; 538 mutableManifest[@"id"] = [NSString stringWithFormat:@"%@%@%@%@", securityPrefix, _httpManifestUrl.host, _httpManifestUrl.path ?: @"", slugSuffix]; 539 mutableManifest[@"isVerified"] = @(YES); 540 } 541 542 // set verified to false by default 543 if (!mutableManifest[@"isVerified"]) { 544 mutableManifest[@"isVerified"] = @(NO); 545 } 546 547 // if the app bypassed verification or the manifest is scoped to a random anonymous 548 // scope key, automatically verify it 549 if (![mutableManifest[@"isVerified"] boolValue] && (EXEnvironment.sharedEnvironment.isManifestVerificationBypassed || [EXAppLoaderExpoUpdates _isAnonymousExperience:manifest])) { 550 mutableManifest[@"isVerified"] = @(YES); 551 } 552 553 // when the manifest is not verified at this point, make the scope key a salted and hashed version of the claimed scope key 554 if (![mutableManifest[@"isVerified"] boolValue]) { 555 NSString *currentScopeKeyAndSaltToHash = [NSString stringWithFormat:@"unverified-%@", manifest.scopeKey]; 556 NSString *currentScopeKeyHash = [currentScopeKeyAndSaltToHash hexEncodedSHA256]; 557 NSString *newScopeKey = [NSString stringWithFormat:@"%@-%@", currentScopeKeyAndSaltToHash, currentScopeKeyHash]; 558 if ([manifest isKindOfClass:EXManifestsNewManifest.class]) { 559 NSDictionary *extra = mutableManifest[@"extra"] ?: @{}; 560 NSMutableDictionary *mutableExtra = [extra mutableCopy]; 561 mutableExtra[@"scopeKey"] = newScopeKey; 562 mutableManifest[@"extra"] = mutableExtra; 563 } else { 564 mutableManifest[@"scopeKey"] = newScopeKey; 565 mutableManifest[@"id"] = newScopeKey; 566 } 567 } 568 569 return [EXManifestsManifestFactory manifestForManifestJSON:[mutableManifest copy]]; 570 } 571 @catch (NSException *exception) { 572 // Catch parsing errors related to invalid or unexpected manifest properties. For example, if a manifest 573 // is missing the `id` property, it'll raise an exception which we want to forward to the user so they 574 // can adjust their manifest JSON accordingly. 575 _error = [NSError errorWithDomain:@"ExpoParsingManifest" 576 code:1025 577 userInfo:@{NSLocalizedDescriptionKey: [@"Failed to parse manifest JSON: " stringByAppendingString:exception.reason] }]; 578 if (self.delegate) { 579 [self.delegate appLoader:self didFailWithError:_error]; 580 } 581 } 582 return nil; 583} 584 585+ (BOOL)_isAnonymousExperience:(EXManifestsManifest *)manifest 586{ 587 return [manifest.scopeKey hasPrefix:@"@anonymous/"]; 588} 589 590#pragma mark - headers 591 592- (NSDictionary *)_requestHeaders 593{ 594 NSDictionary *requestHeaders = @{ 595 @"Exponent-SDK-Version": [self _sdkVersions], 596 @"Exponent-Accept-Signature": @"true", 597 @"Exponent-Platform": @"ios", 598 @"Exponent-Version": [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 599 @"Expo-Client-Environment": [self _clientEnvironment], 600 @"Expo-Updates-Environment": [self _clientEnvironment], 601 @"User-Agent": [self _userAgentString], 602 @"Expo-Client-Release-Type": [EXClientReleaseType clientReleaseType] 603 }; 604 605 NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret]; 606 if (sessionSecret) { 607 NSMutableDictionary *requestHeadersMutable = [requestHeaders mutableCopy]; 608 requestHeadersMutable[@"Expo-Session"] = sessionSecret; 609 requestHeaders = requestHeadersMutable; 610 } 611 612 return requestHeaders; 613} 614 615- (NSString *)_userAgentString 616{ 617 struct utsname systemInfo; 618 uname(&systemInfo); 619 NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; 620 return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)", 621 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 622 deviceModel, 623 [UIDevice currentDevice].systemName, 624 [UIDevice currentDevice].systemVersion, 625 [UIScreen mainScreen].scale, 626 [NSLocale autoupdatingCurrentLocale].localeIdentifier]; 627} 628 629- (NSString *)_clientEnvironment 630{ 631 if ([EXEnvironment sharedEnvironment].isDetached) { 632 return @"STANDALONE"; 633 } else { 634 return @"EXPO_DEVICE"; 635#if TARGET_IPHONE_SIMULATOR 636 return @"EXPO_SIMULATOR"; 637#endif 638 } 639} 640 641- (NSString *)_sdkVersions 642{ 643 NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"]; 644 if (versionsAvailable) { 645 return [versionsAvailable componentsJoinedByString:@","]; 646 } else { 647 return [EXVersions sharedInstance].temporarySdkVersion; 648 } 649} 650 651@end 652 653NS_ASSUME_NONNULL_END 654