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