diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index dac37fd036..f001f4d8e7 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -976,240 +976,192 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; messages:deviceMessages relay:recipient.relay timeStamp:message.timestamp]; - if (YES) { + if (TSSocketManager.sharedManager.canMakeRequests) { [TSSocketManager.sharedManager makeRequest:request - success:^{ - DDLogInfo(@"%@ Message send succeeded.", self.logTag); - if (isLocalNumber && deviceMessages.count == 0) { - DDLogInfo( - @"%@ Sent a message with no device messages; clearing 'mayHaveLinkedDevices'.", self.logTag); - // In order to avoid skipping necessary sync messages, the default value - // for mayHaveLinkedDevices is YES. Once we've successfully sent a - // sync message with no device messages (e.g. the service has confirmed - // that we have no linked devices), we can set mayHaveLinkedDevices to NO - // to avoid unnecessary message sends for sync messages until we learn - // of a linked device (e.g. through the device linking UI or by receiving - // a sync message, etc.). - [OWSDeviceManager.sharedManager clearMayHaveLinkedDevicesIfNotSet]; - } - - dispatch_async([OWSDispatch sendingQueue], ^{ - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [recipient saveWithTransaction:transaction]; - [message updateWithSentRecipient:recipient.uniqueId transaction:transaction]; - }]; - - [self handleMessageSentLocally:message]; - successHandler(); - }); + success:^(id _Nullable responseObject) { + [self messageSendDidSucceed:message + recipient:recipient + isLocalNumber:isLocalNumber + deviceMessages:deviceMessages + success:successHandler]; } failure:^(NSInteger statusCode, NSError *error) { - DDLogError(@"%@ Message send failed.", self.logTag); - DDLogInfo(@"%@ sending to recipient: %@, failed with error.", self.logTag, recipient.uniqueId); - [DDLog flushLog]; - - // NSHTTPURLResponse *response = (NSHTTPURLResponse - // *)task.response; long statuscode = response.statusCode; - NSData *responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; - - void (^retrySend)(void) = ^void() { - if (remainingAttempts <= 0) { - // Since we've already repeatedly failed to send to the messaging API, - // it's unlikely that repeating the whole process will succeed. - [error setIsRetryable:NO]; - return failureHandler(error); - } - - dispatch_async([OWSDispatch sendingQueue], ^{ - DDLogDebug(@"%@ Retrying: %@", self.logTag, message.debugDescription); - [self sendMessageToService:message - recipient:recipient - thread:thread - attempts:remainingAttempts - success:successHandler - failure:failureHandler]; - }); - }; - - switch (statusCode) { - case 401: { - DDLogWarn( - @"%@ Unable to send due to invalid credentials. Did the user's client get de-authed by " - @"registering elsewhere?", - self.logTag); - NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceFailure, - NSLocalizedString(@"ERROR_DESCRIPTION_SENDING_UNAUTHORIZED", - @"Error message when attempting to send message")); - // No need to retry if we've been de-authed. - [error setIsRetryable:NO]; - return failureHandler(error); - } - case 404: { - DDLogWarn(@"%@ Unregistered recipient: %@", self.logTag, recipient.uniqueId); - - [self unregisteredRecipient:recipient message:message thread:thread]; - NSError *error = OWSErrorMakeNoSuchSignalRecipientError(); - // No need to retry if the recipient is not registered. - [error setIsRetryable:NO]; - // If one member of a group deletes their account, - // the group should ignore errors when trying to send - // messages to this ex-member. - [error setShouldBeIgnoredForGroups:YES]; - return failureHandler(error); - } - case 409: { - // Mismatched devices - DDLogWarn(@"%@ Mismatch Devices for recipient: %@", self.logTag, recipient.uniqueId); - - NSError *error; - NSDictionary *serializedResponse = - [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error]; - if (error) { - OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotParseMismatchedDevicesJson]); - [error setIsRetryable:YES]; - return failureHandler(error); - } - - [self handleMismatchedDevices:serializedResponse recipient:recipient completion:retrySend]; - break; - } - case 410: { - // Stale devices - DDLogWarn(@"%@ Stale devices for recipient: %@", self.logTag, recipient.uniqueId); - - if (!responseData) { - DDLogWarn(@"Stale devices but server didn't specify devices in response."); - NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); - [error setIsRetryable:YES]; - return failureHandler(error); - } - - [self handleStaleDevicesWithResponse:responseData - recipientId:recipient.uniqueId - completion:retrySend]; - break; - } - default: - retrySend(); - break; - } + [self messageSendDidFail:message + recipient:recipient + thread:thread + isLocalNumber:isLocalNumber + deviceMessages:deviceMessages + remainingAttempts:remainingAttempts + statusCode:statusCode + error:error + success:successHandler + failure:failureHandler]; }]; - } else + } else { [self.networkManager makeRequest:request success:^(NSURLSessionDataTask *task, id responseObject) { - if (isLocalNumber && deviceMessages.count == 0) { - DDLogInfo( - @"%@ Sent a message with no device messages; clearing 'mayHaveLinkedDevices'.", self.logTag); - // In order to avoid skipping necessary sync messages, the default value - // for mayHaveLinkedDevices is YES. Once we've successfully sent a - // sync message with no device messages (e.g. the service has confirmed - // that we have no linked devices), we can set mayHaveLinkedDevices to NO - // to avoid unnecessary message sends for sync messages until we learn - // of a linked device (e.g. through the device linking UI or by receiving - // a sync message, etc.). - [OWSDeviceManager.sharedManager clearMayHaveLinkedDevicesIfNotSet]; - } - - dispatch_async([OWSDispatch sendingQueue], ^{ - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [recipient saveWithTransaction:transaction]; - [message updateWithSentRecipient:recipient.uniqueId transaction:transaction]; - }]; - - [self handleMessageSentLocally:message]; - successHandler(); - }); + [self messageSendDidSucceed:message + recipient:recipient + isLocalNumber:isLocalNumber + deviceMessages:deviceMessages + success:successHandler]; } failure:^(NSURLSessionDataTask *task, NSError *error) { - DDLogInfo(@"%@ sending to recipient: %@, failed with error.", self.logTag, recipient.uniqueId); - [DDLog flushLog]; - NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; - long statuscode = response.statusCode; - NSData *responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; + NSInteger statusCode = response.statusCode; - void (^retrySend)(void) = ^void() { - if (remainingAttempts <= 0) { - // Since we've already repeatedly failed to send to the messaging API, - // it's unlikely that repeating the whole process will succeed. - [error setIsRetryable:NO]; - return failureHandler(error); - } - - dispatch_async([OWSDispatch sendingQueue], ^{ - DDLogDebug(@"%@ Retrying: %@", self.logTag, message.debugDescription); - [self sendMessageToService:message - recipient:recipient - thread:thread - attempts:remainingAttempts - success:successHandler - failure:failureHandler]; - }); - }; - - switch (statuscode) { - case 401: { - DDLogWarn( - @"%@ Unable to send due to invalid credentials. Did the user's client get de-authed by " - @"registering elsewhere?", - self.logTag); - NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceFailure, - NSLocalizedString(@"ERROR_DESCRIPTION_SENDING_UNAUTHORIZED", - @"Error message when attempting to send message")); - // No need to retry if we've been de-authed. - [error setIsRetryable:NO]; - return failureHandler(error); - } - case 404: { - DDLogWarn(@"%@ Unregistered recipient: %@", self.logTag, recipient.uniqueId); - - [self unregisteredRecipient:recipient message:message thread:thread]; - NSError *error = OWSErrorMakeNoSuchSignalRecipientError(); - // No need to retry if the recipient is not registered. - [error setIsRetryable:NO]; - // If one member of a group deletes their account, - // the group should ignore errors when trying to send - // messages to this ex-member. - [error setShouldBeIgnoredForGroups:YES]; - return failureHandler(error); - } - case 409: { - // Mismatched devices - DDLogWarn(@"%@ Mismatch Devices for recipient: %@", self.logTag, recipient.uniqueId); - - NSError *error; - NSDictionary *serializedResponse = - [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error]; - if (error) { - OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotParseMismatchedDevicesJson]); - [error setIsRetryable:YES]; - return failureHandler(error); - } - - [self handleMismatchedDevices:serializedResponse recipient:recipient completion:retrySend]; - break; - } - case 410: { - // Stale devices - DDLogWarn(@"%@ Stale devices for recipient: %@", self.logTag, recipient.uniqueId); - - if (!responseData) { - DDLogWarn(@"Stale devices but server didn't specify devices in response."); - NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); - [error setIsRetryable:YES]; - return failureHandler(error); - } - - [self handleStaleDevicesWithResponse:responseData - recipientId:recipient.uniqueId - completion:retrySend]; - break; - } - default: - retrySend(); - break; - } + [self messageSendDidFail:message + recipient:recipient + thread:thread + isLocalNumber:isLocalNumber + deviceMessages:deviceMessages + remainingAttempts:remainingAttempts + statusCode:statusCode + error:error + success:successHandler + failure:failureHandler]; }]; + } +} + +- (void)messageSendDidSucceed:(TSOutgoingMessage *)message + recipient:(SignalRecipient *)recipient + isLocalNumber:(BOOL)isLocalNumber + deviceMessages:(NSArray *)deviceMessages + success:(void (^)(void))successHandler +{ + OWSAssert(message); + OWSAssert(recipient); + OWSAssert(deviceMessages); + OWSAssert(successHandler); + + DDLogInfo(@"%@ Message send succeeded.", self.logTag); + + if (isLocalNumber && deviceMessages.count == 0) { + DDLogInfo(@"%@ Sent a message with no device messages; clearing 'mayHaveLinkedDevices'.", self.logTag); + // In order to avoid skipping necessary sync messages, the default value + // for mayHaveLinkedDevices is YES. Once we've successfully sent a + // sync message with no device messages (e.g. the service has confirmed + // that we have no linked devices), we can set mayHaveLinkedDevices to NO + // to avoid unnecessary message sends for sync messages until we learn + // of a linked device (e.g. through the device linking UI or by receiving + // a sync message, etc.). + [OWSDeviceManager.sharedManager clearMayHaveLinkedDevicesIfNotSet]; + } + + dispatch_async([OWSDispatch sendingQueue], ^{ + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [recipient saveWithTransaction:transaction]; + [message updateWithSentRecipient:recipient.uniqueId transaction:transaction]; + }]; + + [self handleMessageSentLocally:message]; + successHandler(); + }); +} + +- (void)messageSendDidFail:(TSOutgoingMessage *)message + recipient:(SignalRecipient *)recipient + thread:(TSThread *)thread + isLocalNumber:(BOOL)isLocalNumber + deviceMessages:(NSArray *)deviceMessages + remainingAttempts:(int)remainingAttempts + statusCode:(NSInteger)statusCode + error:(NSError *)error + success:(void (^)(void))successHandler + failure:(RetryableFailureHandler)failureHandler +{ + OWSAssert(message); + OWSAssert(recipient); + OWSAssert(thread); + OWSAssert(deviceMessages); + OWSAssert(error); + OWSAssert(successHandler); + OWSAssert(failureHandler); + + DDLogInfo(@"%@ sending to recipient: %@, failed with error.", self.logTag, recipient.uniqueId); + [DDLog flushLog]; + + NSData *responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; + + void (^retrySend)(void) = ^void() { + if (remainingAttempts <= 0) { + // Since we've already repeatedly failed to send to the messaging API, + // it's unlikely that repeating the whole process will succeed. + [error setIsRetryable:NO]; + return failureHandler(error); + } + + dispatch_async([OWSDispatch sendingQueue], ^{ + DDLogDebug(@"%@ Retrying: %@", self.logTag, message.debugDescription); + [self sendMessageToService:message + recipient:recipient + thread:thread + attempts:remainingAttempts + success:successHandler + failure:failureHandler]; + }); + }; + + switch (statusCode) { + case 401: { + DDLogWarn(@"%@ Unable to send due to invalid credentials. Did the user's client get de-authed by " + @"registering elsewhere?", + self.logTag); + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceFailure, + NSLocalizedString( + @"ERROR_DESCRIPTION_SENDING_UNAUTHORIZED", @"Error message when attempting to send message")); + // No need to retry if we've been de-authed. + [error setIsRetryable:NO]; + return failureHandler(error); + } + case 404: { + DDLogWarn(@"%@ Unregistered recipient: %@", self.logTag, recipient.uniqueId); + + [self unregisteredRecipient:recipient message:message thread:thread]; + NSError *error = OWSErrorMakeNoSuchSignalRecipientError(); + // No need to retry if the recipient is not registered. + [error setIsRetryable:NO]; + // If one member of a group deletes their account, + // the group should ignore errors when trying to send + // messages to this ex-member. + [error setShouldBeIgnoredForGroups:YES]; + return failureHandler(error); + } + case 409: { + // Mismatched devices + DDLogWarn(@"%@ Mismatch Devices for recipient: %@", self.logTag, recipient.uniqueId); + + NSError *error; + NSDictionary *serializedResponse = + [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error]; + if (error) { + OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotParseMismatchedDevicesJson]); + [error setIsRetryable:YES]; + return failureHandler(error); + } + + [self handleMismatchedDevices:serializedResponse recipient:recipient completion:retrySend]; + break; + } + case 410: { + // Stale devices + DDLogWarn(@"%@ Stale devices for recipient: %@", self.logTag, recipient.uniqueId); + + if (!responseData) { + DDLogWarn(@"Stale devices but server didn't specify devices in response."); + NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); + [error setIsRetryable:YES]; + return failureHandler(error); + } + + [self handleStaleDevicesWithResponse:responseData recipientId:recipient.uniqueId completion:retrySend]; + break; + } + default: + retrySend(); + break; + } } - (void)handleMismatchedDevices:(NSDictionary *)dictionary diff --git a/SignalServiceKit/src/Network/WebSockets/TSSocketManager.h b/SignalServiceKit/src/Network/WebSockets/TSSocketManager.h index 955ac5af6f..00d26231d9 100644 --- a/SignalServiceKit/src/Network/WebSockets/TSSocketManager.h +++ b/SignalServiceKit/src/Network/WebSockets/TSSocketManager.h @@ -4,6 +4,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + static void *SocketManagerStateObservationContext = &SocketManagerStateObservationContext; extern NSString *const kNSNotification_SocketManagerStateDidChange; @@ -14,7 +16,8 @@ typedef NS_ENUM(NSUInteger, SocketManagerState) { SocketManagerStateOpen, }; -typedef void (^TSSocketMessageSuccess)(void); +typedef void (^TSSocketMessageSuccess)(id _Nullable responseObject); +// statusCode is zero by default, if request never made or failed. typedef void (^TSSocketMessageFailure)(NSInteger statusCode, NSError *error); @class TSRequest; @@ -22,6 +25,7 @@ typedef void (^TSSocketMessageFailure)(NSInteger statusCode, NSError *error); @interface TSSocketManager : NSObject @property (nonatomic, readonly) SocketManagerState state; +@property (atomic, readonly) BOOL canMakeRequests; + (instancetype)sharedManager; @@ -45,3 +49,5 @@ typedef void (^TSSocketMessageFailure)(NSInteger statusCode, NSError *error); failure:(TSSocketMessageFailure)failure; @end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m b/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m index 91bce694f5..f4956576a4 100644 --- a/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m +++ b/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m @@ -25,6 +25,8 @@ #import "Threading.h" #import "WebSocketResources.pb.h" +NS_ASSUME_NONNULL_BEGIN + static const CGFloat kSocketHeartbeatPeriodSeconds = 30.f; static const CGFloat kSocketReconnectDelaySeconds = 5.f; @@ -51,7 +53,7 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ @implementation TSSocketMessage -- (void)didSucceed +- (void)didSucceedWithResponseObject:(id _Nullable)responseObject { @synchronized(self) { @@ -64,7 +66,7 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ OWSAssert(self.success); OWSAssert(self.failure); - self.success(); + self.success(responseObject); self.success = nil; self.failure = nil; @@ -156,10 +158,11 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ @property (nonatomic) BOOL hasObservedNotifications; -// These two properties should only be accessed while synchronized on the socket manager. -@property (nonatomic) UInt64 requestIdCounter; +// This property should only be accessed while synchronized on the socket manager. @property (nonatomic, readonly) NSMutableDictionary *socketMessageMap; +@property (atomic) BOOL canMakeRequests; + @end #pragma mark - @@ -179,8 +182,6 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ _signalService = [OWSSignalService sharedInstance]; _messageReceiver = [OWSMessageReceiver sharedInstance]; _state = SocketManagerStateClosed; - // Ensure that all request ids are non-zero. - _requestIdCounter = 1; _socketMessageMap = [NSMutableDictionary new]; OWSSingletonAssert(); @@ -366,12 +367,14 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ // the socket failed immediately for some reason), so we update the state // _before_ calling it, not after. _state = state; + _canMakeRequests = state == SocketManagerStateOpen; [socket open]; return; } } _state = state; + _canMakeRequests = state == SocketManagerStateOpen; [self notifyStatusChange]; } @@ -410,18 +413,6 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ - (void)makeRequest:(TSRequest *)request success:(TSSocketMessageSuccess)success failure:(TSSocketMessageFailure)failure { - DispatchMainThreadSafe(^{ - [self makeRequestOnMain:request success:success failure:failure]; - }); -} - -// TODO: Currently we schedule requests on main since we want to consult the state property. -// There are alternatives. -- (void)makeRequestOnMain:(TSRequest *)request - success:(TSSocketMessageSuccess)success - failure:(TSSocketMessageFailure)failure -{ - OWSAssertIsOnMainThread(); OWSAssert(request); OWSAssert(request.HTTPMethod.length > 0); OWSAssert(success); @@ -432,7 +423,8 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ socketMessage.failure = failure; @synchronized(self) { - socketMessage.requestId = self.requestIdCounter++; + // TODO: Should we use another random number generator? + socketMessage.requestId = arc4random(); self.socketMessageMap[@(socketMessage.requestId)] = socketMessage; } @@ -444,9 +436,7 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ NSError *error; jsonData = [NSJSONSerialization dataWithJSONObject:request.parameters - // TODO: options:(NSJSONWritingOptions)0 - // options:NSJSONWritingPrettyPrinted error:&error]; if (!jsonData || error) { OWSProdLogAndFail(@"%@ could not serialize request JSON: %@", self.logTag, error); @@ -456,22 +446,18 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ } } - if (self.state != SocketManagerStateOpen) { - DDLogError(@"%@ makeRequest: socket not open.", self.logTag); - // TODO: - [socketMessage didFailBeforeSending]; - return; - } - WebSocketResourcesWebSocketRequestMessageBuilder *requestBuilder = [WebSocketResourcesWebSocketRequestMessageBuilder new]; requestBuilder.id = socketMessage.requestId; [requestBuilder setVerb:request.HTTPMethod]; [requestBuilder setPath:requestPath]; - [requestBuilder setBody:jsonData]; - [requestBuilder setHeadersArray:@[ - @"content-type:application/json", - ]]; + if (jsonData) { + // TODO: Do we need body & headers for requests with no parameters? + [requestBuilder setBody:jsonData]; + [requestBuilder setHeadersArray:@[ + @"content-type:application/json", + ]]; + } WebSocketResourcesWebSocketMessageBuilder *messageBuilder = [WebSocketResourcesWebSocketMessageBuilder new]; [messageBuilder setType:WebSocketResourcesWebSocketMessageTypeRequest]; @@ -480,18 +466,20 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ NSData *messageData = [messageBuilder build].data; if (!messageData) { OWSProdLogAndFail(@"%@ could not serialize message.", self.logTag); - // TODO: + [socketMessage didFailBeforeSending]; + return; + } + + if (!self.canMakeRequests) { + DDLogError(@"%@ makeRequest: socket not open.", self.logTag); [socketMessage didFailBeforeSending]; return; } NSError *error; - BOOL wasScheduled = - // TODO: Consider using sendDataNoCopy. - [self.websocket sendData:messageData error:&error]; + BOOL wasScheduled = [self.websocket sendDataNoCopy:messageData error:&error]; if (!wasScheduled || error) { OWSProdLogAndFail(@"%@ could not serialize request JSON: %@", self.logTag, error); - // TODO: [socketMessage didFailBeforeSending]; return; } @@ -501,9 +489,99 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ request.HTTPMethod, requestPath, jsonData.length); - // [socketMessage didSucceed]; + + // Ensure requests "timeout" after 30 seconds. + __weak TSSocketMessage *weakSocketMessage = socketMessage; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + ^{ + [weakSocketMessage didFailBeforeSending]; + }); } +- (void)processWebSocketResponseMessage:(WebSocketResourcesWebSocketResponseMessage *)message +{ + OWSAssertIsOnMainThread(); + OWSAssert(message); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self processWebSocketResponseMessageAsync:message]; + }); +} + +- (void)processWebSocketResponseMessageAsync:(WebSocketResourcesWebSocketResponseMessage *)message +{ + OWSAssert(message); + + DDLogInfo(@"%@ received WebSocket response.", self.logTag); + + if (![message hasId]) { + DDLogError(@"%@ received incomplete WebSocket response.", self.logTag); + return; + } + + UInt64 requestId = message.id; + UInt32 responseStatus = 0; + if (message.hasStatus) { + responseStatus = message.status; + } + NSString *_Nullable responseMessage; + if (message.hasMessage) { + responseMessage = message.message; + } + NSData *_Nullable responseBody; + if (message.hasBody) { + responseBody = message.body; + } + NSArray *_Nullable responseHeaders = message.headers; + + id responseObject = responseBody; + if (responseBody) { + NSError *error; + id _Nullable responseJson = + [NSJSONSerialization JSONObjectWithData:responseBody options:(NSJSONReadingOptions)0 error:&error]; + if (!responseJson || error) { + DDLogError(@"%@ could not parse WebSocket response JSON: %@.", self.logTag, error); + // TODO: Should we require JSON parsing to succeed? + } else { + responseObject = responseJson; + } + } + + TSSocketMessage *_Nullable socketMessage; + @synchronized(self) + { + socketMessage = self.socketMessageMap[@(requestId)]; + [self.socketMessageMap removeObjectForKey:@(requestId)]; + } + + if (!socketMessage) { + DDLogError(@"%@ received response to unknown request.", self.logTag); + } else { + BOOL didSucceed = 200 <= responseStatus && responseStatus <= 299; + if (didSucceed) { + [socketMessage didSucceedWithResponseObject:responseObject]; + } else { + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageResponseFailed, + NSLocalizedString( + @"ERROR_DESCRIPTION_RESPONSE_FAILED", @"Error indicating that a socket response failed.")); + [socketMessage didFailWithStatusCode:(NSInteger)responseStatus error:error]; + } + } + + DDLogVerbose(@"%@ received WebSocket response: %llu, %zd, %@, %zd, %@, %d, %@.", + self.logTag, + (unsigned long long)requestId, + (NSInteger)responseStatus, + responseMessage, + responseBody.length, + responseHeaders, + socketMessage != nil, + responseObject); +} + +// didSucceedWithResponseObject:(id _Nullable)responseObject + #pragma mark - Delegate methods - (void)webSocketDidOpen:(SRWebSocket *)webSocket { @@ -538,13 +616,18 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ return; } - WebSocketResourcesWebSocketMessage *wsMessage = [WebSocketResourcesWebSocketMessage parseFromData:data]; + WebSocketResourcesWebSocketMessage *wsMessage; + @try { + wsMessage = [WebSocketResourcesWebSocketMessage parseFromData:data]; + } @catch (NSException *exception) { + OWSProdLogAndFail(@"%@ Received an invalid message: %@", self.logTag, exception.debugDescription); + // TODO: Add analytics. + return; + } if (wsMessage.type == WebSocketResourcesWebSocketMessageTypeRequest) { - DDLogVerbose(@"%@ webSocket:didReceiveMessage: request.", self.logTag); [self processWebSocketRequestMessage:wsMessage.request]; } else if (wsMessage.type == WebSocketResourcesWebSocketMessageTypeResponse) { - DDLogVerbose(@"%@ webSocket:didReceiveMessage: response.", self.logTag); [self processWebSocketResponseMessage:wsMessage.response]; } else { DDLogWarn(@"%@ webSocket:didReceiveMessage: unknown.", self.logTag); @@ -609,64 +692,6 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ } } -- (void)processWebSocketResponseMessage:(WebSocketResourcesWebSocketResponseMessage *)message -{ - OWSAssertIsOnMainThread(); - OWSAssert(message); - - DDLogInfo(@"%@ received WebSocket response.", self.logTag); - - if (![message hasId]) { - DDLogError(@"%@ received incomplete WebSocket response.", self.logTag); - return; - } - - UInt64 requestId = message.id; - UInt32 responseStatus = 0; - if (message.hasStatus) { - responseStatus = message.status; - } - NSString *_Nullable responseMessage; - if (message.hasMessage) { - responseMessage = message.message; - } - NSData *_Nullable responseBody; - if (message.hasBody) { - responseBody = message.body; - } - NSArray *_Nullable responseHeaders = message.headers; - - TSSocketMessage *_Nullable socketMessage; - @synchronized(self) - { - socketMessage = self.socketMessageMap[@(requestId)]; - [self.socketMessageMap removeObjectForKey:@(requestId)]; - - if (!socketMessage) { - DDLogError(@"%@ received response to unknown request.", self.logTag); - } else { - BOOL didSucceed = 200 <= responseStatus && responseStatus <= 299; - if (didSucceed) { - [socketMessage didSucceed]; - } else { - NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageResponseFailed, - NSLocalizedString( - @"ERROR_DESCRIPTION_RESPONSE_FAILED", @"Error indicating that a socket response failed.")); - [socketMessage didFailWithStatusCode:(NSInteger)responseStatus error:error]; - } - } - } - - DDLogVerbose(@"%@ received WebSocket response: %llu, %zd, %@, %zd, %@, %d.", - self.logTag, - (unsigned long long)requestId, - (NSInteger)responseStatus, - responseMessage, - responseBody.length, - responseHeaders, - socketMessage != nil); -} - - (void)sendWebSocketResourcesWebSocketMessageAcknowledgement:(WebSocketResourcesWebSocketRequestMessage *)request { OWSAssertIsOnMainThread(); @@ -705,9 +730,10 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ } - (void)webSocket:(SRWebSocket *)webSocket - didCloseWithCode:(NSInteger)code - reason:(NSString *)reason - wasClean:(BOOL)wasClean { + didCloseWithCode:(NSInteger)code + reason:(nullable NSString *)reason + wasClean:(BOOL)wasClean +{ OWSAssertIsOnMainThread(); OWSAssert(webSocket); if (webSocket != self.websocket) { @@ -960,3 +986,5 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ } @end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/OWSError.m b/SignalServiceKit/src/Util/OWSError.m index ba15574cc3..870958f53c 100644 --- a/SignalServiceKit/src/Util/OWSError.m +++ b/SignalServiceKit/src/Util/OWSError.m @@ -59,7 +59,7 @@ NSError *OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError() NSError *OWSErrorMakeMessageSendFailedToBlockListError() { - return OWSErrorWithCodeDescription(OWSErrorCodeMessageRequestFailedToBlockList, + return OWSErrorWithCodeDescription(OWSErrorCodeMessageSendFailedToBlockList, NSLocalizedString(@"ERROR_DESCRIPTION_MESSAGE_SEND_FAILED_DUE_TO_BLOCK_LIST", @"Error message indicating that message send failed due to block list")); }