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 "EXProvisioningProfile.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  NSString * clientReleaseType= [EXProvisioningProfile clientReleaseTypeToString:[EXProvisioningProfile clientReleaseType]];
102
103  [request setValue:releaseChannel forHTTPHeaderField:@"Expo-Release-Channel"];
104  [request setValue:@"true" forHTTPHeaderField:@"Expo-JSON-Error"];
105  [request setValue:requestAbiVersion forHTTPHeaderField:@"Exponent-SDK-Version"];
106  [request setValue:@"ios" forHTTPHeaderField:@"Exponent-Platform"];
107  [request setValue:@"true" forHTTPHeaderField:@"Exponent-Accept-Signature"];
108  [request setValue:@"application/expo+json,application/json" forHTTPHeaderField:@"Accept"];
109  [request setValue:@"1" forHTTPHeaderField:@"Expo-Api-Version"];
110  [request setValue:clientEnvironment forHTTPHeaderField:@"Expo-Client-Environment"];
111  [request setValue:clientReleaseType forHTTPHeaderField:@"Expo-Client-Release-Type"];
112
113  NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret];
114  if (sessionSecret) {
115    [request setValue:sessionSecret forHTTPHeaderField:@"Expo-Session"];
116  }
117}
118
119- (NSString *)_userAgentString
120{
121  struct utsname systemInfo;
122  uname(&systemInfo);
123  NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
124  return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)",
125          [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
126          deviceModel,
127          [UIDevice currentDevice].systemName,
128          [UIDevice currentDevice].systemVersion,
129          [UIScreen mainScreen].scale,
130          [NSLocale autoupdatingCurrentLocale].localeIdentifier];
131}
132
133#pragma mark - NSURLSessionTaskDelegate
134
135- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler
136{
137  completionHandler(request);
138}
139
140#pragma mark - NSURLSessionDataDelegate
141
142- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
143{
144  completionHandler(proposedResponse);
145}
146
147#pragma mark - Parsing the response
148
149- (NSStringEncoding)_encodingFromResponse:(NSURLResponse *)response
150{
151  if (response.textEncodingName) {
152    CFStringRef cfEncodingName = (__bridge CFStringRef)response.textEncodingName;
153    CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding(cfEncodingName);
154    if (cfEncoding != kCFStringEncodingInvalidId) {
155      return CFStringConvertEncodingToNSStringEncoding(cfEncoding);
156    }
157  }
158  // Default to UTF-8
159  return NSUTF8StringEncoding;
160}
161
162- (NSError *)_errorFromResponse:(NSHTTPURLResponse *)response body:(NSString *)body
163{
164  NSDictionary *userInfo;
165  id errorInfo = RCTJSONParse(body, nil);
166  if ([errorInfo isKindOfClass:[NSDictionary class]]) {
167    userInfo = [self _formattedErrorInfo:(NSDictionary *)errorInfo];
168  } else {
169    userInfo = @{
170                 NSLocalizedDescriptionKey: body,
171                 };
172  }
173  return [NSError errorWithDomain:EXNetworkErrorDomain code:response.statusCode userInfo:userInfo];
174}
175
176- (NSDictionary *)_formattedErrorInfo:(NSDictionary *)errorInfo
177{
178  NSString *message = errorInfo[@"message"] ?: errorInfo[@"message"] ?: @"There was a server error";
179  NSString *errorCode = errorInfo[@"errorCode"] ?: @"UNEXPECTED_ERROR";
180  NSString *errorMetadata = errorInfo[@"metadata"] ?: @{};
181  NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: message,
182                                                                                   @"errorCode": errorCode,
183                                                                                   @"metadata": errorMetadata,
184                                                                                   }];
185
186  if ([errorInfo[@"errors"] isKindOfClass:[NSArray class]]) {
187    NSMutableArray *formattedErrorItems = [NSMutableArray array];
188    for (NSDictionary *errorItem in errorInfo[@"errors"]) {
189      if ([errorItem isKindOfClass:[NSDictionary class]]) {
190        NSMutableDictionary *formattedErrorItem = [NSMutableDictionary dictionary];
191        if (errorItem[@"description"]) {
192          formattedErrorItem[@"methodName"] = errorItem[@"description"];
193        }
194        if (errorItem[@"filename"]) {
195          formattedErrorItem[@"file"] = errorItem[@"filename"];
196        }
197        if (errorItem[@"lineNumber"]) {
198          formattedErrorItem[@"lineNumber"] = errorItem[@"lineNumber"];
199        }
200        [formattedErrorItems addObject:formattedErrorItem];
201      }
202    }
203    userInfo[@"stack"] = formattedErrorItems;
204  }
205
206  return userInfo;
207}
208
209@end
210