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