// Copyright 2015-present 650 Industries. All rights reserved. #import "EXEnvironment.h" #import "EXHomeModule.h" #import "EXFileDownloader.h" #import "EXSession.h" #import "EXVersions.h" #import "EXKernelUtil.h" #import "EXClientReleaseType.h" #import @import UIKit; #import NSString * const EXNetworkErrorDomain = @"EXNetwork"; NSTimeInterval const EXFileDownloaderDefaultTimeoutInterval = 60; @interface EXFileDownloader () @end @implementation EXFileDownloader - (instancetype)init { if (self = [super init]) { _timeoutInterval = EXFileDownloaderDefaultTimeoutInterval; } return self; } - (void)downloadFileFromURL:(NSURL *)url successBlock:(EXFileDownloaderSuccessBlock)successBlock errorBlock:(EXFileDownloaderErrorBlock)errorBlock { NSURLSessionConfiguration *configuration = _urlSessionConfiguration ?: [NSURLSessionConfiguration defaultSessionConfiguration]; // also pass any custom cache policy onto this specific request NSURLRequestCachePolicy cachePolicy = _urlSessionConfiguration ? _urlSessionConfiguration.requestCachePolicy : NSURLRequestUseProtocolCachePolicy; NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:cachePolicy timeoutInterval:_timeoutInterval]; [self setHTTPHeaderFields:request]; __weak typeof(self) weakSelf = self; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!error && [response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; if (httpResponse.statusCode != 200) { NSStringEncoding encoding = [weakSelf _encodingFromResponse:response]; NSString *body = [[NSString alloc] initWithData:data encoding:encoding]; error = [weakSelf _errorFromResponse:httpResponse body:body]; } } if (error) { errorBlock(error, response); } else { successBlock(data, response); } }]; [task resume]; [session finishTasksAndInvalidate]; } #pragma mark - Configuring the request - (void)setHTTPHeaderFields:(NSMutableURLRequest *)request { [request setValue:[self _userAgentString] forHTTPHeaderField:@"User-Agent"]; NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; [request setValue:version forHTTPHeaderField:@"Exponent-Version"]; NSString *requestAbiVersion; if (_abiVersion && _abiVersion.length) { requestAbiVersion = _abiVersion; } else { NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"]; if (versionsAvailable) { requestAbiVersion = [versionsAvailable componentsJoinedByString:@","]; } else { requestAbiVersion = [EXVersions sharedInstance].temporarySdkVersion; } } NSString *releaseChannel; if (_releaseChannel) { releaseChannel = _releaseChannel; } else { releaseChannel = @"default"; } NSString *clientEnvironment; if ([EXEnvironment sharedEnvironment].isDetached) { clientEnvironment = @"STANDALONE"; } else { clientEnvironment = @"EXPO_DEVICE"; #if TARGET_IPHONE_SIMULATOR clientEnvironment = @"EXPO_SIMULATOR"; #endif } [request setValue:releaseChannel forHTTPHeaderField:@"Expo-Release-Channel"]; [request setValue:@"true" forHTTPHeaderField:@"Expo-JSON-Error"]; [request setValue:requestAbiVersion forHTTPHeaderField:@"Exponent-SDK-Version"]; [request setValue:@"ios" forHTTPHeaderField:@"Exponent-Platform"]; [request setValue:@"true" forHTTPHeaderField:@"Exponent-Accept-Signature"]; [request setValue:@"application/expo+json,application/json" forHTTPHeaderField:@"Accept"]; [request setValue:@"1" forHTTPHeaderField:@"Expo-Api-Version"]; [request setValue:clientEnvironment forHTTPHeaderField:@"Expo-Client-Environment"]; [request setValue:[EXClientReleaseType clientReleaseType] forHTTPHeaderField:@"Expo-Client-Release-Type"]; NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret]; if (sessionSecret) { [request setValue:sessionSecret forHTTPHeaderField:@"Expo-Session"]; } } - (NSString *)_userAgentString { struct utsname systemInfo; uname(&systemInfo); NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], deviceModel, [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion, [UIScreen mainScreen].scale, [NSLocale autoupdatingCurrentLocale].localeIdentifier]; } #pragma mark - NSURLSessionTaskDelegate - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler { completionHandler(request); } #pragma mark - NSURLSessionDataDelegate - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { completionHandler(proposedResponse); } #pragma mark - Parsing the response - (NSStringEncoding)_encodingFromResponse:(NSURLResponse *)response { if (response.textEncodingName) { CFStringRef cfEncodingName = (__bridge CFStringRef)response.textEncodingName; CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding(cfEncodingName); if (cfEncoding != kCFStringEncodingInvalidId) { return CFStringConvertEncodingToNSStringEncoding(cfEncoding); } } // Default to UTF-8 return NSUTF8StringEncoding; } - (NSError *)_errorFromResponse:(NSHTTPURLResponse *)response body:(NSString *)body { NSDictionary *userInfo; id errorInfo = RCTJSONParse(body, nil); if ([errorInfo isKindOfClass:[NSDictionary class]]) { userInfo = [self _formattedErrorInfo:(NSDictionary *)errorInfo]; } else { userInfo = @{ NSLocalizedDescriptionKey: body, }; } return [NSError errorWithDomain:EXNetworkErrorDomain code:response.statusCode userInfo:userInfo]; } - (NSDictionary *)_formattedErrorInfo:(NSDictionary *)errorInfo { NSString *message = errorInfo[@"message"] ?: errorInfo[@"message"] ?: @"There was a server error"; NSString *errorCode = errorInfo[@"errorCode"] ?: @"UNEXPECTED_ERROR"; NSString *errorMetadata = errorInfo[@"metadata"] ?: @{}; NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: message, @"errorCode": errorCode, @"metadata": errorMetadata, }]; if ([errorInfo[@"errors"] isKindOfClass:[NSArray class]]) { NSMutableArray *formattedErrorItems = [NSMutableArray array]; for (NSDictionary *errorItem in errorInfo[@"errors"]) { if ([errorItem isKindOfClass:[NSDictionary class]]) { NSMutableDictionary *formattedErrorItem = [NSMutableDictionary dictionary]; if (errorItem[@"description"]) { formattedErrorItem[@"methodName"] = errorItem[@"description"]; } if (errorItem[@"filename"]) { formattedErrorItem[@"file"] = errorItem[@"filename"]; } if (errorItem[@"lineNumber"]) { formattedErrorItem[@"lineNumber"] = errorItem[@"lineNumber"]; } [formattedErrorItems addObject:formattedErrorItem]; } } userInfo[@"stack"] = formattedErrorItems; } return userInfo; } @end