ably-cocoa/Source/ARTWebSocketTransport.m

417 lines
16 KiB
Objective-C

#import "ARTWebSocketTransport+Private.h"
#import "ARTRest.h"
#import "ARTRest+Private.h"
#import "ARTProtocolMessage.h"
#import "ARTClientOptions.h"
#import "ARTTokenParams.h"
#import "ARTTokenDetails.h"
#import "ARTStatus.h"
#import "ARTEncoder.h"
#import "ARTDefault.h"
#import "ARTRealtimeTransport.h"
#import "ARTGCD.h"
#import "ARTLog+Private.h"
#import "ARTEventEmitter+Private.h"
#import "NSURLQueryItem+Stringifiable.h"
#import "ARTNSMutableDictionary+ARTDictionaryUtil.h"
#import "ARTStringifiable.h"
enum {
ARTWsNeverConnected = -1,
ARTWsBuggyClose = -2,
ARTWsCloseNormal = 1000,
ARTWsGoingAway = 1001,
ARTWsCloseProtocolError = 1002,
ARTWsRefuse = 1003,
ARTWsNoUtf8 = 1007,
ARTWsPolicyValidation = 1008,
ARTWsTooBig = 1009,
ARTWsExtension = 1010,
ARTWsUnexpectedCondition = 1011,
ARTWsTlsError = 1015
};
NSString *WebSocketStateToStr(ARTSRReadyState state);
@interface ARTSRWebSocket () <ARTWebSocket>
@end
Class configuredWebsocketClass = nil;
@implementation ARTWebSocketTransport {
id<ARTRealtimeTransportDelegate> _delegate;
ARTRealtimeTransportState _state;
/**
The dispatch queue for firing the events. Must be the same for the whole library.
*/
_Nonnull dispatch_queue_t _workQueue;
}
@synthesize delegate = _delegate;
@synthesize stateEmitter = _stateEmitter;
+ (void)setWebSocketClass:(const Class)webSocketClass {
configuredWebsocketClass = webSocketClass;
}
- (instancetype)initWithRest:(ARTRestInternal *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial {
self = [super init];
if (self) {
_workQueue = rest.queue;
_websocket = nil;
_state = ARTRealtimeTransportStateClosed;
_encoder = rest.defaultEncoder;
_logger = rest.logger;
_protocolMessagesLogger = [[ARTLog alloc] initCapturingOutput:false historyLines:50];
_options = [options copy];
_resumeKey = resumeKey;
_connectionSerial = connectionSerial;
_stateEmitter = [[ARTInternalEventEmitter alloc] initWithQueue:_workQueue];
[self.logger verbose:__FILE__ line:__LINE__ message:@"R:%p WS:%p alloc", _delegate, self];
}
return self;
}
- (void)dealloc {
[self.logger verbose:__FILE__ line:__LINE__ message:@"R:%p WS:%p dealloc", _delegate, self];
self.websocket.delegate = nil;
self.websocket = nil;
self.delegate = nil;
}
- (BOOL)send:(NSData *)data withSource:(id)decodedObject {
if (self.websocket.readyState == ARTSR_OPEN) {
if ([decodedObject isKindOfClass:[ARTProtocolMessage class]]) {
[_protocolMessagesLogger info:@"send %@", [decodedObject description]];
}
[self.websocket send:data];
return true;
}
else {
NSString *extraInformation = @"";
if ([decodedObject isKindOfClass:[ARTProtocolMessage class]]) {
ARTProtocolMessage *msg = (ARTProtocolMessage *)decodedObject;
extraInformation = [NSString stringWithFormat:@"with action \"%tu - %@\" ", msg.action, ARTProtocolMessageActionToStr(msg.action)];
}
[self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p sending message %@was ignored because websocket isn't ready", _delegate, self, extraInformation];
return false;
}
}
- (void)internalSend:(ARTProtocolMessage *)msg {
[self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket sending action %tu - %@", _delegate, self, msg.action, ARTProtocolMessageActionToStr(msg.action)];
[_protocolMessagesLogger info:@"send %@", [msg description]];
NSData *data = [self.encoder encodeProtocolMessage:msg error:nil];
[self send:data withSource:msg];
}
- (void)receive:(ARTProtocolMessage *)msg {
[_protocolMessagesLogger info:@"recv %@", [msg description]];
[self.delegate realtimeTransport:self didReceiveMessage:msg];
}
- (ARTProtocolMessage *)receiveWithData:(NSData *)data {
ARTProtocolMessage *pm = [self.encoder decodeProtocolMessage:data error:nil];
[self receive:pm];
return pm;
}
- (void)connectWithKey:(NSString *)key {
_state = ARTRealtimeTransportStateOpening;
[self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket connect with key", _delegate, self];
NSURLQueryItem *keyParam = [NSURLQueryItem queryItemWithName:@"key" value:key];
[self setupWebSocket:@{keyParam.name: keyParam} withOptions:self.options resumeKey:self.resumeKey connectionSerial:self.connectionSerial];
// Connect
[self.websocket open];
}
- (void)connectWithToken:(NSString *)token {
_state = ARTRealtimeTransportStateOpening;
[self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket connect with token", _delegate, self];
NSURLQueryItem *accessTokenParam = [NSURLQueryItem queryItemWithName:@"accessToken" value:token];
[self setupWebSocket:@{accessTokenParam.name: accessTokenParam} withOptions:self.options resumeKey:self.resumeKey connectionSerial:self.connectionSerial];
// Connect
[self.websocket open];
}
- (NSURL *)setupWebSocket:(NSDictionary<NSString *, NSURLQueryItem *> *)params withOptions:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial {
__block NSMutableDictionary<NSString*, NSURLQueryItem*> *queryItems = [params mutableCopy];
// ClientID
if (options.clientId) {
[queryItems addValueAsURLQueryItem:options.clientId forKey:@"clientId"];
}
// Echo
[queryItems addValueAsURLQueryItem:options.echoMessages ? @"true" : @"false" forKey:@"echo"];
// Format: MsgPack, JSON
[queryItems addValueAsURLQueryItem:[_encoder formatAsString] forKey:@"format"];
if (options.recover) {
NSArray *recoverParts = [options.recover componentsSeparatedByString:@":"];
if (recoverParts.count > 1 && recoverParts.count <= 3) {
NSString *key = [recoverParts objectAtIndex:0];
NSString *serial = [recoverParts objectAtIndex:1];
[self.logger info:@"R:%p WS:%p ARTWebSocketTransport: attempting recovery of connection %@", _delegate, self, key];
[queryItems addValueAsURLQueryItem:key forKey:@"recover"];
[queryItems addValueAsURLQueryItem:serial forKey:@"connectionSerial"];
int64_t msgSerial = [[recoverParts lastObject] longLongValue];
if (msgSerial) {
[_delegate realtimeTransportSetMsgSerial:self msgSerial:msgSerial];
}
}
else {
[self.logger error:@"R:%p WS:%p ARTWebSocketTransport: recovery string is malformed, ignoring: '%@'", _delegate, self, options.recover];
}
}
else if (resumeKey != nil && connectionSerial != nil) {
[queryItems addValueAsURLQueryItem:resumeKey forKey:@"resume"];
[queryItems addValueAsURLQueryItem:[NSString stringWithFormat:@"%lld", (long long)[connectionSerial integerValue]] forKey:@"connectionSerial"];
}
[queryItems addValueAsURLQueryItem:[ARTDefault apiVersion] forKey:@"v"];
// Lib
[queryItems addValueAsURLQueryItem:[options agents] forKey:@"agent"];
// Transport Params
if (options.transportParams != nil) {
[options.transportParams enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, ARTStringifiable * _Nonnull obj, BOOL * _Nonnull stop) {
[queryItems addValueAsURLQueryItem:obj.stringValue forKey:key];
}];
}
// URL
NSURLComponents *urlComponents = [NSURLComponents componentsWithString:@"/"];
urlComponents.queryItems = [queryItems allValues];
NSURL *url = [urlComponents URLRelativeToURL:[options realtimeUrl]];
[_logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p url %@", _delegate, self, url];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
const Class websocketClass = configuredWebsocketClass ? configuredWebsocketClass : [ARTSRWebSocket class];
self.websocket = [[websocketClass alloc] initWithURLRequest:request];
[self.websocket setDelegateDispatchQueue:_workQueue];
self.websocket.delegate = self;
self.websocketURL = url;
return url;
}
- (void)sendClose {
_state = ARTRealtimeTransportStateClosing;
ARTProtocolMessage *closeMessage = [[ARTProtocolMessage alloc] init];
closeMessage.action = ARTProtocolMessageClose;
[self internalSend:closeMessage];
}
- (void)sendPing {
ARTProtocolMessage *heartbeatMessage = [[ARTProtocolMessage alloc] init];
heartbeatMessage.action = ARTProtocolMessageHeartbeat;
[self internalSend:heartbeatMessage];
}
- (void)close {
self.delegate = nil;
if (!_websocket) return;
self.websocket.delegate = nil;
[self.websocket closeWithCode:ARTWsCloseNormal reason:@"Normal Closure"];
self.websocket = nil;
}
- (void)abort:(ARTStatus *)reason {
self.delegate = nil;
if (!_websocket) return;
self.websocket.delegate = nil;
if (reason.errorInfo) {
[self.websocket closeWithCode:ARTWsCloseNormal reason:reason.errorInfo.description];
}
else {
[self.websocket closeWithCode:ARTWsCloseNormal reason:@"Abnormal Closure"];
}
self.websocket = nil;
}
- (void)setHost:(NSString *)host {
self.options.realtimeHost = host;
}
- (NSString *)host {
return self.options.realtimeHost;
}
- (ARTRealtimeTransportState)state {
if (self.websocket.readyState == ARTSR_OPEN) {
return ARTRealtimeTransportStateOpened;
}
return _state;
}
- (void)setState:(ARTRealtimeTransportState)state {
_state = state;
}
#pragma mark - ARTSRWebSocketDelegate
// All delegate methods from SocketRocket are called from rest's serial queue,
// since we pass it as delegate queue on setupWebSocket. So we can safely
// call all our delegate's methods.
- (void)webSocketDidOpen:(id<ARTWebSocket>)websocket {
[self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket did open", _delegate, self];
[_stateEmitter emit:[ARTEvent newWithTransportState:ARTRealtimeTransportStateOpened] with:nil];
[_delegate realtimeTransportAvailable:self];
}
- (void)webSocket:(id<ARTWebSocket>)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
[self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket did disconnect (code %ld) %@", _delegate, self, (long)code, reason];
switch (code) {
case ARTWsCloseNormal:
[_delegate realtimeTransportClosed:self];
break;
case ARTWsNeverConnected:
[_delegate realtimeTransportNeverConnected:self];
break;
case ARTWsBuggyClose:
case ARTWsGoingAway:
// Connectivity issue
[_delegate realtimeTransportDisconnected:self withError:nil];
break;
case ARTWsRefuse:
case ARTWsPolicyValidation:
[_delegate realtimeTransportRefused:self withError:[[ARTRealtimeTransportError alloc] initWithError:[ARTErrorInfo createWithCode:code message:reason] type:ARTRealtimeTransportErrorTypeRefused url:self.websocketURL]];
break;
case ARTWsTooBig:
[_delegate realtimeTransportTooBig:self];
break;
case ARTWsNoUtf8:
case ARTWsCloseProtocolError:
case ARTWsUnexpectedCondition:
case ARTWsExtension:
case ARTWsTlsError:
// Failed
[_delegate realtimeTransportFailed:self withError:[[ARTRealtimeTransportError alloc] initWithError:[ARTErrorInfo createWithCode:code message:reason] type:ARTRealtimeTransportErrorTypeOther url:self.websocketURL]];
break;
default:
NSAssert(true, @"WebSocket close: unknown code");
break;
}
_state = ARTRealtimeTransportStateClosed;
[_stateEmitter emit:[ARTEvent newWithTransportState:ARTRealtimeTransportStateClosed] with:nil];
}
- (void)webSocket:(id<ARTWebSocket>)webSocket didFailWithError:(NSError *)error {
[self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket did receive error %@", _delegate, self, error];
[_delegate realtimeTransportFailed:self withError:[self classifyError:error]];
_state = ARTRealtimeTransportStateClosed;
}
- (ARTRealtimeTransportError *)classifyError:(NSError *)error {
ARTRealtimeTransportErrorType type = ARTRealtimeTransportErrorTypeOther;
if ([error.domain isEqualToString:@"com.squareup.SocketRocket"] && error.code == 504) {
type = ARTRealtimeTransportErrorTypeTimeout;
} else if ([error.domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork]) {
type = ARTRealtimeTransportErrorTypeHostUnreachable;
} else if ([error.domain isEqualToString:@"NSPOSIXErrorDomain"] && (error.code == 57 || error.code == 50)) {
type = ARTRealtimeTransportErrorTypeNoInternet;
} else if ([error.domain isEqualToString:ARTSRWebSocketErrorDomain] && error.code == 2132) {
id status = error.userInfo[ARTSRHTTPResponseErrorKey];
if (status) {
return [[ARTRealtimeTransportError alloc] initWithError:error
badResponseCode:[(NSNumber *)status integerValue]
url:self.websocketURL];
}
}
return [[ARTRealtimeTransportError alloc] initWithError:error type:type url:self.websocketURL];
}
- (void)webSocket:(id<ARTWebSocket>)webSocket didReceiveMessage:(id)message {
[self.logger verbose:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket did receive message", _delegate, self];
if (self.websocket.readyState == ARTSR_CLOSED) {
[self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket is closed, message has been ignored", _delegate, self];
return;
}
if ([message isKindOfClass:[NSString class]]) {
[self webSocketMessageText:(NSString *)message];
} else if ([message isKindOfClass:[NSData class]]) {
[self webSocketMessageData:(NSData *)message];
} else if ([message isKindOfClass:[ARTProtocolMessage class]]) {
[self webSocketMessageProtocol:(ARTProtocolMessage *)message];
}
}
- (void)webSocketMessageText:(NSString *)text {
[self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket in %@ state did receive message %@", _delegate, self, WebSocketStateToStr(self.websocket.readyState), text];
NSData *data = nil;
data = [((NSString *)text) dataUsingEncoding:NSUTF8StringEncoding];
[self receiveWithData:data];
}
- (void)webSocketMessageData:(NSData *)data {
[self.logger verbose:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket in %@ state did receive data %@", _delegate, self, WebSocketStateToStr(self.websocket.readyState), data];
[self receiveWithData:data];
}
- (void)webSocketMessageProtocol:(ARTProtocolMessage *)message {
[self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket in %@ state did receive protocol message %@", _delegate, self, WebSocketStateToStr(self.websocket.readyState), message];
[self receive:message];
}
@end
NSString *WebSocketStateToStr(ARTSRReadyState state) {
switch (state) {
case ARTSR_CONNECTING:
return @"Connecting"; //0
case ARTSR_OPEN:
return @"Open"; //1
case ARTSR_CLOSING:
return @"Closing"; //2
case ARTSR_CLOSED:
return @"Closed"; //3
}
}
NSString *ARTRealtimeTransportStateToStr(ARTRealtimeTransportState state) {
switch (state) {
case ARTRealtimeTransportStateOpening:
return @"Connecting"; //0
case ARTRealtimeTransportStateOpened:
return @"Open"; //1
case ARTRealtimeTransportStateClosing:
return @"Closing"; //2
case ARTRealtimeTransportStateClosed:
return @"Closed"; //3
}
}
#pragma mark - ARTEvent
@implementation ARTEvent (TransportState)
- (instancetype)initWithTransportState:(ARTRealtimeTransportState)value {
return [self initWithString:[NSString stringWithFormat:@"ARTRealtimeTransportState%@", ARTRealtimeTransportStateToStr(value)]];
}
+ (instancetype)newWithTransportState:(ARTRealtimeTransportState)value {
return [[self alloc] initWithTransportState:value];
}
@end