1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXEnvironment.h"
4#import "EXFileDownloader.h"
5#import "EXSession.h"
6#import "EXVersions.h"
7#import "EXKernelUtil.h"
8
9#import <React/RCTUtils.h>
10
11@import UIKit;
12
13#import <sys/utsname.h>
14
15NSString * const EXNetworkErrorDomain = @"EXNetwork";
16NSTimeInterval const EXFileDownloaderDefaultTimeoutInterval = 60;
17
18@interface EXFileDownloader () <NSURLSessionDataDelegate>
19@end
20
21@implementation EXFileDownloader
22
23- (instancetype)init
24{
25  if (self = [super init]) {
26    _timeoutInterval = EXFileDownloaderDefaultTimeoutInterval;
27  }
28  return self;
29}
30
31- (void)downloadFileFromURL:(NSURL *)url
32                     successBlock:(EXFileDownloaderSuccessBlock)successBlock
33                       errorBlock:(EXFileDownloaderErrorBlock)errorBlock
34{
35  NSURLSessionConfiguration *configuration = _urlSessionConfiguration ?: [NSURLSessionConfiguration defaultSessionConfiguration];
36
37  // also pass any custom cache policy onto this specific request
38  NSURLRequestCachePolicy cachePolicy = _urlSessionConfiguration ? _urlSessionConfiguration.requestCachePolicy : NSURLRequestUseProtocolCachePolicy;
39
40  NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
41  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:cachePolicy timeoutInterval:_timeoutInterval];
42  [self setHTTPHeaderFields:request];
43
44  __weak typeof(self) weakSelf = self;
45  NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
46    if (!error && [response isKindOfClass:[NSHTTPURLResponse class]]) {
47      NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
48      if (httpResponse.statusCode != 200) {
49        NSStringEncoding encoding = [weakSelf _encodingFromResponse:response];
50        NSString *body = [[NSString alloc] initWithData:data encoding:encoding];
51        error = [weakSelf _errorFromResponse:httpResponse body:body];
52      }
53    }
54
55    if (error) {
56      errorBlock(error, response);
57    } else {
58      successBlock(data, response);
59    }
60  }];
61  [task resume];
62  [session finishTasksAndInvalidate];
63}
64
65#pragma mark - Configuring the request
66
67- (void)setHTTPHeaderFields:(NSMutableURLRequest *)request
68{
69  [request setValue:[self _userAgentString] forHTTPHeaderField:@"User-Agent"];
70
71  NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
72  [request setValue:version forHTTPHeaderField:@"Exponent-Version"];
73  NSString *requestAbiVersion;
74  if (_abiVersion && _abiVersion.length) {
75    requestAbiVersion = _abiVersion;
76  } else {
77    NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"];
78    if (versionsAvailable) {
79      requestAbiVersion = [versionsAvailable componentsJoinedByString:@","];
80    } else {
81      requestAbiVersion = [EXVersions sharedInstance].temporarySdkVersion;
82    }
83  }
84  NSString *releaseChannel;
85  if (_releaseChannel) {
86    releaseChannel = _releaseChannel;
87  } else {
88    releaseChannel = @"default";
89  }
90  NSString *clientEnvironment;
91  if ([EXEnvironment sharedEnvironment].isDetached) {
92    clientEnvironment = @"STANDALONE";
93  } else {
94    clientEnvironment = @"EXPO_DEVICE";
95#if TARGET_IPHONE_SIMULATOR
96    clientEnvironment = @"EXPO_SIMULATOR";
97#endif
98  }
99  [request setValue:releaseChannel forHTTPHeaderField:@"Expo-Release-Channel"];
100  [request setValue:@"true" forHTTPHeaderField:@"Expo-JSON-Error"];
101  [request setValue:requestAbiVersion forHTTPHeaderField:@"Exponent-SDK-Version"];
102  [request setValue:@"ios" forHTTPHeaderField:@"Exponent-Platform"];
103  [request setValue:@"true" forHTTPHeaderField:@"Exponent-Accept-Signature"];
104  [request setValue:@"application/expo+json,application/json" forHTTPHeaderField:@"Accept"];
105  [request setValue:@"1" forHTTPHeaderField:@"Expo-Api-Version"];
106  [request setValue:clientEnvironment forHTTPHeaderField:@"Expo-Client-Environment"];
107
108  NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret];
109  if (sessionSecret) {
110    [request setValue:sessionSecret forHTTPHeaderField:@"Expo-Session"];
111  }
112}
113
114- (NSString *)_userAgentString
115{
116  struct utsname systemInfo;
117  uname(&systemInfo);
118  NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
119  return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)",
120          [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
121          deviceModel,
122          [UIDevice currentDevice].systemName,
123          [UIDevice currentDevice].systemVersion,
124          [UIScreen mainScreen].scale,
125          [NSLocale autoupdatingCurrentLocale].localeIdentifier];
126}
127
128#pragma mark - NSURLSessionTaskDelegate
129
130- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler
131{
132  completionHandler(request);
133}
134
135#pragma mark - NSURLSessionDataDelegate
136
137- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
138{
139  completionHandler(proposedResponse);
140}
141
142#pragma mark - Parsing the response
143
144- (NSStringEncoding)_encodingFromResponse:(NSURLResponse *)response
145{
146  if (response.textEncodingName) {
147    CFStringRef cfEncodingName = (__bridge CFStringRef)response.textEncodingName;
148    CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding(cfEncodingName);
149    if (cfEncoding != kCFStringEncodingInvalidId) {
150      return CFStringConvertEncodingToNSStringEncoding(cfEncoding);
151    }
152  }
153  // Default to UTF-8
154  return NSUTF8StringEncoding;
155}
156
157- (NSError *)_errorFromResponse:(NSHTTPURLResponse *)response body:(NSString *)body
158{
159  NSDictionary *userInfo;
160  id errorInfo = RCTJSONParse(body, nil);
161  if ([errorInfo isKindOfClass:[NSDictionary class]]) {
162    userInfo = [self _formattedErrorInfo:(NSDictionary *)errorInfo];
163  } else {
164    userInfo = @{
165                 NSLocalizedDescriptionKey: body,
166                 };
167  }
168  return [NSError errorWithDomain:EXNetworkErrorDomain code:response.statusCode userInfo:userInfo];
169}
170
171- (NSDictionary *)_formattedErrorInfo:(NSDictionary *)errorInfo
172{
173  NSString *message = errorInfo[@"message"] ?: errorInfo[@"message"] ?: @"There was a server error";
174  NSString *errorCode = errorInfo[@"errorCode"] ?: @"UNEXPECTED_ERROR";
175  NSString *errorMetadata = errorInfo[@"metadata"] ?: @{};
176  NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: message,
177                                                                                   @"errorCode": errorCode,
178                                                                                   @"metadata": errorMetadata,
179                                                                                   }];
180
181  if ([errorInfo[@"errors"] isKindOfClass:[NSArray class]]) {
182    NSMutableArray *formattedErrorItems = [NSMutableArray array];
183    for (NSDictionary *errorItem in errorInfo[@"errors"]) {
184      if ([errorItem isKindOfClass:[NSDictionary class]]) {
185        NSMutableDictionary *formattedErrorItem = [NSMutableDictionary dictionary];
186        if (errorItem[@"description"]) {
187          formattedErrorItem[@"methodName"] = errorItem[@"description"];
188        }
189        if (errorItem[@"filename"]) {
190          formattedErrorItem[@"file"] = errorItem[@"filename"];
191        }
192        if (errorItem[@"lineNumber"]) {
193          formattedErrorItem[@"lineNumber"] = errorItem[@"lineNumber"];
194        }
195        [formattedErrorItems addObject:formattedErrorItem];
196      }
197    }
198    userInfo[@"stack"] = formattedErrorItems;
199  }
200
201  return userInfo;
202}
203
204@end
205