session-ios/Signal/src/textsecure/Network/WebSockets/TSSocketManager.m

307 lines
11 KiB
Objective-C

//
// TSSocketManager.m
// TextSecureiOS
//
// Created by Frederic Jacobs on 17/05/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import "SubProtocol.pb.h"
#import "TSConstants.h"
#import "TSAccountManager.h"
#import "TSMessagesManager.h"
#import "TSSocketManager.h"
#import "TSStorageManager+keyingMaterial.h"
#import <CocoaLumberjack/DDLog.h>
#import "NSData+Base64.h"
#import "Cryptography.h"
#import "IncomingPushMessageSignal.pb.h"
#define kWebSocketHeartBeat 30
#define kWebSocketReconnectTry 5
#define kBackgroundConnectTimer 120
NSString * const SocketOpenedNotification = @"SocketOpenedNotification";
NSString * const SocketClosedNotification = @"SocketClosedNotification";
NSString * const SocketConnectingNotification = @"SocketConnectingNotification";
@interface TSSocketManager ()
@property (nonatomic, retain) NSTimer *pingTimer;
@property (nonatomic, retain) NSTimer *reconnectTimer;
@property (nonatomic, retain) SRWebSocket *websocket;
@property (nonatomic) SocketStatus status;
@property (nonatomic, retain) NSTimer *backgroundConnectTimer;
@property (nonatomic) UIBackgroundTaskIdentifier fetchingTaskIdentifier;
@end
@implementation TSSocketManager
- (instancetype)init{
self = [super init];
if (self) {
self.websocket = nil;
[self addObserver:self forKeyPath:@"status" options:0 context:kSocketStatusObservationContext];
}
return self;
}
+ (instancetype)sharedManager {
static TSSocketManager *sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
});
return sharedMyManager;
}
#pragma mark - Manage Socket
+ (void)becomeActive {
TSSocketManager *sharedInstance = [self sharedManager];
SRWebSocket *socket = [sharedInstance websocket];
if (socket) {
switch ([socket readyState]) {
case SR_OPEN:
DDLogVerbose(@"WebSocket already open on connection request");
sharedInstance.status = kSocketStatusOpen;
return;
case SR_CONNECTING:
DDLogVerbose(@"WebSocket is already connecting");
sharedInstance.status = kSocketStatusConnecting;
return;
default:
[socket close];
sharedInstance.status = kSocketStatusClosed;
socket.delegate = nil;
socket = nil;
break;
}
}
NSString* webSocketConnect = [textSecureWebSocketAPI stringByAppendingString:[[self sharedManager] webSocketAuthenticationString]];
NSURL* webSocketConnectURL = [NSURL URLWithString:webSocketConnect];
NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:webSocketConnectURL];
NSString* cerPath = [[NSBundle mainBundle] pathForResource:@"textsecure" ofType:@"cer"];
NSData* certData = [[NSData alloc] initWithContentsOfFile:cerPath];
CFDataRef certDataRef = (__bridge CFDataRef)certData;
SecCertificateRef certRef = SecCertificateCreateWithData(NULL, certDataRef);
id certificate = (__bridge id)certRef;
[request setSR_SSLPinnedCertificates:@[ certificate ]];
socket = [[SRWebSocket alloc] initWithURLRequest:request];
socket.delegate = [self sharedManager];
[socket open];
[[self sharedManager] setWebsocket:socket];
}
+ (void)resignActivity{
SRWebSocket *socket = [[self sharedManager] websocket];
[socket close];
}
#pragma mark - Delegate methods
- (void) webSocketDidOpen:(SRWebSocket *)webSocket {
self.pingTimer = [NSTimer scheduledTimerWithTimeInterval:kWebSocketHeartBeat
target:self
selector:@selector(webSocketHeartBeat)
userInfo:nil
repeats:YES];
self.status = kSocketStatusOpen;
[self.reconnectTimer invalidate];
self.reconnectTimer = nil;
}
- (void) webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
DDLogError(@"Error connecting to socket %@", error);
[self.pingTimer invalidate];
self.status = kSocketStatusClosed;
[self scheduleRetry];
}
- (void) webSocket:(SRWebSocket *)webSocket didReceiveMessage:(NSData*)data {
WebSocketMessage *wsMessage = [WebSocketMessage parseFromData:data];
if (wsMessage.type == WebSocketMessageTypeRequest) {
[self processWebSocketRequestMessage:wsMessage.request];
} else if (wsMessage.type == WebSocketMessageTypeResponse){
[self processWebSocketResponseMessage:wsMessage.response];
} else{
DDLogWarn(@"Got a WebSocketMessage of unknown type");
}
}
- (void)processWebSocketRequestMessage:(WebSocketRequestMessage*)message {
DDLogInfo(@"Got message with verb: %@ and path: %@", message.verb, message.path);
[self sendWebSocketMessageAcknowledgement:message];
if ([message.path isEqualToString:@"/api/v1/message"] && [message.verb isEqualToString:@"PUT"]){
NSData *decryptedPayload = [Cryptography decryptAppleMessagePayload:message.body
withSignalingKey:TSStorageManager.signalingKey];
if (!decryptedPayload) {
DDLogWarn(@"Failed to decrypt incoming payload or bad HMAC");
return;
}
IncomingPushMessageSignal *messageSignal = [IncomingPushMessageSignal parseFromData:decryptedPayload];
[[TSMessagesManager sharedManager] handleMessageSignal:messageSignal];
} else{
DDLogWarn(@"Unsupported WebSocket Request");
}
}
- (void)processWebSocketResponseMessage:(WebSocketResponseMessage*)message {
DDLogWarn(@"Client should not receive WebSocket Respond messages");
}
- (void)sendWebSocketMessageAcknowledgement:(WebSocketRequestMessage*)request {
WebSocketResponseMessageBuilder *response = [WebSocketResponseMessage builder];
[response setStatus:200];
[response setMessage:@"OK"];
[response setId:request.id];
WebSocketMessageBuilder *message = [WebSocketMessage builder];
[message setResponse:response.build];
[message setType:WebSocketMessageTypeResponse];
@try {
[self.websocket send:message.build.data];
}
@catch (NSException *exception) {
DDLogWarn(@"Caught exception while trying to write on the socket %@", exception.debugDescription);
}
}
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
DDLogVerbose(@"WebSocket did close");
[self.pingTimer invalidate];
self.status = kSocketStatusClosed;
[self scheduleRetry];
}
- (void)webSocketHeartBeat {
@try {
[self.websocket sendPing:nil];
}
@catch (NSException *exception) {
DDLogWarn(@"Caught exception while trying to write on the socket %@", exception.debugDescription);
}
}
- (NSString*)webSocketAuthenticationString {
return [NSString stringWithFormat:@"?login=%@&password=%@", [[TSAccountManager registeredNumber] stringByReplacingOccurrencesOfString:@"+" withString:@"%2B"],[TSStorageManager serverAuthToken]];
}
- (void)scheduleRetry {
if (!self.reconnectTimer || ![self.reconnectTimer isValid]) {
self.reconnectTimer = [NSTimer scheduledTimerWithTimeInterval:kWebSocketReconnectTry
target:[self class]
selector:@selector(becomeActive)
userInfo:nil
repeats:YES];
}
}
#pragma mark - Background Connect
+ (void)becomeActiveFromForeground {
TSSocketManager *sharedInstance = [self sharedManager];
[sharedInstance.backgroundConnectTimer invalidate];
sharedInstance.backgroundConnectTimer = nil;
sharedInstance.fetchingTaskIdentifier = 0;
[self becomeActive];
}
+ (void)becomeActiveFromBackground {
TSSocketManager *sharedInstance = [TSSocketManager sharedManager];
if (sharedInstance.fetchingTaskIdentifier == 0) {
sharedInstance.fetchingTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
sharedInstance.fetchingTaskIdentifier = 0;
[TSSocketManager resignActivity];
}];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer * timer = [NSTimer timerWithTimeInterval:kBackgroundConnectTimer
target:sharedInstance
selector:@selector(closeBackgroundTask)
userInfo:nil
repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer
forMode:NSDefaultRunLoopMode];
});
[self becomeActive];
}
}
- (void)closeBackgroundTask {
UIBackgroundTaskIdentifier identifier = self.fetchingTaskIdentifier;
self.fetchingTaskIdentifier = 0;
if ([_websocket readyState] != SR_OPEN) {
[self backgroundConnectTimedOut];
}
[TSSocketManager resignActivity];
[[UIApplication sharedApplication] endBackgroundTask:identifier];
}
- (void)backgroundConnectTimedOut {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertBody = NSLocalizedString(@"APN_FETCHED_FAILED", nil);
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
}
#pragma mark UI Delegates
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == kSocketStatusObservationContext)
{
[self notifyStatusChange];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)notifyStatusChange {
switch (self.status) {
case kSocketStatusOpen:
[[NSNotificationCenter defaultCenter] postNotificationName:SocketOpenedNotification object:self];
break;
case kSocketStatusClosed:
[[NSNotificationCenter defaultCenter] postNotificationName:SocketClosedNotification object:self];
break;
case kSocketStatusConnecting:
[[NSNotificationCenter defaultCenter] postNotificationName:SocketConnectingNotification object:self];
break;
default:
break;
}
}
+ (void)sendNotification {
[[self sharedManager] notifyStatusChange];
}
@end