From c6b4b03fffc3cea7c9525e5c79dce28f52900521 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 11 Jul 2018 08:28:35 -0700 Subject: Move GoogleUtilities out of Firebase directory (#1516) --- GoogleUtilities/Network/GULMutableDictionary.m | 97 +++ GoogleUtilities/Network/GULNetwork.m | 388 ++++++++++++ GoogleUtilities/Network/GULNetworkConstants.m | 40 ++ GoogleUtilities/Network/GULNetworkURLSession.m | 669 +++++++++++++++++++++ .../Network/Private/GULMutableDictionary.h | 46 ++ GoogleUtilities/Network/Private/GULNetwork.h | 87 +++ .../Network/Private/GULNetworkConstants.h | 79 +++ .../Network/Private/GULNetworkLoggerProtocol.h | 51 ++ .../Network/Private/GULNetworkMessageCode.h | 44 ++ .../Network/Private/GULNetworkURLSession.h | 60 ++ 10 files changed, 1561 insertions(+) create mode 100644 GoogleUtilities/Network/GULMutableDictionary.m create mode 100644 GoogleUtilities/Network/GULNetwork.m create mode 100644 GoogleUtilities/Network/GULNetworkConstants.m create mode 100644 GoogleUtilities/Network/GULNetworkURLSession.m create mode 100644 GoogleUtilities/Network/Private/GULMutableDictionary.h create mode 100644 GoogleUtilities/Network/Private/GULNetwork.h create mode 100644 GoogleUtilities/Network/Private/GULNetworkConstants.h create mode 100644 GoogleUtilities/Network/Private/GULNetworkLoggerProtocol.h create mode 100644 GoogleUtilities/Network/Private/GULNetworkMessageCode.h create mode 100644 GoogleUtilities/Network/Private/GULNetworkURLSession.h (limited to 'GoogleUtilities/Network') diff --git a/GoogleUtilities/Network/GULMutableDictionary.m b/GoogleUtilities/Network/GULMutableDictionary.m new file mode 100644 index 0000000..d281eb4 --- /dev/null +++ b/GoogleUtilities/Network/GULMutableDictionary.m @@ -0,0 +1,97 @@ +// 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/GULMutableDictionary.h" + +@implementation GULMutableDictionary { + /// The mutable dictionary. + NSMutableDictionary *_objects; + + /// Serial synchronization queue. All reads should use dispatch_sync, while writes use + /// dispatch_async. + dispatch_queue_t _queue; +} + +- (instancetype)init { + self = [super init]; + + if (self) { + _objects = [[NSMutableDictionary alloc] init]; + _queue = dispatch_queue_create("GULMutableDictionary", DISPATCH_QUEUE_SERIAL); + } + + return self; +} + +- (NSString *)description { + __block NSString *description; + dispatch_sync(_queue, ^{ + description = self->_objects.description; + }); + return description; +} + +- (id)objectForKey:(id)key { + __block id object; + dispatch_sync(_queue, ^{ + object = self->_objects[key]; + }); + return object; +} + +- (void)setObject:(id)object forKey:(id)key { + dispatch_async(_queue, ^{ + self->_objects[key] = object; + }); +} + +- (void)removeObjectForKey:(id)key { + dispatch_async(_queue, ^{ + [self->_objects removeObjectForKey:key]; + }); +} + +- (void)removeAllObjects { + dispatch_async(_queue, ^{ + [self->_objects removeAllObjects]; + }); +} + +- (NSUInteger)count { + __block NSUInteger count; + dispatch_sync(_queue, ^{ + count = self->_objects.count; + }); + return count; +} + +- (id)objectForKeyedSubscript:(id)key { + // The method this calls is already synchronized. + return [self objectForKey:key]; +} + +- (void)setObject:(id)obj forKeyedSubscript:(id)key { + // The method this calls is already synchronized. + [self setObject:obj forKey:key]; +} + +- (NSDictionary *)dictionary { + __block NSDictionary *dictionary; + dispatch_sync(_queue, ^{ + dictionary = [self->_objects copy]; + }); + return dictionary; +} + +@end diff --git a/GoogleUtilities/Network/GULNetwork.m b/GoogleUtilities/Network/GULNetwork.m new file mode 100644 index 0000000..233500b --- /dev/null +++ b/GoogleUtilities/Network/GULNetwork.m @@ -0,0 +1,388 @@ +// 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 diff --git a/GoogleUtilities/Network/GULNetworkConstants.m b/GoogleUtilities/Network/GULNetworkConstants.m new file mode 100644 index 0000000..90bd03d --- /dev/null +++ b/GoogleUtilities/Network/GULNetworkConstants.m @@ -0,0 +1,40 @@ +// 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/GULNetworkConstants.h" + +#import + +NSString *const kGULNetworkBackgroundSessionConfigIDPrefix = @"com.gul.network.background-upload"; +NSString *const kGULNetworkApplicationSupportSubdirectory = @"GUL/Network"; +NSString *const kGULNetworkTempDirectoryName = @"GULNetworkTemporaryDirectory"; +const NSTimeInterval kGULNetworkTempFolderExpireTime = 60 * 60; // 1 hour +const NSTimeInterval kGULNetworkTimeOutInterval = 60; // 1 minute. +NSString *const kGULNetworkReachabilityHost = @"app-measurement.com"; +NSString *const kGULNetworkErrorContext = @"Context"; + +const int kGULNetworkHTTPStatusOK = 200; +const int kGULNetworkHTTPStatusNoContent = 204; +const int kGULNetworkHTTPStatusCodeMultipleChoices = 300; +const int kGULNetworkHTTPStatusCodeMovedPermanently = 301; +const int kGULNetworkHTTPStatusCodeFound = 302; +const int kGULNetworkHTTPStatusCodeNotModified = 304; +const int kGULNetworkHTTPStatusCodeMovedTemporarily = 307; +const int kGULNetworkHTTPStatusCodeNotFound = 404; +const int kGULNetworkHTTPStatusCodeCannotAcceptTraffic = 429; +const int kGULNetworkHTTPStatusCodeUnavailable = 503; + +NSString *const kGULNetworkErrorDomain = @"com.gul.network.ErrorDomain"; + +GULLoggerService kGULLoggerNetwork = @"[GULNetwork]"; diff --git a/GoogleUtilities/Network/GULNetworkURLSession.m b/GoogleUtilities/Network/GULNetworkURLSession.m new file mode 100644 index 0000000..cb8a204 --- /dev/null +++ b/GoogleUtilities/Network/GULNetworkURLSession.m @@ -0,0 +1,669 @@ +// 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 + +#import "Private/GULNetworkURLSession.h" + +#import +#import "Private/GULMutableDictionary.h" +#import "Private/GULNetworkConstants.h" +#import "Private/GULNetworkMessageCode.h" + +@implementation GULNetworkURLSession { + /// The handler to be called when the request completes or error has occurs. + GULNetworkURLSessionCompletionHandler _completionHandler; + + /// Session ID generated randomly with a fixed prefix. + NSString *_sessionID; + + /// The session configuration. + NSURLSessionConfiguration *_sessionConfig; + + /// The path to the directory where all temporary files are stored before uploading. + NSURL *_networkDirectoryURL; + + /// The downloaded data from fetching. + NSData *_downloadedData; + + /// The path to the temporary file which stores the uploading data. + NSURL *_uploadingFileURL; + + /// The current request. + NSURLRequest *_request; +} + +#pragma mark - Init + +- (instancetype)initWithNetworkLoggerDelegate:(id)networkLoggerDelegate { + self = [super init]; + if (self) { + // Create URL to the directory where all temporary files to upload have to be stored. + NSArray *paths = + NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSString *applicationSupportDirectory = paths.firstObject; + NSArray *tempPathComponents = @[ + applicationSupportDirectory, kGULNetworkApplicationSupportSubdirectory, + kGULNetworkTempDirectoryName + ]; + _networkDirectoryURL = [NSURL fileURLWithPathComponents:tempPathComponents]; + _sessionID = [NSString stringWithFormat:@"%@-%@", kGULNetworkBackgroundSessionConfigIDPrefix, + [[NSUUID UUID] UUIDString]]; + _loggerDelegate = networkLoggerDelegate; + } + return self; +} + +#pragma mark - External Methods + +#pragma mark - To be called from AppDelegate + ++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID + completionHandler: + (GULNetworkSystemCompletionHandler)systemCompletionHandler { + // The session may not be Analytics background. Ignore those that do not have the prefix. + if (![sessionID hasPrefix:kGULNetworkBackgroundSessionConfigIDPrefix]) { + return; + } + GULNetworkURLSession *fetcher = [self fetcherWithSessionIdentifier:sessionID]; + if (fetcher != nil) { + [fetcher addSystemCompletionHandler:systemCompletionHandler forSession:sessionID]; + } else { + GULLogError(kGULLoggerNetwork, NO, + [NSString stringWithFormat:@"I-NET%06ld", (long)kGULNetworkMessageCodeNetwork003], + @"Failed to retrieve background session with ID %@ after app is relaunched.", + sessionID); + } +} + +#pragma mark - External Methods + +/// Sends an async POST request using NSURLSession for iOS >= 7.0, and returns an ID of the +/// connection. +- (NSString *)sessionIDFromAsyncPOSTRequest:(NSURLRequest *)request + completionHandler:(GULNetworkURLSessionCompletionHandler)handler { + // NSURLSessionUploadTask does not work with NSData in the background. + // To avoid this issue, write the data to a temporary file to upload it. + // Make a temporary file with the data subset. + _uploadingFileURL = [self temporaryFilePathWithSessionID:_sessionID]; + NSError *writeError; + NSURLSessionUploadTask *postRequestTask; + NSURLSession *session; + BOOL didWriteFile = NO; + + // Clean up the entire temp folder to avoid temp files that remain in case the previous session + // crashed and did not clean up. + [self maybeRemoveTempFilesAtURL:_networkDirectoryURL + expiringTime:kGULNetworkTempFolderExpireTime]; + + // If there is no background network enabled, no need to write to file. This will allow default + // network session which runs on the foreground. + if (_backgroundNetworkEnabled && [self ensureTemporaryDirectoryExists]) { + didWriteFile = [request.HTTPBody writeToFile:_uploadingFileURL.path + options:NSDataWritingAtomic + error:&writeError]; + + if (writeError) { + [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError + messageCode:kGULNetworkMessageCodeURLSession000 + message:@"Failed to write request data to file" + context:writeError]; + } + } + + if (didWriteFile) { + // Exclude this file from backing up to iTunes. There are conflicting reports that excluding + // directory from backing up does not excluding files of that directory from backing up. + [self excludeFromBackupForURL:_uploadingFileURL]; + + _sessionConfig = [self backgroundSessionConfigWithSessionID:_sessionID]; + [self populateSessionConfig:_sessionConfig withRequest:request]; + session = [NSURLSession sessionWithConfiguration:_sessionConfig + delegate:self + delegateQueue:[NSOperationQueue mainQueue]]; + postRequestTask = [session uploadTaskWithRequest:request fromFile:_uploadingFileURL]; + } else { + // If we cannot write to file, just send it in the foreground. + _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; + [self populateSessionConfig:_sessionConfig withRequest:request]; + _sessionConfig.URLCache = nil; + session = [NSURLSession sessionWithConfiguration:_sessionConfig + delegate:self + delegateQueue:[NSOperationQueue mainQueue]]; + postRequestTask = [session uploadTaskWithRequest:request fromData:request.HTTPBody]; + } + + if (!session || !postRequestTask) { + NSError *error = [[NSError alloc] + initWithDomain:kGULNetworkErrorDomain + code:GULErrorCodeNetworkRequestCreation + userInfo:@{kGULNetworkErrorContext : @"Cannot create network session"}]; + [self callCompletionHandler:handler withResponse:nil data:nil error:error]; + return nil; + } + + // Save the session into memory. + NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIDToFetcherMap]; + [sessionIdentifierToFetcherMap setObject:self forKey:_sessionID]; + + _request = [request copy]; + + // Store completion handler because background session does not accept handler block but custom + // delegate. + _completionHandler = [handler copy]; + [postRequestTask resume]; + + return _sessionID; +} + +/// Sends an async GET request using NSURLSession for iOS >= 7.0, and returns an ID of the session. +- (NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request + completionHandler:(GULNetworkURLSessionCompletionHandler)handler { + if (_backgroundNetworkEnabled) { + _sessionConfig = [self backgroundSessionConfigWithSessionID:_sessionID]; + } else { + _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; + } + + [self populateSessionConfig:_sessionConfig withRequest:request]; + + // Do not cache the GET request. + _sessionConfig.URLCache = nil; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:_sessionConfig + delegate:self + delegateQueue:[NSOperationQueue mainQueue]]; + NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request]; + + if (!session || !downloadTask) { + NSError *error = [[NSError alloc] + initWithDomain:kGULNetworkErrorDomain + code:GULErrorCodeNetworkRequestCreation + userInfo:@{kGULNetworkErrorContext : @"Cannot create network session"}]; + [self callCompletionHandler:handler withResponse:nil data:nil error:error]; + return nil; + } + + // Save the session into memory. + NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIDToFetcherMap]; + [sessionIdentifierToFetcherMap setObject:self forKey:_sessionID]; + + _request = [request copy]; + + _completionHandler = [handler copy]; + [downloadTask resume]; + + return _sessionID; +} + +#pragma mark - NSURLSessionTaskDelegate + +/// Called by the NSURLSession once the download task is completed. The file is saved in the +/// provided URL so we need to read the data and store into _downloadedData. Once the session is +/// completed, URLSession:task:didCompleteWithError will be called and the completion handler will +/// be called with the downloaded data. +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)task + didFinishDownloadingToURL:(NSURL *)url { + if (!url.path) { + [_loggerDelegate + GULNetwork_logWithLevel:kGULNetworkLogLevelError + messageCode:kGULNetworkMessageCodeURLSession001 + message:@"Unable to read downloaded data from empty temp path"]; + _downloadedData = nil; + return; + } + + NSError *error; + _downloadedData = [NSData dataWithContentsOfFile:url.path options:0 error:&error]; + + if (error) { + [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError + messageCode:kGULNetworkMessageCodeURLSession002 + message:@"Cannot read the content of downloaded data" + context:error]; + _downloadedData = nil; + } +} + +#if TARGET_OS_IOS || TARGET_OS_TV +- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { + [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelDebug + messageCode:kGULNetworkMessageCodeURLSession003 + message:@"Background session finished" + context:session.configuration.identifier]; + [self callSystemCompletionHandler:session.configuration.identifier]; +} +#endif + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didCompleteWithError:(NSError *)error { + // Avoid any chance of recursive behavior leading to it being used repeatedly. + GULNetworkURLSessionCompletionHandler handler = _completionHandler; + _completionHandler = nil; + + if (task.response) { + // The following assertion should always be true for HTTP requests, see https://goo.gl/gVLxT7. + NSAssert([task.response isKindOfClass:[NSHTTPURLResponse class]], @"URL response must be HTTP"); + + // The server responded so ignore the error created by the system. + error = nil; + } else if (!error) { + error = [[NSError alloc] + initWithDomain:kGULNetworkErrorDomain + code:GULErrorCodeNetworkInvalidResponse + userInfo:@{kGULNetworkErrorContext : @"Network Error: Empty network response"}]; + } + + [self callCompletionHandler:handler + withResponse:(NSHTTPURLResponse *)task.response + data:_downloadedData + error:error]; + + // Remove the temp file to avoid trashing devices with lots of temp files. + [self removeTempItemAtURL:_uploadingFileURL]; + + // Try to clean up stale files again. + [self maybeRemoveTempFilesAtURL:_networkDirectoryURL + expiringTime:kGULNetworkTempFolderExpireTime]; +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, + NSURLCredential *credential))completionHandler { + // The handling is modeled after GTMSessionFetcher. + if ([challenge.protectionSpace.authenticationMethod + isEqualToString:NSURLAuthenticationMethodServerTrust]) { + SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; + if (serverTrust == NULL) { + [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelDebug + messageCode:kGULNetworkMessageCodeURLSession004 + message:@"Received empty server trust for host. Host" + context:_request.URL]; + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + return; + } + NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; + if (!credential) { + [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelWarning + messageCode:kGULNetworkMessageCodeURLSession005 + message:@"Unable to verify server identity. Host" + context:_request.URL]; + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); + return; + } + + [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelDebug + messageCode:kGULNetworkMessageCodeURLSession006 + message:@"Received SSL challenge for host. Host" + context:_request.URL]; + + void (^callback)(BOOL) = ^(BOOL allow) { + if (allow) { + completionHandler(NSURLSessionAuthChallengeUseCredential, credential); + } else { + [self->_loggerDelegate + GULNetwork_logWithLevel:kGULNetworkLogLevelDebug + messageCode:kGULNetworkMessageCodeURLSession007 + message:@"Cancelling authentication challenge for host. Host" + context:self->_request.URL]; + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); + } + }; + + // Retain the trust object to avoid a SecTrustEvaluate() crash on iOS 7. + CFRetain(serverTrust); + + // Evaluate the certificate chain. + // + // The delegate queue may be the main thread. Trust evaluation could cause some + // blocking network activity, so we must evaluate async, as documented at + // https://developer.apple.com/library/ios/technotes/tn2232/ + dispatch_queue_t evaluateBackgroundQueue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + + dispatch_async(evaluateBackgroundQueue, ^{ + SecTrustResultType trustEval = kSecTrustResultInvalid; + BOOL shouldAllow; + OSStatus trustError; + + @synchronized([GULNetworkURLSession class]) { + trustError = SecTrustEvaluate(serverTrust, &trustEval); + } + + if (trustError != errSecSuccess) { + [self->_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError + messageCode:kGULNetworkMessageCodeURLSession008 + message:@"Cannot evaluate server trust. Error, host" + contexts:@[ @(trustError), self->_request.URL ]]; + shouldAllow = NO; + } else { + // Having a trust level "unspecified" by the user is the usual result, described at + // https://developer.apple.com/library/mac/qa/qa1360 + shouldAllow = + (trustEval == kSecTrustResultUnspecified || trustEval == kSecTrustResultProceed); + } + + // Call the call back with the permission. + callback(shouldAllow); + + CFRelease(serverTrust); + }); + return; + } + + // Default handling for other Auth Challenges. + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); +} + +#pragma mark - Internal Methods + +/// Stores system completion handler with session ID as key. +- (void)addSystemCompletionHandler:(GULNetworkSystemCompletionHandler)handler + forSession:(NSString *)identifier { + if (!handler) { + [_loggerDelegate + GULNetwork_logWithLevel:kGULNetworkLogLevelError + messageCode:kGULNetworkMessageCodeURLSession009 + message:@"Cannot store nil system completion handler in network"]; + return; + } + + if (!identifier.length) { + [_loggerDelegate + GULNetwork_logWithLevel:kGULNetworkLogLevelError + messageCode:kGULNetworkMessageCodeURLSession010 + message: + @"Cannot store system completion handler with empty network " + "session identifier"]; + return; + } + + GULMutableDictionary *systemCompletionHandlers = + [[self class] sessionIDToSystemCompletionHandlerDictionary]; + if (systemCompletionHandlers[identifier]) { + [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelWarning + messageCode:kGULNetworkMessageCodeURLSession011 + message:@"Got multiple system handlers for a single session ID" + context:identifier]; + } + + systemCompletionHandlers[identifier] = handler; +} + +/// Calls the system provided completion handler with the session ID stored in the dictionary. +/// The handler will be removed from the dictionary after being called. +- (void)callSystemCompletionHandler:(NSString *)identifier { + GULMutableDictionary *systemCompletionHandlers = + [[self class] sessionIDToSystemCompletionHandlerDictionary]; + GULNetworkSystemCompletionHandler handler = [systemCompletionHandlers objectForKey:identifier]; + + if (handler) { + [systemCompletionHandlers removeObjectForKey:identifier]; + + dispatch_async(dispatch_get_main_queue(), ^{ + handler(); + }); + } +} + +/// Sets or updates the session ID of this session. +- (void)setSessionID:(NSString *)sessionID { + _sessionID = [sessionID copy]; +} + +/// Creates a background session configuration with the session ID using the supported method. +- (NSURLSessionConfiguration *)backgroundSessionConfigWithSessionID:(NSString *)sessionID { +#if (TARGET_OS_OSX && defined(MAC_OS_X_VERSION_10_10) && \ + MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) || \ + TARGET_OS_TV || \ + (TARGET_OS_IOS && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0) + + // iOS 8/10.10 builds require the new backgroundSessionConfiguration method name. + return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID]; + +#elif (TARGET_OS_OSX && defined(MAC_OS_X_VERSION_10_10) && \ + MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) || \ + (TARGET_OS_IOS && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0) + + // Do a runtime check to avoid a deprecation warning about using + // +backgroundSessionConfiguration: on iOS 8. + if ([NSURLSessionConfiguration + respondsToSelector:@selector(backgroundSessionConfigurationWithIdentifier:)]) { + // Running on iOS 8+/OS X 10.10+. + return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID]; + } else { + // Running on iOS 7/OS X 10.9. + return [NSURLSessionConfiguration backgroundSessionConfiguration:sessionID]; + } + +#else + // Building with an SDK earlier than iOS 8/OS X 10.10. + return [NSURLSessionConfiguration backgroundSessionConfiguration:sessionID]; +#endif +} + +- (void)maybeRemoveTempFilesAtURL:(NSURL *)folderURL expiringTime:(NSTimeInterval)staleTime { + if (!folderURL.absoluteString.length) { + return; + } + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error = nil; + + NSArray *properties = @[ NSURLCreationDateKey ]; + NSArray *directoryContent = + [fileManager contentsOfDirectoryAtURL:folderURL + includingPropertiesForKeys:properties + options:NSDirectoryEnumerationSkipsSubdirectoryDescendants + error:&error]; + if (error && error.code != NSFileReadNoSuchFileError) { + [_loggerDelegate + GULNetwork_logWithLevel:kGULNetworkLogLevelDebug + messageCode:kGULNetworkMessageCodeURLSession012 + message:@"Cannot get files from the temporary network folder. Error" + context:error]; + return; + } + + if (!directoryContent.count) { + return; + } + + NSTimeInterval now = [NSDate date].timeIntervalSince1970; + for (NSURL *tempFile in directoryContent) { + NSDate *creationDate; + BOOL getCreationDate = + [tempFile getResourceValue:&creationDate forKey:NSURLCreationDateKey error:NULL]; + if (!getCreationDate) { + continue; + } + NSTimeInterval creationTimeInterval = creationDate.timeIntervalSince1970; + if (fabs(now - creationTimeInterval) > staleTime) { + [self removeTempItemAtURL:tempFile]; + } + } +} + +/// Removes the temporary file written to disk for sending the request. It has to be cleaned up +/// after the session is done. +- (void)removeTempItemAtURL:(NSURL *)fileURL { + if (!fileURL.absoluteString.length) { + return; + } + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error = nil; + + if (![fileManager removeItemAtURL:fileURL error:&error] && error.code != NSFileNoSuchFileError) { + [_loggerDelegate + GULNetwork_logWithLevel:kGULNetworkLogLevelError + messageCode:kGULNetworkMessageCodeURLSession013 + message:@"Failed to remove temporary uploading data file. Error" + context:error.localizedDescription]; + } +} + +/// Gets the fetcher with the session ID. ++ (instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier { + NSMapTable *sessionIdentifierToFetcherMap = [self sessionIDToFetcherMap]; + GULNetworkURLSession *session = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier]; + if (!session && [sessionIdentifier hasPrefix:kGULNetworkBackgroundSessionConfigIDPrefix]) { + session = [[GULNetworkURLSession alloc] initWithNetworkLoggerDelegate:nil]; + [session setSessionID:sessionIdentifier]; + [sessionIdentifierToFetcherMap setObject:session forKey:sessionIdentifier]; + } + return session; +} + +/// Returns a map of the fetcher by session ID. Creates a map if it is not created. ++ (NSMapTable *)sessionIDToFetcherMap { + static NSMapTable *sessionIDToFetcherMap; + + static dispatch_once_t sessionMapOnceToken; + dispatch_once(&sessionMapOnceToken, ^{ + sessionIDToFetcherMap = [NSMapTable strongToWeakObjectsMapTable]; + }); + return sessionIDToFetcherMap; +} + +/// Returns a map of system provided completion handler by session ID. Creates a map if it is not +/// created. ++ (GULMutableDictionary *)sessionIDToSystemCompletionHandlerDictionary { + static GULMutableDictionary *systemCompletionHandlers; + + static dispatch_once_t systemCompletionHandlerOnceToken; + dispatch_once(&systemCompletionHandlerOnceToken, ^{ + systemCompletionHandlers = [[GULMutableDictionary alloc] init]; + }); + return systemCompletionHandlers; +} + +- (NSURL *)temporaryFilePathWithSessionID:(NSString *)sessionID { + NSString *tempName = [NSString stringWithFormat:@"GULUpload_temp_%@", sessionID]; + return [_networkDirectoryURL URLByAppendingPathComponent:tempName]; +} + +/// Makes sure that the directory to store temp files exists. If not, tries to create it and returns +/// YES. If there is anything wrong, returns NO. +- (BOOL)ensureTemporaryDirectoryExists { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error = nil; + + // Create a temporary directory if it does not exist or was deleted. + if ([_networkDirectoryURL checkResourceIsReachableAndReturnError:&error]) { + return YES; + } + + if (error && error.code != NSFileReadNoSuchFileError) { + [_loggerDelegate + GULNetwork_logWithLevel:kGULNetworkLogLevelWarning + messageCode:kGULNetworkMessageCodeURLSession014 + message:@"Error while trying to access Network temp folder. Error" + context:error]; + } + + NSError *writeError = nil; + + [fileManager createDirectoryAtURL:_networkDirectoryURL + withIntermediateDirectories:YES + attributes:nil + error:&writeError]; + if (writeError) { + [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError + messageCode:kGULNetworkMessageCodeURLSession015 + message:@"Cannot create temporary directory. Error" + context:writeError]; + return NO; + } + + // Set the iCloud exclusion attribute on the Documents URL. + [self excludeFromBackupForURL:_networkDirectoryURL]; + + return YES; +} + +- (void)excludeFromBackupForURL:(NSURL *)url { + if (!url.path) { + return; + } + + // Set the iCloud exclusion attribute on the Documents URL. + NSError *preventBackupError = nil; + [url setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&preventBackupError]; + if (preventBackupError) { + [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError + messageCode:kGULNetworkMessageCodeURLSession016 + message:@"Cannot exclude temporary folder from iTunes backup"]; + } +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLRequest *))completionHandler { + NSArray *nonAllowedRedirectionCodes = @[ + @(kGULNetworkHTTPStatusCodeFound), @(kGULNetworkHTTPStatusCodeMovedPermanently), + @(kGULNetworkHTTPStatusCodeMovedTemporarily), @(kGULNetworkHTTPStatusCodeMultipleChoices) + ]; + + // Allow those not in the non allowed list to be followed. + if (![nonAllowedRedirectionCodes containsObject:@(response.statusCode)]) { + completionHandler(request); + return; + } + + // Do not allow redirection if the response code is in the non-allowed list. + NSURLRequest *newRequest = request; + + if (response) { + newRequest = nil; + } + + completionHandler(newRequest); +} + +#pragma mark - Helper Methods + +- (void)callCompletionHandler:(GULNetworkURLSessionCompletionHandler)handler + withResponse:(NSHTTPURLResponse *)response + data:(NSData *)data + error:(NSError *)error { + if (error) { + [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError + messageCode:kGULNetworkMessageCodeURLSession017 + message:@"Encounter network error. Code, error" + contexts:@[ @(error.code), error ]]; + } + + if (handler) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(response, data, self->_sessionID, error); + }); + } +} + +- (void)populateSessionConfig:(NSURLSessionConfiguration *)sessionConfig + withRequest:(NSURLRequest *)request { + sessionConfig.HTTPAdditionalHeaders = request.allHTTPHeaderFields; + sessionConfig.timeoutIntervalForRequest = request.timeoutInterval; + sessionConfig.timeoutIntervalForResource = request.timeoutInterval; + sessionConfig.requestCachePolicy = request.cachePolicy; +} + +@end diff --git a/GoogleUtilities/Network/Private/GULMutableDictionary.h b/GoogleUtilities/Network/Private/GULMutableDictionary.h new file mode 100644 index 0000000..a8cc45b --- /dev/null +++ b/GoogleUtilities/Network/Private/GULMutableDictionary.h @@ -0,0 +1,46 @@ +/* + * 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 + +/// A mutable dictionary that provides atomic accessor and mutators. +@interface GULMutableDictionary : NSObject + +/// Returns an object given a key in the dictionary or nil if not found. +- (id)objectForKey:(id)key; + +/// Updates the object given its key or adds it to the dictionary if it is not in the dictionary. +- (void)setObject:(id)object forKey:(id)key; + +/// Removes the object given its session ID from the dictionary. +- (void)removeObjectForKey:(id)key; + +/// Removes all objects. +- (void)removeAllObjects; + +/// Returns the number of current objects in the dictionary. +- (NSUInteger)count; + +/// Returns an object given a key in the dictionary or nil if not found. +- (id)objectForKeyedSubscript:(id)key; + +/// Updates the object given its key or adds it to the dictionary if it is not in the dictionary. +- (void)setObject:(id)obj forKeyedSubscript:(id)key; + +/// Returns the immutable dictionary. +- (NSDictionary *)dictionary; + +@end diff --git a/GoogleUtilities/Network/Private/GULNetwork.h b/GoogleUtilities/Network/Private/GULNetwork.h new file mode 100644 index 0000000..0e75ae5 --- /dev/null +++ b/GoogleUtilities/Network/Private/GULNetwork.h @@ -0,0 +1,87 @@ +/* + * 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 + +#import "GULNetworkConstants.h" +#import "GULNetworkLoggerProtocol.h" +#import "GULNetworkURLSession.h" + +/// Delegate protocol for GULNetwork events. +@protocol GULNetworkReachabilityDelegate + +/// Tells the delegate to handle events when the network reachability changes to connected or not +/// connected. +- (void)reachabilityDidChange; + +@end + +/// The Network component that provides network status and handles network requests and responses. +/// This is not thread safe. +/// +/// NOTE: +/// User must add FIRAnalytics handleEventsForBackgroundURLSessionID:completionHandler to the +/// AppDelegate application:handleEventsForBackgroundURLSession:completionHandler: +@interface GULNetwork : NSObject + +/// Indicates if network connectivity is available. +@property(nonatomic, readonly, getter=isNetworkConnected) BOOL networkConnected; + +/// Indicates if there are any uploads in progress. +@property(nonatomic, readonly, getter=hasUploadInProgress) BOOL uploadInProgress; + +/// An optional delegate that can be used in the event when network reachability changes. +@property(nonatomic, weak) id reachabilityDelegate; + +/// An optional delegate that can be used to log messages, warnings or errors that occur in the +/// network operations. +@property(nonatomic, weak) id loggerDelegate; + +/// Indicates whether the logger should display debug messages. +@property(nonatomic, assign) BOOL isDebugModeEnabled; + +/// The time interval in seconds for the network request to timeout. +@property(nonatomic, assign) NSTimeInterval timeoutInterval; + +/// Initializes with the default reachability host. +- (instancetype)init; + +/// Initializes with a custom reachability host. +- (instancetype)initWithReachabilityHost:(NSString *)reachabilityHost; + +/// Handles events when background session with the given ID has finished. ++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID + completionHandler:(GULNetworkSystemCompletionHandler)completionHandler; + +/// Compresses and sends a POST request with the provided data to the URL. The session will be +/// background session if usingBackgroundSession is YES. Otherwise, the POST session is default +/// session. Returns a session ID or nil if an error occurs. +- (NSString *)postURL:(NSURL *)url + payload:(NSData *)payload + queue:(dispatch_queue_t)queue + usingBackgroundSession:(BOOL)usingBackgroundSession + completionHandler:(GULNetworkCompletionHandler)handler; + +/// Sends a GET request with the provided data to the URL. The session will be background session +/// if usingBackgroundSession is YES. Otherwise, the GET session is default session. Returns a +/// session ID or nil if an error occurs. +- (NSString *)getURL:(NSURL *)url + headers:(NSDictionary *)headers + queue:(dispatch_queue_t)queue + usingBackgroundSession:(BOOL)usingBackgroundSession + completionHandler:(GULNetworkCompletionHandler)handler; + +@end diff --git a/GoogleUtilities/Network/Private/GULNetworkConstants.h b/GoogleUtilities/Network/Private/GULNetworkConstants.h new file mode 100644 index 0000000..44d440b --- /dev/null +++ b/GoogleUtilities/Network/Private/GULNetworkConstants.h @@ -0,0 +1,79 @@ +/* + * 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 +#import + +/// Error codes in Firebase Network error domain. +/// Note: these error codes should never change. It would make it harder to decode the errors if +/// we inadvertently altered any of these codes in a future SDK version. +typedef NS_ENUM(NSInteger, GULNetworkErrorCode) { + /// Unknown error. + GULNetworkErrorCodeUnknown = 0, + /// Error occurs when the request URL is invalid. + GULErrorCodeNetworkInvalidURL = 1, + /// Error occurs when request cannot be constructed. + GULErrorCodeNetworkRequestCreation = 2, + /// Error occurs when payload cannot be compressed. + GULErrorCodeNetworkPayloadCompression = 3, + /// Error occurs when session task cannot be created. + GULErrorCodeNetworkSessionTaskCreation = 4, + /// Error occurs when there is no response. + GULErrorCodeNetworkInvalidResponse = 5 +}; + +#pragma mark - Network constants + +/// The prefix of the ID of the background session. +extern NSString *const kGULNetworkBackgroundSessionConfigIDPrefix; + +/// The sub directory to store the files of data that is being uploaded in the background. +extern NSString *const kGULNetworkApplicationSupportSubdirectory; + +/// Name of the temporary directory that stores files for background uploading. +extern NSString *const kGULNetworkTempDirectoryName; + +/// The period when the temporary uploading file can stay. +extern const NSTimeInterval kGULNetworkTempFolderExpireTime; + +/// The default network request timeout interval. +extern const NSTimeInterval kGULNetworkTimeOutInterval; + +/// The host to check the reachability of the network. +extern NSString *const kGULNetworkReachabilityHost; + +/// The key to get the error context of the UserInfo. +extern NSString *const kGULNetworkErrorContext; + +#pragma mark - Network Status Code + +extern const int kGULNetworkHTTPStatusOK; +extern const int kGULNetworkHTTPStatusNoContent; +extern const int kGULNetworkHTTPStatusCodeMultipleChoices; +extern const int kGULNetworkHTTPStatusCodeMovedPermanently; +extern const int kGULNetworkHTTPStatusCodeFound; +extern const int kGULNetworkHTTPStatusCodeNotModified; +extern const int kGULNetworkHTTPStatusCodeMovedTemporarily; +extern const int kGULNetworkHTTPStatusCodeNotFound; +extern const int kGULNetworkHTTPStatusCodeCannotAcceptTraffic; +extern const int kGULNetworkHTTPStatusCodeUnavailable; + +#pragma mark - Error Domain + +extern NSString *const kGULNetworkErrorDomain; + +/// The logger service for GULNetwork. +extern GULLoggerService kGULLoggerNetwork; diff --git a/GoogleUtilities/Network/Private/GULNetworkLoggerProtocol.h b/GoogleUtilities/Network/Private/GULNetworkLoggerProtocol.h new file mode 100644 index 0000000..f1be590 --- /dev/null +++ b/GoogleUtilities/Network/Private/GULNetworkLoggerProtocol.h @@ -0,0 +1,51 @@ +/* + * 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 + +#import + +#import "GULNetworkMessageCode.h" + +/// The log levels used by GULNetworkLogger. +typedef NS_ENUM(NSInteger, GULNetworkLogLevel) { + kGULNetworkLogLevelError = GULLoggerLevelError, + kGULNetworkLogLevelWarning = GULLoggerLevelWarning, + kGULNetworkLogLevelInfo = GULLoggerLevelInfo, + kGULNetworkLogLevelDebug = GULLoggerLevelDebug, +}; + +@protocol GULNetworkLoggerDelegate + +@required +/// Tells the delegate to log a message with an array of contexts and the log level. +- (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel + messageCode:(GULNetworkMessageCode)messageCode + message:(NSString *)message + contexts:(NSArray *)contexts; + +/// Tells the delegate to log a message with a context and the log level. +- (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel + messageCode:(GULNetworkMessageCode)messageCode + message:(NSString *)message + context:(id)context; + +/// Tells the delegate to log a message with the log level. +- (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel + messageCode:(GULNetworkMessageCode)messageCode + message:(NSString *)message; + +@end diff --git a/GoogleUtilities/Network/Private/GULNetworkMessageCode.h b/GoogleUtilities/Network/Private/GULNetworkMessageCode.h new file mode 100644 index 0000000..ce78e60 --- /dev/null +++ b/GoogleUtilities/Network/Private/GULNetworkMessageCode.h @@ -0,0 +1,44 @@ +/* + * 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. + */ + +// Make sure these codes do not overlap with any contained in the FIRAMessageCode enum. +typedef NS_ENUM(NSInteger, GULNetworkMessageCode) { + // GULNetwork.m + kGULNetworkMessageCodeNetwork000 = 900000, // I-NET900000 + kGULNetworkMessageCodeNetwork001 = 900001, // I-NET900001 + kGULNetworkMessageCodeNetwork002 = 900002, // I-NET900002 + kGULNetworkMessageCodeNetwork003 = 900003, // I-NET900003 + // GULNetworkURLSession.m + kGULNetworkMessageCodeURLSession000 = 901000, // I-NET901000 + kGULNetworkMessageCodeURLSession001 = 901001, // I-NET901001 + kGULNetworkMessageCodeURLSession002 = 901002, // I-NET901002 + kGULNetworkMessageCodeURLSession003 = 901003, // I-NET901003 + kGULNetworkMessageCodeURLSession004 = 901004, // I-NET901004 + kGULNetworkMessageCodeURLSession005 = 901005, // I-NET901005 + kGULNetworkMessageCodeURLSession006 = 901006, // I-NET901006 + kGULNetworkMessageCodeURLSession007 = 901007, // I-NET901007 + kGULNetworkMessageCodeURLSession008 = 901008, // I-NET901008 + kGULNetworkMessageCodeURLSession009 = 901009, // I-NET901009 + kGULNetworkMessageCodeURLSession010 = 901010, // I-NET901010 + kGULNetworkMessageCodeURLSession011 = 901011, // I-NET901011 + kGULNetworkMessageCodeURLSession012 = 901012, // I-NET901012 + kGULNetworkMessageCodeURLSession013 = 901013, // I-NET901013 + kGULNetworkMessageCodeURLSession014 = 901014, // I-NET901014 + kGULNetworkMessageCodeURLSession015 = 901015, // I-NET901015 + kGULNetworkMessageCodeURLSession016 = 901016, // I-NET901016 + kGULNetworkMessageCodeURLSession017 = 901017, // I-NET901017 + kGULNetworkMessageCodeURLSession018 = 901018, // I-NET901018 +}; diff --git a/GoogleUtilities/Network/Private/GULNetworkURLSession.h b/GoogleUtilities/Network/Private/GULNetworkURLSession.h new file mode 100644 index 0000000..81190c6 --- /dev/null +++ b/GoogleUtilities/Network/Private/GULNetworkURLSession.h @@ -0,0 +1,60 @@ +/* + * 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 + +#import "GULNetworkLoggerProtocol.h" + +typedef void (^GULNetworkCompletionHandler)(NSHTTPURLResponse *response, + NSData *data, + NSError *error); +typedef void (^GULNetworkURLSessionCompletionHandler)(NSHTTPURLResponse *response, + NSData *data, + NSString *sessionID, + NSError *error); +typedef void (^GULNetworkSystemCompletionHandler)(void); + +/// The protocol that uses NSURLSession for iOS >= 7.0 to handle requests and responses. +@interface GULNetworkURLSession + : NSObject + +/// Indicates whether the background network is enabled. Default value is NO. +@property(nonatomic, getter=isBackgroundNetworkEnabled) BOOL backgroundNetworkEnabled; + +/// The logger delegate to log message, errors or warnings that occur during the network operations. +@property(nonatomic, weak) id loggerDelegate; + +/// Calls the system provided completion handler after the background session is finished. ++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID + completionHandler:(GULNetworkSystemCompletionHandler)completionHandler; + +/// Initializes with logger delegate. +- (instancetype)initWithNetworkLoggerDelegate:(id)networkLoggerDelegate + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/// Sends an asynchronous POST request and calls the provided completion handler when the request +/// completes or when errors occur, and returns an ID of the session/connection. +- (NSString *)sessionIDFromAsyncPOSTRequest:(NSURLRequest *)request + completionHandler:(GULNetworkURLSessionCompletionHandler)handler; + +/// Sends an asynchronous GET request and calls the provided completion handler when the request +/// completes or when errors occur, and returns an ID of the session. +- (NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request + completionHandler:(GULNetworkURLSessionCompletionHandler)handler; + +@end -- cgit v1.2.3