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