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