You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SignalServiceKit/src/Network/API/TSNetworkManager.m

457 lines
18 KiB
Matlab

10 years ago
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
10 years ago
//
#import "TSNetworkManager.h"
#import "AppContext.h"
#import "NSError+messageSending.h"
10 years ago
#import "NSURLSessionDataTask+StatusCode.h"
#import "OWSError.h"
#import "OWSSignalService.h"
#import "SSKEnvironment.h"
10 years ago
#import "TSAccountManager.h"
#import "TSRequest.h"
#import <AFNetworking/AFNetworking.h>
#import <SignalCoreKit/NSData+OWS.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
10 years ago
NSErrorDomain const TSNetworkManagerErrorDomain = @"SignalServiceKit.TSNetworkManager";
10 years ago
BOOL IsNSErrorNetworkFailure(NSError *_Nullable error)
{
return ([error.domain isEqualToString:TSNetworkManagerErrorDomain]
&& error.code == TSNetworkManagerErrorFailedConnection);
}
10 years ago
@interface TSNetworkManager ()
// This property should only be accessed on udSerialQueue.
@property (atomic, readonly) AFHTTPSessionManager *udSessionManager;
@property (atomic, readonly) NSDictionary *udSessionManagerDefaultHeaders;
7 years ago
@property (atomic, readonly) dispatch_queue_t udSerialQueue;
10 years ago
typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
@end
@implementation TSNetworkManager
@synthesize udSessionManager = _udSessionManager;
7 years ago
@synthesize udSerialQueue = _udSerialQueue;
10 years ago
#pragma mark Singleton implementation
+ (instancetype)sharedManager
{
OWSAssertDebug(SSKEnvironment.shared.networkManager);
10 years ago
return SSKEnvironment.shared.networkManager;
}
- (instancetype)initDefault
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
9 years ago
{
self = [super init];
if (!self) {
return self;
10 years ago
}
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
9 years ago
7 years ago
_udSerialQueue = dispatch_queue_create("org.whispersystems.networkManager.udQueue", DISPATCH_QUEUE_SERIAL);
OWSSingletonAssert();
10 years ago
return self;
}
#pragma mark Manager Methods
- (void)makeRequest:(TSRequest *)request
success:(TSNetworkManagerSuccess)success
failure:(TSNetworkManagerFailure)failure
{
7 years ago
return [self makeRequest:request completionQueue:dispatch_get_main_queue() success:success failure:failure];
}
- (void)makeRequest:(TSRequest *)request
7 years ago
completionQueue:(dispatch_queue_t)completionQueue
success:(TSNetworkManagerSuccess)successBlock
7 years ago
failure:(TSNetworkManagerFailure)failureBlock
{
OWSAssertDebug(request);
OWSAssertDebug(successBlock);
OWSAssertDebug(failureBlock);
if (request.isUDRequest) {
dispatch_async(self.udSerialQueue, ^{
[self makeUDRequestSync:request success:successBlock failure:failureBlock];
});
} else {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self makeRequestSync:request completionQueue:completionQueue success:successBlock failure:failureBlock];
});
}
}
7 years ago
- (void)makeRequestSync:(TSRequest *)request
completionQueue:(dispatch_queue_t)completionQueue
success:(TSNetworkManagerSuccess)successBlock
failure:(TSNetworkManagerFailure)failureBlock
{
OWSAssertDebug(request);
OWSAssertDebug(successBlock);
OWSAssertDebug(failureBlock);
OWSLogInfo(@"Making Non-UD request: %@", request);
// TODO: Remove this logging when the call connection issues have been resolved.
TSNetworkManagerSuccess success = ^(NSURLSessionDataTask *task, _Nullable id responseObject) {
OWSLogInfo(@"Non-UD request succeeded : %@", request);
if (request.shouldHaveAuthorizationHeaders) {
[TSAccountManager.sharedInstance setIsDeregistered:NO];
}
successBlock(task, responseObject);
7 years ago
[OutageDetection.sharedManager reportConnectionSuccess];
};
TSNetworkManagerFailure failure = [TSNetworkManager errorPrettifyingForFailureBlock:failureBlock request:request];
10 years ago
AFHTTPSessionManager *sessionManager = [OWSSignalService sharedInstance].signalServiceSessionManager;
// [OWSSignalService signalServiceSessionManager] always returns a new instance of
// session manager, so its safe to reconfigure it here.
7 years ago
sessionManager.completionQueue = completionQueue;
10 years ago
if (request.shouldHaveAuthorizationHeaders) {
[sessionManager.requestSerializer setAuthorizationHeaderFieldWithUsername:request.authUsername
password:request.authPassword];
}
10 years ago
// Honor the request's headers.
for (NSString *headerField in request.allHTTPHeaderFields) {
NSString *headerValue = request.allHTTPHeaderFields[headerField];
[sessionManager.requestSerializer setValue:headerValue forHTTPHeaderField:headerField];
}
[self performRequest:request sessionManager:sessionManager success:success failure:failure];
}
// This method should only be invoked on udSerialQueue.
- (AFHTTPSessionManager *)udSessionManager
{
if (!_udSessionManager) {
AFHTTPSessionManager *udSessionManager = [OWSSignalService sharedInstance].signalServiceSessionManager;
udSessionManager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// NOTE: We could enable HTTPShouldUsePipelining here.
_udSessionManager = udSessionManager;
// Make a copy of the default headers for this session manager.
_udSessionManagerDefaultHeaders = [udSessionManager.requestSerializer.HTTPRequestHeaders copy];
}
return _udSessionManager;
}
- (void)makeUDRequestSync:(TSRequest *)request
success:(TSNetworkManagerSuccess)successBlock
failure:(TSNetworkManagerFailure)failureBlock
{
OWSAssertDebug(request);
OWSAssert(!request.shouldHaveAuthorizationHeaders);
OWSAssertDebug(successBlock);
OWSAssertDebug(failureBlock);
OWSLogInfo(@"Making UD request: %@", request);
TSNetworkManagerSuccess success = ^(NSURLSessionDataTask *task, _Nullable id responseObject) {
OWSLogInfo(@"UD request succeeded : %@", request);
successBlock(task, responseObject);
[OutageDetection.sharedManager reportConnectionSuccess];
};
TSNetworkManagerFailure failure = [TSNetworkManager errorPrettifyingForFailureBlock:failureBlock request:request];
AFHTTPSessionManager *sessionManager = self.udSessionManager;
// Clear all headers so that we don't retain headers from previous requests.
for (NSString *headerField in sessionManager.requestSerializer.HTTPRequestHeaders.allKeys.copy) {
[sessionManager.requestSerializer setValue:nil forHTTPHeaderField:headerField];
}
// Apply the default headers for this session manager.
for (NSString *headerField in self.udSessionManagerDefaultHeaders) {
NSString *headerValue = self.udSessionManagerDefaultHeaders[headerField];
[sessionManager.requestSerializer setValue:headerValue forHTTPHeaderField:headerField];
}
// Honor the request's headers.
for (NSString *headerField in request.allHTTPHeaderFields) {
NSString *headerValue = request.allHTTPHeaderFields[headerField];
[sessionManager.requestSerializer setValue:headerValue forHTTPHeaderField:headerField];
}
[self performRequest:request sessionManager:sessionManager success:success failure:failure];
}
- (void)performRequest:(TSRequest *)request
sessionManager:(AFHTTPSessionManager *)sessionManager
success:(TSNetworkManagerSuccess)success
failure:(TSNetworkManagerFailure)failure
{
OWSAssertDebug(request);
OWSAssertDebug(sessionManager);
OWSAssertDebug(success);
OWSAssertDebug(failure);
if ([request.HTTPMethod isEqualToString:@"GET"]) {
[sessionManager GET:request.URL.absoluteString
parameters:request.parameters
progress:nil
success:success
failure:failure];
} else if ([request.HTTPMethod isEqualToString:@"POST"]) {
[sessionManager POST:request.URL.absoluteString
parameters:request.parameters
progress:nil
success:success
failure:failure];
} else if ([request.HTTPMethod isEqualToString:@"PUT"]) {
[sessionManager PUT:request.URL.absoluteString parameters:request.parameters success:success failure:failure];
} else if ([request.HTTPMethod isEqualToString:@"DELETE"]) {
[sessionManager DELETE:request.URL.absoluteString
parameters:request.parameters
success:success
failure:failure];
} else {
OWSLogError(@"Trying to perform HTTP operation with unknown verb: %@", request.HTTPMethod);
10 years ago
}
}
#ifdef DEBUG
+ (void)logCurlForTask:(NSURLSessionDataTask *)task
{
NSMutableArray<NSString *> *curlComponents = [NSMutableArray new];
[curlComponents addObject:@"curl"];
// Verbose
[curlComponents addObject:@"-v"];
// Insecure
[curlComponents addObject:@"-k"];
// Method, e.g. GET
[curlComponents addObject:@"-X"];
[curlComponents addObject:task.originalRequest.HTTPMethod];
// Headers
for (NSString *header in task.originalRequest.allHTTPHeaderFields) {
NSString *headerValue = task.originalRequest.allHTTPHeaderFields[header];
// We don't yet support escaping header values.
// If these asserts trip, we'll need to add that.
OWSAssertDebug([header rangeOfString:@"'"].location == NSNotFound);
OWSAssertDebug([headerValue rangeOfString:@"'"].location == NSNotFound);
[curlComponents addObject:@"-H"];
[curlComponents addObject:[NSString stringWithFormat:@"'%@: %@'", header, headerValue]];
}
// Body/parameters (e.g. JSON payload)
if (task.originalRequest.HTTPBody) {
NSString *jsonBody =
[[NSString alloc] initWithData:task.originalRequest.HTTPBody encoding:NSUTF8StringEncoding];
// We don't yet support escaping JSON.
// If these asserts trip, we'll need to add that.
OWSAssertDebug([jsonBody rangeOfString:@"'"].location == NSNotFound);
[curlComponents addObject:@"--data-ascii"];
[curlComponents addObject:[NSString stringWithFormat:@"'%@'", jsonBody]];
}
// TODO: Add support for cookies.
[curlComponents addObject:task.originalRequest.URL.absoluteString];
NSString *curlCommand = [curlComponents componentsJoinedByString:@" "];
OWSLogVerbose(@"curl for failed request: %@", curlCommand);
}
#endif
+ (failureBlock)errorPrettifyingForFailureBlock:(failureBlock)failureBlock request:(TSRequest *)request
{
OWSAssertDebug(failureBlock);
OWSAssertDebug(request);
10 years ago
return ^(NSURLSessionDataTask *_Nullable task, NSError *_Nonnull networkError) {
NSInteger statusCode = [task statusCode];
#ifdef DEBUG
[TSNetworkManager logCurlForTask:task];
#endif
7 years ago
[OutageDetection.sharedManager reportConnectionFailure];
7 years ago
NSError *error = [self errorWithHTTPCode:statusCode
10 years ago
description:nil
failureReason:nil
recoverySuggestion:nil
fallbackError:networkError];
switch (statusCode) {
case 0: {
NSError *connectivityError =
[self errorWithHTTPCode:TSNetworkManagerErrorFailedConnection
description:NSLocalizedString(@"ERROR_DESCRIPTION_NO_INTERNET",
@"Generic error used whenever Signal can't contact the server")
failureReason:networkError.localizedFailureReason
recoverySuggestion:NSLocalizedString(@"NETWORK_ERROR_RECOVERY", nil)
fallbackError:networkError];
connectivityError.isRetryable = YES;
OWSLogWarn(@"The network request failed because of a connectivity error: %@", request);
failureBlock(task, connectivityError);
10 years ago
break;
}
case 400: {
OWSLogError(
@"The request contains an invalid parameter : %@, %@", networkError.debugDescription, request);
error.isRetryable = NO;
10 years ago
failureBlock(task, error);
break;
}
case 401: {
OWSLogError(@"The server returned an error about the authorization header: %@, %@",
networkError.debugDescription,
request);
error.isRetryable = NO;
[self deregisterAfterAuthErrorIfNecessary:task request:request statusCode:statusCode];
10 years ago
failureBlock(task, error);
break;
}
case 403: {
OWSLogError(
@"The server returned an authentication failure: %@, %@", networkError.debugDescription, request);
error.isRetryable = NO;
[self deregisterAfterAuthErrorIfNecessary:task request:request statusCode:statusCode];
10 years ago
failureBlock(task, error);
break;
}
case 404: {
OWSLogError(@"The requested resource could not be found: %@, %@", networkError.debugDescription, request);
error.isRetryable = NO;
10 years ago
failureBlock(task, error);
break;
}
case 411: {
OWSLogInfo(
@"Multi-device pairing: %ld, %@, %@", (long)statusCode, networkError.debugDescription, request);
NSError *customError =
[self errorWithHTTPCode:statusCode
description:NSLocalizedString(@"MULTIDEVICE_PAIRING_MAX_DESC",
@"alert title: cannot link - reached max linked devices")
failureReason:networkError.localizedFailureReason
recoverySuggestion:NSLocalizedString(@"MULTIDEVICE_PAIRING_MAX_RECOVERY",
@"alert body: cannot link - reached max linked devices")
fallbackError:networkError];
customError.isRetryable = NO;
failureBlock(task, customError);
10 years ago
break;
}
case 413: {
OWSLogWarn(@"Rate limit exceeded: %@", request);
NSError *customError = [self errorWithHTTPCode:statusCode
description:NSLocalizedString(@"REGISTER_RATE_LIMITING_ERROR", nil)
failureReason:networkError.localizedFailureReason
recoverySuggestion:NSLocalizedString(@"REGISTER_RATE_LIMITING_BODY", nil)
fallbackError:networkError];
customError.isRetryable = NO;
failureBlock(task, customError);
10 years ago
break;
}
case 417: {
// TODO: Is this response code obsolete?
OWSLogWarn(@"The number is already registered on a relay. Please unregister there first: %@", request);
NSError *customError = [self errorWithHTTPCode:statusCode
description:NSLocalizedString(@"REGISTRATION_ERROR", nil)
failureReason:networkError.localizedFailureReason
recoverySuggestion:NSLocalizedString(@"RELAY_REGISTERED_ERROR_RECOVERY", nil)
fallbackError:networkError];
customError.isRetryable = NO;
failureBlock(task, customError);
10 years ago
break;
}
case 422: {
OWSLogError(@"The registration was requested over an unknown transport: %@, %@",
networkError.debugDescription,
request);
error.isRetryable = NO;
10 years ago
failureBlock(task, error);
break;
}
default: {
OWSLogWarn(@"Unknown error: %ld, %@, %@", (long)statusCode, networkError.debugDescription, request);
error.isRetryable = NO;
10 years ago
failureBlock(task, error);
break;
}
}
};
}
+ (void)deregisterAfterAuthErrorIfNecessary:(NSURLSessionDataTask *)task
request:(TSRequest *)request
statusCode:(NSInteger)statusCode {
OWSLogVerbose(@"Invalid auth: %@", task.originalRequest.allHTTPHeaderFields);
// We only want to de-register for:
//
// * Auth errors...
// * ...received from Signal service...
// * ...that used standard authorization.
//
// * We don't want want to deregister for:
//
// * CDS requests.
// * Requests using UD auth.
// * etc.
if ([task.originalRequest.URL.absoluteString hasPrefix:textSecureServerURL]
&& request.shouldHaveAuthorizationHeaders) {
[TSAccountManager.sharedInstance setIsDeregistered:YES];
} else {
OWSLogWarn(@"Ignoring %d for URL: %@", (int)statusCode, task.originalRequest.URL.absoluteString);
}
}
10 years ago
+ (NSError *)errorWithHTTPCode:(NSInteger)code
description:(NSString *)description
failureReason:(NSString *)failureReason
recoverySuggestion:(NSString *)recoverySuggestion
fallbackError:(NSError *)fallbackError
{
OWSAssertDebug(fallbackError);
10 years ago
if (!description) {
description = fallbackError.localizedDescription;
}
if (!failureReason) {
failureReason = fallbackError.localizedFailureReason;
}
if (!recoverySuggestion) {
recoverySuggestion = fallbackError.localizedRecoverySuggestion;
}
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
if (description) {
[dict setObject:description forKey:NSLocalizedDescriptionKey];
}
if (failureReason) {
[dict setObject:failureReason forKey:NSLocalizedFailureReasonErrorKey];
}
if (recoverySuggestion) {
[dict setObject:recoverySuggestion forKey:NSLocalizedRecoverySuggestionErrorKey];
}
NSData *failureData = fallbackError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
if (failureData) {
[dict setObject:failureData forKey:AFNetworkingOperationFailingURLResponseDataErrorKey];
}
dict[NSUnderlyingErrorKey] = fallbackError;
return [NSError errorWithDomain:TSNetworkManagerErrorDomain code:code userInfo:dict];
10 years ago
}
@end