diff --git a/SignalMessaging/profiles/ProfileFetcherJob.swift b/SignalMessaging/profiles/ProfileFetcherJob.swift index 7d92d542b..4cd472ace 100644 --- a/SignalMessaging/profiles/ProfileFetcherJob.swift +++ b/SignalMessaging/profiles/ProfileFetcherJob.swift @@ -135,46 +135,18 @@ public class ProfileFetcherJob: NSObject { Logger.error("getProfile: \(recipientId)") let unidentifiedAccess: SSKUnidentifiedAccess? = self.getUnidentifiedAccess(forRecipientId: recipientId) - let socketType: OWSWebSocketType = unidentifiedAccess == nil ? .default : .UD - if socketManager.canMakeRequests(of: socketType) { - let (promise, resolver) = Promise.pending() - - let socketSuccess = { (responseObject: Any?) -> Void in - do { - let profile = try SignalServiceProfile(recipientId: recipientId, responseObject: responseObject) - resolver.fulfill(profile) - } catch { - resolver.reject(error) - } - } - - let request = OWSRequestFactory.getProfileRequest(recipientId: recipientId, unidentifiedAccess: unidentifiedAccess) - self.socketManager.make(request, - webSocketType: socketType, - success: socketSuccess, - failure: { (statusCode: NSInteger, _:Data?, error: Error) in - - // If UD auth fails, try again with non-UD auth. - if unidentifiedAccess != nil && (statusCode == 401 || statusCode == 403) { - Logger.info("Profile request failing over to non-UD auth.") - - self.udManager.setUnidentifiedAccessMode(.disabled, recipientId: recipientId) - - let nonUDRequest = OWSRequestFactory.getProfileRequest(recipientId: recipientId, unidentifiedAccess: nil) - self.socketManager.make(nonUDRequest, - webSocketType: .default, - success: socketSuccess, - failure: { (_: NSInteger, _:Data?, error: Error) in - resolver.reject(error) - }) - return - } - - resolver.reject(error) - }) - return promise - } else { - return self.signalServiceClient.retrieveProfile(recipientId: recipientId, unidentifiedAccess: unidentifiedAccess) + let requestMaker = RequestMaker(requestFactoryBlock: { (unidentifiedAccessForRequest) -> TSRequest in + return OWSRequestFactory.getProfileRequest(recipientId: recipientId, unidentifiedAccess: unidentifiedAccessForRequest) + }, udAuthFailureBlock: { + // Do nothing + }, websocketFailureBlock: { + // Do nothing + }, recipientId: recipientId, + unidentifiedAccess: unidentifiedAccess) + return requestMaker.makeRequest() + .then { (result: RequestMakerResult) -> Promise in + let responseObject: Any? = result.responseObject + return Promise.value(try SignalServiceProfile(recipientId: recipientId, responseObject: responseObject)) } } diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index b33e61c44..181878f02 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -1016,74 +1016,53 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; OWSLogWarn(@"Sending a message with no device messages."); } - const BOOL isUDSend = messageSend.isUDSend; - TSRequest *request = [OWSRequestFactory submitMessageRequestWithRecipient:recipient.uniqueId - messages:deviceMessages - timeStamp:message.timestamp - unidentifiedAccess:messageSend.unidentifiedAccess]; - OWSWebSocketType webSocketType = (isUDSend ? OWSWebSocketTypeUD : OWSWebSocketTypeDefault); - BOOL canMakeWebsocketRequests = ([TSSocketManager.shared canMakeRequestsOfType:webSocketType] && - !messageSend.hasWebsocketSendFailed); - if (canMakeWebsocketRequests) { - [TSSocketManager.shared makeRequest:request - webSocketType:webSocketType - success:^(id _Nullable responseObject) { - [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:isUDSend]; - } - failure:^(NSInteger statusCode, NSData *_Nullable responseData, NSError *error) { + OWSRequestMaker *requestMaker = [[OWSRequestMaker alloc] + initWithRequestFactoryBlock:^(SSKUnidentifiedAccess *_Nullable unidentifiedAccess) { + return [OWSRequestFactory submitMessageRequestWithRecipient:recipient.recipientId + messages:deviceMessages + timeStamp:message.timestamp + unidentifiedAccess:unidentifiedAccess]; + } + udAuthFailureBlock:^{ + [messageSend setHasUDAuthFailed]; + } + websocketFailureBlock:^{ + messageSend.hasWebsocketSendFailed = YES; + } + recipientId:recipient.recipientId + unidentifiedAccess:messageSend.unidentifiedAccess]; + [[requestMaker makeRequestObjc] + .then(^(OWSRequestMakerResult *result) { dispatch_async([OWSDispatch sendingQueue], ^{ - OWSLogDebug(@"Web socket send failed; failing over to REST."); - - if (isUDSend && (statusCode == 401 || statusCode == 403)) { - // If a UD send fails due to service response (as opposed to network - // failure), mark recipient as _not_ in UD mode, then retry. - OWSLogDebug(@"UD send failed; failing over to non-UD send."); - [self.udManager setUnidentifiedAccessMode:UnidentifiedAccessModeDisabled - recipientId:recipient.uniqueId]; - [messageSend setHasUDAuthFailed]; - dispatch_async([OWSDispatch sendingQueue], ^{ - [self sendMessageToRecipient:messageSend]; - }); - return; + const BOOL wasSentByUD = result.wasSentByUD; + [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:wasSentByUD]; + }); + }) + .catch(^(NSError *error) { + dispatch_async([OWSDispatch sendingQueue], ^{ + NSUInteger statusCode = 0; + NSData *_Nullable responseData = nil; + if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) { + statusCode = error.code; + + NSError *_Nullable underlyingError = error.userInfo[NSUnderlyingErrorKey]; + if (underlyingError) { + responseData + = underlyingError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; + } else { + OWSFailDebug(@"Missing underlying error: %@", error); + } + } else { + OWSFailDebug(@"Unexpected error: %@", error); } - // Websockets can fail in different ways, so we don't decrement remainingAttempts for websocket - // failure. Instead we fall back to REST, which will decrement retries. e.g. after linking a new - // device, sync messages will fail until the websocket re-opens. - messageSend.hasWebsocketSendFailed = YES; - [self sendMessageToRecipient:messageSend]; + [self messageSendDidFail:messageSend + deviceMessages:deviceMessages + statusCode:statusCode + error:error + responseData:responseData]; }); - }]; - } else { - [self.networkManager makeRequest:request - success:^(NSURLSessionDataTask *task, id responseObject) { - [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:isUDSend]; - } - failure:^(NSURLSessionDataTask *task, NSError *error) { - NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; - NSInteger statusCode = response.statusCode; - NSData *_Nullable responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; - - if (isUDSend && (statusCode == 401 || statusCode == 403)) { - // If a UD send fails due to service response (as opposed to network - // failure), mark recipient as _not_ in UD mode, then retry. - OWSLogDebug(@"UD send failed; failing over to non-UD send."); - [self.udManager setUnidentifiedAccessMode:UnidentifiedAccessModeDisabled - recipientId:recipient.uniqueId]; - [messageSend setHasUDAuthFailed]; - dispatch_async([OWSDispatch sendingQueue], ^{ - [self sendMessageToRecipient:messageSend]; - }); - return; - } - - [self messageSendDidFail:messageSend - deviceMessages:deviceMessages - statusCode:statusCode - error:error - responseData:responseData]; - }]; - } + }) retainUntilComplete]; } - (void)messageSendDidSucceed:(OWSMessageSend *)messageSend @@ -1497,90 +1476,41 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; NSString *recipientId = recipient.recipientId; OWSAssertDebug(recipientId.length > 0); - const BOOL isUDSend = messageSend.isUDSend; - TSRequest *request = [OWSRequestFactory recipientPrekeyRequestWithRecipient:recipientId - deviceId:[deviceId stringValue] - unidentifiedAccess:messageSend.unidentifiedAccess]; - OWSWebSocketType webSocketType = (isUDSend ? OWSWebSocketTypeUD : OWSWebSocketTypeDefault); - BOOL canMakeWebsocketRequests - = ([TSSocketManager.shared canMakeRequestsOfType:webSocketType] && !messageSend.hasWebsocketSendFailed); - if (canMakeWebsocketRequests) { - [TSSocketManager.shared makeRequest:request - webSocketType:webSocketType - success:^(id _Nullable responseObject) { - dispatch_async([OWSDispatch sendingQueue], ^{ - PreKeyBundle *_Nullable bundle = [PreKeyBundle preKeyBundleFromDictionary:responseObject - forDeviceNumber:deviceId]; - success(bundle); - }); - } - failure:^(NSInteger statusCode, NSData *_Nullable responseData, NSError *error) { - dispatch_async([OWSDispatch sendingQueue], ^{ - OWSLogDebug(@"Web socket prekey request failed; failing over to REST: %@.", error); - - if (isUDSend && (statusCode == 401 || statusCode == 403)) { - // If a UD send fails due to service response (as opposed to network - // failure), mark recipient as _not_ in UD mode, then retry. - OWSLogDebug(@"UD prekey request failed; failing over to non-UD prekey request."); - [self.udManager setUnidentifiedAccessMode:UnidentifiedAccessModeDisabled - recipientId:recipient.uniqueId]; - [messageSend setHasUDAuthFailed]; - // Try again without UD auth. - [self makePrekeyRequestForMessageSend:messageSend - deviceId:deviceId - success:success - failure:failure]; - return; - } - - // Websockets can fail in different ways, so we don't decrement remainingAttempts for websocket - // failure. Instead we fall back to REST, which will decrement retries. e.g. after linking a new - // device, sync messages will fail until the websocket re-opens. - messageSend.hasWebsocketSendFailed = YES; - // Try again without websocket. - [self makePrekeyRequestForMessageSend:messageSend - deviceId:deviceId - success:success - failure:failure]; - }); - }]; - } else { - [self.networkManager makeRequest:request - completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) - success:^(NSURLSessionDataTask *task, id responseObject) { - dispatch_async([OWSDispatch sendingQueue], ^{ - PreKeyBundle *_Nullable bundle = [PreKeyBundle preKeyBundleFromDictionary:responseObject - forDeviceNumber:deviceId]; - success(bundle); - }); - } - failure:^(NSURLSessionDataTask *task, NSError *error) { - dispatch_async([OWSDispatch sendingQueue], ^{ - if (!IsNSErrorNetworkFailure(error)) { - OWSProdError([OWSAnalyticsEvents messageSenderErrorRecipientPrekeyRequestFailed]); - } - OWSLogDebug(@"REST prekey request failed: %@.", error); - NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; - NSUInteger statusCode = response.statusCode; - if (isUDSend && (statusCode == 401 || statusCode == 403)) { - // If a UD send fails due to service response (as opposed to network - // failure), mark recipient as _not_ in UD mode, then retry. - OWSLogDebug(@"UD prekey request failed; failing over to non-UD prekey request."); - [self.udManager setUnidentifiedAccessMode:UnidentifiedAccessModeDisabled - recipientId:recipient.uniqueId]; - [messageSend setHasUDAuthFailed]; - // Try again without UD auth. - [self makePrekeyRequestForMessageSend:messageSend - deviceId:deviceId - success:success - failure:failure]; - return; - } + OWSRequestMaker *requestMaker = [[OWSRequestMaker alloc] + initWithRequestFactoryBlock:^(SSKUnidentifiedAccess *_Nullable unidentifiedAccess) { + return [OWSRequestFactory recipientPrekeyRequestWithRecipient:recipientId + deviceId:[deviceId stringValue] + unidentifiedAccess:unidentifiedAccess]; + } + udAuthFailureBlock:^{ + [messageSend setHasUDAuthFailed]; + } + websocketFailureBlock:^{ + messageSend.hasWebsocketSendFailed = YES; + } + recipientId:recipientId + unidentifiedAccess:messageSend.unidentifiedAccess]; + [[requestMaker makeRequestObjc] + .then(^(OWSRequestMakerResult *result) { + // We _do not_ want to dispatch to the sendingQueue here; we're + // using a semaphore on the sendingQueue to block on this request. + const id responseObject = result.responseObject; + PreKeyBundle *_Nullable bundle = + [PreKeyBundle preKeyBundleFromDictionary:responseObject forDeviceNumber:deviceId]; + success(bundle); + }) + .catch(^(NSError *error) { + // We _do not_ want to dispatch to the sendingQueue here; we're + // using a semaphore on the sendingQueue to block on this request. + NSUInteger statusCode = 0; + if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) { + statusCode = error.code; + } else { + OWSFailDebug(@"Unexpected error: %@", error); + } - failure(statusCode); - }); - }]; - } + failure(statusCode); + }) retainUntilComplete]; } // NOTE: This method uses exceptions for control flow. diff --git a/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift b/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift new file mode 100644 index 000000000..05ba0fdfe --- /dev/null +++ b/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift @@ -0,0 +1,157 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +public enum RequestMakerError: Error { + case websocketRequestError(statusCode : Int, responseData : Data?, underlyingError : Error) +} + +@objc(OWSRequestMakerResult) +public class RequestMakerResult: NSObject { + @objc + public let responseObject: Any? + + @objc + public let wasSentByUD: Bool + + @objc + public init(responseObject: Any?, + wasSentByUD: Bool) { + self.responseObject = responseObject + self.wasSentByUD = wasSentByUD + } +} + +// A utility class that handles: +// +// * UD auth-to-Non-UD auth failover. +// * Websocket-to-REST failover. +@objc(OWSRequestMaker) +public class RequestMaker: NSObject { + public typealias RequestFactoryBlock = (SSKUnidentifiedAccess?) -> TSRequest + public typealias UDAuthFailureBlock = () -> Void + public typealias WebsocketFailureBlock = () -> Void + + private let requestFactoryBlock: RequestFactoryBlock + private let udAuthFailureBlock: UDAuthFailureBlock + private let websocketFailureBlock: WebsocketFailureBlock + private let recipientId: String + private let unidentifiedAccess: SSKUnidentifiedAccess? + + @objc + public init(requestFactoryBlock : @escaping RequestFactoryBlock, + udAuthFailureBlock : @escaping UDAuthFailureBlock, + websocketFailureBlock : @escaping WebsocketFailureBlock, + recipientId: String, + unidentifiedAccess: SSKUnidentifiedAccess?) { + self.requestFactoryBlock = requestFactoryBlock + self.udAuthFailureBlock = udAuthFailureBlock + self.websocketFailureBlock = websocketFailureBlock + self.recipientId = recipientId + self.unidentifiedAccess = unidentifiedAccess + } + + // MARK: - Dependencies + + private var socketManager: TSSocketManager { + return SSKEnvironment.shared.socketManager + } + + private var networkManager: TSNetworkManager { + return SSKEnvironment.shared.networkManager + } + + private var udManager: OWSUDManager { + return SSKEnvironment.shared.udManager + } + + // MARK: - + + @objc + public func makeRequestObjc() -> AnyPromise { + let promise = makeRequest() + .recover { (error: Error) -> Promise in + switch error { + case NetworkManagerError.taskError(_, let underlyingError): + throw underlyingError + default: + throw error + } + } + let anyPromise = AnyPromise(promise) + anyPromise.retainUntilComplete() + return anyPromise + } + + public func makeRequest() -> Promise { + return makeRequestInternal(skipUD: false, skipWebsocket: false) + } + + private func makeRequestInternal(skipUD: Bool, skipWebsocket: Bool) -> Promise { + var unidentifiedAccessForRequest: SSKUnidentifiedAccess? + if !skipUD { + unidentifiedAccessForRequest = unidentifiedAccess + } + let isUDSend = unidentifiedAccessForRequest != nil + let request = requestFactoryBlock(unidentifiedAccessForRequest) + let webSocketType: OWSWebSocketType = (isUDSend ? .UD : .default) + let canMakeWebsocketRequests = (socketManager.canMakeRequests(of: webSocketType) && !skipWebsocket) + + if canMakeWebsocketRequests { + return Promise { resolver in + socketManager.make(request, webSocketType: webSocketType, success: { (responseObject: Any?) in + _ = resolver.fulfill(RequestMakerResult(responseObject: responseObject, wasSentByUD: isUDSend)) + }) { (statusCode: Int, responseData: Data?, error: Error) in + resolver.reject(RequestMakerError.websocketRequestError(statusCode: statusCode, responseData: responseData, underlyingError: error)) + } + }.recover { (error: Error) -> Promise in + switch error { + case RequestMakerError.websocketRequestError(let statusCode, _, _): + if isUDSend && (statusCode == 401 || statusCode == 403) { + // If a UD send fails due to service response (as opposed to network + // failure), mark recipient as _not_ in UD mode, then retry. + self.udManager.setUnidentifiedAccessMode(.disabled, recipientId: self.recipientId) + self.udAuthFailureBlock() + Logger.info("UD websocket request failed; failing over to non-UD websocket request.") + return self.makeRequestInternal(skipUD: true, skipWebsocket: skipWebsocket) + } + break + default: + break + } + + self.websocketFailureBlock() + Logger.info("Non-UD Web socket request failed; failing over to REST request: \(error).") + return self.makeRequestInternal(skipUD: skipUD, skipWebsocket: true) + } + } else { + return self.networkManager.makePromise(request: request) + .then { (networkManagerResult: TSNetworkManager.NetworkManagerResult) -> Promise in + // Unwrap the network manager promise into a request maker promise. + return Promise.value(RequestMakerResult(responseObject: networkManagerResult.responseObject, wasSentByUD: isUDSend)) + }.recover { (error: Error) -> Promise in + switch error { + case NetworkManagerError.taskError(let task, _): + let statusCode = task.statusCode() + if isUDSend && (statusCode == 401 || statusCode == 403) { + // If a UD send fails due to service response (as opposed to network + // failure), mark recipient as _not_ in UD mode, then retry. + self.udManager.setUnidentifiedAccessMode(.disabled, recipientId: self.recipientId) + self.udAuthFailureBlock() + Logger.info("UD REST request failed; failing over to non-UD REST request.") + return self.makeRequestInternal(skipUD: true, skipWebsocket: skipWebsocket) + } + break + default: + break + } + + Logger.debug("Non-UD REST request failed: \(error).") + throw error + } + } + } +} diff --git a/SignalServiceKit/src/Network/API/NetworkManager.swift b/SignalServiceKit/src/Network/API/NetworkManager.swift index a18dcbea4..3f7f61de2 100644 --- a/SignalServiceKit/src/Network/API/NetworkManager.swift +++ b/SignalServiceKit/src/Network/API/NetworkManager.swift @@ -27,9 +27,10 @@ extension NetworkManagerError { } extension TSNetworkManager { + public typealias NetworkManagerResult = (task: URLSessionDataTask, responseObject: Any?) - public func makePromise(request: TSRequest) -> Promise<(task: URLSessionDataTask, responseObject: Any?)> { - let (promise, resolver) = Promise<(task: URLSessionDataTask, responseObject: Any?)>.pending() + public func makePromise(request: TSRequest) -> Promise { + let (promise, resolver) = Promise.pending() self.makeRequest(request, success: { task, responseObject in