From 9aacad356df72146be21f049af5ddc6b48e716d3 Mon Sep 17 00:00:00 2001 From: James Chen Date: Wed, 5 Jul 2017 16:47:59 +0800 Subject: [PATCH] Adds SocketRocket library for iOS/macOS --- .../Internal/Delegate/SRDelegateController.h | 52 + .../Internal/Delegate/SRDelegateController.m | 138 ++ .../Internal/IOConsumer/SRIOConsumer.h | 40 + .../Internal/IOConsumer/SRIOConsumer.m | 36 + .../Internal/IOConsumer/SRIOConsumerPool.h | 28 + .../Internal/IOConsumer/SRIOConsumerPool.m | 64 + .../Internal/NSRunLoop+SRWebSocketPrivate.h | 13 + .../NSURLRequest+SRWebSocketPrivate.h | 13 + .../Internal/Proxy/SRProxyConnect.h | 26 + .../Internal/Proxy/SRProxyConnect.m | 481 +++++ .../Internal/RunLoop/SRRunLoopThread.h | 24 + .../Internal/RunLoop/SRRunLoopThread.m | 83 + sources/SocketRocket/Internal/SRConstants.h | 26 + sources/SocketRocket/Internal/SRConstants.m | 19 + .../Security/SRPinningSecurityPolicy.h | 22 + .../Security/SRPinningSecurityPolicy.m | 67 + .../SocketRocket/Internal/Utilities/SRError.h | 20 + .../SocketRocket/Internal/Utilities/SRError.m | 42 + .../Internal/Utilities/SRHTTPConnectMessage.h | 20 + .../Internal/Utilities/SRHTTPConnectMessage.m | 79 + .../SocketRocket/Internal/Utilities/SRHash.h | 19 + .../SocketRocket/Internal/Utilities/SRHash.m | 43 + .../SocketRocket/Internal/Utilities/SRLog.h | 20 + .../SocketRocket/Internal/Utilities/SRLog.m | 33 + .../SocketRocket/Internal/Utilities/SRMutex.h | 22 + .../SocketRocket/Internal/Utilities/SRMutex.m | 47 + .../Internal/Utilities/SRRandom.h | 16 + .../Internal/Utilities/SRRandom.m | 26 + .../Internal/Utilities/SRSIMDHelpers.h | 19 + .../Internal/Utilities/SRSIMDHelpers.m | 73 + .../Internal/Utilities/SRURLUtilities.h | 26 + .../Internal/Utilities/SRURLUtilities.m | 77 + sources/SocketRocket/NSRunLoop+SRWebSocket.h | 27 + sources/SocketRocket/NSRunLoop+SRWebSocket.m | 27 + .../SocketRocket/NSURLRequest+SRWebSocket.h | 34 + .../SocketRocket/NSURLRequest+SRWebSocket.m | 45 + sources/SocketRocket/SRSecurityPolicy.h | 67 + sources/SocketRocket/SRSecurityPolicy.m | 66 + sources/SocketRocket/SRWebSocket.h | 413 +++++ sources/SocketRocket/SRWebSocket.m | 1617 +++++++++++++++++ sources/SocketRocket/SocketRocket.h | 15 + 41 files changed, 4025 insertions(+) create mode 100644 sources/SocketRocket/Internal/Delegate/SRDelegateController.h create mode 100644 sources/SocketRocket/Internal/Delegate/SRDelegateController.m create mode 100644 sources/SocketRocket/Internal/IOConsumer/SRIOConsumer.h create mode 100644 sources/SocketRocket/Internal/IOConsumer/SRIOConsumer.m create mode 100644 sources/SocketRocket/Internal/IOConsumer/SRIOConsumerPool.h create mode 100644 sources/SocketRocket/Internal/IOConsumer/SRIOConsumerPool.m create mode 100644 sources/SocketRocket/Internal/NSRunLoop+SRWebSocketPrivate.h create mode 100644 sources/SocketRocket/Internal/NSURLRequest+SRWebSocketPrivate.h create mode 100644 sources/SocketRocket/Internal/Proxy/SRProxyConnect.h create mode 100644 sources/SocketRocket/Internal/Proxy/SRProxyConnect.m create mode 100644 sources/SocketRocket/Internal/RunLoop/SRRunLoopThread.h create mode 100644 sources/SocketRocket/Internal/RunLoop/SRRunLoopThread.m create mode 100644 sources/SocketRocket/Internal/SRConstants.h create mode 100644 sources/SocketRocket/Internal/SRConstants.m create mode 100644 sources/SocketRocket/Internal/Security/SRPinningSecurityPolicy.h create mode 100644 sources/SocketRocket/Internal/Security/SRPinningSecurityPolicy.m create mode 100644 sources/SocketRocket/Internal/Utilities/SRError.h create mode 100644 sources/SocketRocket/Internal/Utilities/SRError.m create mode 100644 sources/SocketRocket/Internal/Utilities/SRHTTPConnectMessage.h create mode 100644 sources/SocketRocket/Internal/Utilities/SRHTTPConnectMessage.m create mode 100644 sources/SocketRocket/Internal/Utilities/SRHash.h create mode 100644 sources/SocketRocket/Internal/Utilities/SRHash.m create mode 100644 sources/SocketRocket/Internal/Utilities/SRLog.h create mode 100644 sources/SocketRocket/Internal/Utilities/SRLog.m create mode 100644 sources/SocketRocket/Internal/Utilities/SRMutex.h create mode 100644 sources/SocketRocket/Internal/Utilities/SRMutex.m create mode 100644 sources/SocketRocket/Internal/Utilities/SRRandom.h create mode 100644 sources/SocketRocket/Internal/Utilities/SRRandom.m create mode 100644 sources/SocketRocket/Internal/Utilities/SRSIMDHelpers.h create mode 100644 sources/SocketRocket/Internal/Utilities/SRSIMDHelpers.m create mode 100644 sources/SocketRocket/Internal/Utilities/SRURLUtilities.h create mode 100644 sources/SocketRocket/Internal/Utilities/SRURLUtilities.m create mode 100644 sources/SocketRocket/NSRunLoop+SRWebSocket.h create mode 100644 sources/SocketRocket/NSRunLoop+SRWebSocket.m create mode 100644 sources/SocketRocket/NSURLRequest+SRWebSocket.h create mode 100644 sources/SocketRocket/NSURLRequest+SRWebSocket.m create mode 100644 sources/SocketRocket/SRSecurityPolicy.h create mode 100644 sources/SocketRocket/SRSecurityPolicy.m create mode 100644 sources/SocketRocket/SRWebSocket.h create mode 100644 sources/SocketRocket/SRWebSocket.m create mode 100644 sources/SocketRocket/SocketRocket.h diff --git a/sources/SocketRocket/Internal/Delegate/SRDelegateController.h b/sources/SocketRocket/Internal/Delegate/SRDelegateController.h new file mode 100644 index 00000000..7b230022 --- /dev/null +++ b/sources/SocketRocket/Internal/Delegate/SRDelegateController.h @@ -0,0 +1,52 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +struct SRDelegateAvailableMethods { + BOOL didReceiveMessage : 1; + BOOL didReceiveMessageWithString : 1; + BOOL didReceiveMessageWithData : 1; + BOOL didOpen : 1; + BOOL didFailWithError : 1; + BOOL didCloseWithCode : 1; + BOOL didReceivePing : 1; + BOOL didReceivePong : 1; + BOOL shouldConvertTextFrameToString : 1; +}; +typedef struct SRDelegateAvailableMethods SRDelegateAvailableMethods; + +typedef void(^SRDelegateBlock)(id _Nullable delegate, SRDelegateAvailableMethods availableMethods); + +@interface SRDelegateController : NSObject + +@property (nonatomic, weak) id delegate; +@property (atomic, readonly) SRDelegateAvailableMethods availableDelegateMethods; + +#if OS_OBJECT_USE_OBJC +@property (nullable, nonatomic, strong) dispatch_queue_t dispatchQueue; +#else +@property (nullable, nonatomic, assign) dispatch_queue_t dispatchQueue; +#endif +@property (nullable, nonatomic, strong) NSOperationQueue *operationQueue; + +///-------------------------------------- +#pragma mark - Perform +///-------------------------------------- + +- (void)performDelegateBlock:(SRDelegateBlock)block; +- (void)performDelegateQueueBlock:(dispatch_block_t)block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Delegate/SRDelegateController.m b/sources/SocketRocket/Internal/Delegate/SRDelegateController.m new file mode 100644 index 00000000..d75fb27c --- /dev/null +++ b/sources/SocketRocket/Internal/Delegate/SRDelegateController.m @@ -0,0 +1,138 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRDelegateController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SRDelegateController () + +@property (nonatomic, strong, readonly) dispatch_queue_t accessQueue; + +@property (atomic, assign, readwrite) SRDelegateAvailableMethods availableDelegateMethods; + +@end + +@implementation SRDelegateController + +@synthesize delegate = _delegate; +@synthesize dispatchQueue = _dispatchQueue; +@synthesize operationQueue = _operationQueue; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init +{ + self = [super init]; + if (!self) return self; + + _accessQueue = dispatch_queue_create("com.facebook.socketrocket.delegate.access", DISPATCH_QUEUE_CONCURRENT); + _dispatchQueue = dispatch_get_main_queue(); + + return self; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (void)setDelegate:(id _Nullable)delegate +{ + dispatch_barrier_async(self.accessQueue, ^{ + _delegate = delegate; + + self.availableDelegateMethods = (SRDelegateAvailableMethods){ + .didReceiveMessage = [delegate respondsToSelector:@selector(webSocket:didReceiveMessage:)], + .didReceiveMessageWithString = [delegate respondsToSelector:@selector(webSocket:didReceiveMessageWithString:)], + .didReceiveMessageWithData = [delegate respondsToSelector:@selector(webSocket:didReceiveMessageWithData:)], + .didOpen = [delegate respondsToSelector:@selector(webSocketDidOpen:)], + .didFailWithError = [delegate respondsToSelector:@selector(webSocket:didFailWithError:)], + .didCloseWithCode = [delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)], + .didReceivePing = [delegate respondsToSelector:@selector(webSocket:didReceivePingWithData:)], + .didReceivePong = [delegate respondsToSelector:@selector(webSocket:didReceivePong:)], + .shouldConvertTextFrameToString = [delegate respondsToSelector:@selector(webSocketShouldConvertTextFrameToString:)] + }; + }); +} + +- (id _Nullable)delegate +{ + __block id delegate = nil; + dispatch_sync(self.accessQueue, ^{ + delegate = _delegate; + }); + return delegate; +} + +- (void)setDispatchQueue:(dispatch_queue_t _Nullable)queue +{ + dispatch_barrier_async(self.accessQueue, ^{ + _dispatchQueue = queue ?: dispatch_get_main_queue(); + _operationQueue = nil; + }); +} + +- (dispatch_queue_t _Nullable)dispatchQueue +{ + __block dispatch_queue_t queue = nil; + dispatch_sync(self.accessQueue, ^{ + queue = _dispatchQueue; + }); + return queue; +} + +- (void)setOperationQueue:(NSOperationQueue *_Nullable)queue +{ + dispatch_barrier_async(self.accessQueue, ^{ + _dispatchQueue = queue ? nil : dispatch_get_main_queue(); + _operationQueue = queue; + }); +} + +- (NSOperationQueue *_Nullable)operationQueue +{ + __block NSOperationQueue *queue = nil; + dispatch_sync(self.accessQueue, ^{ + queue = _operationQueue; + }); + return queue; +} + +///-------------------------------------- +#pragma mark - Perform +///-------------------------------------- + +- (void)performDelegateBlock:(SRDelegateBlock)block +{ + __block __strong id delegate = nil; + __block SRDelegateAvailableMethods availableMethods = {}; + dispatch_sync(self.accessQueue, ^{ + delegate = _delegate; // Not `OK` to go through `self`, since queue sync. + availableMethods = self.availableDelegateMethods; // `OK` to call through `self`, since no queue sync. + }); + [self performDelegateQueueBlock:^{ + block(delegate, availableMethods); + }]; +} + +- (void)performDelegateQueueBlock:(dispatch_block_t)block +{ + dispatch_queue_t dispatchQueue = self.dispatchQueue; + if (dispatchQueue) { + dispatch_async(dispatchQueue, block); + } else { + [self.operationQueue addOperationWithBlock:block]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/IOConsumer/SRIOConsumer.h b/sources/SocketRocket/Internal/IOConsumer/SRIOConsumer.h new file mode 100644 index 00000000..6b02a3b1 --- /dev/null +++ b/sources/SocketRocket/Internal/IOConsumer/SRIOConsumer.h @@ -0,0 +1,40 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +@class SRWebSocket; // TODO: (nlutsenko) Remove dependency on SRWebSocket here. + +// Returns number of bytes consumed. Returning 0 means you didn't match. +// Sends bytes to callback handler; +typedef size_t (^stream_scanner)(NSData *collected_data); +typedef void (^data_callback)(SRWebSocket *webSocket, NSData *data); + +@interface SRIOConsumer : NSObject { + stream_scanner _scanner; + data_callback _handler; + size_t _bytesNeeded; + BOOL _readToCurrentFrame; + BOOL _unmaskBytes; +} +@property (nonatomic, copy, readonly) stream_scanner consumer; +@property (nonatomic, copy, readonly) data_callback handler; +@property (nonatomic, assign) size_t bytesNeeded; +@property (nonatomic, assign, readonly) BOOL readToCurrentFrame; +@property (nonatomic, assign, readonly) BOOL unmaskBytes; + +- (void)resetWithScanner:(stream_scanner)scanner + handler:(data_callback)handler + bytesNeeded:(size_t)bytesNeeded + readToCurrentFrame:(BOOL)readToCurrentFrame + unmaskBytes:(BOOL)unmaskBytes; + +@end diff --git a/sources/SocketRocket/Internal/IOConsumer/SRIOConsumer.m b/sources/SocketRocket/Internal/IOConsumer/SRIOConsumer.m new file mode 100644 index 00000000..8b17e3e8 --- /dev/null +++ b/sources/SocketRocket/Internal/IOConsumer/SRIOConsumer.m @@ -0,0 +1,36 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRIOConsumer.h" + +@implementation SRIOConsumer + +@synthesize bytesNeeded = _bytesNeeded; +@synthesize consumer = _scanner; +@synthesize handler = _handler; +@synthesize readToCurrentFrame = _readToCurrentFrame; +@synthesize unmaskBytes = _unmaskBytes; + +- (void)resetWithScanner:(stream_scanner)scanner + handler:(data_callback)handler + bytesNeeded:(size_t)bytesNeeded + readToCurrentFrame:(BOOL)readToCurrentFrame + unmaskBytes:(BOOL)unmaskBytes +{ + _scanner = [scanner copy]; + _handler = [handler copy]; + _bytesNeeded = bytesNeeded; + _readToCurrentFrame = readToCurrentFrame; + _unmaskBytes = unmaskBytes; + assert(_scanner || _bytesNeeded); +} + +@end diff --git a/sources/SocketRocket/Internal/IOConsumer/SRIOConsumerPool.h b/sources/SocketRocket/Internal/IOConsumer/SRIOConsumerPool.h new file mode 100644 index 00000000..1e7ad320 --- /dev/null +++ b/sources/SocketRocket/Internal/IOConsumer/SRIOConsumerPool.h @@ -0,0 +1,28 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +#import "SRIOConsumer.h" // TODO: (nlutsenko) Convert to @class and constants file for block types + +// This class is not thread-safe, and is expected to always be run on the same queue. +@interface SRIOConsumerPool : NSObject + +- (instancetype)initWithBufferCapacity:(NSUInteger)poolSize; + +- (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner + handler:(data_callback)handler + bytesNeeded:(size_t)bytesNeeded + readToCurrentFrame:(BOOL)readToCurrentFrame + unmaskBytes:(BOOL)unmaskBytes; +- (void)returnConsumer:(SRIOConsumer *)consumer; + +@end diff --git a/sources/SocketRocket/Internal/IOConsumer/SRIOConsumerPool.m b/sources/SocketRocket/Internal/IOConsumer/SRIOConsumerPool.m new file mode 100644 index 00000000..2c527dae --- /dev/null +++ b/sources/SocketRocket/Internal/IOConsumer/SRIOConsumerPool.m @@ -0,0 +1,64 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRIOConsumerPool.h" + +@implementation SRIOConsumerPool { + NSUInteger _poolSize; + NSMutableArray *_bufferedConsumers; +} + +- (instancetype)initWithBufferCapacity:(NSUInteger)poolSize; +{ + self = [super init]; + if (self) { + _poolSize = poolSize; + _bufferedConsumers = [NSMutableArray arrayWithCapacity:poolSize]; + } + return self; +} + +- (instancetype)init +{ + return [self initWithBufferCapacity:8]; +} + +- (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner + handler:(data_callback)handler + bytesNeeded:(size_t)bytesNeeded + readToCurrentFrame:(BOOL)readToCurrentFrame + unmaskBytes:(BOOL)unmaskBytes +{ + SRIOConsumer *consumer = nil; + if (_bufferedConsumers.count) { + consumer = [_bufferedConsumers lastObject]; + [_bufferedConsumers removeLastObject]; + } else { + consumer = [[SRIOConsumer alloc] init]; + } + + [consumer resetWithScanner:scanner + handler:handler + bytesNeeded:bytesNeeded + readToCurrentFrame:readToCurrentFrame + unmaskBytes:unmaskBytes]; + + return consumer; +} + +- (void)returnConsumer:(SRIOConsumer *)consumer; +{ + if (_bufferedConsumers.count < _poolSize) { + [_bufferedConsumers addObject:consumer]; + } +} + +@end diff --git a/sources/SocketRocket/Internal/NSRunLoop+SRWebSocketPrivate.h b/sources/SocketRocket/Internal/NSRunLoop+SRWebSocketPrivate.h new file mode 100644 index 00000000..098f7a81 --- /dev/null +++ b/sources/SocketRocket/Internal/NSRunLoop+SRWebSocketPrivate.h @@ -0,0 +1,13 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +// Empty function that force links the object file for the category. +extern void import_NSRunLoop_SRWebSocket(void); diff --git a/sources/SocketRocket/Internal/NSURLRequest+SRWebSocketPrivate.h b/sources/SocketRocket/Internal/NSURLRequest+SRWebSocketPrivate.h new file mode 100644 index 00000000..b09dde42 --- /dev/null +++ b/sources/SocketRocket/Internal/NSURLRequest+SRWebSocketPrivate.h @@ -0,0 +1,13 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +// Empty function that force links the object file for the category. +extern void import_NSURLRequest_SRWebSocket(void); diff --git a/sources/SocketRocket/Internal/Proxy/SRProxyConnect.h b/sources/SocketRocket/Internal/Proxy/SRProxyConnect.h new file mode 100644 index 00000000..e947c482 --- /dev/null +++ b/sources/SocketRocket/Internal/Proxy/SRProxyConnect.h @@ -0,0 +1,26 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^SRProxyConnectCompletion)(NSError *_Nullable error, + NSInputStream *_Nullable readStream, + NSOutputStream *_Nullable writeStream); + +@interface SRProxyConnect : NSObject + +- (instancetype)initWithURL:(NSURL *)url; + +- (void)openNetworkStreamWithCompletion:(SRProxyConnectCompletion)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Proxy/SRProxyConnect.m b/sources/SocketRocket/Internal/Proxy/SRProxyConnect.m new file mode 100644 index 00000000..43cdfef4 --- /dev/null +++ b/sources/SocketRocket/Internal/Proxy/SRProxyConnect.m @@ -0,0 +1,481 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRProxyConnect.h" + +#import "NSRunLoop+SRWebSocket.h" +#import "SRConstants.h" +#import "SRError.h" +#import "SRLog.h" +#import "SRURLUtilities.h" + +@interface SRProxyConnect() + +@property (nonatomic, strong) NSURL *url; +@property (nonatomic, strong) NSInputStream *inputStream; +@property (nonatomic, strong) NSOutputStream *outputStream; + +@end + +@implementation SRProxyConnect +{ + SRProxyConnectCompletion _completion; + + NSString *_httpProxyHost; + uint32_t _httpProxyPort; + + CFHTTPMessageRef _receivedHTTPHeaders; + + NSString *_socksProxyHost; + uint32_t _socksProxyPort; + NSString *_socksProxyUsername; + NSString *_socksProxyPassword; + + BOOL _connectionRequiresSSL; + + NSMutableArray *_inputQueue; + dispatch_queue_t _writeQueue; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +-(instancetype)initWithURL:(NSURL *)url +{ + self = [super init]; + if (!self) return self; + + _url = url; + _connectionRequiresSSL = SRURLRequiresSSL(url); + + _writeQueue = dispatch_queue_create("com.facebook.socketrocket.proxyconnect.write", DISPATCH_QUEUE_SERIAL); + _inputQueue = [NSMutableArray arrayWithCapacity:2]; + + return self; +} + +- (void)dealloc +{ + // If we get deallocated before the socket open finishes - we need to cleanup everything. + + [self.inputStream removeFromRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; + self.inputStream.delegate = nil; + [self.inputStream close]; + self.inputStream = nil; + + self.outputStream.delegate = nil; + [self.outputStream close]; + self.outputStream = nil; +} + +///-------------------------------------- +#pragma mark - Open +///-------------------------------------- + +- (void)openNetworkStreamWithCompletion:(SRProxyConnectCompletion)completion +{ + _completion = completion; + [self _configureProxy]; +} + +///-------------------------------------- +#pragma mark - Flow +///-------------------------------------- + +- (void)_didConnect +{ + SRDebugLog(@"_didConnect, return streams"); + if (_connectionRequiresSSL) { + if (_httpProxyHost) { + // Must set the real peer name before turning on SSL + SRDebugLog(@"proxy set peer name to real host %@", self.url.host); + [self.outputStream setProperty:self.url.host forKey:@"_kCFStreamPropertySocketPeerName"]; + } + } + if (_receivedHTTPHeaders) { + CFRelease(_receivedHTTPHeaders); + _receivedHTTPHeaders = NULL; + } + + NSInputStream *inputStream = self.inputStream; + NSOutputStream *outputStream = self.outputStream; + + self.inputStream = nil; + self.outputStream = nil; + + [inputStream removeFromRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; + inputStream.delegate = nil; + outputStream.delegate = nil; + + _completion(nil, inputStream, outputStream); +} + +- (void)_failWithError:(NSError *)error +{ + SRDebugLog(@"_failWithError, return error"); + if (!error) { + error = SRHTTPErrorWithCodeDescription(500, 2132,@"Proxy Error"); + } + + if (_receivedHTTPHeaders) { + CFRelease(_receivedHTTPHeaders); + _receivedHTTPHeaders = NULL; + } + + self.inputStream.delegate = nil; + self.outputStream.delegate = nil; + + [self.inputStream removeFromRunLoop:[NSRunLoop SR_networkRunLoop] + forMode:NSDefaultRunLoopMode]; + [self.inputStream close]; + [self.outputStream close]; + self.inputStream = nil; + self.outputStream = nil; + _completion(error, nil, nil); +} + +// get proxy setting from device setting +- (void)_configureProxy +{ + SRDebugLog(@"configureProxy"); + NSDictionary *proxySettings = CFBridgingRelease(CFNetworkCopySystemProxySettings()); + + // CFNetworkCopyProxiesForURL doesn't understand ws:// or wss:// + NSURL *httpURL; + if (_connectionRequiresSSL) { + httpURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", _url.host]]; + } else { + httpURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", _url.host]]; + } + + NSArray *proxies = CFBridgingRelease(CFNetworkCopyProxiesForURL((__bridge CFURLRef)httpURL, (__bridge CFDictionaryRef)proxySettings)); + if (proxies.count == 0) { + SRDebugLog(@"configureProxy no proxies"); + [self _openConnection]; + return; // no proxy + } + NSDictionary *settings = [proxies objectAtIndex:0]; + NSString *proxyType = settings[(NSString *)kCFProxyTypeKey]; + if ([proxyType isEqualToString:(NSString *)kCFProxyTypeAutoConfigurationURL]) { + NSURL *pacURL = settings[(NSString *)kCFProxyAutoConfigurationURLKey]; + if (pacURL) { + [self _fetchPAC:pacURL withProxySettings:proxySettings]; + return; + } + } + if ([proxyType isEqualToString:(__bridge NSString *)kCFProxyTypeAutoConfigurationJavaScript]) { + NSString *script = settings[(__bridge NSString *)kCFProxyAutoConfigurationJavaScriptKey]; + if (script) { + [self _runPACScript:script withProxySettings:proxySettings]; + return; + } + } + [self _readProxySettingWithType:proxyType settings:settings]; + + [self _openConnection]; +} + +- (void)_readProxySettingWithType:(NSString *)proxyType settings:(NSDictionary *)settings +{ + if ([proxyType isEqualToString:(NSString *)kCFProxyTypeHTTP] || + [proxyType isEqualToString:(NSString *)kCFProxyTypeHTTPS]) { + _httpProxyHost = settings[(NSString *)kCFProxyHostNameKey]; + NSNumber *portValue = settings[(NSString *)kCFProxyPortNumberKey]; + if (portValue) { + _httpProxyPort = [portValue intValue]; + } + } + if ([proxyType isEqualToString:(NSString *)kCFProxyTypeSOCKS]) { + _socksProxyHost = settings[(NSString *)kCFProxyHostNameKey]; + NSNumber *portValue = settings[(NSString *)kCFProxyPortNumberKey]; + if (portValue) + _socksProxyPort = [portValue intValue]; + _socksProxyUsername = settings[(NSString *)kCFProxyUsernameKey]; + _socksProxyPassword = settings[(NSString *)kCFProxyPasswordKey]; + } + if (_httpProxyHost) { + SRDebugLog(@"Using http proxy %@:%u", _httpProxyHost, _httpProxyPort); + } else if (_socksProxyHost) { + SRDebugLog(@"Using socks proxy %@:%u", _socksProxyHost, _socksProxyPort); + } else { + SRDebugLog(@"configureProxy no proxies"); + } +} + +- (void)_fetchPAC:(NSURL *)PACurl withProxySettings:(NSDictionary *)proxySettings +{ + SRDebugLog(@"SRWebSocket fetchPAC:%@", PACurl); + + if ([PACurl isFileURL]) { + NSError *error = nil; + NSString *script = [NSString stringWithContentsOfURL:PACurl + usedEncoding:NULL + error:&error]; + + if (error) { + [self _openConnection]; + } else { + [self _runPACScript:script withProxySettings:proxySettings]; + } + return; + } + + NSString *scheme = [PACurl.scheme lowercaseString]; + if (![scheme isEqualToString:@"http"] && ![scheme isEqualToString:@"https"]) { + // Don't know how to read data from this URL, we'll have to give up + // We'll simply assume no proxies, and start the request as normal + [self _openConnection]; + return; + } + __weak __typeof__(self) wself = self; + NSURLRequest *request = [NSURLRequest requestWithURL:PACurl]; + NSURLSession *session = [NSURLSession sharedSession]; + [[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + __strong __typeof__(wself) sself = wself; + if (!error) { + NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + [sself _runPACScript:script withProxySettings:proxySettings]; + } else { + [sself _openConnection]; + } + }] resume]; +} + +- (void)_runPACScript:(NSString *)script withProxySettings:(NSDictionary *)proxySettings +{ + if (!script) { + [self _openConnection]; + return; + } + SRDebugLog(@"runPACScript"); + // From: http://developer.apple.com/samplecode/CFProxySupportTool/listing1.html + // Work around . This dummy call to + // CFNetworkCopyProxiesForURL initialise some state within CFNetwork + // that is required by CFNetworkCopyProxiesForAutoConfigurationScript. + CFBridgingRelease(CFNetworkCopyProxiesForURL((__bridge CFURLRef)_url, (__bridge CFDictionaryRef)proxySettings)); + + // Obtain the list of proxies by running the autoconfiguration script + CFErrorRef err = NULL; + + // CFNetworkCopyProxiesForAutoConfigurationScript doesn't understand ws:// or wss:// + NSURL *httpURL; + if (_connectionRequiresSSL) + httpURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", _url.host]]; + else + httpURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", _url.host]]; + + NSArray *proxies = CFBridgingRelease(CFNetworkCopyProxiesForAutoConfigurationScript((__bridge CFStringRef)script,(__bridge CFURLRef)httpURL, &err)); + if (!err && [proxies count] > 0) { + NSDictionary *settings = [proxies objectAtIndex:0]; + NSString *proxyType = settings[(NSString *)kCFProxyTypeKey]; + [self _readProxySettingWithType:proxyType settings:settings]; + } + [self _openConnection]; +} + +- (void)_openConnection +{ + [self _initializeStreams]; + + [self.inputStream scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] + forMode:NSDefaultRunLoopMode]; + //[self.outputStream scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] + // forMode:NSDefaultRunLoopMode]; + [self.outputStream open]; + [self.inputStream open]; +} + +- (void)_initializeStreams +{ + assert(_url.port.unsignedIntValue <= UINT32_MAX); + uint32_t port = _url.port.unsignedIntValue; + if (port == 0) { + port = (_connectionRequiresSSL ? 443 : 80); + } + NSString *host = _url.host; + + if (_httpProxyHost) { + host = _httpProxyHost; + port = (_httpProxyPort ?: 80); + } + + CFReadStreamRef readStream = NULL; + CFWriteStreamRef writeStream = NULL; + + SRDebugLog(@"ProxyConnect connect stream to %@:%u", host, port); + CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream); + + self.outputStream = CFBridgingRelease(writeStream); + self.inputStream = CFBridgingRelease(readStream); + + if (_socksProxyHost) { + SRDebugLog(@"ProxyConnect set sock property stream to %@:%u user %@ password %@", _socksProxyHost, _socksProxyPort, _socksProxyUsername, _socksProxyPassword); + NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithCapacity:4]; + settings[NSStreamSOCKSProxyHostKey] = _socksProxyHost; + if (_socksProxyPort) { + settings[NSStreamSOCKSProxyPortKey] = @(_socksProxyPort); + } + if (_socksProxyUsername) { + settings[NSStreamSOCKSProxyUserKey] = _socksProxyUsername; + } + if (_socksProxyPassword) { + settings[NSStreamSOCKSProxyPasswordKey] = _socksProxyPassword; + } + [self.inputStream setProperty:settings forKey:NSStreamSOCKSProxyConfigurationKey]; + [self.outputStream setProperty:settings forKey:NSStreamSOCKSProxyConfigurationKey]; + } + self.inputStream.delegate = self; + self.outputStream.delegate = self; +} + +- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode; +{ + SRDebugLog(@"stream handleEvent %u", eventCode); + switch (eventCode) { + case NSStreamEventOpenCompleted: { + if (aStream == self.inputStream) { + if (_httpProxyHost) { + [self _proxyDidConnect]; + } else { + [self _didConnect]; + } + } + } break; + case NSStreamEventErrorOccurred: { + [self _failWithError:aStream.streamError]; + } break; + case NSStreamEventEndEncountered: { + [self _failWithError:aStream.streamError]; + } break; + case NSStreamEventHasBytesAvailable: { + if (aStream == _inputStream) { + [self _processInputStream]; + } + } break; + case NSStreamEventHasSpaceAvailable: + case NSStreamEventNone: + SRDebugLog(@"(default) %@", aStream); + break; + } +} + +- (void)_proxyDidConnect +{ + SRDebugLog(@"Proxy Connected"); + uint32_t port = _url.port.unsignedIntValue; + if (port == 0) { + port = (_connectionRequiresSSL ? 443 : 80); + } + // Send HTTP CONNECT Request + NSString *connectRequestStr = [NSString stringWithFormat:@"CONNECT %@:%u HTTP/1.1\r\nHost: %@\r\nConnection: keep-alive\r\nProxy-Connection: keep-alive\r\n\r\n", _url.host, port, _url.host]; + + NSData *message = [connectRequestStr dataUsingEncoding:NSUTF8StringEncoding]; + SRDebugLog(@"Proxy sending %@", connectRequestStr); + + [self _writeData:message]; +} + +///handles the incoming bytes and sending them to the proper processing method +- (void)_processInputStream +{ + NSMutableData *buf = [NSMutableData dataWithCapacity:SRDefaultBufferSize()]; + uint8_t *buffer = buf.mutableBytes; + NSInteger length = [_inputStream read:buffer maxLength:SRDefaultBufferSize()]; + + if (length <= 0) { + return; + } + + BOOL process = (_inputQueue.count == 0); + [_inputQueue addObject:[NSData dataWithBytes:buffer length:length]]; + + if (process) { + [self _dequeueInput]; + } +} + +// dequeue the incoming input so it is processed in order + +- (void)_dequeueInput +{ + while (_inputQueue.count > 0) { + NSData *data = _inputQueue.firstObject; + [_inputQueue removeObjectAtIndex:0]; + + // No need to process any data further, we got the full header data. + if ([self _proxyProcessHTTPResponseWithData:data]) { + break; + } + } +} +//handle checking the proxy connection status +- (BOOL)_proxyProcessHTTPResponseWithData:(NSData *)data +{ + if (_receivedHTTPHeaders == NULL) { + _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO); + } + + CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length); + if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) { + SRDebugLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders))); + [self _proxyHTTPHeadersDidFinish]; + return YES; + } + + return NO; +} + +- (void)_proxyHTTPHeadersDidFinish +{ + NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders); + + if (responseCode >= 299) { + SRDebugLog(@"Connect to Proxy Request failed with response code %d", responseCode); + NSError *error = SRHTTPErrorWithCodeDescription(responseCode, 2132, + [NSString stringWithFormat:@"Received bad response code from proxy server: %d.", + (int)responseCode]); + [self _failWithError:error]; + return; + } + SRDebugLog(@"proxy connect return %d, call socket connect", responseCode); + [self _didConnect]; +} + +static NSTimeInterval const SRProxyConnectWriteTimeout = 5.0; + +- (void)_writeData:(NSData *)data +{ + const uint8_t * bytes = data.bytes; + __block NSInteger timeout = (NSInteger)(SRProxyConnectWriteTimeout * 1000000); // wait timeout before giving up + __weak __typeof__(self) wself = self; + dispatch_async(_writeQueue, ^{ + __strong __typeof__(wself) sself = self; + if (!sself) { + return; + } + NSOutputStream *outStream = sself.outputStream; + if (!outStream) { + return; + } + while (![outStream hasSpaceAvailable]) { + usleep(100); //wait until the socket is ready + timeout -= 100; + if (timeout < 0) { + NSError *error = SRHTTPErrorWithCodeDescription(408, 2132, @"Proxy timeout"); + [sself _failWithError:error]; + } else if (outStream.streamError != nil) { + [sself _failWithError:outStream.streamError]; + } + } + [outStream write:bytes maxLength:data.length]; + }); +} + +@end diff --git a/sources/SocketRocket/Internal/RunLoop/SRRunLoopThread.h b/sources/SocketRocket/Internal/RunLoop/SRRunLoopThread.h new file mode 100644 index 00000000..380cfa0e --- /dev/null +++ b/sources/SocketRocket/Internal/RunLoop/SRRunLoopThread.h @@ -0,0 +1,24 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SRRunLoopThread : NSThread + +@property (nonatomic, strong, readonly) NSRunLoop *runLoop; + ++ (instancetype)sharedThread; + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/RunLoop/SRRunLoopThread.m b/sources/SocketRocket/Internal/RunLoop/SRRunLoopThread.m new file mode 100644 index 00000000..baf031f4 --- /dev/null +++ b/sources/SocketRocket/Internal/RunLoop/SRRunLoopThread.m @@ -0,0 +1,83 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRRunLoopThread.h" + +@interface SRRunLoopThread () +{ + dispatch_group_t _waitGroup; +} + +@property (nonatomic, strong, readwrite) NSRunLoop *runLoop; + +@end + +@implementation SRRunLoopThread + ++ (instancetype)sharedThread +{ + static SRRunLoopThread *thread; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + thread = [[SRRunLoopThread alloc] init]; + thread.name = @"com.facebook.SocketRocket.NetworkThread"; + [thread start]; + }); + return thread; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _waitGroup = dispatch_group_create(); + dispatch_group_enter(_waitGroup); + } + return self; +} + +- (void)main +{ + @autoreleasepool { + _runLoop = [NSRunLoop currentRunLoop]; + dispatch_group_leave(_waitGroup); + + // Add an empty run loop source to prevent runloop from spinning. + CFRunLoopSourceContext sourceCtx = { + .version = 0, + .info = NULL, + .retain = NULL, + .release = NULL, + .copyDescription = NULL, + .equal = NULL, + .hash = NULL, + .schedule = NULL, + .cancel = NULL, + .perform = NULL + }; + CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &sourceCtx); + CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); + CFRelease(source); + + while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { + + } + assert(NO); + } +} + +- (NSRunLoop *)runLoop; +{ + dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER); + return _runLoop; +} + +@end diff --git a/sources/SocketRocket/Internal/SRConstants.h b/sources/SocketRocket/Internal/SRConstants.h new file mode 100644 index 00000000..86fee970 --- /dev/null +++ b/sources/SocketRocket/Internal/SRConstants.h @@ -0,0 +1,26 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +typedef NS_ENUM(NSInteger, SROpCode) +{ + SROpCodeTextFrame = 0x1, + SROpCodeBinaryFrame = 0x2, + // 3-7 reserved. + SROpCodeConnectionClose = 0x8, + SROpCodePing = 0x9, + SROpCodePong = 0xA, + // B-F reserved. +}; + +/** + Default buffer size that is used for reading/writing to streams. + */ +extern size_t SRDefaultBufferSize(void); diff --git a/sources/SocketRocket/Internal/SRConstants.m b/sources/SocketRocket/Internal/SRConstants.m new file mode 100644 index 00000000..1dbd774d --- /dev/null +++ b/sources/SocketRocket/Internal/SRConstants.m @@ -0,0 +1,19 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRConstants.h" + +size_t SRDefaultBufferSize(void) { + static size_t size; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + size = getpagesize(); + }); + return size; +} diff --git a/sources/SocketRocket/Internal/Security/SRPinningSecurityPolicy.h b/sources/SocketRocket/Internal/Security/SRPinningSecurityPolicy.h new file mode 100644 index 00000000..6ab5e1e9 --- /dev/null +++ b/sources/SocketRocket/Internal/Security/SRPinningSecurityPolicy.h @@ -0,0 +1,22 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SRPinningSecurityPolicy : SRSecurityPolicy + +- (instancetype)initWithCertificates:(NSArray *)pinnedCertificates; + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Security/SRPinningSecurityPolicy.m b/sources/SocketRocket/Internal/Security/SRPinningSecurityPolicy.m new file mode 100644 index 00000000..0eecd2be --- /dev/null +++ b/sources/SocketRocket/Internal/Security/SRPinningSecurityPolicy.m @@ -0,0 +1,67 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRPinningSecurityPolicy.h" + +#import + +#import "SRLog.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SRPinningSecurityPolicy () + +@property (nonatomic, copy, readonly) NSArray *pinnedCertificates; + +@end + +@implementation SRPinningSecurityPolicy + +- (instancetype)initWithCertificates:(NSArray *)pinnedCertificates +{ + // Do not validate certificate chain since we're pinning to specific certificates. + self = [super initWithCertificateChainValidationEnabled:NO]; + if (!self) { return self; } + + if (pinnedCertificates.count == 0) { + @throw [NSException exceptionWithName:@"Creating security policy failed." + reason:@"Must specify at least one certificate when creating a pinning policy." + userInfo:nil]; + } + _pinnedCertificates = [pinnedCertificates copy]; + + return self; +} + +- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain +{ + SRDebugLog(@"Pinned cert count: %d", self.pinnedCertificates.count); + NSUInteger requiredCertCount = self.pinnedCertificates.count; + + NSUInteger validatedCertCount = 0; + CFIndex serverCertCount = SecTrustGetCertificateCount(serverTrust); + for (CFIndex i = 0; i < serverCertCount; i++) { + SecCertificateRef cert = SecTrustGetCertificateAtIndex(serverTrust, i); + NSData *data = CFBridgingRelease(SecCertificateCopyData(cert)); + for (id ref in self.pinnedCertificates) { + SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref; + // TODO: (nlutsenko) Add caching, so we don't copy the data for every pinned cert all the time. + NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert)); + if ([trustedCertData isEqualToData:data]) { + validatedCertCount++; + break; + } + } + } + return (requiredCertCount == validatedCertCount); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRError.h b/sources/SocketRocket/Internal/Utilities/SRError.h new file mode 100644 index 00000000..7e13a820 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRError.h @@ -0,0 +1,20 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSError *SRErrorWithDomainCodeDescription(NSString *domain, NSInteger code, NSString *description); +extern NSError *SRErrorWithCodeDescription(NSInteger code, NSString *description); +extern NSError *SRErrorWithCodeDescriptionUnderlyingError(NSInteger code, NSString *description, NSError *underlyingError); + +extern NSError *SRHTTPErrorWithCodeDescription(NSInteger httpCode, NSInteger errorCode, NSString *description); + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRError.m b/sources/SocketRocket/Internal/Utilities/SRError.m new file mode 100644 index 00000000..eeabe032 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRError.m @@ -0,0 +1,42 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRError.h" + +#import "SRWebSocket.h" + +NS_ASSUME_NONNULL_BEGIN + +NSError *SRErrorWithDomainCodeDescription(NSString *domain, NSInteger code, NSString *description) +{ + return [NSError errorWithDomain:domain code:code userInfo:@{ NSLocalizedDescriptionKey: description }]; +} + +NSError *SRErrorWithCodeDescription(NSInteger code, NSString *description) +{ + return SRErrorWithDomainCodeDescription(SRWebSocketErrorDomain, code, description); +} + +NSError *SRErrorWithCodeDescriptionUnderlyingError(NSInteger code, NSString *description, NSError *underlyingError) +{ + return [NSError errorWithDomain:SRWebSocketErrorDomain + code:code + userInfo:@{ NSLocalizedDescriptionKey: description, + NSUnderlyingErrorKey: underlyingError }]; +} + +NSError *SRHTTPErrorWithCodeDescription(NSInteger httpCode, NSInteger errorCode, NSString *description) +{ + return [NSError errorWithDomain:SRWebSocketErrorDomain + code:errorCode + userInfo:@{ NSLocalizedDescriptionKey: description, + SRHTTPResponseErrorKey: @(httpCode) }]; +} + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRHTTPConnectMessage.h b/sources/SocketRocket/Internal/Utilities/SRHTTPConnectMessage.h new file mode 100644 index 00000000..1b5d4931 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRHTTPConnectMessage.h @@ -0,0 +1,20 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern CFHTTPMessageRef SRHTTPConnectMessageCreate(NSURLRequest *request, + NSString *securityKey, + uint8_t webSocketProtocolVersion, + NSArray *_Nullable cookies, + NSArray *_Nullable requestedProtocols); + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRHTTPConnectMessage.m b/sources/SocketRocket/Internal/Utilities/SRHTTPConnectMessage.m new file mode 100644 index 00000000..a111a6d6 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRHTTPConnectMessage.m @@ -0,0 +1,79 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRHTTPConnectMessage.h" + +#import "SRURLUtilities.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *_SRHTTPConnectMessageHost(NSURL *url) +{ + NSString *host = url.host; + if (url.port) { + host = [host stringByAppendingFormat:@":%@", url.port]; + } + return host; +} + +CFHTTPMessageRef SRHTTPConnectMessageCreate(NSURLRequest *request, + NSString *securityKey, + uint8_t webSocketProtocolVersion, + NSArray *_Nullable cookies, + NSArray *_Nullable requestedProtocols) +{ + NSURL *url = request.URL; + + CFHTTPMessageRef message = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)url, kCFHTTPVersion1_1); + + // Set host first so it defaults + CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Host"), (__bridge CFStringRef)_SRHTTPConnectMessageHost(url)); + + NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16]; + int result = SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes); + if (result != 0) { + //TODO: (nlutsenko) Check if there was an error. + } + + // Apply cookies if any have been provided + if (cookies) { + NSDictionary *messageCookies = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + [messageCookies enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) { + if (key.length && obj.length) { + CFHTTPMessageSetHeaderFieldValue(message, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); + } + }]; + } + + // set header for http basic auth + NSString *basicAuthorizationString = SRBasicAuthorizationHeaderFromURL(url); + if (basicAuthorizationString) { + CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Authorization"), (__bridge CFStringRef)basicAuthorizationString); + } + + CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Upgrade"), CFSTR("websocket")); + CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Connection"), CFSTR("Upgrade")); + CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)securityKey); + CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)@(webSocketProtocolVersion).stringValue); + + CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Origin"), (__bridge CFStringRef)SRURLOrigin(url)); + + if (requestedProtocols.count) { + CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Sec-WebSocket-Protocol"), + (__bridge CFStringRef)[requestedProtocols componentsJoinedByString:@", "]); + } + + [request.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + CFHTTPMessageSetHeaderFieldValue(message, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); + }]; + + return message; +} + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRHash.h b/sources/SocketRocket/Internal/Utilities/SRHash.h new file mode 100644 index 00000000..3db14de4 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRHash.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSData *SRSHA1HashFromString(NSString *string); +extern NSData *SRSHA1HashFromBytes(const char *bytes, size_t length); + +extern NSString *SRBase64EncodedStringFromData(NSData *data); + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRHash.m b/sources/SocketRocket/Internal/Utilities/SRHash.m new file mode 100644 index 00000000..1657cae6 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRHash.m @@ -0,0 +1,43 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRHash.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +NSData *SRSHA1HashFromString(NSString *string) +{ + size_t length = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + return SRSHA1HashFromBytes(string.UTF8String, length); +} + +NSData *SRSHA1HashFromBytes(const char *bytes, size_t length) +{ + uint8_t outputLength = CC_SHA1_DIGEST_LENGTH; + unsigned char output[outputLength]; + CC_SHA1(bytes, (CC_LONG)length, output); + + return [NSData dataWithBytes:output length:outputLength]; +} + +NSString *SRBase64EncodedStringFromData(NSData *data) +{ + if ([data respondsToSelector:@selector(base64EncodedStringWithOptions:)]) { + return [data base64EncodedStringWithOptions:0]; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [data base64Encoding]; +#pragma clang diagnostic pop +} + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRLog.h b/sources/SocketRocket/Internal/Utilities/SRLog.h new file mode 100644 index 00000000..99689efa --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRLog.h @@ -0,0 +1,20 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +// Uncomment this line to enable debug logging +//#define SR_DEBUG_LOG_ENABLED + +extern void SRErrorLog(NSString *format, ...); +extern void SRDebugLog(NSString *format, ...); + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRLog.m b/sources/SocketRocket/Internal/Utilities/SRLog.m new file mode 100644 index 00000000..45960178 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRLog.m @@ -0,0 +1,33 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRLog.h" + +NS_ASSUME_NONNULL_BEGIN + +extern void SRErrorLog(NSString *format, ...) +{ + __block va_list arg_list; + va_start (arg_list, format); + + NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; + + va_end(arg_list); + + NSLog(@"[SocketRocket] %@", formattedString); +} + +extern void SRDebugLog(NSString *format, ...) +{ +#ifdef SR_DEBUG_LOG_ENABLED + SRErrorLog(tag, format); +#endif +} + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRMutex.h b/sources/SocketRocket/Internal/Utilities/SRMutex.h new file mode 100644 index 00000000..8226ce62 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRMutex.h @@ -0,0 +1,22 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef __attribute__((capability("mutex"))) pthread_mutex_t *SRMutex; + +extern SRMutex SRMutexInitRecursive(void); +extern void SRMutexDestroy(SRMutex mutex); + +extern void SRMutexLock(SRMutex mutex) __attribute__((acquire_capability(mutex))); +extern void SRMutexUnlock(SRMutex mutex) __attribute__((release_capability(mutex))); + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRMutex.m b/sources/SocketRocket/Internal/Utilities/SRMutex.m new file mode 100644 index 00000000..03b5939c --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRMutex.m @@ -0,0 +1,47 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRMutex.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +SRMutex SRMutexInitRecursive(void) +{ + pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t)); + pthread_mutexattr_t attributes; + + pthread_mutexattr_init(&attributes); + pthread_mutexattr_settype(&attributes, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(mutex, &attributes); + pthread_mutexattr_destroy(&attributes); + + return mutex; +} + +void SRMutexDestroy(SRMutex mutex) +{ + pthread_mutex_destroy(mutex); + free(mutex); +} + +__attribute__((no_thread_safety_analysis)) +void SRMutexLock(SRMutex mutex) +{ + pthread_mutex_lock(mutex); +} + +__attribute__((no_thread_safety_analysis)) +void SRMutexUnlock(SRMutex mutex) +{ + pthread_mutex_unlock(mutex); +} + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRRandom.h b/sources/SocketRocket/Internal/Utilities/SRRandom.h new file mode 100644 index 00000000..9b116cfa --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRRandom.h @@ -0,0 +1,16 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSData *SRRandomData(NSUInteger length); + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRRandom.m b/sources/SocketRocket/Internal/Utilities/SRRandom.m new file mode 100644 index 00000000..2d2eb20f --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRRandom.m @@ -0,0 +1,26 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRRandom.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +NSData *SRRandomData(NSUInteger length) +{ + NSMutableData *data = [NSMutableData dataWithLength:length]; + int result = SecRandomCopyBytes(kSecRandomDefault, data.length, data.mutableBytes); + if (result != 0) { + [NSException raise:NSInternalInconsistencyException format:@"Failed to generate random bytes with OSStatus: %d", result]; + } + return data; +} + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRSIMDHelpers.h b/sources/SocketRocket/Internal/Utilities/SRSIMDHelpers.h new file mode 100644 index 00000000..8291cc71 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRSIMDHelpers.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +/** + Unmask bytes using XOR via SIMD. + + @param bytes The bytes to unmask. + @param length The number of bytes to unmask. + @param maskKey The mask to XOR with MUST be of length sizeof(uint32_t). + */ +void SRMaskBytesSIMD(uint8_t *bytes, size_t length, uint8_t *maskKey); diff --git a/sources/SocketRocket/Internal/Utilities/SRSIMDHelpers.m b/sources/SocketRocket/Internal/Utilities/SRSIMDHelpers.m new file mode 100644 index 00000000..f9d72ca8 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRSIMDHelpers.m @@ -0,0 +1,73 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRSIMDHelpers.h" + +typedef uint8_t uint8x32_t __attribute__((vector_size(32))); + +static void SRMaskBytesManual(uint8_t *bytes, size_t length, uint8_t *maskKey) { + for (size_t i = 0; i < length; i++) { + bytes[i] = bytes[i] ^ maskKey[i % sizeof(uint32_t)]; + } +} + +/** + Right-shift the elements of a vector, circularly. + + @param vector The vector to circular shift. + @param by The number of elements to shift by. + + @return A shifted vector. + */ +static uint8x32_t SRShiftVector(uint8x32_t vector, size_t by) { + uint8x32_t vectorCopy = vector; + by = by % _Alignof(uint8x32_t); + + uint8_t *vectorPointer = (uint8_t *)&vector; + uint8_t *vectorCopyPointer = (uint8_t *)&vectorCopy; + + memmove(vectorPointer + by, vectorPointer, sizeof(vector) - by); + memcpy(vectorPointer, vectorCopyPointer + (sizeof(vector) - by), by); + + return vector; +} + +void SRMaskBytesSIMD(uint8_t *bytes, size_t length, uint8_t *maskKey) { + size_t alignmentBytes = _Alignof(uint8x32_t) - ((uintptr_t)bytes % _Alignof(uint8x32_t)); + if (alignmentBytes == _Alignof(uint8x32_t)) { + alignmentBytes = 0; + } + + // If the number of bytes that can be processed after aligning is + // less than the number of bytes we can put into a vector, + // then there's no work to do with SIMD, just call the manual version. + if (alignmentBytes > length || (length - alignmentBytes) < sizeof(uint8x32_t)) { + SRMaskBytesManual(bytes, length, maskKey); + return; + } + + size_t vectorLength = (length - alignmentBytes) / sizeof(uint8x32_t); + size_t manualStartOffset = alignmentBytes + (vectorLength * sizeof(uint8x32_t)); + size_t manualLength = length - manualStartOffset; + + uint8x32_t *vector = (uint8x32_t *)(bytes + alignmentBytes); + uint8x32_t maskVector = { }; + + memset_pattern4(&maskVector, maskKey, sizeof(uint8x32_t)); + maskVector = SRShiftVector(maskVector, alignmentBytes); + + SRMaskBytesManual(bytes, alignmentBytes, maskKey); + + for (size_t vectorIndex = 0; vectorIndex < vectorLength; vectorIndex++) { + vector[vectorIndex] = vector[vectorIndex] ^ maskVector; + } + + // Use the shifted mask for the final manual part. + SRMaskBytesManual(bytes + manualStartOffset, manualLength, (uint8_t *) &maskVector); +} diff --git a/sources/SocketRocket/Internal/Utilities/SRURLUtilities.h b/sources/SocketRocket/Internal/Utilities/SRURLUtilities.h new file mode 100644 index 00000000..a4453809 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRURLUtilities.h @@ -0,0 +1,26 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +// The origin isn't really applicable for a native application. +// So instead, just map ws -> http and wss -> https. +extern NSString *SRURLOrigin(NSURL *url); + +extern BOOL SRURLRequiresSSL(NSURL *url); + +// Extracts `user` and `password` from url (if available) into `Basic base64(user:password)`. +extern NSString *_Nullable SRBasicAuthorizationHeaderFromURL(NSURL *url); + +// Returns a valid value for `NSStreamNetworkServiceType` or `nil`. +extern NSString *_Nullable SRStreamNetworkServiceTypeFromURLRequest(NSURLRequest *request); + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/Internal/Utilities/SRURLUtilities.m b/sources/SocketRocket/Internal/Utilities/SRURLUtilities.m new file mode 100644 index 00000000..fbb94296 --- /dev/null +++ b/sources/SocketRocket/Internal/Utilities/SRURLUtilities.m @@ -0,0 +1,77 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRURLUtilities.h" + +#import "SRHash.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *SRURLOrigin(NSURL *url) +{ + NSMutableString *origin = [NSMutableString string]; + + NSString *scheme = url.scheme.lowercaseString; + if ([scheme isEqualToString:@"wss"]) { + scheme = @"https"; + } else if ([scheme isEqualToString:@"ws"]) { + scheme = @"http"; + } + [origin appendFormat:@"%@://%@", scheme, url.host]; + + NSNumber *port = url.port; + BOOL portIsDefault = (!port || + ([scheme isEqualToString:@"http"] && port.integerValue == 80) || + ([scheme isEqualToString:@"https"] && port.integerValue == 443)); + if (!portIsDefault) { + [origin appendFormat:@":%@", port.stringValue]; + } + return origin; +} + +extern BOOL SRURLRequiresSSL(NSURL *url) +{ + NSString *scheme = url.scheme.lowercaseString; + return ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]); +} + +extern NSString *_Nullable SRBasicAuthorizationHeaderFromURL(NSURL *url) +{ + NSData *data = [[NSString stringWithFormat:@"%@:%@", url.user, url.password] dataUsingEncoding:NSUTF8StringEncoding]; + return [NSString stringWithFormat:@"Basic %@", SRBase64EncodedStringFromData(data)]; +} + +extern NSString *_Nullable SRStreamNetworkServiceTypeFromURLRequest(NSURLRequest *request) +{ + NSString *networkServiceType = nil; + switch (request.networkServiceType) { + case NSURLNetworkServiceTypeDefault: + break; + case NSURLNetworkServiceTypeVoIP: + networkServiceType = NSStreamNetworkServiceTypeVoIP; + break; + case NSURLNetworkServiceTypeVideo: + networkServiceType = NSStreamNetworkServiceTypeVideo; + break; + case NSURLNetworkServiceTypeBackground: + networkServiceType = NSStreamNetworkServiceTypeBackground; + break; + case NSURLNetworkServiceTypeVoice: + networkServiceType = NSStreamNetworkServiceTypeVoice; + break; +#if (__MAC_OS_X_VERSION_MAX_ALLOWED >= 101200 || __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 || __TV_OS_VERSION_MAX_ALLOWED >= 100000 || __WATCH_OS_VERSION_MAX_ALLOWED >= 30000) + case NSURLNetworkServiceTypeCallSignaling: + networkServiceType = NSStreamNetworkServiceTypeCallSignaling; + break; +#endif + } + return networkServiceType; +} + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/NSRunLoop+SRWebSocket.h b/sources/SocketRocket/NSRunLoop+SRWebSocket.h new file mode 100644 index 00000000..8f419e31 --- /dev/null +++ b/sources/SocketRocket/NSRunLoop+SRWebSocket.h @@ -0,0 +1,27 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSRunLoop (SRWebSocket) + +/** + Default run loop that will be used to schedule all instances of `SRWebSocket`. + + @return An instance of `NSRunLoop`. + */ ++ (NSRunLoop *)SR_networkRunLoop; + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/NSRunLoop+SRWebSocket.m b/sources/SocketRocket/NSRunLoop+SRWebSocket.m new file mode 100644 index 00000000..116a4cb9 --- /dev/null +++ b/sources/SocketRocket/NSRunLoop+SRWebSocket.m @@ -0,0 +1,27 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "NSRunLoop+SRWebSocket.h" +#import "NSRunLoop+SRWebSocketPrivate.h" + +#import "SRRunLoopThread.h" + +// Required for object file to always be linked. +void import_NSRunLoop_SRWebSocket() { } + +@implementation NSRunLoop (SRWebSocket) + ++ (NSRunLoop *)SR_networkRunLoop +{ + return [SRRunLoopThread sharedThread].runLoop; +} + +@end diff --git a/sources/SocketRocket/NSURLRequest+SRWebSocket.h b/sources/SocketRocket/NSURLRequest+SRWebSocket.h new file mode 100644 index 00000000..32d78da3 --- /dev/null +++ b/sources/SocketRocket/NSURLRequest+SRWebSocket.h @@ -0,0 +1,34 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSURLRequest (SRWebSocket) + +/** + An array of pinned `SecCertificateRef` SSL certificates that `SRWebSocket` will use for validation. + */ +@property (nullable, nonatomic, copy, readonly) NSArray *SR_SSLPinnedCertificates; + +@end + +@interface NSMutableURLRequest (SRWebSocket) + +/** + An array of pinned `SecCertificateRef` SSL certificates that `SRWebSocket` will use for validation. + */ +@property (nullable, nonatomic, copy) NSArray *SR_SSLPinnedCertificates; + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/NSURLRequest+SRWebSocket.m b/sources/SocketRocket/NSURLRequest+SRWebSocket.m new file mode 100644 index 00000000..27f87434 --- /dev/null +++ b/sources/SocketRocket/NSURLRequest+SRWebSocket.m @@ -0,0 +1,45 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "NSURLRequest+SRWebSocket.h" +#import "NSURLRequest+SRWebSocketPrivate.h" + +// Required for object file to always be linked. +void import_NSURLRequest_SRWebSocket() { } + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const SRSSLPinnnedCertificatesKey = @"SocketRocket_SSLPinnedCertificates"; + +@implementation NSURLRequest (SRWebSocket) + +- (nullable NSArray *)SR_SSLPinnedCertificates +{ + return [NSURLProtocol propertyForKey:SRSSLPinnnedCertificatesKey inRequest:self]; +} + +@end + +@implementation NSMutableURLRequest (SRWebSocket) + +- (nullable NSArray *)SR_SSLPinnedCertificates +{ + return [NSURLProtocol propertyForKey:SRSSLPinnnedCertificatesKey inRequest:self]; +} + +- (void)setSR_SSLPinnedCertificates:(nullable NSArray *)SR_SSLPinnedCertificates +{ + [NSURLProtocol setProperty:[SR_SSLPinnedCertificates copy] forKey:SRSSLPinnnedCertificatesKey inRequest:self]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/SRSecurityPolicy.h b/sources/SocketRocket/SRSecurityPolicy.h new file mode 100644 index 00000000..85e41c54 --- /dev/null +++ b/sources/SocketRocket/SRSecurityPolicy.h @@ -0,0 +1,67 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SRSecurityPolicy : NSObject + +/** + A default `SRSecurityPolicy` implementation specifies socket security and + validates the certificate chain. + + Use a subclass of `SRSecurityPolicy` for more fine grained customization. + */ ++ (instancetype)defaultPolicy; + +/** + Specifies socket security and provider certificate pinning, disregarding certificate + chain validation. + + @param pinnedCertificates Array of `SecCertificateRef` SSL certificates to use for validation. + */ ++ (instancetype)pinnningPolicyWithCertificates:(NSArray *)pinnedCertificates; + +/** + Specifies socket security and optional certificate chain validation. + + @param enabled Whether or not to validate the SSL certificate chain. If you + are considering using this method because your certificate was not issued by a + recognized certificate authority, consider using `pinningPolicyWithCertificates` instead. + */ +- (instancetype)initWithCertificateChainValidationEnabled:(BOOL)enabled NS_DESIGNATED_INITIALIZER; + +/** + Updates all the security options for input and output streams, for example you + can set your socket security level here. + + @param stream Stream to update the options in. + */ +- (void)updateSecurityOptionsInStream:(NSStream *)stream; + +/** + Whether or not the specified server trust should be accepted, based on the security policy. + + This method should be used when responding to an authentication challenge from + a server. In the default implemenation, no further validation is done here, but + you're free to override it in a subclass. See `SRPinningSecurityPolicy.h` for + an example. + + @param serverTrust The X.509 certificate trust of the server. + @param domain The domain of serverTrust. + + @return Whether or not to trust the server. + */ +- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain; + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/SRSecurityPolicy.m b/sources/SocketRocket/SRSecurityPolicy.m new file mode 100644 index 00000000..3997c14f --- /dev/null +++ b/sources/SocketRocket/SRSecurityPolicy.m @@ -0,0 +1,66 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRSecurityPolicy.h" +#import "SRPinningSecurityPolicy.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SRSecurityPolicy () + +@property (nonatomic, assign, readonly) BOOL certificateChainValidationEnabled; + +@end + +@implementation SRSecurityPolicy + ++ (instancetype)defaultPolicy +{ + return [self new]; +} + ++ (instancetype)pinnningPolicyWithCertificates:(NSArray *)pinnedCertificates +{ + return [[SRPinningSecurityPolicy alloc] initWithCertificates:pinnedCertificates]; +} + +- (instancetype)initWithCertificateChainValidationEnabled:(BOOL)enabled +{ + self = [super init]; + if (!self) { return self; } + + _certificateChainValidationEnabled = enabled; + + return self; +} + +- (instancetype)init +{ + return [self initWithCertificateChainValidationEnabled:YES]; +} + +- (void)updateSecurityOptionsInStream:(NSStream *)stream +{ + // Enforce TLS 1.2 + [stream setProperty:(__bridge id)CFSTR("kCFStreamSocketSecurityLevelTLSv1_2") forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel]; + + // Validate certificate chain for this stream if enabled. + NSDictionary *sslOptions = @{ (__bridge NSString *)kCFStreamSSLValidatesCertificateChain : @(self.certificateChainValidationEnabled) }; + [stream setProperty:sslOptions forKey:(__bridge NSString *)kCFStreamPropertySSLSettings]; +} + +- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain +{ + // No further evaluation happens in the default policy. + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/SRWebSocket.h b/sources/SocketRocket/SRWebSocket.h new file mode 100644 index 00000000..34b7d436 --- /dev/null +++ b/sources/SocketRocket/SRWebSocket.h @@ -0,0 +1,413 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, SRReadyState) { + SR_CONNECTING = 0, + SR_OPEN = 1, + SR_CLOSING = 2, + SR_CLOSED = 3, +}; + +typedef NS_ENUM(NSInteger, SRStatusCode) { + // 0-999: Reserved and not used. + SRStatusCodeNormal = 1000, + SRStatusCodeGoingAway = 1001, + SRStatusCodeProtocolError = 1002, + SRStatusCodeUnhandledType = 1003, + // 1004 reserved. + SRStatusNoStatusReceived = 1005, + SRStatusCodeAbnormal = 1006, + SRStatusCodeInvalidUTF8 = 1007, + SRStatusCodePolicyViolated = 1008, + SRStatusCodeMessageTooBig = 1009, + SRStatusCodeMissingExtension = 1010, + SRStatusCodeInternalError = 1011, + SRStatusCodeServiceRestart = 1012, + SRStatusCodeTryAgainLater = 1013, + // 1014: Reserved for future use by the WebSocket standard. + SRStatusCodeTLSHandshake = 1015, + // 1016-1999: Reserved for future use by the WebSocket standard. + // 2000-2999: Reserved for use by WebSocket extensions. + // 3000-3999: Available for use by libraries and frameworks. May not be used by applications. Available for registration at the IANA via first-come, first-serve. + // 4000-4999: Available for use by applications. +}; + +@class SRWebSocket; +@class SRSecurityPolicy; + +/** + Error domain used for errors reported by SRWebSocket. + */ +extern NSString *const SRWebSocketErrorDomain; + +/** + Key used for HTTP status code if bad response was received from the server. + */ +extern NSString *const SRHTTPResponseErrorKey; + +@protocol SRWebSocketDelegate; + +///-------------------------------------- +#pragma mark - SRWebSocket +///-------------------------------------- + +/** + A `SRWebSocket` object lets you connect, send and receive data to a remote Web Socket. + */ +@interface SRWebSocket : NSObject + +/** + The delegate of the web socket. + + The web socket delegate is notified on all state changes that happen to the web socket. + */ +@property (nonatomic, weak) id delegate; + +/** + A dispatch queue for scheduling the delegate calls. The queue doesn't need be a serial queue. + + If `nil` and `delegateOperationQueue` is `nil`, the socket uses main queue for performing all delegate method calls. + */ +@property (nullable, nonatomic, strong) dispatch_queue_t delegateDispatchQueue; + +/** + An operation queue for scheduling the delegate calls. + + If `nil` and `delegateOperationQueue` is `nil`, the socket uses main queue for performing all delegate method calls. + */ +@property (nullable, nonatomic, strong) NSOperationQueue *delegateOperationQueue; + +/** + Current ready state of the socket. Default: `SR_CONNECTING`. + + This property is Key-Value Observable and fully thread-safe. + */ +@property (atomic, assign, readonly) SRReadyState readyState; + +/** + An instance of `NSURL` that this socket connects to. + */ +@property (nullable, nonatomic, strong, readonly) NSURL *url; + +/** + All HTTP headers that were received by socket or `nil` if none were received so far. + */ +@property (nullable, nonatomic, assign, readonly) CFHTTPMessageRef receivedHTTPHeaders; + +/** + Array of `NSHTTPCookie` cookies to apply to the connection. + */ +@property (nullable, nonatomic, copy) NSArray *requestCookies; + +/** + The negotiated web socket protocol or `nil` if handshake did not yet complete. + */ +@property (nullable, nonatomic, copy, readonly) NSString *protocol; + +/** + A boolean value indicating whether this socket will allow connection without SSL trust chain evaluation. + For DEBUG builds this flag is ignored, and SSL connections are allowed regardless of the certificate trust configuration + */ +@property (nonatomic, assign, readonly) BOOL allowsUntrustedSSLCertificates; + +///-------------------------------------- +#pragma mark - Constructors +///-------------------------------------- + +/** + Initializes a web socket with a given `NSURLRequest`. + + @param request Request to initialize with. + */ +- (instancetype)initWithURLRequest:(NSURLRequest *)request; + +/** + Initializes a web socket with a given `NSURLRequest`, specifying a transport security policy (e.g. SSL configuration). + + @param request Request to initialize with. + @param securityPolicy Policy object describing transport security behavior. + */ +- (instancetype)initWithURLRequest:(NSURLRequest *)request securityPolicy:(SRSecurityPolicy *)securityPolicy; + +/** + Initializes a web socket with a given `NSURLRequest` and list of sub-protocols. + + @param request Request to initialize with. + @param protocols An array of strings that turn into `Sec-WebSocket-Protocol`. Default: `nil`. + */ +- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(nullable NSArray *)protocols; + +/** + Initializes a web socket with a given `NSURLRequest`, list of sub-protocols and whether untrusted SSL certificates are allowed. + + @param request Request to initialize with. + @param protocols An array of strings that turn into `Sec-WebSocket-Protocol`. Default: `nil`. + @param allowsUntrustedSSLCertificates Boolean value indicating whether untrusted SSL certificates are allowed. Default: `false`. + */ +- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(nullable NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates; + +/** + Initializes a web socket with a given `NSURLRequest`, list of sub-protocols and whether untrusted SSL certificates are allowed. + + @param request Request to initialize with. + @param protocols An array of strings that turn into `Sec-WebSocket-Protocol`. Default: `nil`. + @param securityPolicy Policy object describing transport security behavior. + */ +- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(nullable NSArray *)protocols securityPolicy:(SRSecurityPolicy *)securityPolicy NS_DESIGNATED_INITIALIZER; + +/** + Initializes a web socket with a given `NSURL`. + + @param url URL to initialize with. + */ +- (instancetype)initWithURL:(NSURL *)url; + +/** + Initializes a web socket with a given `NSURL` and list of sub-protocols. + + @param url URL to initialize with. + @param protocols An array of strings that turn into `Sec-WebSocket-Protocol`. Default: `nil`. + */ +- (instancetype)initWithURL:(NSURL *)url protocols:(nullable NSArray *)protocols; + +/** + Initializes a web socket with a given `NSURL`, specifying a transport security policy (e.g. SSL configuration). + + @param url URL to initialize with. + @param securityPolicy Policy object describing transport security behavior. + */ +- (instancetype)initWithURL:(NSURL *)url securityPolicy:(SRSecurityPolicy *)securityPolicy; + +/** + Initializes a web socket with a given `NSURL`, list of sub-protocols and whether untrusted SSL certificates are allowed. + + @param url URL to initialize with. + @param protocols An array of strings that turn into `Sec-WebSocket-Protocol`. Default: `nil`. + @param allowsUntrustedSSLCertificates Boolean value indicating whether untrusted SSL certificates are allowed. Default: `false`. + */ +- (instancetype)initWithURL:(NSURL *)url protocols:(nullable NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates; + +/** + Unavailable initializer. Please use any other initializer. + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + Unavailable constructor. Please use any other initializer. + */ ++ (instancetype)new NS_UNAVAILABLE; + +///-------------------------------------- +#pragma mark - Schedule +///-------------------------------------- + +/** + Schedules a received on a given run loop in a given mode. + By default, a web socket will schedule itself on `+[NSRunLoop SR_networkRunLoop]` using `NSDefaultRunLoopMode`. + + @param runLoop The run loop on which to schedule the receiver. + @param mode The mode for the run loop. + */ +- (void)scheduleInRunLoop:(NSRunLoop *)runLoop forMode:(NSString *)mode NS_SWIFT_NAME(schedule(in:forMode:)); + +/** + Removes the receiver from a given run loop running in a given mode. + + @param runLoop The run loop on which the receiver was scheduled. + @param mode The mode for the run loop. + */ +- (void)unscheduleFromRunLoop:(NSRunLoop *)runLoop forMode:(NSString *)mode NS_SWIFT_NAME(unschedule(from:forMode:)); + +///-------------------------------------- +#pragma mark - Open / Close +///-------------------------------------- + +/** + Opens web socket, which will trigger connection, authentication and start receiving/sending events. + An instance of `SRWebSocket` is intended for one-time-use only. This method should be called once and only once. + */ +- (void)open; + +/** + Closes a web socket using `SRStatusCodeNormal` code and no reason. + */ +- (void)close; + +/** + Closes a web socket using a given code and reason. + + @param code Code to close the socket with. + @param reason Reason to send to the server or `nil`. + */ +- (void)closeWithCode:(NSInteger)code reason:(nullable NSString *)reason; + +///-------------------------------------- +#pragma mark Send +///-------------------------------------- + +/** + Send a UTF-8 string or binary data to the server. + + @param message UTF-8 String or Data to send. + + @deprecated Please use `sendString:` or `sendData` instead. + */ +- (void)send:(nullable id)message __attribute__((deprecated("Please use `sendString:error:` or `sendData:error:` instead."))); + +/** + Send a UTF-8 String to the server. + + @param string String to send. + @param error On input, a pointer to variable for an `NSError` object. + If an error occurs, this pointer is set to an `NSError` object containing information about the error. + You may specify `nil` to ignore the error information. + + @return `YES` if the string was scheduled to send, otherwise - `NO`. + */ +- (BOOL)sendString:(NSString *)string error:(NSError **)error NS_SWIFT_NAME(send(string:)); + +/** + Send binary data to the server. + + @param data Data to send. + @param error On input, a pointer to variable for an `NSError` object. + If an error occurs, this pointer is set to an `NSError` object containing information about the error. + You may specify `nil` to ignore the error information. + + @return `YES` if the string was scheduled to send, otherwise - `NO`. + */ +- (BOOL)sendData:(nullable NSData *)data error:(NSError **)error NS_SWIFT_NAME(send(data:)); + +/** + Send binary data to the server, without making a defensive copy of it first. + + @param data Data to send. + @param error On input, a pointer to variable for an `NSError` object. + If an error occurs, this pointer is set to an `NSError` object containing information about the error. + You may specify `nil` to ignore the error information. + + @return `YES` if the string was scheduled to send, otherwise - `NO`. + */ +- (BOOL)sendDataNoCopy:(nullable NSData *)data error:(NSError **)error NS_SWIFT_NAME(send(dataNoCopy:)); + +/** + Send Ping message to the server with optional data. + + @param data Instance of `NSData` or `nil`. + @param error On input, a pointer to variable for an `NSError` object. + If an error occurs, this pointer is set to an `NSError` object containing information about the error. + You may specify `nil` to ignore the error information. + + @return `YES` if the string was scheduled to send, otherwise - `NO`. + */ +- (BOOL)sendPing:(nullable NSData *)data error:(NSError **)error NS_SWIFT_NAME(sendPing(_:)); + +@end + +///-------------------------------------- +#pragma mark - SRWebSocketDelegate +///-------------------------------------- + +/** + The `SRWebSocketDelegate` protocol describes the methods that `SRWebSocket` objects + call on their delegates to handle status and messsage events. + */ +@protocol SRWebSocketDelegate + +@optional + +#pragma mark Receive Messages + +/** + Called when any message was received from a web socket. + This method is suboptimal and might be deprecated in a future release. + + @param webSocket An instance of `SRWebSocket` that received a message. + @param message Received message. Either a `String` or `NSData`. + */ +- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message; + +/** + Called when a frame was received from a web socket. + + @param webSocket An instance of `SRWebSocket` that received a message. + @param string Received text in a form of UTF-8 `String`. + */ +- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessageWithString:(NSString *)string; + +/** + Called when a frame was received from a web socket. + + @param webSocket An instance of `SRWebSocket` that received a message. + @param data Received data in a form of `NSData`. + */ +- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessageWithData:(NSData *)data; + +#pragma mark Status & Connection + +/** + Called when a given web socket was open and authenticated. + + @param webSocket An instance of `SRWebSocket` that was open. + */ +- (void)webSocketDidOpen:(SRWebSocket *)webSocket; + +/** + Called when a given web socket encountered an error. + + @param webSocket An instance of `SRWebSocket` that failed with an error. + @param error An instance of `NSError`. + */ +- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error; + +/** + Called when a given web socket was closed. + + @param webSocket An instance of `SRWebSocket` that was closed. + @param code Code reported by the server. + @param reason Reason in a form of a String that was reported by the server or `nil`. + @param wasClean Boolean value indicating whether a socket was closed in a clean state. + */ +- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(nullable NSString *)reason wasClean:(BOOL)wasClean; + +/** + Called on receive of a ping message from the server. + + @param webSocket An instance of `SRWebSocket` that received a ping frame. + @param data Payload that was received or `nil` if there was no payload. + */ +- (void)webSocket:(SRWebSocket *)webSocket didReceivePingWithData:(nullable NSData *)data; + +/** + Called when a pong data was received in response to ping. + + @param webSocket An instance of `SRWebSocket` that received a pong frame. + @param pongData Payload that was received or `nil` if there was no payload. + */ +- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(nullable NSData *)pongData; + +/** + Sent before reporting a text frame to be able to configure if it shuold be convert to a UTF-8 String or passed as `NSData`. + If the method is not implemented - it will always convert text frames to String. + + @param webSocket An instance of `SRWebSocket` that received a text frame. + + @return `YES` if text frame should be converted to UTF-8 String, otherwise - `NO`. Default: `YES`. + */ +- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket NS_SWIFT_NAME(webSocketShouldConvertTextFrameToString(_:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/sources/SocketRocket/SRWebSocket.m b/sources/SocketRocket/SRWebSocket.m new file mode 100644 index 00000000..8d226bf3 --- /dev/null +++ b/sources/SocketRocket/SRWebSocket.m @@ -0,0 +1,1617 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "SRWebSocket.h" + +#if TARGET_OS_IPHONE +#define HAS_ICU +#endif + +#ifdef HAS_ICU +#import +#endif + +#import + +#import "SRDelegateController.h" +#import "SRIOConsumer.h" +#import "SRIOConsumerPool.h" +#import "SRHash.h" +#import "SRURLUtilities.h" +#import "SRError.h" +#import "NSURLRequest+SRWebSocket.h" +#import "NSRunLoop+SRWebSocket.h" +#import "SRProxyConnect.h" +#import "SRSecurityPolicy.h" +#import "SRHTTPConnectMessage.h" +#import "SRRandom.h" +#import "SRLog.h" +#import "SRMutex.h" +#import "SRSIMDHelpers.h" +#import "NSURLRequest+SRWebSocketPrivate.h" +#import "NSRunLoop+SRWebSocketPrivate.h" +#import "SRConstants.h" + +#if !__has_feature(objc_arc) +#error SocketRocket must be compiled with ARC enabled +#endif + +__attribute__((used)) static void importCategories() +{ + import_NSURLRequest_SRWebSocket(); + import_NSRunLoop_SRWebSocket(); +} + +typedef struct { + BOOL fin; + // BOOL rsv1; + // BOOL rsv2; + // BOOL rsv3; + uint8_t opcode; + BOOL masked; + uint64_t payload_length; +} frame_header; + +static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +static inline int32_t validate_dispatch_data_partial_string(NSData *data); + +static uint8_t const SRWebSocketProtocolVersion = 13; + +NSString *const SRWebSocketErrorDomain = @"SRWebSocketErrorDomain"; +NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode"; + +@interface SRWebSocket () + +@property (atomic, assign, readwrite) SRReadyState readyState; + +// Specifies whether SSL trust chain should NOT be evaluated. +// By default this flag is set to NO, meaning only secure SSL connections are allowed. +// For DEBUG builds this flag is ignored, and SSL connections are allowed regardless +// of the certificate trust configuration +@property (nonatomic, assign, readwrite) BOOL allowsUntrustedSSLCertificates; + +@property (nonatomic, strong, readonly) SRDelegateController *delegateController; + +@end + +@implementation SRWebSocket { + SRMutex _kvoLock; + OSSpinLock _propertyLock; + + dispatch_queue_t _workQueue; + NSMutableArray *_consumers; + + NSInputStream *_inputStream; + NSOutputStream *_outputStream; + + dispatch_data_t _readBuffer; + NSUInteger _readBufferOffset; + + dispatch_data_t _outputBuffer; + NSUInteger _outputBufferOffset; + + uint8_t _currentFrameOpcode; + size_t _currentFrameCount; + size_t _readOpCount; + uint32_t _currentStringScanPosition; + NSMutableData *_currentFrameData; + + NSString *_closeReason; + + NSString *_secKey; + + SRSecurityPolicy *_securityPolicy; + BOOL _requestRequiresSSL; + BOOL _streamSecurityValidated; + + uint8_t _currentReadMaskKey[4]; + size_t _currentReadMaskOffset; + + BOOL _closeWhenFinishedWriting; + BOOL _failed; + + NSURLRequest *_urlRequest; + + BOOL _sentClose; + BOOL _didFail; + BOOL _cleanupScheduled; + int _closeCode; + + BOOL _isPumping; + + NSMutableSet *_scheduledRunloops; // Set<[RunLoop, Mode]>. TODO: (nlutsenko) Fix clowntown + + // We use this to retain ourselves. + __strong SRWebSocket *_selfRetain; + + NSArray *_requestedProtocols; + SRIOConsumerPool *_consumerPool; + + // proxy support + SRProxyConnect *_proxyConnect; +} + +@synthesize readyState = _readyState; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols securityPolicy:(SRSecurityPolicy *)securityPolicy +{ + self = [super init]; + if (!self) return self; + + assert(request.URL); + _url = request.URL; + _urlRequest = request; + _requestedProtocols = [protocols copy]; + _securityPolicy = securityPolicy; + _requestRequiresSSL = SRURLRequiresSSL(_url); + + _readyState = SR_CONNECTING; + + _propertyLock = OS_SPINLOCK_INIT; + _kvoLock = SRMutexInitRecursive(); + _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + + // Going to set a specific on the queue so we can validate we're on the work queue + dispatch_queue_set_specific(_workQueue, (__bridge void *)self, (__bridge void *)(_workQueue), NULL); + + _delegateController = [[SRDelegateController alloc] init]; + + _readBuffer = dispatch_data_empty; + _outputBuffer = dispatch_data_empty; + + _currentFrameData = [[NSMutableData alloc] init]; + + _consumers = [[NSMutableArray alloc] init]; + + _consumerPool = [[SRIOConsumerPool alloc] init]; + + _scheduledRunloops = [[NSMutableSet alloc] init]; + + return self; +} + +- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates +{ + SRSecurityPolicy *securityPolicy; + NSArray *pinnedCertificates = request.SR_SSLPinnedCertificates; + if (pinnedCertificates) { + securityPolicy = [SRSecurityPolicy pinnningPolicyWithCertificates:pinnedCertificates]; + } else { + BOOL certificateChainValidationEnabled = !allowsUntrustedSSLCertificates; + securityPolicy = [[SRSecurityPolicy alloc] initWithCertificateChainValidationEnabled:certificateChainValidationEnabled]; + } + + return [self initWithURLRequest:request protocols:protocols securityPolicy:securityPolicy]; +} + +- (instancetype)initWithURLRequest:(NSURLRequest *)request securityPolicy:(SRSecurityPolicy *)securityPolicy +{ + return [self initWithURLRequest:request protocols:nil securityPolicy:securityPolicy]; +} + +- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols +{ + return [self initWithURLRequest:request protocols:protocols allowsUntrustedSSLCertificates:NO]; +} + +- (instancetype)initWithURLRequest:(NSURLRequest *)request +{ + return [self initWithURLRequest:request protocols:nil]; +} + +- (instancetype)initWithURL:(NSURL *)url; +{ + return [self initWithURL:url protocols:nil]; +} + +- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; +{ + return [self initWithURL:url protocols:protocols allowsUntrustedSSLCertificates:NO]; +} + +- (instancetype)initWithURL:(NSURL *)url securityPolicy:(SRSecurityPolicy *)securityPolicy +{ + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + return [self initWithURLRequest:request protocols:nil securityPolicy:securityPolicy]; +} + +- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates +{ + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + return [self initWithURLRequest:request protocols:protocols allowsUntrustedSSLCertificates:allowsUntrustedSSLCertificates]; +} + +- (void)assertOnWorkQueue; +{ + assert(dispatch_get_specific((__bridge void *)self) == (__bridge void *)_workQueue); +} + +///-------------------------------------- +#pragma mark - Dealloc +///-------------------------------------- + +- (void)dealloc +{ + _inputStream.delegate = nil; + _outputStream.delegate = nil; + + [_inputStream close]; + [_outputStream close]; + + if (_receivedHTTPHeaders) { + CFRelease(_receivedHTTPHeaders); + _receivedHTTPHeaders = NULL; + } + + SRMutexDestroy(_kvoLock); +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +#pragma mark readyState + +- (void)setReadyState:(SRReadyState)readyState +{ + @try { + SRMutexLock(_kvoLock); + if (_readyState != readyState) { + [self willChangeValueForKey:@"readyState"]; + OSSpinLockLock(&_propertyLock); + _readyState = readyState; + OSSpinLockUnlock(&_propertyLock); + [self didChangeValueForKey:@"readyState"]; + } + } + @finally { + SRMutexUnlock(_kvoLock); + } +} + +- (SRReadyState)readyState +{ + SRReadyState state = 0; + OSSpinLockLock(&_propertyLock); + state = _readyState; + OSSpinLockUnlock(&_propertyLock); + return state; +} + ++ (BOOL)automaticallyNotifiesObserversOfReadyState { + return NO; +} + +///-------------------------------------- +#pragma mark - Open / Close +///-------------------------------------- + +- (void)open +{ + assert(_url); + NSAssert(self.readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once."); + + _selfRetain = self; + + if (_urlRequest.timeoutInterval > 0) { + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_urlRequest.timeoutInterval * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^{ + if (self.readyState == SR_CONNECTING) { + NSError *error = SRErrorWithDomainCodeDescription(NSURLErrorDomain, NSURLErrorTimedOut, @"Timed out connecting to server."); + [self _failWithError:error]; + } + }); + } + + _proxyConnect = [[SRProxyConnect alloc] initWithURL:_url]; + + __weak __typeof__(self) wself = self; + [_proxyConnect openNetworkStreamWithCompletion:^(NSError *error, NSInputStream *readStream, NSOutputStream *writeStream) { + [wself _connectionDoneWithError:error readStream:readStream writeStream:writeStream]; + }]; +} + +- (void)_connectionDoneWithError:(NSError *)error readStream:(NSInputStream *)readStream writeStream:(NSOutputStream *)writeStream +{ + if (error != nil) { + [self _failWithError:error]; + } else { + _outputStream = writeStream; + _inputStream = readStream; + + _inputStream.delegate = self; + _outputStream.delegate = self; + [self _updateSecureStreamOptions]; + + if (!_scheduledRunloops.count) { + [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; + } + + // If we don't require SSL validation - consider that we connected. + // Otherwise `didConnect` is called when SSL validation finishes. + if (!_requestRequiresSSL) { + dispatch_async(_workQueue, ^{ + [self didConnect]; + }); + } + } + // Schedule to run on a work queue, to make sure we don't run this inline and deallocate `self` inside `SRProxyConnect`. + // TODO: (nlutsenko) Find a better structure for this, maybe Bolts Tasks? + dispatch_async(_workQueue, ^{ + _proxyConnect = nil; + }); +} + +- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; +{ + NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept"))); + + if (acceptHeader == nil) { + return NO; + } + + NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString]; + NSData *hashedString = SRSHA1HashFromString(concattedString); + NSString *expectedAccept = SRBase64EncodedStringFromData(hashedString); + return [acceptHeader isEqualToString:expectedAccept]; +} + +- (void)_HTTPHeadersDidFinish; +{ + NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders); + if (responseCode >= 400) { + SRDebugLog(@"Request failed with response code %d", responseCode); + NSError *error = SRHTTPErrorWithCodeDescription(responseCode, 2132, + [NSString stringWithFormat:@"Received bad response code from server: %d.", + (int)responseCode]); + [self _failWithError:error]; + return; + } + + if(![self _checkHandshake:_receivedHTTPHeaders]) { + NSError *error = SRErrorWithCodeDescription(2133, @"Invalid Sec-WebSocket-Accept response."); + [self _failWithError:error]; + return; + } + + NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol"))); + if (negotiatedProtocol) { + // Make sure we requested the protocol + if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) { + NSError *error = SRErrorWithCodeDescription(2133, @"Server specified Sec-WebSocket-Protocol that wasn't requested."); + [self _failWithError:error]; + return; + } + + _protocol = negotiatedProtocol; + } + + self.readyState = SR_OPEN; + + if (!_didFail) { + [self _readFrameNew]; + } + + [self.delegateController performDelegateBlock:^(id _Nullable delegate, SRDelegateAvailableMethods availableMethods) { + if (availableMethods.didOpen) { + [delegate webSocketDidOpen:self]; + } + }]; +} + + +- (void)_readHTTPHeader; +{ + if (_receivedHTTPHeaders == NULL) { + _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO); + } + + [self _readUntilHeaderCompleteWithCallback:^(SRWebSocket *socket, NSData *data) { + CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length); + + if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) { + SRDebugLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders))); + [self _HTTPHeadersDidFinish]; + } else { + [self _readHTTPHeader]; + } + }]; +} + +- (void)didConnect; +{ + SRDebugLog(@"Connected"); + + _secKey = SRBase64EncodedStringFromData(SRRandomData(16)); + assert([_secKey length] == 24); + + CFHTTPMessageRef message = SRHTTPConnectMessageCreate(_urlRequest, + _secKey, + SRWebSocketProtocolVersion, + self.requestCookies, + _requestedProtocols); + + NSData *messageData = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(message)); + + CFRelease(message); + + [self _writeData:messageData]; + [self _readHTTPHeader]; +} + +- (void)_updateSecureStreamOptions +{ + if (_requestRequiresSSL) { + SRDebugLog(@"Setting up security for streams."); + [_securityPolicy updateSecurityOptionsInStream:_inputStream]; + [_securityPolicy updateSecurityOptionsInStream:_outputStream]; + } + + NSString *networkServiceType = SRStreamNetworkServiceTypeFromURLRequest(_urlRequest); + if (networkServiceType != nil) { + [_inputStream setProperty:networkServiceType forKey:NSStreamNetworkServiceType]; + [_outputStream setProperty:networkServiceType forKey:NSStreamNetworkServiceType]; + } +} + +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +{ + [_outputStream scheduleInRunLoop:aRunLoop forMode:mode]; + [_inputStream scheduleInRunLoop:aRunLoop forMode:mode]; + + [_scheduledRunloops addObject:@[aRunLoop, mode]]; +} + +- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +{ + [_outputStream removeFromRunLoop:aRunLoop forMode:mode]; + [_inputStream removeFromRunLoop:aRunLoop forMode:mode]; + + [_scheduledRunloops removeObject:@[aRunLoop, mode]]; +} + +- (void)close; +{ + [self closeWithCode:SRStatusCodeNormal reason:nil]; +} + +- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; +{ + assert(code); + dispatch_async(_workQueue, ^{ + if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) { + return; + } + + BOOL wasConnecting = self.readyState == SR_CONNECTING; + + self.readyState = SR_CLOSING; + + SRDebugLog(@"Closing with code %d reason %@", code, reason); + + if (wasConnecting) { + [self closeConnection]; + return; + } + + size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize]; + NSData *payload = mutablePayload; + + ((uint16_t *)mutablePayload.mutableBytes)[0] = CFSwapInt16BigToHost((uint16_t)code); + + if (reason) { + NSRange remainingRange = {0}; + + NSUInteger usedLength = 0; + + BOOL success = [reason getBytes:(char *)mutablePayload.mutableBytes + sizeof(uint16_t) maxLength:payload.length - sizeof(uint16_t) usedLength:&usedLength encoding:NSUTF8StringEncoding options:NSStringEncodingConversionExternalRepresentation range:NSMakeRange(0, reason.length) remainingRange:&remainingRange]; +#pragma unused (success) + + assert(success); + assert(remainingRange.length == 0); + + if (usedLength != maxMsgSize) { + payload = [payload subdataWithRange:NSMakeRange(0, usedLength + sizeof(uint16_t))]; + } + } + + + [self _sendFrameWithOpcode:SROpCodeConnectionClose data:payload]; + }); +} + +- (void)_closeWithProtocolError:(NSString *)message; +{ + // Need to shunt this on the _callbackQueue first to see if they received any messages + [self.delegateController performDelegateQueueBlock:^{ + [self closeWithCode:SRStatusCodeProtocolError reason:message]; + dispatch_async(_workQueue, ^{ + [self closeConnection]; + }); + }]; +} + +- (void)_failWithError:(NSError *)error; +{ + dispatch_async(_workQueue, ^{ + if (self.readyState != SR_CLOSED) { + _failed = YES; + [self.delegateController performDelegateBlock:^(id _Nullable delegate, SRDelegateAvailableMethods availableMethods) { + if (availableMethods.didFailWithError) { + [delegate webSocket:self didFailWithError:error]; + } + }]; + + self.readyState = SR_CLOSED; + + SRDebugLog(@"Failing with error %@", error.localizedDescription); + + [self closeConnection]; + [self _scheduleCleanup]; + } + }); +} + +- (void)_writeData:(NSData *)data; +{ + [self assertOnWorkQueue]; + + if (_closeWhenFinishedWriting) { + return; + } + + __block NSData *strongData = data; + dispatch_data_t newData = dispatch_data_create(data.bytes, data.length, nil, ^{ + strongData = nil; + }); + _outputBuffer = dispatch_data_create_concat(_outputBuffer, newData); + [self _pumpWriting]; +} + +- (void)send:(nullable id)message +{ + if (!message) { + [self sendData:nil error:nil]; // Send Data, but it doesn't matter since we are going to send the same text frame with 0 length. + } else if ([message isKindOfClass:[NSString class]]) { + [self sendString:message error:nil]; + } else if ([message isKindOfClass:[NSData class]]) { + [self sendData:message error:nil]; + } else { + NSAssert(NO, @"Unrecognized message. Not able to send anything other than a String or NSData."); + } +} + +- (BOOL)sendString:(NSString *)string error:(NSError **)error +{ + if (self.readyState != SR_OPEN) { + NSString *message = @"Invalid State: Cannot call `sendString:error:` until connection is open."; + if (error) { + *error = SRErrorWithCodeDescription(2134, message); + } + SRDebugLog(message); + return NO; + } + + string = [string copy]; + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:SROpCodeTextFrame data:[string dataUsingEncoding:NSUTF8StringEncoding]]; + }); + return YES; +} + +- (BOOL)sendData:(nullable NSData *)data error:(NSError **)error +{ + data = [data copy]; + return [self sendDataNoCopy:data error:error]; +} + +- (BOOL)sendDataNoCopy:(nullable NSData *)data error:(NSError **)error +{ + if (self.readyState != SR_OPEN) { + NSString *message = @"Invalid State: Cannot call `sendDataNoCopy:error:` until connection is open."; + if (error) { + *error = SRErrorWithCodeDescription(2134, message); + } + SRDebugLog(message); + return NO; + } + + dispatch_async(_workQueue, ^{ + if (data) { + [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data]; + } else { + [self _sendFrameWithOpcode:SROpCodeTextFrame data:nil]; + } + }); + return YES; +} + +- (BOOL)sendPing:(nullable NSData *)data error:(NSError **)error +{ + if (self.readyState != SR_OPEN) { + NSString *message = @"Invalid State: Cannot call `sendPing:error:` until connection is open."; + if (error) { + *error = SRErrorWithCodeDescription(2134, message); + } + SRDebugLog(message); + return NO; + } + + data = [data copy] ?: [NSData data]; // It's okay for a ping to be empty + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:SROpCodePing data:data]; + }); + return YES; +} + +- (void)_handlePingWithData:(nullable NSData *)data +{ + // Need to pingpong this off _callbackQueue first to make sure messages happen in order + [self.delegateController performDelegateBlock:^(id _Nullable delegate, SRDelegateAvailableMethods availableMethods) { + if (availableMethods.didReceivePing) { + [delegate webSocket:self didReceivePingWithData:data]; + } + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:SROpCodePong data:data]; + }); + }]; +} + +- (void)handlePong:(NSData *)pongData; +{ + SRDebugLog(@"Received pong"); + [self.delegateController performDelegateBlock:^(id _Nullable delegate, SRDelegateAvailableMethods availableMethods) { + if (availableMethods.didReceivePong) { + [delegate webSocket:self didReceivePong:pongData]; + } + }]; +} + + +static inline BOOL closeCodeIsValid(int closeCode) { + if (closeCode < 1000) { + return NO; + } + + if (closeCode >= 1000 && closeCode <= 1011) { + if (closeCode == 1004 || + closeCode == 1005 || + closeCode == 1006) { + return NO; + } + return YES; + } + + if (closeCode >= 3000 && closeCode <= 3999) { + return YES; + } + + if (closeCode >= 4000 && closeCode <= 4999) { + return YES; + } + + return NO; +} + +// Note from RFC: +// +// If there is a body, the first two +// bytes of the body MUST be a 2-byte unsigned integer (in network byte +// order) representing a status code with value /code/ defined in +// Section 7.4. Following the 2-byte integer the body MAY contain UTF-8 +// encoded data with value /reason/, the interpretation of which is not +// defined by this specification. + +- (void)handleCloseWithData:(NSData *)data; +{ + size_t dataSize = data.length; + __block uint16_t closeCode = 0; + + SRDebugLog(@"Received close frame"); + + if (dataSize == 1) { + // TODO handle error + [self _closeWithProtocolError:@"Payload for close must be larger than 2 bytes"]; + return; + } else if (dataSize >= 2) { + [data getBytes:&closeCode length:sizeof(closeCode)]; + _closeCode = CFSwapInt16BigToHost(closeCode); + if (!closeCodeIsValid(_closeCode)) { + [self _closeWithProtocolError:[NSString stringWithFormat:@"Cannot have close code of %d", _closeCode]]; + return; + } + if (dataSize > 2) { + _closeReason = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(2, dataSize - 2)] encoding:NSUTF8StringEncoding]; + if (!_closeReason) { + [self _closeWithProtocolError:@"Close reason MUST be valid UTF-8"]; + return; + } + } + } else { + _closeCode = SRStatusNoStatusReceived; + } + + [self assertOnWorkQueue]; + + if (self.readyState == SR_OPEN) { + [self closeWithCode:1000 reason:nil]; + } + dispatch_async(_workQueue, ^{ + [self closeConnection]; + }); +} + +- (void)closeConnection; +{ + [self assertOnWorkQueue]; + SRDebugLog(@"Trying to disconnect"); + _closeWhenFinishedWriting = YES; + [self _pumpWriting]; +} + +- (void)_handleFrameWithData:(NSData *)frameData opCode:(SROpCode)opcode +{ + // Check that the current data is valid UTF8 + + BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose); + if (isControlFrame) { + //frameData will be copied before passing to handlers + //otherwise there can be misbehaviours when value at the pointer is changed + frameData = [frameData copy]; + + dispatch_async(_workQueue, ^{ + [self _readFrameContinue]; + }); + } else { + [self _readFrameNew]; + } + + switch (opcode) { + case SROpCodeTextFrame: { + NSString *string = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding]; + if (!string && frameData) { + [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8."]; + dispatch_async(_workQueue, ^{ + [self closeConnection]; + }); + return; + } + SRDebugLog(@"Received text message."); + [self.delegateController performDelegateBlock:^(id _Nullable delegate, SRDelegateAvailableMethods availableMethods) { + // Don't convert into string - iff `delegate` tells us not to. Otherwise - create UTF8 string and handle that. + if (availableMethods.shouldConvertTextFrameToString && ![delegate webSocketShouldConvertTextFrameToString:self]) { + if (availableMethods.didReceiveMessage) { + [delegate webSocket:self didReceiveMessage:frameData]; + } + if (availableMethods.didReceiveMessageWithData) { + [delegate webSocket:self didReceiveMessageWithData:frameData]; + } + } else { + if (availableMethods.didReceiveMessage) { + [delegate webSocket:self didReceiveMessage:string]; + } + if (availableMethods.didReceiveMessageWithString) { + [delegate webSocket:self didReceiveMessageWithString:string]; + } + } + }]; + break; + } + case SROpCodeBinaryFrame: { + SRDebugLog(@"Received data message."); + [self.delegateController performDelegateBlock:^(id _Nullable delegate, SRDelegateAvailableMethods availableMethods) { + if (availableMethods.didReceiveMessage) { + [delegate webSocket:self didReceiveMessage:frameData]; + } + if (availableMethods.didReceiveMessageWithData) { + [delegate webSocket:self didReceiveMessageWithData:frameData]; + } + }]; + } + break; + case SROpCodeConnectionClose: + [self handleCloseWithData:frameData]; + break; + case SROpCodePing: + [self _handlePingWithData:frameData]; + break; + case SROpCodePong: + [self handlePong:frameData]; + break; + default: + [self _closeWithProtocolError:[NSString stringWithFormat:@"Unknown opcode %ld", (long)opcode]]; + // TODO: Handle invalid opcode + break; + } +} + +- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData; +{ + assert(frame_header.opcode != 0); + + if (self.readyState == SR_CLOSED) { + return; + } + + + BOOL isControlFrame = (frame_header.opcode == SROpCodePing || frame_header.opcode == SROpCodePong || frame_header.opcode == SROpCodeConnectionClose); + + if (isControlFrame && !frame_header.fin) { + [self _closeWithProtocolError:@"Fragmented control frames not allowed"]; + return; + } + + if (isControlFrame && frame_header.payload_length >= 126) { + [self _closeWithProtocolError:@"Control frames cannot have payloads larger than 126 bytes"]; + return; + } + + if (!isControlFrame) { + _currentFrameOpcode = frame_header.opcode; + _currentFrameCount += 1; + } + + if (frame_header.payload_length == 0) { + if (isControlFrame) { + [self _handleFrameWithData:curData opCode:frame_header.opcode]; + } else { + if (frame_header.fin) { + [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode]; + } else { + // TODO add assert that opcode is not a control; + [self _readFrameContinue]; + } + } + } else { + assert(frame_header.payload_length <= SIZE_T_MAX); + [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(SRWebSocket *sself, NSData *newData) { + if (isControlFrame) { + [sself _handleFrameWithData:newData opCode:frame_header.opcode]; + } else { + if (frame_header.fin) { + [sself _handleFrameWithData:sself->_currentFrameData opCode:frame_header.opcode]; + } else { + // TODO add assert that opcode is not a control; + [sself _readFrameContinue]; + } + } + } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked]; + } +} + +/* From RFC: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-------+-+-------------+-------------------------------+ + |F|R|R|R| opcode|M| Payload len | Extended payload length | + |I|S|S|S| (4) |A| (7) | (16/64) | + |N|V|V|V| |S| | (if payload len==126/127) | + | |1|2|3| |K| | | + +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + | Extended payload length continued, if payload len == 127 | + + - - - - - - - - - - - - - - - +-------------------------------+ + | |Masking-key, if MASK set to 1 | + +-------------------------------+-------------------------------+ + | Masking-key (continued) | Payload Data | + +-------------------------------- - - - - - - - - - - - - - - - + + : Payload Data continued ... : + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + | Payload Data continued ... | + +---------------------------------------------------------------+ + */ + +static const uint8_t SRFinMask = 0x80; +static const uint8_t SROpCodeMask = 0x0F; +static const uint8_t SRRsvMask = 0x70; +static const uint8_t SRMaskMask = 0x80; +static const uint8_t SRPayloadLenMask = 0x7F; + + +- (void)_readFrameContinue; +{ + assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0)); + + [self _addConsumerWithDataLength:2 callback:^(SRWebSocket *sself, NSData *data) { + __block frame_header header = {0}; + + const uint8_t *headerBuffer = data.bytes; + assert(data.length >= 2); + + if (headerBuffer[0] & SRRsvMask) { + [sself _closeWithProtocolError:@"Server used RSV bits"]; + return; + } + + uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]); + + BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose); + + if (!isControlFrame && receivedOpcode != 0 && sself->_currentFrameCount > 0) { + [sself _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"]; + return; + } + + if (receivedOpcode == 0 && sself->_currentFrameCount == 0) { + [sself _closeWithProtocolError:@"cannot continue a message"]; + return; + } + + header.opcode = receivedOpcode == 0 ? sself->_currentFrameOpcode : receivedOpcode; + + header.fin = !!(SRFinMask & headerBuffer[0]); + + + header.masked = !!(SRMaskMask & headerBuffer[1]); + header.payload_length = SRPayloadLenMask & headerBuffer[1]; + + headerBuffer = NULL; + + if (header.masked) { + [sself _closeWithProtocolError:@"Client must receive unmasked data"]; + return; + } + + size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0; + + if (header.payload_length == 126) { + extra_bytes_needed += sizeof(uint16_t); + } else if (header.payload_length == 127) { + extra_bytes_needed += sizeof(uint64_t); + } + + if (extra_bytes_needed == 0) { + [sself _handleFrameHeader:header curData:sself->_currentFrameData]; + } else { + [sself _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *eself, NSData *edata) { + size_t mapped_size = edata.length; +#pragma unused (mapped_size) + const void *mapped_buffer = edata.bytes; + size_t offset = 0; + + if (header.payload_length == 126) { + assert(mapped_size >= sizeof(uint16_t)); + uint16_t payloadLength = 0; + memcpy(&payloadLength, mapped_buffer, sizeof(uint16_t)); + payloadLength = CFSwapInt16BigToHost(payloadLength); + + header.payload_length = payloadLength; + offset += sizeof(uint16_t); + } else if (header.payload_length == 127) { + assert(mapped_size >= sizeof(uint64_t)); + uint64_t payloadLength = 0; + memcpy(&payloadLength, mapped_buffer, sizeof(uint64_t)); + payloadLength = CFSwapInt64BigToHost(payloadLength); + + header.payload_length = payloadLength; + offset += sizeof(uint64_t); + } else { + assert(header.payload_length < 126 && header.payload_length >= 0); + } + + if (header.masked) { + assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset); + memcpy(eself->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(eself->_currentReadMaskKey)); + } + + [eself _handleFrameHeader:header curData:eself->_currentFrameData]; + } readToCurrentFrame:NO unmaskBytes:NO]; + } + } readToCurrentFrame:NO unmaskBytes:NO]; +} + +- (void)_readFrameNew; +{ + dispatch_async(_workQueue, ^{ + // Don't reset the length, since Apple doesn't guarantee that this will free the memory (and in tests on + // some platforms, it doesn't seem to, effectively causing a leak the size of the biggest frame so far). + _currentFrameData = [[NSMutableData alloc] init]; + + _currentFrameOpcode = 0; + _currentFrameCount = 0; + _readOpCount = 0; + _currentStringScanPosition = 0; + + [self _readFrameContinue]; + }); +} + +- (void)_pumpWriting; +{ + [self assertOnWorkQueue]; + + NSUInteger dataLength = dispatch_data_get_size(_outputBuffer); + if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) { + __block NSInteger bytesWritten = 0; + __block BOOL streamFailed = NO; + + dispatch_data_t dataToSend = dispatch_data_create_subrange(_outputBuffer, _outputBufferOffset, dataLength - _outputBufferOffset); + dispatch_data_apply(dataToSend, ^bool(dispatch_data_t region, size_t offset, const void *buffer, size_t size) { + NSInteger sentLength = [_outputStream write:buffer maxLength:size]; + if (sentLength == -1) { + streamFailed = YES; + return false; + } + bytesWritten += sentLength; + return (sentLength >= (NSInteger)size); // If we can't write all the data into the stream - bail-out early. + }); + if (streamFailed) { + NSInteger code = 2145; + NSString *description = @"Error writing to stream."; + NSError *streamError = _outputStream.streamError; + NSError *error = streamError ? SRErrorWithCodeDescriptionUnderlyingError(code, description, streamError) : SRErrorWithCodeDescription(code, description); + [self _failWithError:error]; + return; + } + + _outputBufferOffset += bytesWritten; + + if (_outputBufferOffset > SRDefaultBufferSize() && _outputBufferOffset > dataLength / 2) { + _outputBuffer = dispatch_data_create_subrange(_outputBuffer, _outputBufferOffset, dataLength - _outputBufferOffset); + _outputBufferOffset = 0; + } + } + + if (_closeWhenFinishedWriting && + (dispatch_data_get_size(_outputBuffer) - _outputBufferOffset) == 0 && + (_inputStream.streamStatus != NSStreamStatusNotOpen && + _inputStream.streamStatus != NSStreamStatusClosed) && + !_sentClose) { + _sentClose = YES; + + @synchronized(self) { + [_outputStream close]; + [_inputStream close]; + + + for (NSArray *runLoop in [_scheduledRunloops copy]) { + [self unscheduleFromRunLoop:[runLoop objectAtIndex:0] forMode:[runLoop objectAtIndex:1]]; + } + } + + if (!_failed) { + [self.delegateController performDelegateBlock:^(id _Nullable delegate, SRDelegateAvailableMethods availableMethods) { + if (availableMethods.didCloseWithCode) { + [delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES]; + } + }]; + } + + [self _scheduleCleanup]; + } +} + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; +{ + [self assertOnWorkQueue]; + [self _addConsumerWithScanner:consumer callback:callback dataLength:0]; +} + +- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +{ + [self assertOnWorkQueue]; + assert(dataLength); + + [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]]; + [self _pumpScanner]; +} + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; +{ + [self assertOnWorkQueue]; + [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]]; + [self _pumpScanner]; +} + + +- (void)_scheduleCleanup +{ + @synchronized(self) { + if (_cleanupScheduled) { + return; + } + + _cleanupScheduled = YES; + + // Cleanup NSStream delegate's in the same RunLoop used by the streams themselves: + // This way we'll prevent race conditions between handleEvent and SRWebsocket's dealloc + NSTimer *timer = [NSTimer timerWithTimeInterval:(0.0f) target:self selector:@selector(_cleanupSelfReference:) userInfo:nil repeats:NO]; + [[NSRunLoop SR_networkRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; + } +} + +- (void)_cleanupSelfReference:(NSTimer *)timer +{ + @synchronized(self) { + // Nuke NSStream delegate's + _inputStream.delegate = nil; + _outputStream.delegate = nil; + + // Remove the streams, right now, from the networkRunLoop + [_inputStream close]; + [_outputStream close]; + } + + // Cleanup selfRetain in the same GCD queue as usual + dispatch_async(_workQueue, ^{ + _selfRetain = nil; + }); +} + + +static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'}; + +- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; +{ + [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler]; +} + +- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; +{ + // TODO optimize so this can continue from where we last searched + stream_scanner consumer = ^size_t(NSData *data) { + __block size_t found_size = 0; + __block size_t match_count = 0; + + size_t size = data.length; + const unsigned char *buffer = data.bytes; + for (size_t i = 0; i < size; i++ ) { + if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) { + match_count += 1; + if (match_count == length) { + found_size = i + 1; + break; + } + } else { + match_count = 0; + } + } + return found_size; + }; + [self _addConsumerWithScanner:consumer callback:dataHandler]; +} + + +// Returns true if did work +- (BOOL)_innerPumpScanner { + + BOOL didWork = NO; + + if (self.readyState >= SR_CLOSED) { + return didWork; + } + + size_t readBufferSize = dispatch_data_get_size(_readBuffer); + + if (!_consumers.count) { + return didWork; + } + + size_t curSize = readBufferSize - _readBufferOffset; + if (!curSize) { + return didWork; + } + + SRIOConsumer *consumer = [_consumers objectAtIndex:0]; + + size_t bytesNeeded = consumer.bytesNeeded; + + size_t foundSize = 0; + if (consumer.consumer) { + NSData *subdata = (NSData *)dispatch_data_create_subrange(_readBuffer, _readBufferOffset, readBufferSize - _readBufferOffset); + foundSize = consumer.consumer(subdata); + } else { + assert(consumer.bytesNeeded); + if (curSize >= bytesNeeded) { + foundSize = bytesNeeded; + } else if (consumer.readToCurrentFrame) { + foundSize = curSize; + } + } + + if (consumer.readToCurrentFrame || foundSize) { + dispatch_data_t slice = dispatch_data_create_subrange(_readBuffer, _readBufferOffset, foundSize); + + _readBufferOffset += foundSize; + + if (_readBufferOffset > SRDefaultBufferSize() && _readBufferOffset > readBufferSize / 2) { + _readBuffer = dispatch_data_create_subrange(_readBuffer, _readBufferOffset, readBufferSize - _readBufferOffset); + _readBufferOffset = 0; + } + + if (consumer.unmaskBytes) { + __block NSMutableData *mutableSlice = [slice mutableCopy]; + + NSUInteger len = mutableSlice.length; + uint8_t *bytes = mutableSlice.mutableBytes; + + for (NSUInteger i = 0; i < len; i++) { + bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)]; + _currentReadMaskOffset += 1; + } + + slice = dispatch_data_create(bytes, len, nil, ^{ + mutableSlice = nil; + }); + } + + if (consumer.readToCurrentFrame) { + dispatch_data_apply(slice, ^bool(dispatch_data_t region, size_t offset, const void *buffer, size_t size) { + [_currentFrameData appendBytes:buffer length:size]; + return true; + }); + + _readOpCount += 1; + + if (_currentFrameOpcode == SROpCodeTextFrame) { + // Validate UTF8 stuff. + size_t currentDataSize = _currentFrameData.length; + if (_currentFrameOpcode == SROpCodeTextFrame && currentDataSize > 0) { + // TODO: Optimize the crap out of this. Don't really have to copy all the data each time + + size_t scanSize = currentDataSize - _currentStringScanPosition; + + NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)]; + int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data); + + if (valid_utf8_size == -1) { + [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; + dispatch_async(_workQueue, ^{ + [self closeConnection]; + }); + return didWork; + } else { + _currentStringScanPosition += valid_utf8_size; + } + } + + } + + consumer.bytesNeeded -= foundSize; + + if (consumer.bytesNeeded == 0) { + [_consumers removeObjectAtIndex:0]; + consumer.handler(self, nil); + [_consumerPool returnConsumer:consumer]; + didWork = YES; + } + } else if (foundSize) { + [_consumers removeObjectAtIndex:0]; + consumer.handler(self, (NSData *)slice); + [_consumerPool returnConsumer:consumer]; + didWork = YES; + } + } + return didWork; +} + +-(void)_pumpScanner; +{ + [self assertOnWorkQueue]; + + if (!_isPumping) { + _isPumping = YES; + } else { + return; + } + + while ([self _innerPumpScanner]) { + + } + + _isPumping = NO; +} + +//#define NOMASK + +static const size_t SRFrameHeaderOverhead = 32; + +- (void)_sendFrameWithOpcode:(SROpCode)opCode data:(NSData *)data +{ + [self assertOnWorkQueue]; + + if (!data) { + return; + } + + size_t payloadLength = data.length; + + NSMutableData *frameData = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead]; + if (!frameData) { + [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"]; + return; + } + uint8_t *frameBuffer = (uint8_t *)frameData.mutableBytes; + + // set fin + frameBuffer[0] = SRFinMask | opCode; + + // set the mask and header + frameBuffer[1] |= SRMaskMask; + + size_t frameBufferSize = 2; + + if (payloadLength < 126) { + frameBuffer[1] |= payloadLength; + } else { + uint64_t declaredPayloadLength = 0; + size_t declaredPayloadLengthSize = 0; + + if (payloadLength <= UINT16_MAX) { + frameBuffer[1] |= 126; + + declaredPayloadLength = CFSwapInt16BigToHost((uint16_t)payloadLength); + declaredPayloadLengthSize = sizeof(uint16_t); + } else { + frameBuffer[1] |= 127; + + declaredPayloadLength = CFSwapInt64BigToHost((uint64_t)payloadLength); + declaredPayloadLengthSize = sizeof(uint64_t); + } + + memcpy((frameBuffer + frameBufferSize), &declaredPayloadLength, declaredPayloadLengthSize); + frameBufferSize += declaredPayloadLengthSize; + } + + const uint8_t *unmaskedPayloadBuffer = (uint8_t *)data.bytes; + uint8_t *maskKey = frameBuffer + frameBufferSize; + + size_t randomBytesSize = sizeof(uint32_t); + int result = SecRandomCopyBytes(kSecRandomDefault, randomBytesSize, maskKey); + if (result != 0) { + //TODO: (nlutsenko) Check if there was an error. + } + frameBufferSize += randomBytesSize; + + // Copy and unmask the buffer + uint8_t *frameBufferPayloadPointer = frameBuffer + frameBufferSize; + + memcpy(frameBufferPayloadPointer, unmaskedPayloadBuffer, payloadLength); + SRMaskBytesSIMD(frameBufferPayloadPointer, payloadLength, maskKey); + frameBufferSize += payloadLength; + + assert(frameBufferSize <= frameData.length); + frameData.length = frameBufferSize; + + [self _writeData:frameData]; +} + +- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode +{ + __weak __typeof__(self) wself = self; + + if (_requestRequiresSSL && !_streamSecurityValidated && + (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) { + SecTrustRef trust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust]; + if (trust) { + _streamSecurityValidated = [_securityPolicy evaluateServerTrust:trust forDomain:_urlRequest.URL.host]; + } + if (!_streamSecurityValidated) { + dispatch_async(_workQueue, ^{ + NSError *error = SRErrorWithDomainCodeDescription(NSURLErrorDomain, + NSURLErrorClientCertificateRejected, + @"Invalid server certificate."); + [wself _failWithError:error]; + }); + return; + } + dispatch_async(_workQueue, ^{ + [self didConnect]; + }); + } + dispatch_async(_workQueue, ^{ + [wself safeHandleEvent:eventCode stream:aStream]; + }); +} + +- (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream +{ + switch (eventCode) { + case NSStreamEventOpenCompleted: { + SRDebugLog(@"NSStreamEventOpenCompleted %@", aStream); + if (self.readyState >= SR_CLOSING) { + return; + } + assert(_readBuffer); + + if (!_requestRequiresSSL && self.readyState == SR_CONNECTING && aStream == _inputStream) { + [self didConnect]; + } + + [self _pumpWriting]; + [self _pumpScanner]; + + break; + } + + case NSStreamEventErrorOccurred: { + SRDebugLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]); + /// TODO specify error better! + [self _failWithError:aStream.streamError]; + _readBufferOffset = 0; + _readBuffer = dispatch_data_empty; + break; + + } + + case NSStreamEventEndEncountered: { + [self _pumpScanner]; + SRDebugLog(@"NSStreamEventEndEncountered %@", aStream); + if (aStream.streamError) { + [self _failWithError:aStream.streamError]; + } else { + dispatch_async(_workQueue, ^{ + if (self.readyState != SR_CLOSED) { + self.readyState = SR_CLOSED; + [self _scheduleCleanup]; + } + + if (!_sentClose && !_failed) { + _sentClose = YES; + // If we get closed in this state it's probably not clean because we should be sending this when we send messages + [self.delegateController performDelegateBlock:^(id _Nullable delegate, SRDelegateAvailableMethods availableMethods) { + if (availableMethods.didCloseWithCode) { + [delegate webSocket:self + didCloseWithCode:SRStatusCodeGoingAway + reason:@"Stream end encountered" + wasClean:NO]; + } + }]; + } + }); + } + + break; + } + + case NSStreamEventHasBytesAvailable: { + SRDebugLog(@"NSStreamEventHasBytesAvailable %@", aStream); + uint8_t buffer[SRDefaultBufferSize()]; + + while (_inputStream.hasBytesAvailable) { + NSInteger bytesRead = [_inputStream read:buffer maxLength:SRDefaultBufferSize()]; + if (bytesRead > 0) { + dispatch_data_t data = dispatch_data_create(buffer, bytesRead, nil, DISPATCH_DATA_DESTRUCTOR_DEFAULT); + if (!data) { + NSError *error = SRErrorWithCodeDescription(SRStatusCodeMessageTooBig, + @"Unable to allocate memory to read from socket."); + [self _failWithError:error]; + return; + } + _readBuffer = dispatch_data_create_concat(_readBuffer, data); + } else if (bytesRead == -1) { + [self _failWithError:_inputStream.streamError]; + } + } + [self _pumpScanner]; + break; + } + + case NSStreamEventHasSpaceAvailable: { + SRDebugLog(@"NSStreamEventHasSpaceAvailable %@", aStream); + [self _pumpWriting]; + break; + } + + case NSStreamEventNone: + SRDebugLog(@"(default) %@", aStream); + break; + } +} + +///-------------------------------------- +#pragma mark - Delegate +///-------------------------------------- + +- (id _Nullable)delegate +{ + return self.delegateController.delegate; +} + +- (void)setDelegate:(id _Nullable)delegate +{ + self.delegateController.delegate = delegate; +} + +- (void)setDelegateDispatchQueue:(dispatch_queue_t _Nullable)queue +{ + self.delegateController.dispatchQueue = queue; +} + +- (dispatch_queue_t _Nullable)delegateDispatchQueue +{ + return self.delegateController.dispatchQueue; +} + +- (void)setDelegateOperationQueue:(NSOperationQueue *_Nullable)queue +{ + self.delegateController.operationQueue = queue; +} + +- (NSOperationQueue *_Nullable)delegateOperationQueue +{ + return self.delegateController.operationQueue; +} + +@end + +#ifdef HAS_ICU + +static inline int32_t validate_dispatch_data_partial_string(NSData *data) { + if ([data length] > INT32_MAX) { + // INT32_MAX is the limit so long as this Framework is using 32 bit ints everywhere. + return -1; + } + + int32_t size = (int32_t)[data length]; + + const void * contents = [data bytes]; + const uint8_t *str = (const uint8_t *)contents; + + UChar32 codepoint = 1; + int32_t offset = 0; + int32_t lastOffset = 0; + while(offset < size && codepoint > 0) { + lastOffset = offset; + U8_NEXT(str, offset, size, codepoint); + } + + if (codepoint == -1) { + // Check to see if the last byte is valid or whether it was just continuing + if (!U8_IS_LEAD(str[lastOffset]) || U8_COUNT_TRAIL_BYTES(str[lastOffset]) + lastOffset < (int32_t)size) { + + size = -1; + } else { + uint8_t leadByte = str[lastOffset]; + U8_MASK_LEAD_BYTE(leadByte, U8_COUNT_TRAIL_BYTES(leadByte)); + + for (int i = lastOffset + 1; i < offset; i++) { + if (U8_IS_SINGLE(str[i]) || U8_IS_LEAD(str[i]) || !U8_IS_TRAIL(str[i])) { + size = -1; + } + } + + if (size != -1) { + size = lastOffset; + } + } + } + + if (size != -1 && ![[NSString alloc] initWithBytesNoCopy:(char *)[data bytes] length:size encoding:NSUTF8StringEncoding freeWhenDone:NO]) { + size = -1; + } + + return size; +} + +#else + +// This is a hack, and probably not optimal +static inline int32_t validate_dispatch_data_partial_string(NSData *data) { + static const int maxCodepointSize = 3; + + for (int i = 0; i < maxCodepointSize; i++) { + NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO]; + if (str) { + return (int32_t)data.length - i; + } + } + + return -1; +} + +#endif diff --git a/sources/SocketRocket/SocketRocket.h b/sources/SocketRocket/SocketRocket.h new file mode 100644 index 00000000..c7ab0622 --- /dev/null +++ b/sources/SocketRocket/SocketRocket.h @@ -0,0 +1,15 @@ +// +// Copyright 2012 Square Inc. +// Portions Copyright (c) 2016-present, Facebook, Inc. +// +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import +#import +#import +#import