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