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