// Copyright 2017 Google // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #import "Private/GULNetwork.h" #import "Private/GULNetworkMessageCode.h" #import #import #import #import "Private/GULMutableDictionary.h" #import "Private/GULNetworkConstants.h" /// Constant string for request header Content-Encoding. static NSString *const kGULNetworkContentCompressionKey = @"Content-Encoding"; /// Constant string for request header Content-Encoding value. static NSString *const kGULNetworkContentCompressionValue = @"gzip"; /// Constant string for request header Content-Length. static NSString *const kGULNetworkContentLengthKey = @"Content-Length"; /// Constant string for request header Content-Type. static NSString *const kGULNetworkContentTypeKey = @"Content-Type"; /// Constant string for request header Content-Type value. static NSString *const kGULNetworkContentTypeValue = @"application/x-www-form-urlencoded"; /// Constant string for GET request method. static NSString *const kGULNetworkGETRequestMethod = @"GET"; /// Constant string for POST request method. static NSString *const kGULNetworkPOSTRequestMethod = @"POST"; /// Default constant string as a prefix for network logger. static NSString *const kGULNetworkLogTag = @"Google/Utilities/Network"; @interface GULNetwork () @end @implementation GULNetwork { /// Network reachability. GULReachabilityChecker *_reachability; /// The dictionary of requests by session IDs { NSString : id }. GULMutableDictionary *_requests; } - (instancetype)init { return [self initWithReachabilityHost:kGULNetworkReachabilityHost]; } - (instancetype)initWithReachabilityHost:(NSString *)reachabilityHost { self = [super init]; if (self) { // Setup reachability. _reachability = [[GULReachabilityChecker alloc] initWithReachabilityDelegate:self withHost:reachabilityHost]; if (![_reachability start]) { return nil; } _requests = [[GULMutableDictionary alloc] init]; _timeoutInterval = kGULNetworkTimeOutInterval; } return self; } - (void)dealloc { _reachability.reachabilityDelegate = nil; [_reachability stop]; } #pragma mark - External Methods + (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID completionHandler:(GULNetworkSystemCompletionHandler)completionHandler { [GULNetworkURLSession handleEventsForBackgroundURLSessionID:sessionID completionHandler:completionHandler]; } - (NSString *)postURL:(NSURL *)url payload:(NSData *)payload queue:(dispatch_queue_t)queue usingBackgroundSession:(BOOL)usingBackgroundSession completionHandler:(GULNetworkCompletionHandler)handler { if (!url.absoluteString.length) { [self handleErrorWithCode:GULErrorCodeNetworkInvalidURL queue:queue withHandler:handler]; return nil; } NSTimeInterval timeOutInterval = _timeoutInterval ?: kGULNetworkTimeOutInterval; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:timeOutInterval]; if (!request) { [self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation queue:queue withHandler:handler]; return nil; } NSError *compressError = nil; NSData *compressedData = [NSData gul_dataByGzippingData:payload error:&compressError]; if (!compressedData || compressError) { if (compressError || payload.length > 0) { // If the payload is not empty but it fails to compress the payload, something has been wrong. [self handleErrorWithCode:GULErrorCodeNetworkPayloadCompression queue:queue withHandler:handler]; return nil; } compressedData = [[NSData alloc] init]; } NSString *postLength = @(compressedData.length).stringValue; // Set up the request with the compressed data. [request setValue:postLength forHTTPHeaderField:kGULNetworkContentLengthKey]; request.HTTPBody = compressedData; request.HTTPMethod = kGULNetworkPOSTRequestMethod; [request setValue:kGULNetworkContentTypeValue forHTTPHeaderField:kGULNetworkContentTypeKey]; [request setValue:kGULNetworkContentCompressionValue forHTTPHeaderField:kGULNetworkContentCompressionKey]; GULNetworkURLSession *fetcher = [[GULNetworkURLSession alloc] initWithNetworkLoggerDelegate:self]; fetcher.backgroundNetworkEnabled = usingBackgroundSession; __weak GULNetwork *weakSelf = self; NSString *requestID = [fetcher sessionIDFromAsyncPOSTRequest:request completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSString *sessionID, NSError *error) { GULNetwork *strongSelf = weakSelf; if (!strongSelf) { return; } dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue(); dispatch_async(queueToDispatch, ^{ if (sessionID.length) { [strongSelf->_requests removeObjectForKey:sessionID]; } if (handler) { handler(response, data, error); } }); }]; if (!requestID) { [self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation queue:queue withHandler:handler]; return nil; } [self GULNetwork_logWithLevel:kGULNetworkLogLevelDebug messageCode:kGULNetworkMessageCodeNetwork000 message:@"Uploading data. Host" context:url]; _requests[requestID] = fetcher; return requestID; } - (NSString *)getURL:(NSURL *)url headers:(NSDictionary *)headers queue:(dispatch_queue_t)queue usingBackgroundSession:(BOOL)usingBackgroundSession completionHandler:(GULNetworkCompletionHandler)handler { if (!url.absoluteString.length) { [self handleErrorWithCode:GULErrorCodeNetworkInvalidURL queue:queue withHandler:handler]; return nil; } NSTimeInterval timeOutInterval = _timeoutInterval ?: kGULNetworkTimeOutInterval; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:timeOutInterval]; if (!request) { [self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation queue:queue withHandler:handler]; return nil; } request.HTTPMethod = kGULNetworkGETRequestMethod; request.allHTTPHeaderFields = headers; GULNetworkURLSession *fetcher = [[GULNetworkURLSession alloc] initWithNetworkLoggerDelegate:self]; fetcher.backgroundNetworkEnabled = usingBackgroundSession; __weak GULNetwork *weakSelf = self; NSString *requestID = [fetcher sessionIDFromAsyncGETRequest:request completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSString *sessionID, NSError *error) { GULNetwork *strongSelf = weakSelf; if (!strongSelf) { return; } dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue(); dispatch_async(queueToDispatch, ^{ if (sessionID.length) { [strongSelf->_requests removeObjectForKey:sessionID]; } if (handler) { handler(response, data, error); } }); }]; if (!requestID) { [self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation queue:queue withHandler:handler]; return nil; } [self GULNetwork_logWithLevel:kGULNetworkLogLevelDebug messageCode:kGULNetworkMessageCodeNetwork001 message:@"Downloading data. Host" context:url]; _requests[requestID] = fetcher; return requestID; } - (BOOL)hasUploadInProgress { return _requests.count > 0; } #pragma mark - Network Reachability /// Tells reachability delegate to call reachabilityDidChangeToStatus: to notify the network /// reachability has changed. - (void)reachability:(GULReachabilityChecker *)reachability statusChanged:(GULReachabilityStatus)status { _networkConnected = (status == kGULReachabilityViaCellular || status == kGULReachabilityViaWifi); [_reachabilityDelegate reachabilityDidChange]; } #pragma mark - Network logger delegate - (void)setLoggerDelegate:(id)loggerDelegate { // Explicitly check whether the delegate responds to the methods because conformsToProtocol does // not work correctly even though the delegate does respond to the methods. if (!loggerDelegate || ![loggerDelegate respondsToSelector:@selector(GULNetwork_logWithLevel:messageCode:message:contexts:)] || ![loggerDelegate respondsToSelector:@selector(GULNetwork_logWithLevel:messageCode:message:context:)] || ! [loggerDelegate respondsToSelector:@selector(GULNetwork_logWithLevel:messageCode:message:)]) { GULLogError(kGULLoggerNetwork, NO, [NSString stringWithFormat:@"I-NET%06ld", (long)kGULNetworkMessageCodeNetwork002], @"Cannot set the network logger delegate: delegate does not conform to the network " "logger protocol."); return; } _loggerDelegate = loggerDelegate; } #pragma mark - Private methods /// Handles network error and calls completion handler with the error. - (void)handleErrorWithCode:(NSInteger)code queue:(dispatch_queue_t)queue withHandler:(GULNetworkCompletionHandler)handler { NSDictionary *userInfo = @{kGULNetworkErrorContext : @"Failed to create network request"}; NSError *error = [[NSError alloc] initWithDomain:kGULNetworkErrorDomain code:code userInfo:userInfo]; [self GULNetwork_logWithLevel:kGULNetworkLogLevelWarning messageCode:kGULNetworkMessageCodeNetwork002 message:@"Failed to create network request. Code, error" contexts:@[ @(code), error ]]; if (handler) { dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue(); dispatch_async(queueToDispatch, ^{ handler(nil, nil, error); }); } } #pragma mark - Network logger - (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel messageCode:(GULNetworkMessageCode)messageCode message:(NSString *)message contexts:(NSArray *)contexts { // Let the delegate log the message if there is a valid logger delegate. Otherwise, just log // errors/warnings/info messages to the console log. if (_loggerDelegate) { [_loggerDelegate GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:contexts]; return; } if (_isDebugModeEnabled || logLevel == kGULNetworkLogLevelError || logLevel == kGULNetworkLogLevelWarning || logLevel == kGULNetworkLogLevelInfo) { NSString *formattedMessage = GULStringWithLogMessage(message, logLevel, contexts); NSLog(@"%@", formattedMessage); GULLogBasic((GULLoggerLevel)logLevel, kGULLoggerNetwork, NO, [NSString stringWithFormat:@"I-NET%06ld", (long)messageCode], formattedMessage, NULL); } } - (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel messageCode:(GULNetworkMessageCode)messageCode message:(NSString *)message context:(id)context { if (_loggerDelegate) { [_loggerDelegate GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message context:context]; return; } NSArray *contexts = context ? @[ context ] : @[]; [self GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:contexts]; } - (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel messageCode:(GULNetworkMessageCode)messageCode message:(NSString *)message { if (_loggerDelegate) { [_loggerDelegate GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message]; return; } [self GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:@[]]; } /// Returns a string for the given log level (e.g. kGULNetworkLogLevelError -> @"ERROR"). static NSString *GULLogLevelDescriptionFromLogLevel(GULNetworkLogLevel logLevel) { static NSDictionary *levelNames = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ levelNames = @{ @(kGULNetworkLogLevelError) : @"ERROR", @(kGULNetworkLogLevelWarning) : @"WARNING", @(kGULNetworkLogLevelInfo) : @"INFO", @(kGULNetworkLogLevelDebug) : @"DEBUG" }; }); return levelNames[@(logLevel)]; } /// Returns a formatted string to be used for console logging. static NSString *GULStringWithLogMessage(NSString *message, GULNetworkLogLevel logLevel, NSArray *contexts) { if (!message) { message = @"(Message was nil)"; } else if (!message.length) { message = @"(Message was empty)"; } NSMutableString *result = [[NSMutableString alloc] initWithFormat:@"<%@/%@> %@", kGULNetworkLogTag, GULLogLevelDescriptionFromLogLevel(logLevel), message]; if (!contexts.count) { return result; } NSMutableArray *formattedContexts = [[NSMutableArray alloc] init]; for (id item in contexts) { [formattedContexts addObject:(item != [NSNull null] ? item : @"(nil)")]; } [result appendString:@": "]; [result appendString:[formattedContexts componentsJoinedByString:@", "]]; return result; } @end