/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import "RCTNetworking.h" #import "RCTAssert.h" #import "RCTConvert.h" #import "RCTURLRequestHandler.h" #import "RCTEventDispatcher.h" #import "RCTHTTPRequestHandler.h" #import "RCTLog.h" #import "RCTUtils.h" typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result); @interface RCTNetworking () - (void)processDataForHTTPQuery:(NSDictionary *)data callback:(void (^)(NSError *error, NSDictionary *result))callback; @end /** * Helper to convert FormData payloads into multipart/formdata requests. */ @interface RCTHTTPFormDataHelper : NSObject @property (nonatomic, weak) RCTNetworking *dataManager; @end @implementation RCTHTTPFormDataHelper { NSMutableArray *parts; NSMutableData *multipartBody; RCTHTTPQueryResult _callback; NSString *boundary; } - (void)process:(NSArray *)formData callback:(void (^)(NSError *error, NSDictionary *result))callback { if (![formData count]) { callback(nil, nil); return; } parts = [formData mutableCopy]; _callback = callback; multipartBody = [[NSMutableData alloc] init]; boundary = [self generateBoundary]; NSDictionary *currentPart = [parts objectAtIndex: 0]; [_dataManager processDataForHTTPQuery:currentPart callback:^(NSError *e, NSDictionary *r) { [self handleResult:r error:e]; }]; } - (NSString *)generateBoundary { NSString *const boundaryChars = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./"; const NSUInteger boundaryLength = 70; NSMutableString *output = [NSMutableString stringWithCapacity:boundaryLength]; NSUInteger numchars = [boundaryChars length]; for (NSUInteger i = 0; i < boundaryLength; i++) { [output appendFormat:@"%C", [boundaryChars characterAtIndex:arc4random_uniform((u_int32_t)numchars)]]; } return output; } - (void)handleResult:(NSDictionary *)result error:(NSError *)error { if (error) { _callback(error, nil); return; } NSDictionary *currentPart = parts[0]; [parts removeObjectAtIndex:0]; // Start with boundary. [multipartBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; // Print headers. NSMutableDictionary *headers = [(NSDictionary*)currentPart[@"headers"] mutableCopy]; NSString *partContentType = result[@"contentType"]; if (partContentType != nil) { [headers setObject:partContentType forKey:@"content-type"]; } [headers enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) { [multipartBody appendData:[[NSString stringWithFormat:@"%@: %@\r\n", parameterKey, parameterValue] dataUsingEncoding:NSUTF8StringEncoding]]; }]; // Add the body. [multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; [multipartBody appendData:result[@"body"]]; [multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; if ([parts count]) { NSDictionary *nextPart = [parts objectAtIndex: 0]; [_dataManager processDataForHTTPQuery:nextPart callback:^(NSError *e, NSDictionary *r) { [self handleResult:r error:e]; }]; return; } // We've processed the last item. Finish and return. [multipartBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=\"%@\"", boundary]; _callback(nil, @{@"body": multipartBody, @"contentType": contentType}); } @end /** * Helper to package in-flight requests together with their response data. */ @interface RCTActiveURLRequest : NSObject @property (nonatomic, strong) NSNumber *requestID; @property (nonatomic, strong) NSURLRequest *request; @property (nonatomic, strong) id handler; @property (nonatomic, assign) BOOL incrementalUpdates; @property (nonatomic, strong) NSURLResponse *response; @property (nonatomic, strong) NSMutableData *data; @end @implementation RCTActiveURLRequest - (instancetype)init { if ((self = [super init])) { _data = [[NSMutableData alloc] init]; } return self; } @end /** * Helper to load request body data using a handler. */ @interface RCTDataLoader : NSObject @end typedef void (^RCTDataLoaderCallback)(NSData *data, NSString *MIMEType, NSError *error); @implementation RCTDataLoader { RCTDataLoaderCallback _callback; RCTActiveURLRequest *_request; id _requestToken; } - (instancetype)initWithRequest:(NSURLRequest *)request handler:(id)handler callback:(RCTDataLoaderCallback)callback { RCTAssertParam(request); RCTAssertParam(handler); RCTAssertParam(callback); if ((self = [super init])) { _callback = callback; _request = [[RCTActiveURLRequest alloc] init]; _request.request = request; _request.handler = handler; _request.incrementalUpdates = NO; _requestToken = [handler sendRequest:request withDelegate:self]; } return self; } - (instancetype)init { return [self initWithRequest:nil handler:nil callback:nil]; } - (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total { RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen"); } - (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response { RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen"); _request.response = response; } - (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data { RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen"); [_request.data appendData:data]; } - (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error { RCTAssert(_callback != nil, @"The callback property must be set"); _callback(_request.data, _request.response.MIMEType, error); } @end /** * Bridge module that provides the JS interface to the network stack. */ @implementation RCTNetworking { NSInteger _currentRequestID; NSMapTable *_activeRequests; } @synthesize bridge = _bridge; @synthesize methodQueue = _methodQueue; RCT_EXPORT_MODULE() - (instancetype)init { if ((self = [super init])) { _currentRequestID = 0; _activeRequests = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory capacity:0]; } return self; } - (void)buildRequest:(NSDictionary *)query completionBlock:(void (^)(NSURLRequest *request))block { NSURL *URL = [RCTConvert NSURL:query[@"url"]]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; request.HTTPMethod = [[RCTConvert NSString:query[@"method"]] uppercaseString] ?: @"GET"; request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]]; NSDictionary *data = [RCTConvert NSDictionary:query[@"data"]]; [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) { if (error) { RCTLogError(@"Error processing request body: %@", error); // Ideally we'd circle back to JS here and notify an error/abort on the request. return; } request.HTTPBody = result[@"body"]; NSString *contentType = result[@"contentType"]; if (contentType) { [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; } // Gzip the request body if ([request.allHTTPHeaderFields[@"Content-Encoding"] isEqualToString:@"gzip"]) { request.HTTPBody = RCTGzipData(request.HTTPBody, -1 /* default */); [request setValue:[@(request.HTTPBody.length) description] forHTTPHeaderField:@"Content-Length"]; } block(request); }]; } - (id)handlerForRequest:(NSURLRequest *)request { NSMutableArray *handlers = [NSMutableArray array]; for (id module in _bridge.modules.allValues) { if ([module conformsToProtocol:@protocol(RCTURLRequestHandler)]) { if ([(id)module canHandleRequest:request]) { [handlers addObject:module]; } } } [handlers sortUsingComparator:^NSComparisonResult(id a, id b) { float priorityA = [a respondsToSelector:@selector(handlerPriority)] ? [a handlerPriority] : 0; float priorityB = [b respondsToSelector:@selector(handlerPriority)] ? [b handlerPriority] : 0; if (priorityA < priorityB) { return NSOrderedAscending; } else if (priorityA > priorityB) { return NSOrderedDescending; } else { RCTLogError(@"The RCTURLRequestHandlers %@ and %@ both reported that" " they can handle the request %@, and have equal priority" " (%g). This could result in non-deterministic behavior.", a, b, request, priorityA); return NSOrderedSame; } }]; id handler = [handlers lastObject]; if (!handler) { RCTLogError(@"No suitable request handler found for %@", request.URL); } return handler; } /** * Process the 'data' part of an HTTP query. * * 'data' can be a JSON value of the following forms: * * - {"string": "..."}: a simple JS string that will be UTF-8 encoded and sent as the body * * - {"uri": "some-uri://..."}: reference to a system resource, e.g. an image in the asset library * * - {"formData": [...]}: list of data payloads that will be combined into a multipart/form-data request * * If successful, the callback be called with a result dictionary containing the following (optional) keys: * * - @"body" (NSData): the body of the request * * - @"contentType" (NSString): the content type header of the request * */ - (void)processDataForHTTPQuery:(NSDictionary *)query callback:(void (^)(NSError *error, NSDictionary *result))callback { if (!query) { callback(nil, nil); return; } NSData *body = [RCTConvert NSData:query[@"string"]]; if (body) { callback(nil, @{@"body": body}); return; } NSURLRequest *request = [RCTConvert NSURLRequest:query[@"uri"]]; if (request) { id handler = [self handlerForRequest:request]; if (!handler) { return; } (void)[[RCTDataLoader alloc] initWithRequest:request handler:handler callback:^(NSData *data, NSString *MIMEType, NSError *error) { if (data) { callback(nil, @{@"body": data, @"contentType": MIMEType}); } else { callback(error, nil); } }]; return; } NSDictionaryArray *formData = [RCTConvert NSDictionaryArray:query[@"formData"]]; if (formData != nil) { RCTHTTPFormDataHelper *formDataHelper = [[RCTHTTPFormDataHelper alloc] init]; formDataHelper.dataManager = self; [formDataHelper process:formData callback:callback]; return; } // Nothing in the data payload, at least nothing we could understand anyway. // Ignore and treat it as if it were null. callback(nil, nil); } - (void)sendRequest:(NSURLRequest *)request incrementalUpdates:(BOOL)incrementalUpdates responseSender:(RCTResponseSenderBlock)responseSender { id handler = [self handlerForRequest:request]; id token = [handler sendRequest:request withDelegate:self]; if (token) { RCTActiveURLRequest *activeRequest = [[RCTActiveURLRequest alloc] init]; activeRequest.requestID = @(++_currentRequestID); activeRequest.request = request; activeRequest.handler = handler; activeRequest.incrementalUpdates = incrementalUpdates; [_activeRequests setObject:activeRequest forKey:token]; responseSender(@[activeRequest.requestID]); } } - (void)sendData:(NSData *)data forRequestToken:(id)requestToken { if (data.length == 0) { return; } RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; // Get text encoding NSURLResponse *response = request.response; NSStringEncoding encoding = NSUTF8StringEncoding; if (response.textEncodingName) { CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding]; if (!responseText && data.length) { RCTLogWarn(@"Received data was invalid."); return; } NSArray *responseJSON = @[request.requestID, responseText ?: @""]; [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData" body:responseJSON]; } #pragma mark - RCTURLRequestDelegate - (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total { dispatch_async(_methodQueue, ^{ RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); NSArray *responseJSON = @[request.requestID, @(progress), @(total)]; [_bridge.eventDispatcher sendDeviceEventWithName:@"didUploadProgress" body:responseJSON]; }); } - (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response { dispatch_async(_methodQueue, ^{ RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); request.response = response; NSHTTPURLResponse *httpResponse = nil; if ([response isKindOfClass:[NSHTTPURLResponse class]]) { // Might be a local file request httpResponse = (NSHTTPURLResponse *)response; } NSArray *responseJSON = @[request.requestID, @(httpResponse.statusCode ?: 200), httpResponse.allHeaderFields ?: @{}, ]; [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse" body:responseJSON]; }); } - (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data { dispatch_async(_methodQueue, ^{ RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); if (request.incrementalUpdates) { [self sendData:data forRequestToken:requestToken]; } else { [request.data appendData:data]; } }); } - (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error { dispatch_async(_methodQueue, ^{ RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); if (!request.incrementalUpdates) { [self sendData:request.data forRequestToken:requestToken]; } NSArray *responseJSON = @[ request.requestID, RCTNullIfNil(error.localizedDescription), ]; [_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse" body:responseJSON]; [_activeRequests removeObjectForKey:requestToken]; }); } #pragma mark - JS API RCT_EXPORT_METHOD(sendRequest:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender) { [self buildRequest:query completionBlock:^(NSURLRequest *request) { BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]]; [self sendRequest:request incrementalUpdates:incrementalUpdates responseSender:responseSender]; }]; } RCT_EXPORT_METHOD(cancelRequest:(NSNumber *)requestID) { id requestToken = nil; RCTActiveURLRequest *activeRequest = nil; for (id token in _activeRequests) { RCTActiveURLRequest *request = [_activeRequests objectForKey:token]; if ([request.requestID isEqualToNumber:requestID]) { activeRequest = request; requestToken = token; break; } } id handler = activeRequest.handler; if ([handler respondsToSelector:@selector(cancelRequest:)]) { [activeRequest.handler cancelRequest:requestToken]; } } @end