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