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