1 // Copyright 2017 Google
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
7 // http://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
15 #import "Private/GULNetwork.h"
16 #import "Private/GULNetworkMessageCode.h"
18 #import <GoogleUtilities/GULLogger.h>
19 #import <GoogleUtilities/GULNSData+zlib.h>
20 #import <GoogleUtilities/GULReachabilityChecker.h>
21 #import "Private/GULMutableDictionary.h"
22 #import "Private/GULNetworkConstants.h"
24 /// Constant string for request header Content-Encoding.
25 static NSString *const kGULNetworkContentCompressionKey = @"Content-Encoding";
27 /// Constant string for request header Content-Encoding value.
28 static NSString *const kGULNetworkContentCompressionValue = @"gzip";
30 /// Constant string for request header Content-Length.
31 static NSString *const kGULNetworkContentLengthKey = @"Content-Length";
33 /// Constant string for request header Content-Type.
34 static NSString *const kGULNetworkContentTypeKey = @"Content-Type";
36 /// Constant string for request header Content-Type value.
37 static NSString *const kGULNetworkContentTypeValue = @"application/x-www-form-urlencoded";
39 /// Constant string for GET request method.
40 static NSString *const kGULNetworkGETRequestMethod = @"GET";
42 /// Constant string for POST request method.
43 static NSString *const kGULNetworkPOSTRequestMethod = @"POST";
45 /// Default constant string as a prefix for network logger.
46 static NSString *const kGULNetworkLogTag = @"Google/Utilities/Network";
48 @interface GULNetwork () <GULReachabilityDelegate, GULNetworkLoggerDelegate>
51 @implementation GULNetwork {
52 /// Network reachability.
53 GULReachabilityChecker *_reachability;
55 /// The dictionary of requests by session IDs { NSString : id }.
56 GULMutableDictionary *_requests;
59 - (instancetype)init {
60 return [self initWithReachabilityHost:kGULNetworkReachabilityHost];
63 - (instancetype)initWithReachabilityHost:(NSString *)reachabilityHost {
66 // Setup reachability.
67 _reachability = [[GULReachabilityChecker alloc] initWithReachabilityDelegate:self
68 withHost:reachabilityHost];
69 if (![_reachability start]) {
73 _requests = [[GULMutableDictionary alloc] init];
74 _timeoutInterval = kGULNetworkTimeOutInterval;
80 _reachability.reachabilityDelegate = nil;
84 #pragma mark - External Methods
86 + (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
87 completionHandler:(GULNetworkSystemCompletionHandler)completionHandler {
88 [GULNetworkURLSession handleEventsForBackgroundURLSessionID:sessionID
89 completionHandler:completionHandler];
92 - (NSString *)postURL:(NSURL *)url
93 payload:(NSData *)payload
94 queue:(dispatch_queue_t)queue
95 usingBackgroundSession:(BOOL)usingBackgroundSession
96 completionHandler:(GULNetworkCompletionHandler)handler {
97 if (!url.absoluteString.length) {
98 [self handleErrorWithCode:GULErrorCodeNetworkInvalidURL queue:queue withHandler:handler];
102 NSTimeInterval timeOutInterval = _timeoutInterval ?: kGULNetworkTimeOutInterval;
104 NSMutableURLRequest *request =
105 [[NSMutableURLRequest alloc] initWithURL:url
106 cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
107 timeoutInterval:timeOutInterval];
110 [self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation
112 withHandler:handler];
116 NSError *compressError = nil;
117 NSData *compressedData = [NSData gul_dataByGzippingData:payload error:&compressError];
118 if (!compressedData || compressError) {
119 if (compressError || payload.length > 0) {
120 // If the payload is not empty but it fails to compress the payload, something has been wrong.
121 [self handleErrorWithCode:GULErrorCodeNetworkPayloadCompression
123 withHandler:handler];
126 compressedData = [[NSData alloc] init];
129 NSString *postLength = @(compressedData.length).stringValue;
131 // Set up the request with the compressed data.
132 [request setValue:postLength forHTTPHeaderField:kGULNetworkContentLengthKey];
133 request.HTTPBody = compressedData;
134 request.HTTPMethod = kGULNetworkPOSTRequestMethod;
135 [request setValue:kGULNetworkContentTypeValue forHTTPHeaderField:kGULNetworkContentTypeKey];
136 [request setValue:kGULNetworkContentCompressionValue
137 forHTTPHeaderField:kGULNetworkContentCompressionKey];
139 GULNetworkURLSession *fetcher = [[GULNetworkURLSession alloc] initWithNetworkLoggerDelegate:self];
140 fetcher.backgroundNetworkEnabled = usingBackgroundSession;
142 __weak GULNetwork *weakSelf = self;
143 NSString *requestID = [fetcher
144 sessionIDFromAsyncPOSTRequest:request
145 completionHandler:^(NSHTTPURLResponse *response, NSData *data,
146 NSString *sessionID, NSError *error) {
147 GULNetwork *strongSelf = weakSelf;
151 dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
152 dispatch_async(queueToDispatch, ^{
153 if (sessionID.length) {
154 [strongSelf->_requests removeObjectForKey:sessionID];
157 handler(response, data, error);
162 [self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation
164 withHandler:handler];
168 [self GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
169 messageCode:kGULNetworkMessageCodeNetwork000
170 message:@"Uploading data. Host"
172 _requests[requestID] = fetcher;
176 - (NSString *)getURL:(NSURL *)url
177 headers:(NSDictionary *)headers
178 queue:(dispatch_queue_t)queue
179 usingBackgroundSession:(BOOL)usingBackgroundSession
180 completionHandler:(GULNetworkCompletionHandler)handler {
181 if (!url.absoluteString.length) {
182 [self handleErrorWithCode:GULErrorCodeNetworkInvalidURL queue:queue withHandler:handler];
186 NSTimeInterval timeOutInterval = _timeoutInterval ?: kGULNetworkTimeOutInterval;
187 NSMutableURLRequest *request =
188 [[NSMutableURLRequest alloc] initWithURL:url
189 cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
190 timeoutInterval:timeOutInterval];
193 [self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation
195 withHandler:handler];
199 request.HTTPMethod = kGULNetworkGETRequestMethod;
200 request.allHTTPHeaderFields = headers;
202 GULNetworkURLSession *fetcher = [[GULNetworkURLSession alloc] initWithNetworkLoggerDelegate:self];
203 fetcher.backgroundNetworkEnabled = usingBackgroundSession;
205 __weak GULNetwork *weakSelf = self;
206 NSString *requestID = [fetcher
207 sessionIDFromAsyncGETRequest:request
208 completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSString *sessionID,
210 GULNetwork *strongSelf = weakSelf;
214 dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
215 dispatch_async(queueToDispatch, ^{
216 if (sessionID.length) {
217 [strongSelf->_requests removeObjectForKey:sessionID];
220 handler(response, data, error);
226 [self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation
228 withHandler:handler];
232 [self GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
233 messageCode:kGULNetworkMessageCodeNetwork001
234 message:@"Downloading data. Host"
236 _requests[requestID] = fetcher;
240 - (BOOL)hasUploadInProgress {
241 return _requests.count > 0;
244 #pragma mark - Network Reachability
246 /// Tells reachability delegate to call reachabilityDidChangeToStatus: to notify the network
247 /// reachability has changed.
248 - (void)reachability:(GULReachabilityChecker *)reachability
249 statusChanged:(GULReachabilityStatus)status {
250 _networkConnected = (status == kGULReachabilityViaCellular || status == kGULReachabilityViaWifi);
251 [_reachabilityDelegate reachabilityDidChange];
254 #pragma mark - Network logger delegate
256 - (void)setLoggerDelegate:(id<GULNetworkLoggerDelegate>)loggerDelegate {
257 // Explicitly check whether the delegate responds to the methods because conformsToProtocol does
258 // not work correctly even though the delegate does respond to the methods.
259 if (!loggerDelegate ||
261 respondsToSelector:@selector(GULNetwork_logWithLevel:messageCode:message:contexts:)] ||
263 respondsToSelector:@selector(GULNetwork_logWithLevel:messageCode:message:context:)] ||
265 [loggerDelegate respondsToSelector:@selector(GULNetwork_logWithLevel:messageCode:message:)]) {
266 GULLogError(kGULLoggerNetwork, NO,
267 [NSString stringWithFormat:@"I-NET%06ld", (long)kGULNetworkMessageCodeNetwork002],
268 @"Cannot set the network logger delegate: delegate does not conform to the network "
272 _loggerDelegate = loggerDelegate;
275 #pragma mark - Private methods
277 /// Handles network error and calls completion handler with the error.
278 - (void)handleErrorWithCode:(NSInteger)code
279 queue:(dispatch_queue_t)queue
280 withHandler:(GULNetworkCompletionHandler)handler {
281 NSDictionary *userInfo = @{kGULNetworkErrorContext : @"Failed to create network request"};
283 [[NSError alloc] initWithDomain:kGULNetworkErrorDomain code:code userInfo:userInfo];
284 [self GULNetwork_logWithLevel:kGULNetworkLogLevelWarning
285 messageCode:kGULNetworkMessageCodeNetwork002
286 message:@"Failed to create network request. Code, error"
287 contexts:@[ @(code), error ]];
289 dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
290 dispatch_async(queueToDispatch, ^{
291 handler(nil, nil, error);
296 #pragma mark - Network logger
298 - (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel
299 messageCode:(GULNetworkMessageCode)messageCode
300 message:(NSString *)message
301 contexts:(NSArray *)contexts {
302 // Let the delegate log the message if there is a valid logger delegate. Otherwise, just log
303 // errors/warnings/info messages to the console log.
304 if (_loggerDelegate) {
305 [_loggerDelegate GULNetwork_logWithLevel:logLevel
306 messageCode:messageCode
311 if (_isDebugModeEnabled || logLevel == kGULNetworkLogLevelError ||
312 logLevel == kGULNetworkLogLevelWarning || logLevel == kGULNetworkLogLevelInfo) {
313 NSString *formattedMessage = GULStringWithLogMessage(message, logLevel, contexts);
314 NSLog(@"%@", formattedMessage);
315 GULLogBasic((GULLoggerLevel)logLevel, kGULLoggerNetwork, NO,
316 [NSString stringWithFormat:@"I-NET%06ld", (long)messageCode], formattedMessage,
321 - (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel
322 messageCode:(GULNetworkMessageCode)messageCode
323 message:(NSString *)message
324 context:(id)context {
325 if (_loggerDelegate) {
326 [_loggerDelegate GULNetwork_logWithLevel:logLevel
327 messageCode:messageCode
332 NSArray *contexts = context ? @[ context ] : @[];
333 [self GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:contexts];
336 - (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel
337 messageCode:(GULNetworkMessageCode)messageCode
338 message:(NSString *)message {
339 if (_loggerDelegate) {
340 [_loggerDelegate GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message];
343 [self GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:@[]];
346 /// Returns a string for the given log level (e.g. kGULNetworkLogLevelError -> @"ERROR").
347 static NSString *GULLogLevelDescriptionFromLogLevel(GULNetworkLogLevel logLevel) {
348 static NSDictionary *levelNames = nil;
349 static dispatch_once_t onceToken;
350 dispatch_once(&onceToken, ^{
352 @(kGULNetworkLogLevelError) : @"ERROR",
353 @(kGULNetworkLogLevelWarning) : @"WARNING",
354 @(kGULNetworkLogLevelInfo) : @"INFO",
355 @(kGULNetworkLogLevelDebug) : @"DEBUG"
358 return levelNames[@(logLevel)];
361 /// Returns a formatted string to be used for console logging.
362 static NSString *GULStringWithLogMessage(NSString *message,
363 GULNetworkLogLevel logLevel,
366 message = @"(Message was nil)";
367 } else if (!message.length) {
368 message = @"(Message was empty)";
370 NSMutableString *result = [[NSMutableString alloc]
371 initWithFormat:@"<%@/%@> %@", kGULNetworkLogTag, GULLogLevelDescriptionFromLogLevel(logLevel),
374 if (!contexts.count) {
378 NSMutableArray *formattedContexts = [[NSMutableArray alloc] init];
379 for (id item in contexts) {
380 [formattedContexts addObject:(item != [NSNull null] ? item : @"(nil)")];
383 [result appendString:@": "];
384 [result appendString:[formattedContexts componentsJoinedByString:@", "]];