aboutsummaryrefslogtreecommitdiffhomepage
path: root/GoogleUtilities/Network
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2018-07-11 08:28:35 -0700
committerGravatar GitHub <noreply@github.com>2018-07-11 08:28:35 -0700
commitc6b4b03fffc3cea7c9525e5c79dce28f52900521 (patch)
tree0e8a237940dcd4b4100c00b9fac428e657619ab8 /GoogleUtilities/Network
parent25f8691970a9f765a87ab3125776598c92e02744 (diff)
Move GoogleUtilities out of Firebase directory (#1516)
Diffstat (limited to 'GoogleUtilities/Network')
-rw-r--r--GoogleUtilities/Network/GULMutableDictionary.m97
-rw-r--r--GoogleUtilities/Network/GULNetwork.m388
-rw-r--r--GoogleUtilities/Network/GULNetworkConstants.m40
-rw-r--r--GoogleUtilities/Network/GULNetworkURLSession.m669
-rw-r--r--GoogleUtilities/Network/Private/GULMutableDictionary.h46
-rw-r--r--GoogleUtilities/Network/Private/GULNetwork.h87
-rw-r--r--GoogleUtilities/Network/Private/GULNetworkConstants.h79
-rw-r--r--GoogleUtilities/Network/Private/GULNetworkLoggerProtocol.h51
-rw-r--r--GoogleUtilities/Network/Private/GULNetworkMessageCode.h44
-rw-r--r--GoogleUtilities/Network/Private/GULNetworkURLSession.h60
10 files changed, 1561 insertions, 0 deletions
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<NSCopying>)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<NSCopying>)key {
+ // The method this calls is already synchronized.
+ return [self objectForKey:key];
+}
+
+- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)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 <GoogleUtilities/GULLogger.h>
+#import <GoogleUtilities/GULNSData+zlib.h>
+#import <GoogleUtilities/GULReachabilityChecker.h>
+#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 () <GULReachabilityDelegate, GULNetworkLoggerDelegate>
+@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<GULNetworkLoggerDelegate>)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 <Foundation/Foundation.h>
+
+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 <Foundation/Foundation.h>
+
+#import "Private/GULNetworkURLSession.h"
+
+#import <GoogleUtilities/GULLogger.h>
+#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<GULNetworkLoggerDelegate>)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 <Foundation/Foundation.h>
+
+/// 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<NSCopying>)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<NSCopying>)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<NSCopying>)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 <Foundation/Foundation.h>
+
+#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<GULNetworkReachabilityDelegate> reachabilityDelegate;
+
+/// An optional delegate that can be used to log messages, warnings or errors that occur in the
+/// network operations.
+@property(nonatomic, weak) id<GULNetworkLoggerDelegate> 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 <Foundation/Foundation.h>
+#import <GoogleUtilities/GULLogger.h>
+
+/// 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 <Foundation/Foundation.h>
+
+#import <GoogleUtilities/GULLoggerLevel.h>
+
+#import "GULNetworkMessageCode.h"
+
+/// The log levels used by GULNetworkLogger.
+typedef NS_ENUM(NSInteger, GULNetworkLogLevel) {
+ kGULNetworkLogLevelError = GULLoggerLevelError,
+ kGULNetworkLogLevelWarning = GULLoggerLevelWarning,
+ kGULNetworkLogLevelInfo = GULLoggerLevelInfo,
+ kGULNetworkLogLevelDebug = GULLoggerLevelDebug,
+};
+
+@protocol GULNetworkLoggerDelegate <NSObject>
+
+@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 <Foundation/Foundation.h>
+
+#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 <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate>
+
+/// 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<GULNetworkLoggerDelegate> 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<GULNetworkLoggerDelegate>)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