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