From c6c893f8124850de9851a7c3691966d43d58f5bc Mon Sep 17 00:00:00 2001 From: Harrison Harnisch Date: Thu, 16 Apr 2015 14:11:56 -0700 Subject: [PATCH 1/4] First cut at WebSocket Polyfill --- .../InitializeJavaScriptAppEngine.js | 5 + .../RCTWebSocket.xcodeproj/project.pbxproj | 246 ++++++++++++++++++ Libraries/WebSocket/WebSocket.h | 16 ++ Libraries/WebSocket/WebSocket.ios.js | 99 +++++++ Libraries/WebSocket/WebSocket.m | 73 ++++++ Libraries/WebSocket/WebSocketBase.js | 100 +++++++ Libraries/WebSocket/WebSocketFactory.h | 14 + Libraries/WebSocket/WebSocketFactory.m | 49 ++++ React.podspec | 6 + 9 files changed, 608 insertions(+) create mode 100644 Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj create mode 100644 Libraries/WebSocket/WebSocket.h create mode 100644 Libraries/WebSocket/WebSocket.ios.js create mode 100644 Libraries/WebSocket/WebSocket.m create mode 100644 Libraries/WebSocket/WebSocketBase.js create mode 100644 Libraries/WebSocket/WebSocketFactory.h create mode 100644 Libraries/WebSocket/WebSocketFactory.m diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 51f6809ccd9b28..655da95f6bef01 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -133,6 +133,10 @@ function setupGeolocation() { GLOBAL.navigator.geolocation = require('Geolocation'); } +function setupWebSockets() { + GLOBAL.WebSocket = require('WebSocket'); +} + setupDocumentShim(); setupRedBoxErrorHandler(); setupTimers(); @@ -140,3 +144,4 @@ setupAlert(); setupPromise(); setupXHR(); setupGeolocation(); +setupWebSockets(); diff --git a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj new file mode 100644 index 00000000000000..8903e77f04f7ea --- /dev/null +++ b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj @@ -0,0 +1,246 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 3C86DF791ADF646A0047B81A /* WebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF781ADF646A0047B81A /* WebSocket.m */; }; + 3C86DF7C1ADF695F0047B81A /* WebSocketFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF7B1ADF695F0047B81A /* WebSocketFactory.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3C86DF461ADF2C930047B81A /* libWebSocket.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libWebSocket.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 3C86DF771ADF646A0047B81A /* WebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebSocket.h; sourceTree = ""; }; + 3C86DF781ADF646A0047B81A /* WebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebSocket.m; sourceTree = ""; }; + 3C86DF7A1ADF695F0047B81A /* WebSocketFactory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebSocketFactory.h; sourceTree = ""; }; + 3C86DF7B1ADF695F0047B81A /* WebSocketFactory.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = WebSocketFactory.m; sourceTree = ""; tabWidth = 2; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3C86DF431ADF2C930047B81A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3C86DF3D1ADF2C930047B81A = { + isa = PBXGroup; + children = ( + 3C86DF7A1ADF695F0047B81A /* WebSocketFactory.h */, + 3C86DF7B1ADF695F0047B81A /* WebSocketFactory.m */, + 3C86DF771ADF646A0047B81A /* WebSocket.h */, + 3C86DF781ADF646A0047B81A /* WebSocket.m */, + 3C86DF471ADF2C930047B81A /* Products */, + ); + sourceTree = ""; + }; + 3C86DF471ADF2C930047B81A /* Products */ = { + isa = PBXGroup; + children = ( + 3C86DF461ADF2C930047B81A /* libWebSocket.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3C86DF451ADF2C930047B81A /* WebSocket */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3C86DF5A1ADF2C930047B81A /* Build configuration list for PBXNativeTarget "WebSocket" */; + buildPhases = ( + 3C86DF421ADF2C930047B81A /* Sources */, + 3C86DF431ADF2C930047B81A /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WebSocket; + productName = WebSocket; + productReference = 3C86DF461ADF2C930047B81A /* libWebSocket.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3C86DF3E1ADF2C930047B81A /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0630; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 3C86DF451ADF2C930047B81A = { + CreatedOnToolsVersion = 6.3; + }; + }; + }; + buildConfigurationList = 3C86DF411ADF2C930047B81A /* Build configuration list for PBXProject "RCTWebSocket" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 3C86DF3D1ADF2C930047B81A; + productRefGroup = 3C86DF471ADF2C930047B81A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3C86DF451ADF2C930047B81A /* WebSocket */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 3C86DF421ADF2C930047B81A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C86DF7C1ADF695F0047B81A /* WebSocketFactory.m in Sources */, + 3C86DF791ADF646A0047B81A /* WebSocket.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 3C86DF581ADF2C930047B81A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 3C86DF591ADF2C930047B81A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3C86DF5B1ADF2C930047B81A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../RCTWebSocketDebugger", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 3C86DF5C1ADF2C930047B81A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../RCTWebSocketDebugger", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3C86DF411ADF2C930047B81A /* Build configuration list for PBXProject "RCTWebSocket" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3C86DF581ADF2C930047B81A /* Debug */, + 3C86DF591ADF2C930047B81A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3C86DF5A1ADF2C930047B81A /* Build configuration list for PBXNativeTarget "WebSocket" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3C86DF5B1ADF2C930047B81A /* Debug */, + 3C86DF5C1ADF2C930047B81A /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3C86DF3E1ADF2C930047B81A /* Project object */; +} diff --git a/Libraries/WebSocket/WebSocket.h b/Libraries/WebSocket/WebSocket.h new file mode 100644 index 00000000000000..bd20876f2295ce --- /dev/null +++ b/Libraries/WebSocket/WebSocket.h @@ -0,0 +1,16 @@ +// +// WebSocket.h +// WebSocket +// +// Created by Harrison Harnisch on 4/15/15. +// Copyright (c) 2015 React. All rights reserved. +// + +#import "RCTBridgeModule.h" +#import "SRWebSocket.h" + +@interface WebSocket : NSObject + +- (instancetype) initWithURLString: (NSString *)URLString bridge:(RCTBridge *)bridge socketIndex: (NSNumber *)socketIndex; + +@end diff --git a/Libraries/WebSocket/WebSocket.ios.js b/Libraries/WebSocket/WebSocket.ios.js new file mode 100644 index 00000000000000..d1947e2c85c803 --- /dev/null +++ b/Libraries/WebSocket/WebSocket.ios.js @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule WebSocket + * + */ +'use strict'; + +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTWebSocketFactory = require('NativeModules').WebSocketFactory; + +var WebSocketBase = require('WebSocketBase'); + +var WebSocketId = 0; + +class WebSocket extends WebSocketBase { + _socketId: number; + _subs: any; + + connectToSocketImpl(url: string): void { + RCTWebSocketFactory.connect(url); + this._socketId = WebSocketId++; + this._registerEvents(this._socketId); + } + + closeConnectionImpl(): void{ + RCTWebSocketFactory.close(this._socketId) + } + + cancelConnectionImpl(): void { + RCTWebSocketFactory.close(this._socketId) + } + + sendStringImpl(message: string): void { + RCTWebSocketFactory.send(message, this._socketId); + } + + sendArrayBufferImpl(): void { + // TODO + } + + _unregisterEvents(): void{ + this._subs.forEach(e => e.remove()) + this._subs = []; + } + + _registerEvents(id: number): void{ + this._subs = [ + RCTDeviceEventEmitter.addListener( + 'websocketMessage', + function(ev){ + if(ev.id != id) + return; + this.onmessage && this.onmessage({ + data:ev.data + }); + }.bind(this) + ), + RCTDeviceEventEmitter.addListener( + 'websocketOpen', + function(ev){ + if(ev.id != id) + return; + this.readyState = this.OPEN + this.onopen && this.onopen(); + }.bind(this) + ), + RCTDeviceEventEmitter.addListener( + 'websocketClosed', + function(ev){ + if(ev.id != id) + return; + this.readyState = this.CLOSED + this.onclose && this.onclose(ev) + this._unregisterEvents(); + RCTWebSocketFactory.destroy(id) + }.bind(this) + ), + RCTDeviceEventEmitter.addListener( + 'websocketFailed', + function(ev){ + if(ev.id != id) + return; + this.onerror && this.onerror(new Error(ev.message)) + this._unregisterEvents(); + RCTWebSocketFactory.destroy(id) + }.bind(this) + ) + ] + } + +} + +module.exports = WebSocket; diff --git a/Libraries/WebSocket/WebSocket.m b/Libraries/WebSocket/WebSocket.m new file mode 100644 index 00000000000000..295840d9d9191a --- /dev/null +++ b/Libraries/WebSocket/WebSocket.m @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-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 "WebSocket.h" +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" + +@implementation WebSocket { + SRWebSocket *_webSocket; + RCTBridge *_bridge; + NSNumber *_socketIndex; +} + + +- (instancetype) initWithURLString: (NSString *)URLString + bridge:(RCTBridge *)bridge + socketIndex: (NSNumber *)socketIndex +{ + if (self = [super init]) { + _bridge = bridge; + _socketIndex = socketIndex; + _webSocket = [[SRWebSocket alloc] initWithURL:[NSURL URLWithString:URLString]]; + _webSocket.delegate = self; + + [_webSocket open]; + } + + return self; +} + +- (void) send:(id)data { + [_webSocket send:data]; +} + +- (void) close { + [_webSocket close]; +} + +- (void) webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message { + [_bridge.eventDispatcher sendDeviceEventWithName:@"websocketMessage" + body:@{@"data":message, @"id":_socketIndex}]; +} + +- (void)webSocketDidOpen:(SRWebSocket *)webSocket { + [_bridge.eventDispatcher sendDeviceEventWithName:@"websocketOpen" + body:@{@"id":_socketIndex}]; +} + +- (void) webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error { + [_bridge.eventDispatcher sendDeviceEventWithName:@"websocketFailed" + body:@{@"message":[error localizedDescription], @"id":_socketIndex}]; + _webSocket = nil; +} + +- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean { + [_bridge.eventDispatcher sendDeviceEventWithName:@"websocketClosed" + body:@{ + @"code": [NSNumber numberWithInt:code], + @"reason": reason, + @"clean": [NSNumber numberWithBool:wasClean], + @"id":_socketIndex + }]; + _webSocket = nil; +} + + +@end diff --git a/Libraries/WebSocket/WebSocketBase.js b/Libraries/WebSocket/WebSocketBase.js new file mode 100644 index 00000000000000..a9f8a272e3c509 --- /dev/null +++ b/Libraries/WebSocket/WebSocketBase.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule WebSocketBase + * + */ +'use strict'; + +/** + * Shared base for platform-specific WebSocket implementations. + */ +class WebSocketBase { + CONNECTING: number; + OPEN: number; + CLOSING: number; + CLOSED: number; + + onclose: ?Function; + onerror: ?Function; + onmessage: ?Function; + onopen: ?Function; + + binaryType: ?string; + bufferedAmount: number; + extension: ?string; + protocol: ?string; + readyState: number; + url: ?string; + + constructor(url: string, protocols: ?any) { + this.CONNECTING = 0; + this.OPEN = 1; + this.CLOSING = 2; + this.CLOSED = 3; + + + if (!protocols) { + protocols = []; + } + + this.connectToSocketImpl(url); + } + + close(): void { + if (this.readyState === WebSocketBase.CLOSING || + this.readyState === WebSocketBase.CLOSED) { + return; + } + + if (this.readyState === WebSocketBase.CONNECTING) { + this.cancelConnectionImpl(); + } + + this.closeConnectionImpl(); + } + + send(data: any): void { + if (this.readyState === WebSocketBase.CONNECTING) { + throw new Error('INVALID_STATE_ERR'); + } + + if (typeof data === 'string') { + this.sendStringImpl(data); + } + else if (data instanceof ArrayBuffer) { + this.sendArrayBufferImpl(data); + } + else { + throw new Error('Not supported data type'); + } + } + + closeConnectionImpl(): void { + throw new Error('Subclass must define closeConnectionImpl method'); + } + + connectToSocketImpl(): void { + throw new Error('Subclass must define connectToSocketImpl method'); + } + + cancelConnectionImpl(): void { + throw new Error('Subclass must define cancelConnectionImpl method'); + } + + sendStringImpl(): void { + throw new Error('Subclass must define sendStringImpl method'); + } + + sendArrayBufferImpl(): void { + throw new Error('Subclass must define sendArrayBufferImpl method'); + } + +} + +module.exports = WebSocketBase; diff --git a/Libraries/WebSocket/WebSocketFactory.h b/Libraries/WebSocket/WebSocketFactory.h new file mode 100644 index 00000000000000..eb3078ec1d0407 --- /dev/null +++ b/Libraries/WebSocket/WebSocketFactory.h @@ -0,0 +1,14 @@ +// +// WebSocketFactory.h +// RCTWebSocket +// +// Created by Harrison Harnisch on 4/15/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + + +#import "RCTBridgeModule.h" + +@interface WebSocketFactory : NSObject + +@end diff --git a/Libraries/WebSocket/WebSocketFactory.m b/Libraries/WebSocket/WebSocketFactory.m new file mode 100644 index 00000000000000..ab7f356d2a689a --- /dev/null +++ b/Libraries/WebSocket/WebSocketFactory.m @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-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 "WebSocketFactory.h" +#import "WebSocket.h" +#import "RCTBridge.h" +#import "RCTSparseArray.h" + +@implementation WebSocketFactory { + RCTSparseArray *_sockets; + NSInteger _currentSocketId; +} + +RCT_EXPORT_MODULE() + +@synthesize bridge = _bridge; + +- (instancetype)init { + if ((self = [super init])) { + _sockets = [[RCTSparseArray alloc] init]; + _currentSocketId = 0; + } + + return self; +} + +RCT_EXPORT_METHOD(connect:(NSString *)urlString) { + _sockets[_currentSocketId] = [[WebSocket alloc] initWithURLString:urlString bridge:_bridge socketIndex:[NSNumber numberWithInt:_currentSocketId++]]; +} + +RCT_EXPORT_METHOD(send:(NSString *)message socketIndex: (NSInteger)socketIndex) { + [_sockets[socketIndex] send:message]; +} + +RCT_EXPORT_METHOD(destroy:(NSInteger) socketIndex) { + _sockets[socketIndex] = nil; +} + +RCT_EXPORT_METHOD(close:(NSInteger) socketIndex) { + [_sockets[socketIndex] close]; +} + +@end diff --git a/React.podspec b/React.podspec index 65dc93eb099cb1..ed6e9178940df5 100644 --- a/React.podspec +++ b/React.podspec @@ -80,6 +80,12 @@ Pod::Spec.new do |s| ss.source_files = "Libraries/RCTWebSocketDebugger/*.{h,m}" end + s.subspec 'RCTWebSocket' do |ss| + ss.dependency 'React/Core' + ss.source_files = "Libraries/WebSocket/*.{h,m}" + ss.preserve_paths = "Libraries/WebSocket/*.js" + end + s.subspec 'RCTText' do |ss| ss.dependency 'React/Core' ss.source_files = "Libraries/Text/*.{h,m}" From a3ff21ee72a74aff5edbad26a15bf03be3bc5b89 Mon Sep 17 00:00:00 2001 From: Harrison Harnisch Date: Thu, 16 Apr 2015 15:02:18 -0700 Subject: [PATCH 2/4] Minimize WebSocket library requirements --- .../RCTWebSocket.xcodeproj/project.pbxproj | 9 +- Libraries/WebSocket/SRWebSocket.h | 132 ++ Libraries/WebSocket/SRWebSocket.m | 1777 +++++++++++++++++ 3 files changed, 1916 insertions(+), 2 deletions(-) create mode 100644 Libraries/WebSocket/SRWebSocket.h create mode 100644 Libraries/WebSocket/SRWebSocket.m diff --git a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj index 8903e77f04f7ea..4bb77257b1d159 100644 --- a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj +++ b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 3C86DF791ADF646A0047B81A /* WebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF781ADF646A0047B81A /* WebSocket.m */; }; 3C86DF7C1ADF695F0047B81A /* WebSocketFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF7B1ADF695F0047B81A /* WebSocketFactory.m */; }; + 3CC421A41AE0684B00240844 /* SRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CC421A31AE0684B00240844 /* SRWebSocket.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -17,6 +18,8 @@ 3C86DF781ADF646A0047B81A /* WebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebSocket.m; sourceTree = ""; }; 3C86DF7A1ADF695F0047B81A /* WebSocketFactory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebSocketFactory.h; sourceTree = ""; }; 3C86DF7B1ADF695F0047B81A /* WebSocketFactory.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = WebSocketFactory.m; sourceTree = ""; tabWidth = 2; }; + 3CC421A21AE0684B00240844 /* SRWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRWebSocket.h; sourceTree = ""; }; + 3CC421A31AE0684B00240844 /* SRWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRWebSocket.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -37,6 +40,8 @@ 3C86DF7B1ADF695F0047B81A /* WebSocketFactory.m */, 3C86DF771ADF646A0047B81A /* WebSocket.h */, 3C86DF781ADF646A0047B81A /* WebSocket.m */, + 3CC421A21AE0684B00240844 /* SRWebSocket.h */, + 3CC421A31AE0684B00240844 /* SRWebSocket.m */, 3C86DF471ADF2C930047B81A /* Products */, ); sourceTree = ""; @@ -104,6 +109,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3CC421A41AE0684B00240844 /* SRWebSocket.m in Sources */, 3C86DF7C1ADF695F0047B81A /* WebSocketFactory.m in Sources */, 3C86DF791ADF646A0047B81A /* WebSocket.m in Sources */, ); @@ -197,7 +203,6 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", - "$(SRCROOT)/../RCTWebSocketDebugger", ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -212,7 +217,6 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", - "$(SRCROOT)/../RCTWebSocketDebugger", ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -239,6 +243,7 @@ 3C86DF5C1ADF2C930047B81A /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/Libraries/WebSocket/SRWebSocket.h b/Libraries/WebSocket/SRWebSocket.h new file mode 100644 index 00000000000000..5cce725a348aab --- /dev/null +++ b/Libraries/WebSocket/SRWebSocket.h @@ -0,0 +1,132 @@ +// +// Copyright 2012 Square Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import + +typedef enum { + SR_CONNECTING = 0, + SR_OPEN = 1, + SR_CLOSING = 2, + SR_CLOSED = 3, +} SRReadyState; + +typedef enum SRStatusCode : NSInteger { + SRStatusCodeNormal = 1000, + SRStatusCodeGoingAway = 1001, + SRStatusCodeProtocolError = 1002, + SRStatusCodeUnhandledType = 1003, + // 1004 reserved. + SRStatusNoStatusReceived = 1005, + // 1004-1006 reserved. + SRStatusCodeInvalidUTF8 = 1007, + SRStatusCodePolicyViolated = 1008, + SRStatusCodeMessageTooBig = 1009, +} SRStatusCode; + +@class SRWebSocket; + +extern NSString *const SRWebSocketErrorDomain; +extern NSString *const SRHTTPResponseErrorKey; + +#pragma mark - SRWebSocketDelegate + +@protocol SRWebSocketDelegate; + +#pragma mark - SRWebSocket + +@interface SRWebSocket : NSObject + +@property (nonatomic, weak) id delegate; + +@property (nonatomic, readonly) SRReadyState readyState; +@property (nonatomic, readonly, retain) NSURL *url; + +// This returns the negotiated protocol. +// It will be nil until after the handshake completes. +@property (nonatomic, readonly, copy) NSString *protocol; + +// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol. +- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; +- (id)initWithURLRequest:(NSURLRequest *)request; + +// Some helper constructors. +- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; +- (id)initWithURL:(NSURL *)url; + +// Delegate queue will be dispatch_main_queue by default. +// You cannot set both OperationQueue and dispatch_queue. +- (void)setDelegateOperationQueue:(NSOperationQueue*) queue; +- (void)setDelegateDispatchQueue:(dispatch_queue_t) queue; + +// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes. +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; + +// SRWebSockets are intended for one-time-use only. Open should be called once and only once. +- (void)open; + +- (void)close; +- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; + +// Send a UTF8 String or Data. +- (void)send:(id)data; + +// Send Data (can be nil) in a ping message. +- (void)sendPing:(NSData *)data; + +@end + +#pragma mark - SRWebSocketDelegate + +@protocol SRWebSocketDelegate + +// message will either be an NSString if the server is using text +// or NSData if the server is using binary. +- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message; + +@optional + +- (void)webSocketDidOpen:(SRWebSocket *)webSocket; +- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error; +- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean; +- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload; + +@end + +#pragma mark - NSURLRequest (CertificateAdditions) + +@interface NSURLRequest (CertificateAdditions) + +@property (nonatomic, retain, readonly) NSArray *SR_SSLPinnedCertificates; + +@end + +#pragma mark - NSMutableURLRequest (CertificateAdditions) + +@interface NSMutableURLRequest (CertificateAdditions) + +@property (nonatomic, retain) NSArray *SR_SSLPinnedCertificates; + +@end + +#pragma mark - NSRunLoop (SRWebSocket) + +@interface NSRunLoop (SRWebSocket) + ++ (NSRunLoop *)SR_networkRunLoop; + +@end diff --git a/Libraries/WebSocket/SRWebSocket.m b/Libraries/WebSocket/SRWebSocket.m new file mode 100644 index 00000000000000..3fd675103367ce --- /dev/null +++ b/Libraries/WebSocket/SRWebSocket.m @@ -0,0 +1,1777 @@ +// +// Copyright 2012 Square Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +#import "SRWebSocket.h" + +#import + +//NOTE: libicucore ins't actually needed for the socket to function +//and by commenting this out, we avoid the need to import it into every app. + +//#if TARGET_OS_IPHONE +//#define HAS_ICU +//#endif + +#ifdef HAS_ICU +#import +#endif + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +#import +#import + +#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE +#define sr_dispatch_retain(x) +#define sr_dispatch_release(x) +#define maybe_bridge(x) ((__bridge void *) x) +#else +#define sr_dispatch_retain(x) dispatch_retain(x) +#define sr_dispatch_release(x) dispatch_release(x) +#define maybe_bridge(x) (x) +#endif + +#if !__has_feature(objc_arc) +#error SocketRocket must be compiled with ARC enabled +#endif + + +typedef enum { + SROpCodeTextFrame = 0x1, + SROpCodeBinaryFrame = 0x2, + // 3-7 reserved. + SROpCodeConnectionClose = 0x8, + SROpCodePing = 0x9, + SROpCodePong = 0xA, + // B-F reserved. +} SROpCode; + +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 inline void SRFastLog(NSString *format, ...); + +@interface NSData (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; + +@end + + +@interface NSString (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; + +@end + + +@interface NSURL (SRWebSocket) + +// The origin isn't really applicable for a native application. +// So instead, just map ws -> http and wss -> https. +- (NSString *)SR_origin; + +@end + + +@interface _SRRunLoopThread : NSThread + +@property (nonatomic, readonly) NSRunLoop *runLoop; + +@end + + +static NSString *newSHA1String(const char *bytes, size_t length) { + uint8_t md[CC_SHA1_DIGEST_LENGTH]; + + assert(length >= 0); + assert(length <= UINT32_MAX); + CC_SHA1(bytes, (CC_LONG)length, md); + + NSData *data = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH]; + +#if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0) \ + || (__MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9) + + if (![NSData instancesRespondToSelector:@selector(base64EncodedStringWithOptions:)]) { + return [data base64Encoding]; + } + +#endif + + return [data base64EncodedStringWithOptions:0]; +} + +@implementation NSData (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; +{ + return newSHA1String(self.bytes, self.length); +} + +@end + + +@implementation NSString (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; +{ + return newSHA1String(self.UTF8String, self.length); +} + +@end + +NSString *const SRWebSocketErrorDomain = @"SRWebSocketErrorDomain"; +NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode"; + +// 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; + +@end + +// This class is not thread-safe, and is expected to always be run on the same queue. +@interface SRIOConsumerPool : NSObject + +- (id)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 + +@interface SRWebSocket () + +- (void)_writeData:(NSData *)data; +- (void)_closeWithProtocolError:(NSString *)message; +- (void)_failWithError:(NSError *)error; + +- (void)_disconnect; + +- (void)_readFrameNew; +- (void)_readFrameContinue; + +- (void)_pumpScanner; + +- (void)_pumpWriting; + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; +- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; +- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; +- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; + +- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data; + +- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; +- (void)_SR_commonInit; + +- (void)_initializeStreams; +- (void)_connect; + +@property (nonatomic) SRReadyState readyState; + +@property (nonatomic) NSOperationQueue *delegateOperationQueue; +@property (nonatomic) dispatch_queue_t delegateDispatchQueue; + +@end + + +@implementation SRWebSocket { + NSInteger _webSocketVersion; + + NSOperationQueue *_delegateOperationQueue; + dispatch_queue_t _delegateDispatchQueue; + + dispatch_queue_t _workQueue; + NSMutableArray *_consumers; + + NSInputStream *_inputStream; + NSOutputStream *_outputStream; + + NSMutableData *_readBuffer; + NSUInteger _readBufferOffset; + + NSMutableData *_outputBuffer; + NSUInteger _outputBufferOffset; + + uint8_t _currentFrameOpcode; + size_t _currentFrameCount; + size_t _readOpCount; + uint32_t _currentStringScanPosition; + NSMutableData *_currentFrameData; + + NSString *_closeReason; + + NSString *_secKey; + + BOOL _pinnedCertFound; + + uint8_t _currentReadMaskKey[4]; + size_t _currentReadMaskOffset; + + BOOL _consumerStopped; + + BOOL _closeWhenFinishedWriting; + BOOL _failed; + + BOOL _secure; + NSURLRequest *_urlRequest; + + CFHTTPMessageRef _receivedHTTPHeaders; + + BOOL _sentClose; + BOOL _didFail; + int _closeCode; + + BOOL _isPumping; + + NSMutableSet *_scheduledRunloops; + + // We use this to retain ourselves. + __strong SRWebSocket *_selfRetain; + + NSArray *_requestedProtocols; + SRIOConsumerPool *_consumerPool; +} + +@synthesize delegate = _delegate; +@synthesize url = _url; +@synthesize readyState = _readyState; +@synthesize protocol = _protocol; + +static __strong NSData *CRLFCRLF; + ++ (void)initialize; +{ + CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; +} + +- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; +{ + self = [super init]; + if (self) { + assert(request.URL); + _url = request.URL; + _urlRequest = request; + + _requestedProtocols = [protocols copy]; + + [self _SR_commonInit]; + } + + return self; +} + +- (id)initWithURLRequest:(NSURLRequest *)request; +{ + return [self initWithURLRequest:request protocols:nil]; +} + +- (id)initWithURL:(NSURL *)url; +{ + return [self initWithURL:url protocols:nil]; +} + +- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; +{ + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; + return [self initWithURLRequest:request protocols:protocols]; +} + +- (void)_SR_commonInit; +{ + NSString *scheme = _url.scheme.lowercaseString; + assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]); + + if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) { + _secure = YES; + } + + _readyState = SR_CONNECTING; + _consumerStopped = YES; + _webSocketVersion = 13; + + _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, maybe_bridge(_workQueue), NULL); + + _delegateDispatchQueue = dispatch_get_main_queue(); + sr_dispatch_retain(_delegateDispatchQueue); + + _readBuffer = [[NSMutableData alloc] init]; + _outputBuffer = [[NSMutableData alloc] init]; + + _currentFrameData = [[NSMutableData alloc] init]; + + _consumers = [[NSMutableArray alloc] init]; + + _consumerPool = [[SRIOConsumerPool alloc] init]; + + _scheduledRunloops = [[NSMutableSet alloc] init]; + + [self _initializeStreams]; + + // default handlers +} + +- (void)assertOnWorkQueue; +{ + assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue)); +} + +- (void)dealloc +{ + _inputStream.delegate = nil; + _outputStream.delegate = nil; + + [_inputStream close]; + [_outputStream close]; + + sr_dispatch_release(_workQueue); + _workQueue = NULL; + + if (_receivedHTTPHeaders) { + CFRelease(_receivedHTTPHeaders); + _receivedHTTPHeaders = NULL; + } + + if (_delegateDispatchQueue) { + sr_dispatch_release(_delegateDispatchQueue); + _delegateDispatchQueue = NULL; + } +} + +#ifndef NDEBUG + +- (void)setReadyState:(SRReadyState)aReadyState; +{ + [self willChangeValueForKey:@"readyState"]; + assert(aReadyState > _readyState); + _readyState = aReadyState; + [self didChangeValueForKey:@"readyState"]; +} + +#endif + +- (void)open; +{ + assert(_url); + NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once"); + + _selfRetain = self; + + [self _connect]; +} + +// Calls block on delegate queue +- (void)_performDelegateBlock:(dispatch_block_t)block; +{ + if (_delegateOperationQueue) { + [_delegateOperationQueue addOperationWithBlock:block]; + } else { + assert(_delegateDispatchQueue); + dispatch_async(_delegateDispatchQueue, block); + } +} + +- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue; +{ + if (queue) { + sr_dispatch_retain(queue); + } + + if (_delegateDispatchQueue) { + sr_dispatch_release(_delegateDispatchQueue); + } + + _delegateDispatchQueue = queue; +} + +- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; +{ + NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept"))); + + if (acceptHeader == nil) { + return NO; + } + + NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString]; + NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding]; + + return [acceptHeader isEqualToString:expectedAccept]; +} + +- (void)_HTTPHeadersDidFinish; +{ + NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders); + + if (responseCode >= 400) { + SRFastLog(@"Request failed with response code %d", responseCode); + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2132 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"received bad response code from server %ld", (long)responseCode], SRHTTPResponseErrorKey:@(responseCode)}]]; + return; + } + + if(![self _checkHandshake:_receivedHTTPHeaders]) { + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]]; + return; + } + + NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol"))); + if (negotiatedProtocol) { + // Make sure we requested the protocol + if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) { + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]]; + return; + } + + _protocol = negotiatedProtocol; + } + + self.readyState = SR_OPEN; + + if (!_didFail) { + [self _readFrameNew]; + } + + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) { + [self.delegate webSocketDidOpen:self]; + }; + }]; +} + + +- (void)_readHTTPHeader; +{ + if (_receivedHTTPHeaders == NULL) { + _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO); + } + + [self _readUntilHeaderCompleteWithCallback:^(SRWebSocket *self, NSData *data) { + CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length); + + if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) { + SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders))); + [self _HTTPHeadersDidFinish]; + } else { + [self _readHTTPHeader]; + } + }]; +} + +- (void)didConnect +{ + SRFastLog(@"Connected"); + CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1); + + // Set host first so it defaults + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host)); + + NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16]; + SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes); + +#if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0) \ + || (__MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9) + + if (![NSData instancesRespondToSelector:@selector(base64EncodedStringWithOptions:)]) { + _secKey = [keyBytes base64Encoding]; + } else + +#endif + + { + _secKey = [keyBytes base64EncodedStringWithOptions:0]; + } + + assert([_secKey length] == 24); + + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket")); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade")); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]); + + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin); + + if (_requestedProtocols) { + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]); + } + + [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); + }]; + + NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)); + + CFRelease(request); + + [self _writeData:message]; + [self _readHTTPHeader]; +} + +- (void)_initializeStreams; +{ + assert(_url.port.unsignedIntValue <= UINT32_MAX); + uint32_t port = _url.port.unsignedIntValue; + if (port == 0) { + if (!_secure) { + port = 80; + } else { + port = 443; + } + } + NSString *host = _url.host; + + CFReadStreamRef readStream = NULL; + CFWriteStreamRef writeStream = NULL; + + CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream); + + _outputStream = CFBridgingRelease(writeStream); + _inputStream = CFBridgingRelease(readStream); + + + if (_secure) { + NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init]; + + [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel]; + + // If we're using pinned certs, don't validate the certificate chain + if ([_urlRequest SR_SSLPinnedCertificates].count) { + [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; + } + +#if DEBUG + [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; + NSLog(@"SocketRocket: In debug mode. Allowing connection to any root cert"); +#endif + + [_outputStream setProperty:SSLOptions + forKey:(__bridge id)kCFStreamPropertySSLSettings]; + } + + _inputStream.delegate = self; + _outputStream.delegate = self; +} + +- (void)_connect; +{ + if (!_scheduledRunloops.count) { + [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; + } + + + [_outputStream open]; + [_inputStream open]; +} + +- (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; + + SRFastLog(@"Closing with code %d reason %@", code, reason); + + if (wasConnecting) { + [self _disconnect]; + return; + } + + size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize]; + NSData *payload = mutablePayload; + + ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(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]; + + 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 _performDelegateBlock:^{ + [self closeWithCode:SRStatusCodeProtocolError reason:message]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + }]; +} + +- (void)_failWithError:(NSError *)error; +{ + dispatch_async(_workQueue, ^{ + if (self.readyState != SR_CLOSED) { + _failed = YES; + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) { + [self.delegate webSocket:self didFailWithError:error]; + } + }]; + + self.readyState = SR_CLOSED; + _selfRetain = nil; + + SRFastLog(@"Failing with error %@", error.localizedDescription); + + [self _disconnect]; + } + }); +} + +- (void)_writeData:(NSData *)data; +{ + [self assertOnWorkQueue]; + + if (_closeWhenFinishedWriting) { + return; + } + [_outputBuffer appendData:data]; + [self _pumpWriting]; +} + +- (void)send:(id)data; +{ + NSAssert(self.readyState != SR_CONNECTING, @"Invalid State: Cannot call send: until connection is open"); + // TODO: maybe not copy this for performance + data = [data copy]; + dispatch_async(_workQueue, ^{ + if ([data isKindOfClass:[NSString class]]) { + [self _sendFrameWithOpcode:SROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]]; + } else if ([data isKindOfClass:[NSData class]]) { + [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data]; + } else if (data == nil) { + [self _sendFrameWithOpcode:SROpCodeTextFrame data:data]; + } else { + assert(NO); + } + }); +} + +- (void)sendPing:(NSData *)data; +{ + NSAssert(self.readyState == SR_OPEN, @"Invalid State: Cannot call send: until connection is open"); + // TODO: maybe not copy this for performance + data = [data copy] ?: [NSData data]; // It's okay for a ping to be empty + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:SROpCodePing data:data]; + }); +} + +- (void)handlePing:(NSData *)pingData; +{ + // Need to pingpong this off _callbackQueue first to make sure messages happen in order + [self _performDelegateBlock:^{ + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:SROpCodePong data:pingData]; + }); + }]; +} + +- (void)handlePong:(NSData *)pongData; +{ + SRFastLog(@"Received pong"); + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didReceivePong:)]) { + [self.delegate webSocket:self didReceivePong:pongData]; + } + }]; +} + +- (void)_handleMessage:(id)message +{ + SRFastLog(@"Received message"); + [self _performDelegateBlock:^{ + [self.delegate webSocket:self didReceiveMessage:message]; + }]; +} + + +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; + + SRFastLog(@"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 = EndianU16_BtoN(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 _disconnect]; + }); +} + +- (void)_disconnect; +{ + [self assertOnWorkQueue]; + SRFastLog(@"Trying to disconnect"); + _closeWhenFinishedWriting = YES; + [self _pumpWriting]; +} + +- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode; +{ + // Check that the current data is valid UTF8 + + BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose); + if (!isControlFrame) { + [self _readFrameNew]; + } else { + dispatch_async(_workQueue, ^{ + [self _readFrameContinue]; + }); + } + + switch (opcode) { + case SROpCodeTextFrame: { + NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding]; + if (str == nil && frameData) { + [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + + return; + } + [self _handleMessage:str]; + break; + } + case SROpCodeBinaryFrame: + [self _handleMessage:[frameData copy]]; + break; + case SROpCodeConnectionClose: + [self handleCloseWithData:frameData]; + break; + case SROpCodePing: + [self handlePing: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_OPEN) { + 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 *self, NSData *newData) { + if (isControlFrame) { + [self _handleFrameWithData:newData opCode:frame_header.opcode]; + } else { + if (frame_header.fin) { + [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode]; + } else { + // TODO add assert that opcode is not a control; + [self _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 *self, NSData *data) { + __block frame_header header = {0}; + + const uint8_t *headerBuffer = data.bytes; + assert(data.length >= 2); + + if (headerBuffer[0] & SRRsvMask) { + [self _closeWithProtocolError:@"Server used RSV bits"]; + return; + } + + uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]); + + BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose); + + if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) { + [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"]; + return; + } + + if (receivedOpcode == 0 && self->_currentFrameCount == 0) { + [self _closeWithProtocolError:@"cannot continue a message"]; + return; + } + + header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode; + + header.fin = !!(SRFinMask & headerBuffer[0]); + + + header.masked = !!(SRMaskMask & headerBuffer[1]); + header.payload_length = SRPayloadLenMask & headerBuffer[1]; + + headerBuffer = NULL; + + if (header.masked) { + [self _closeWithProtocolError:@"Client must receive unmasked data"]; + } + + 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) { + [self _handleFrameHeader:header curData:self->_currentFrameData]; + } else { + [self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) { + size_t mapped_size = data.length; + const void *mapped_buffer = data.bytes; + size_t offset = 0; + + if (header.payload_length == 126) { + assert(mapped_size >= sizeof(uint16_t)); + uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer)); + header.payload_length = newLen; + offset += sizeof(uint16_t); + } else if (header.payload_length == 127) { + assert(mapped_size >= sizeof(uint64_t)); + header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer)); + offset += sizeof(uint64_t); + } else { + assert(header.payload_length < 126 && header.payload_length >= 0); + } + + + if (header.masked) { + assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset); + memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey)); + } + + [self _handleFrameHeader:header curData:self->_currentFrameData]; + } readToCurrentFrame:NO unmaskBytes:NO]; + } + } readToCurrentFrame:NO unmaskBytes:NO]; +} + +- (void)_readFrameNew; +{ + dispatch_async(_workQueue, ^{ + [_currentFrameData setLength:0]; + + _currentFrameOpcode = 0; + _currentFrameCount = 0; + _readOpCount = 0; + _currentStringScanPosition = 0; + + [self _readFrameContinue]; + }); +} + +- (void)_pumpWriting; +{ + [self assertOnWorkQueue]; + + NSUInteger dataLength = _outputBuffer.length; + if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) { + NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset]; + if (bytesWritten == -1) { + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]]; + return; + } + + _outputBufferOffset += bytesWritten; + + if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) { + _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset]; + _outputBufferOffset = 0; + } + } + + if (_closeWhenFinishedWriting && + _outputBuffer.length - _outputBufferOffset == 0 && + (_inputStream.streamStatus != NSStreamStatusNotOpen && + _inputStream.streamStatus != NSStreamStatusClosed) && + !_sentClose) { + _sentClose = YES; + + [_outputStream close]; + [_inputStream close]; + + + for (NSArray *runLoop in [_scheduledRunloops copy]) { + [self unscheduleFromRunLoop:[runLoop objectAtIndex:0] forMode:[runLoop objectAtIndex:1]]; + } + + if (!_failed) { + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { + [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES]; + } + }]; + } + + _selfRetain = nil; + } +} + +- (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]; +} + + +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_CLOSING) { + return didWork; + } + + if (!_consumers.count) { + return didWork; + } + + size_t curSize = _readBuffer.length - _readBufferOffset; + if (!curSize) { + return didWork; + } + + SRIOConsumer *consumer = [_consumers objectAtIndex:0]; + + size_t bytesNeeded = consumer.bytesNeeded; + + size_t foundSize = 0; + if (consumer.consumer) { + NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO]; + foundSize = consumer.consumer(tempView); + } else { + assert(consumer.bytesNeeded); + if (curSize >= bytesNeeded) { + foundSize = bytesNeeded; + } else if (consumer.readToCurrentFrame) { + foundSize = curSize; + } + } + + NSData *slice = nil; + if (consumer.readToCurrentFrame || foundSize) { + NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize); + slice = [_readBuffer subdataWithRange:sliceRange]; + + _readBufferOffset += foundSize; + + if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) { + _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset]; _readBufferOffset = 0; + } + + if (consumer.unmaskBytes) { + 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 = mutableSlice; + } + + if (consumer.readToCurrentFrame) { + [_currentFrameData appendData:slice]; + + _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 _disconnect]; + }); + 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, 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:(id)data; +{ + [self assertOnWorkQueue]; + + if (nil == data) { + return; + } + + NSAssert([data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"NSString or NSData"); + + size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length]; + + NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead]; + if (!frame) { + [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"]; + return; + } + uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes]; + + // set fin + frame_buffer[0] = SRFinMask | opcode; + + BOOL useMask = YES; +#ifdef NOMASK + useMask = NO; +#endif + + if (useMask) { + // set the mask and header + frame_buffer[1] |= SRMaskMask; + } + + size_t frame_buffer_size = 2; + + const uint8_t *unmasked_payload = NULL; + if ([data isKindOfClass:[NSData class]]) { + unmasked_payload = (uint8_t *)[data bytes]; + } else if ([data isKindOfClass:[NSString class]]) { + unmasked_payload = (const uint8_t *)[data UTF8String]; + } else { + return; + } + + if (payloadLength < 126) { + frame_buffer[1] |= payloadLength; + } else if (payloadLength <= UINT16_MAX) { + frame_buffer[1] |= 126; + *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength); + frame_buffer_size += sizeof(uint16_t); + } else { + frame_buffer[1] |= 127; + *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength); + frame_buffer_size += sizeof(uint64_t); + } + + if (!useMask) { + for (size_t i = 0; i < payloadLength; i++) { + frame_buffer[frame_buffer_size] = unmasked_payload[i]; + frame_buffer_size += 1; + } + } else { + uint8_t *mask_key = frame_buffer + frame_buffer_size; + SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key); + frame_buffer_size += sizeof(uint32_t); + + // TODO: could probably optimize this with SIMD + for (size_t i = 0; i < payloadLength; i++) { + frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)]; + frame_buffer_size += 1; + } + } + + assert(frame_buffer_size <= [frame length]); + frame.length = frame_buffer_size; + + [self _writeData:frame]; +} + +- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode; +{ + if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) { + + NSArray *sslCerts = [_urlRequest SR_SSLPinnedCertificates]; + if (sslCerts) { + SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust]; + if (secTrust) { + NSInteger numCerts = SecTrustGetCertificateCount(secTrust); + for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) { + SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i); + NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert)); + + for (id ref in sslCerts) { + SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref; + NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert)); + + if ([trustedCertData isEqualToData:certData]) { + _pinnedCertFound = YES; + break; + } + } + } + } + + if (!_pinnedCertFound) { + dispatch_async(_workQueue, ^{ + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:23556 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid server cert"] forKey:NSLocalizedDescriptionKey]]]; + }); + return; + } + } + } + + dispatch_async(_workQueue, ^{ + switch (eventCode) { + case NSStreamEventOpenCompleted: { + SRFastLog(@"NSStreamEventOpenCompleted %@", aStream); + if (self.readyState >= SR_CLOSING) { + return; + } + assert(_readBuffer); + + if (self.readyState == SR_CONNECTING && aStream == _inputStream) { + [self didConnect]; + } + [self _pumpWriting]; + [self _pumpScanner]; + break; + } + + case NSStreamEventErrorOccurred: { + SRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]); + /// TODO specify error better! + [self _failWithError:aStream.streamError]; + _readBufferOffset = 0; + [_readBuffer setLength:0]; + break; + + } + + case NSStreamEventEndEncountered: { + [self _pumpScanner]; + SRFastLog(@"NSStreamEventEndEncountered %@", aStream); + if (aStream.streamError) { + [self _failWithError:aStream.streamError]; + } else { + if (self.readyState != SR_CLOSED) { + self.readyState = SR_CLOSED; + _selfRetain = nil; + } + + 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 _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { + [self.delegate webSocket:self didCloseWithCode:SRStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO]; + } + }]; + } + } + + break; + } + + case NSStreamEventHasBytesAvailable: { + SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream); + const int bufferSize = 2048; + uint8_t buffer[bufferSize]; + + while (_inputStream.hasBytesAvailable) { + NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize]; + + if (bytes_read > 0) { + [_readBuffer appendBytes:buffer length:bytes_read]; + } else if (bytes_read < 0) { + [self _failWithError:_inputStream.streamError]; + } + + if (bytes_read != bufferSize) { + break; + } + }; + [self _pumpScanner]; + break; + } + + case NSStreamEventHasSpaceAvailable: { + SRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream); + [self _pumpWriting]; + break; + } + + default: + SRFastLog(@"(default) %@", aStream); + break; + } + }); +} + +@end + + +@implementation SRIOConsumer + +@synthesize bytesNeeded = _bytesNeeded; +@synthesize consumer = _scanner; +@synthesize handler = _handler; +@synthesize readToCurrentFrame = _readToCurrentFrame; +@synthesize unmaskBytes = _unmaskBytes; + +- (void)setupWithScanner:(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 + + +@implementation SRIOConsumerPool { + NSUInteger _poolSize; + NSMutableArray *_bufferedConsumers; +} + +- (id)initWithBufferCapacity:(NSUInteger)poolSize; +{ + self = [super init]; + if (self) { + _poolSize = poolSize; + _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize]; + } + return self; +} + +- (id)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 setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]; + + return consumer; +} + +- (void)returnConsumer:(SRIOConsumer *)consumer; +{ + if (_bufferedConsumers.count < _poolSize) { + [_bufferedConsumers addObject:consumer]; + } +} + +@end + + +@implementation NSURLRequest (CertificateAdditions) + +- (NSArray *)SR_SSLPinnedCertificates; +{ + return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; +} + +@end + +@implementation NSMutableURLRequest (CertificateAdditions) + +- (NSArray *)SR_SSLPinnedCertificates; +{ + return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; +} + +- (void)setSR_SSLPinnedCertificates:(NSArray *)SR_SSLPinnedCertificates; +{ + [NSURLProtocol setProperty:SR_SSLPinnedCertificates forKey:@"SR_SSLPinnedCertificates" inRequest:self]; +} + +@end + +@implementation NSURL (SRWebSocket) + +- (NSString *)SR_origin; +{ + NSString *scheme = [self.scheme lowercaseString]; + + if ([scheme isEqualToString:@"wss"]) { + scheme = @"https"; + } else if ([scheme isEqualToString:@"ws"]) { + scheme = @"http"; + } + + if (self.port) { + return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port]; + } else { + return [NSString stringWithFormat:@"%@://%@/", scheme, self.host]; + } +} + +@end + +//#define SR_ENABLE_LOG + +static inline void SRFastLog(NSString *format, ...) { +#ifdef SR_ENABLE_LOG + __block va_list arg_list; + va_start (arg_list, format); + + NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; + + va_end(arg_list); + + NSLog(@"[SR] %@", formattedString); +#endif +} + + +#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 data.length - i; + } + } + + return -1; +} + +#endif + +static _SRRunLoopThread *networkThread = nil; +static NSRunLoop *networkRunLoop = nil; + +@implementation NSRunLoop (SRWebSocket) + ++ (NSRunLoop *)SR_networkRunLoop { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + networkThread = [[_SRRunLoopThread alloc] init]; + networkThread.name = @"com.squareup.SocketRocket.NetworkThread"; + [networkThread start]; + networkRunLoop = networkThread.runLoop; + }); + + return networkRunLoop; +} + +@end + + +@implementation _SRRunLoopThread { + dispatch_group_t _waitGroup; +} + +@synthesize runLoop = _runLoop; + +- (void)dealloc +{ + sr_dispatch_release(_waitGroup); +} + +- (id)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); + + NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:nil selector:nil userInfo:nil repeats:NO]; + [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode]; + + while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { + + } + assert(NO); + } +} + +- (NSRunLoop *)runLoop; +{ + dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER); + return _runLoop; +} + +@end From c00f7d54faad605e50f618be98cd6a312d8c3efb Mon Sep 17 00:00:00 2001 From: Harrison Harnisch Date: Mon, 20 Apr 2015 23:10:08 -0700 Subject: [PATCH 3/4] Fix issue when adding RCTWebSocketDebugger and RCTWebSocket to the same project --- .../project.pbxproj | 42 +- .../SRWebSocket.h | 0 .../SRWebSocket.m | 0 .../SRWebSocket.xcodeproj/project.pbxproj | 244 +++ .../RCTWebSocket.xcodeproj/project.pbxproj | 4 +- Libraries/WebSocket/SRWebSocket.h | 132 -- Libraries/WebSocket/SRWebSocket.m | 1777 ----------------- 7 files changed, 286 insertions(+), 1913 deletions(-) rename Libraries/{RCTWebSocketDebugger => SRWebSocket}/SRWebSocket.h (100%) rename Libraries/{RCTWebSocketDebugger => SRWebSocket}/SRWebSocket.m (100%) create mode 100644 Libraries/SRWebSocket/SRWebSocket.xcodeproj/project.pbxproj delete mode 100644 Libraries/WebSocket/SRWebSocket.h delete mode 100644 Libraries/WebSocket/SRWebSocket.m diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj b/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj index acb5daa3e736b2..911918cfb65151 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj @@ -8,9 +8,19 @@ /* Begin PBXBuildFile section */ 00D277161AB8C32C00DC1E48 /* RCTWebSocketExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */; }; - 00D277191AB8C35800DC1E48 /* SRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D277181AB8C35800DC1E48 /* SRWebSocket.m */; }; + 3C8FAB551AE61E3700CA70CA /* libSRWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C8FAB331AE61C8C00CA70CA /* libSRWebSocket.a */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 3C8FAB321AE61C8C00CA70CA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3C8FAB2E1AE61C8C00CA70CA /* SRWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3C8FAB001AE6175200CA70CA; + remoteInfo = SRWebSocket; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 832C817E1AAF6DEF007FA2F7 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -28,6 +38,7 @@ 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketExecutor.m; sourceTree = ""; }; 00D277171AB8C35800DC1E48 /* SRWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRWebSocket.h; sourceTree = ""; }; 00D277181AB8C35800DC1E48 /* SRWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRWebSocket.m; sourceTree = ""; }; + 3C8FAB2E1AE61C8C00CA70CA /* SRWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SRWebSocket.xcodeproj; path = ../SRWebSocket/SRWebSocket.xcodeproj; sourceTree = ""; }; 832C81801AAF6DEF007FA2F7 /* libRCTWebSocketDebugger.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTWebSocketDebugger.a; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -36,15 +47,25 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3C8FAB551AE61E3700CA70CA /* libSRWebSocket.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C8FAB2F1AE61C8C00CA70CA /* Products */ = { + isa = PBXGroup; + children = ( + 3C8FAB331AE61C8C00CA70CA /* libSRWebSocket.a */, + ); + name = Products; + sourceTree = ""; + }; 832C81771AAF6DEF007FA2F7 = { isa = PBXGroup; children = ( + 3C8FAB2E1AE61C8C00CA70CA /* SRWebSocket.xcodeproj */, 00D277171AB8C35800DC1E48 /* SRWebSocket.h */, 00D277181AB8C35800DC1E48 /* SRWebSocket.m */, 00D277141AB8C32C00DC1E48 /* RCTWebSocketExecutor.h */, @@ -107,6 +128,12 @@ mainGroup = 832C81771AAF6DEF007FA2F7; productRefGroup = 832C81811AAF6DEF007FA2F7 /* Products */; projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 3C8FAB2F1AE61C8C00CA70CA /* Products */; + ProjectRef = 3C8FAB2E1AE61C8C00CA70CA /* SRWebSocket.xcodeproj */; + }, + ); projectRoot = ""; targets = ( 832C817F1AAF6DEF007FA2F7 /* RCTWebSocketDebugger */, @@ -114,12 +141,21 @@ }; /* End PBXProject section */ +/* Begin PBXReferenceProxy section */ + 3C8FAB331AE61C8C00CA70CA /* libSRWebSocket.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libSRWebSocket.a; + remoteRef = 3C8FAB321AE61C8C00CA70CA /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + /* Begin PBXSourcesBuildPhase section */ 832C817C1AAF6DEF007FA2F7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 00D277191AB8C35800DC1E48 /* SRWebSocket.m in Sources */, 00D277161AB8C32C00DC1E48 /* RCTWebSocketExecutor.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -218,6 +254,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../SRWebSocket/", ); OTHER_LDFLAGS = ( "-ObjC", @@ -235,6 +272,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../SRWebSocket/", ); OTHER_LDFLAGS = ( "-ObjC", diff --git a/Libraries/RCTWebSocketDebugger/SRWebSocket.h b/Libraries/SRWebSocket/SRWebSocket.h similarity index 100% rename from Libraries/RCTWebSocketDebugger/SRWebSocket.h rename to Libraries/SRWebSocket/SRWebSocket.h diff --git a/Libraries/RCTWebSocketDebugger/SRWebSocket.m b/Libraries/SRWebSocket/SRWebSocket.m similarity index 100% rename from Libraries/RCTWebSocketDebugger/SRWebSocket.m rename to Libraries/SRWebSocket/SRWebSocket.m diff --git a/Libraries/SRWebSocket/SRWebSocket.xcodeproj/project.pbxproj b/Libraries/SRWebSocket/SRWebSocket.xcodeproj/project.pbxproj new file mode 100644 index 00000000000000..598096db8a275d --- /dev/null +++ b/Libraries/SRWebSocket/SRWebSocket.xcodeproj/project.pbxproj @@ -0,0 +1,244 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 3C8FAB1A1AE6179500CA70CA /* SRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C8FAB191AE6179500CA70CA /* SRWebSocket.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 3C8FAAFE1AE6175200CA70CA /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 3C8FAB001AE6175200CA70CA /* libSRWebSocket.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSRWebSocket.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 3C8FAB181AE6179500CA70CA /* SRWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRWebSocket.h; sourceTree = ""; }; + 3C8FAB191AE6179500CA70CA /* SRWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRWebSocket.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3C8FAAFD1AE6175200CA70CA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3C8FAAF51AE6173800CA70CA = { + isa = PBXGroup; + children = ( + 3C8FAB181AE6179500CA70CA /* SRWebSocket.h */, + 3C8FAB191AE6179500CA70CA /* SRWebSocket.m */, + 3C8FAB011AE6175200CA70CA /* Products */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + }; + 3C8FAB011AE6175200CA70CA /* Products */ = { + isa = PBXGroup; + children = ( + 3C8FAB001AE6175200CA70CA /* libSRWebSocket.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3C8FAAFF1AE6175200CA70CA /* SRWebSocket */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3C8FAB121AE6175200CA70CA /* Build configuration list for PBXNativeTarget "SRWebSocket" */; + buildPhases = ( + 3C8FAAFC1AE6175200CA70CA /* Sources */, + 3C8FAAFD1AE6175200CA70CA /* Frameworks */, + 3C8FAAFE1AE6175200CA70CA /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SRWebSocket; + productName = SRWebSocket; + productReference = 3C8FAB001AE6175200CA70CA /* libSRWebSocket.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3C8FAAF61AE6173800CA70CA /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0630; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 3C8FAAFF1AE6175200CA70CA = { + CreatedOnToolsVersion = 6.3; + }; + }; + }; + buildConfigurationList = 3C8FAAF91AE6173800CA70CA /* Build configuration list for PBXProject "SRWebSocket" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 3C8FAAF51AE6173800CA70CA; + productRefGroup = 3C8FAB011AE6175200CA70CA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3C8FAAFF1AE6175200CA70CA /* SRWebSocket */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 3C8FAAFC1AE6175200CA70CA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C8FAB1A1AE6179500CA70CA /* SRWebSocket.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 3C8FAAFA1AE6173800CA70CA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Debug; + }; + 3C8FAAFB1AE6173800CA70CA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Release; + }; + 3C8FAB131AE6175200CA70CA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 3C8FAB141AE6175200CA70CA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.3; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3C8FAAF91AE6173800CA70CA /* Build configuration list for PBXProject "SRWebSocket" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3C8FAAFA1AE6173800CA70CA /* Debug */, + 3C8FAAFB1AE6173800CA70CA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3C8FAB121AE6175200CA70CA /* Build configuration list for PBXNativeTarget "SRWebSocket" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3C8FAB131AE6175200CA70CA /* Debug */, + 3C8FAB141AE6175200CA70CA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3C8FAAF61AE6173800CA70CA /* Project object */; +} diff --git a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj index 4bb77257b1d159..aab59b032b895e 100644 --- a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj +++ b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 3C86DF791ADF646A0047B81A /* WebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF781ADF646A0047B81A /* WebSocket.m */; }; 3C86DF7C1ADF695F0047B81A /* WebSocketFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF7B1ADF695F0047B81A /* WebSocketFactory.m */; }; - 3CC421A41AE0684B00240844 /* SRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CC421A31AE0684B00240844 /* SRWebSocket.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -109,7 +108,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3CC421A41AE0684B00240844 /* SRWebSocket.m in Sources */, 3C86DF7C1ADF695F0047B81A /* WebSocketFactory.m in Sources */, 3C86DF791ADF646A0047B81A /* WebSocket.m in Sources */, ); @@ -203,6 +201,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../SRWebSocket/", ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -217,6 +216,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../SRWebSocket/", ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Libraries/WebSocket/SRWebSocket.h b/Libraries/WebSocket/SRWebSocket.h deleted file mode 100644 index 5cce725a348aab..00000000000000 --- a/Libraries/WebSocket/SRWebSocket.h +++ /dev/null @@ -1,132 +0,0 @@ -// -// Copyright 2012 Square Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import -#import - -typedef enum { - SR_CONNECTING = 0, - SR_OPEN = 1, - SR_CLOSING = 2, - SR_CLOSED = 3, -} SRReadyState; - -typedef enum SRStatusCode : NSInteger { - SRStatusCodeNormal = 1000, - SRStatusCodeGoingAway = 1001, - SRStatusCodeProtocolError = 1002, - SRStatusCodeUnhandledType = 1003, - // 1004 reserved. - SRStatusNoStatusReceived = 1005, - // 1004-1006 reserved. - SRStatusCodeInvalidUTF8 = 1007, - SRStatusCodePolicyViolated = 1008, - SRStatusCodeMessageTooBig = 1009, -} SRStatusCode; - -@class SRWebSocket; - -extern NSString *const SRWebSocketErrorDomain; -extern NSString *const SRHTTPResponseErrorKey; - -#pragma mark - SRWebSocketDelegate - -@protocol SRWebSocketDelegate; - -#pragma mark - SRWebSocket - -@interface SRWebSocket : NSObject - -@property (nonatomic, weak) id delegate; - -@property (nonatomic, readonly) SRReadyState readyState; -@property (nonatomic, readonly, retain) NSURL *url; - -// This returns the negotiated protocol. -// It will be nil until after the handshake completes. -@property (nonatomic, readonly, copy) NSString *protocol; - -// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol. -- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; -- (id)initWithURLRequest:(NSURLRequest *)request; - -// Some helper constructors. -- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; -- (id)initWithURL:(NSURL *)url; - -// Delegate queue will be dispatch_main_queue by default. -// You cannot set both OperationQueue and dispatch_queue. -- (void)setDelegateOperationQueue:(NSOperationQueue*) queue; -- (void)setDelegateDispatchQueue:(dispatch_queue_t) queue; - -// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes. -- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; -- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; - -// SRWebSockets are intended for one-time-use only. Open should be called once and only once. -- (void)open; - -- (void)close; -- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; - -// Send a UTF8 String or Data. -- (void)send:(id)data; - -// Send Data (can be nil) in a ping message. -- (void)sendPing:(NSData *)data; - -@end - -#pragma mark - SRWebSocketDelegate - -@protocol SRWebSocketDelegate - -// message will either be an NSString if the server is using text -// or NSData if the server is using binary. -- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message; - -@optional - -- (void)webSocketDidOpen:(SRWebSocket *)webSocket; -- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error; -- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean; -- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload; - -@end - -#pragma mark - NSURLRequest (CertificateAdditions) - -@interface NSURLRequest (CertificateAdditions) - -@property (nonatomic, retain, readonly) NSArray *SR_SSLPinnedCertificates; - -@end - -#pragma mark - NSMutableURLRequest (CertificateAdditions) - -@interface NSMutableURLRequest (CertificateAdditions) - -@property (nonatomic, retain) NSArray *SR_SSLPinnedCertificates; - -@end - -#pragma mark - NSRunLoop (SRWebSocket) - -@interface NSRunLoop (SRWebSocket) - -+ (NSRunLoop *)SR_networkRunLoop; - -@end diff --git a/Libraries/WebSocket/SRWebSocket.m b/Libraries/WebSocket/SRWebSocket.m deleted file mode 100644 index 3fd675103367ce..00000000000000 --- a/Libraries/WebSocket/SRWebSocket.m +++ /dev/null @@ -1,1777 +0,0 @@ -// -// Copyright 2012 Square Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - - -#import "SRWebSocket.h" - -#import - -//NOTE: libicucore ins't actually needed for the socket to function -//and by commenting this out, we avoid the need to import it into every app. - -//#if TARGET_OS_IPHONE -//#define HAS_ICU -//#endif - -#ifdef HAS_ICU -#import -#endif - -#if TARGET_OS_IPHONE -#import -#else -#import -#endif - -#import -#import - -#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE -#define sr_dispatch_retain(x) -#define sr_dispatch_release(x) -#define maybe_bridge(x) ((__bridge void *) x) -#else -#define sr_dispatch_retain(x) dispatch_retain(x) -#define sr_dispatch_release(x) dispatch_release(x) -#define maybe_bridge(x) (x) -#endif - -#if !__has_feature(objc_arc) -#error SocketRocket must be compiled with ARC enabled -#endif - - -typedef enum { - SROpCodeTextFrame = 0x1, - SROpCodeBinaryFrame = 0x2, - // 3-7 reserved. - SROpCodeConnectionClose = 0x8, - SROpCodePing = 0x9, - SROpCodePong = 0xA, - // B-F reserved. -} SROpCode; - -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 inline void SRFastLog(NSString *format, ...); - -@interface NSData (SRWebSocket) - -- (NSString *)stringBySHA1ThenBase64Encoding; - -@end - - -@interface NSString (SRWebSocket) - -- (NSString *)stringBySHA1ThenBase64Encoding; - -@end - - -@interface NSURL (SRWebSocket) - -// The origin isn't really applicable for a native application. -// So instead, just map ws -> http and wss -> https. -- (NSString *)SR_origin; - -@end - - -@interface _SRRunLoopThread : NSThread - -@property (nonatomic, readonly) NSRunLoop *runLoop; - -@end - - -static NSString *newSHA1String(const char *bytes, size_t length) { - uint8_t md[CC_SHA1_DIGEST_LENGTH]; - - assert(length >= 0); - assert(length <= UINT32_MAX); - CC_SHA1(bytes, (CC_LONG)length, md); - - NSData *data = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH]; - -#if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0) \ - || (__MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9) - - if (![NSData instancesRespondToSelector:@selector(base64EncodedStringWithOptions:)]) { - return [data base64Encoding]; - } - -#endif - - return [data base64EncodedStringWithOptions:0]; -} - -@implementation NSData (SRWebSocket) - -- (NSString *)stringBySHA1ThenBase64Encoding; -{ - return newSHA1String(self.bytes, self.length); -} - -@end - - -@implementation NSString (SRWebSocket) - -- (NSString *)stringBySHA1ThenBase64Encoding; -{ - return newSHA1String(self.UTF8String, self.length); -} - -@end - -NSString *const SRWebSocketErrorDomain = @"SRWebSocketErrorDomain"; -NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode"; - -// 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; - -@end - -// This class is not thread-safe, and is expected to always be run on the same queue. -@interface SRIOConsumerPool : NSObject - -- (id)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 - -@interface SRWebSocket () - -- (void)_writeData:(NSData *)data; -- (void)_closeWithProtocolError:(NSString *)message; -- (void)_failWithError:(NSError *)error; - -- (void)_disconnect; - -- (void)_readFrameNew; -- (void)_readFrameContinue; - -- (void)_pumpScanner; - -- (void)_pumpWriting; - -- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; -- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; -- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; -- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; -- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; - -- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data; - -- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; -- (void)_SR_commonInit; - -- (void)_initializeStreams; -- (void)_connect; - -@property (nonatomic) SRReadyState readyState; - -@property (nonatomic) NSOperationQueue *delegateOperationQueue; -@property (nonatomic) dispatch_queue_t delegateDispatchQueue; - -@end - - -@implementation SRWebSocket { - NSInteger _webSocketVersion; - - NSOperationQueue *_delegateOperationQueue; - dispatch_queue_t _delegateDispatchQueue; - - dispatch_queue_t _workQueue; - NSMutableArray *_consumers; - - NSInputStream *_inputStream; - NSOutputStream *_outputStream; - - NSMutableData *_readBuffer; - NSUInteger _readBufferOffset; - - NSMutableData *_outputBuffer; - NSUInteger _outputBufferOffset; - - uint8_t _currentFrameOpcode; - size_t _currentFrameCount; - size_t _readOpCount; - uint32_t _currentStringScanPosition; - NSMutableData *_currentFrameData; - - NSString *_closeReason; - - NSString *_secKey; - - BOOL _pinnedCertFound; - - uint8_t _currentReadMaskKey[4]; - size_t _currentReadMaskOffset; - - BOOL _consumerStopped; - - BOOL _closeWhenFinishedWriting; - BOOL _failed; - - BOOL _secure; - NSURLRequest *_urlRequest; - - CFHTTPMessageRef _receivedHTTPHeaders; - - BOOL _sentClose; - BOOL _didFail; - int _closeCode; - - BOOL _isPumping; - - NSMutableSet *_scheduledRunloops; - - // We use this to retain ourselves. - __strong SRWebSocket *_selfRetain; - - NSArray *_requestedProtocols; - SRIOConsumerPool *_consumerPool; -} - -@synthesize delegate = _delegate; -@synthesize url = _url; -@synthesize readyState = _readyState; -@synthesize protocol = _protocol; - -static __strong NSData *CRLFCRLF; - -+ (void)initialize; -{ - CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; -} - -- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; -{ - self = [super init]; - if (self) { - assert(request.URL); - _url = request.URL; - _urlRequest = request; - - _requestedProtocols = [protocols copy]; - - [self _SR_commonInit]; - } - - return self; -} - -- (id)initWithURLRequest:(NSURLRequest *)request; -{ - return [self initWithURLRequest:request protocols:nil]; -} - -- (id)initWithURL:(NSURL *)url; -{ - return [self initWithURL:url protocols:nil]; -} - -- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; -{ - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - return [self initWithURLRequest:request protocols:protocols]; -} - -- (void)_SR_commonInit; -{ - NSString *scheme = _url.scheme.lowercaseString; - assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]); - - if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) { - _secure = YES; - } - - _readyState = SR_CONNECTING; - _consumerStopped = YES; - _webSocketVersion = 13; - - _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, maybe_bridge(_workQueue), NULL); - - _delegateDispatchQueue = dispatch_get_main_queue(); - sr_dispatch_retain(_delegateDispatchQueue); - - _readBuffer = [[NSMutableData alloc] init]; - _outputBuffer = [[NSMutableData alloc] init]; - - _currentFrameData = [[NSMutableData alloc] init]; - - _consumers = [[NSMutableArray alloc] init]; - - _consumerPool = [[SRIOConsumerPool alloc] init]; - - _scheduledRunloops = [[NSMutableSet alloc] init]; - - [self _initializeStreams]; - - // default handlers -} - -- (void)assertOnWorkQueue; -{ - assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue)); -} - -- (void)dealloc -{ - _inputStream.delegate = nil; - _outputStream.delegate = nil; - - [_inputStream close]; - [_outputStream close]; - - sr_dispatch_release(_workQueue); - _workQueue = NULL; - - if (_receivedHTTPHeaders) { - CFRelease(_receivedHTTPHeaders); - _receivedHTTPHeaders = NULL; - } - - if (_delegateDispatchQueue) { - sr_dispatch_release(_delegateDispatchQueue); - _delegateDispatchQueue = NULL; - } -} - -#ifndef NDEBUG - -- (void)setReadyState:(SRReadyState)aReadyState; -{ - [self willChangeValueForKey:@"readyState"]; - assert(aReadyState > _readyState); - _readyState = aReadyState; - [self didChangeValueForKey:@"readyState"]; -} - -#endif - -- (void)open; -{ - assert(_url); - NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once"); - - _selfRetain = self; - - [self _connect]; -} - -// Calls block on delegate queue -- (void)_performDelegateBlock:(dispatch_block_t)block; -{ - if (_delegateOperationQueue) { - [_delegateOperationQueue addOperationWithBlock:block]; - } else { - assert(_delegateDispatchQueue); - dispatch_async(_delegateDispatchQueue, block); - } -} - -- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue; -{ - if (queue) { - sr_dispatch_retain(queue); - } - - if (_delegateDispatchQueue) { - sr_dispatch_release(_delegateDispatchQueue); - } - - _delegateDispatchQueue = queue; -} - -- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; -{ - NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept"))); - - if (acceptHeader == nil) { - return NO; - } - - NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString]; - NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding]; - - return [acceptHeader isEqualToString:expectedAccept]; -} - -- (void)_HTTPHeadersDidFinish; -{ - NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders); - - if (responseCode >= 400) { - SRFastLog(@"Request failed with response code %d", responseCode); - [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2132 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"received bad response code from server %ld", (long)responseCode], SRHTTPResponseErrorKey:@(responseCode)}]]; - return; - } - - if(![self _checkHandshake:_receivedHTTPHeaders]) { - [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]]; - return; - } - - NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol"))); - if (negotiatedProtocol) { - // Make sure we requested the protocol - if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) { - [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]]; - return; - } - - _protocol = negotiatedProtocol; - } - - self.readyState = SR_OPEN; - - if (!_didFail) { - [self _readFrameNew]; - } - - [self _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) { - [self.delegate webSocketDidOpen:self]; - }; - }]; -} - - -- (void)_readHTTPHeader; -{ - if (_receivedHTTPHeaders == NULL) { - _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO); - } - - [self _readUntilHeaderCompleteWithCallback:^(SRWebSocket *self, NSData *data) { - CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length); - - if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) { - SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders))); - [self _HTTPHeadersDidFinish]; - } else { - [self _readHTTPHeader]; - } - }]; -} - -- (void)didConnect -{ - SRFastLog(@"Connected"); - CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1); - - // Set host first so it defaults - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host)); - - NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16]; - SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes); - -#if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0) \ - || (__MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9) - - if (![NSData instancesRespondToSelector:@selector(base64EncodedStringWithOptions:)]) { - _secKey = [keyBytes base64Encoding]; - } else - -#endif - - { - _secKey = [keyBytes base64EncodedStringWithOptions:0]; - } - - assert([_secKey length] == 24); - - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket")); - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade")); - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey); - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]); - - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin); - - if (_requestedProtocols) { - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]); - } - - [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); - }]; - - NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)); - - CFRelease(request); - - [self _writeData:message]; - [self _readHTTPHeader]; -} - -- (void)_initializeStreams; -{ - assert(_url.port.unsignedIntValue <= UINT32_MAX); - uint32_t port = _url.port.unsignedIntValue; - if (port == 0) { - if (!_secure) { - port = 80; - } else { - port = 443; - } - } - NSString *host = _url.host; - - CFReadStreamRef readStream = NULL; - CFWriteStreamRef writeStream = NULL; - - CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream); - - _outputStream = CFBridgingRelease(writeStream); - _inputStream = CFBridgingRelease(readStream); - - - if (_secure) { - NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init]; - - [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel]; - - // If we're using pinned certs, don't validate the certificate chain - if ([_urlRequest SR_SSLPinnedCertificates].count) { - [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; - } - -#if DEBUG - [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; - NSLog(@"SocketRocket: In debug mode. Allowing connection to any root cert"); -#endif - - [_outputStream setProperty:SSLOptions - forKey:(__bridge id)kCFStreamPropertySSLSettings]; - } - - _inputStream.delegate = self; - _outputStream.delegate = self; -} - -- (void)_connect; -{ - if (!_scheduledRunloops.count) { - [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; - } - - - [_outputStream open]; - [_inputStream open]; -} - -- (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; - - SRFastLog(@"Closing with code %d reason %@", code, reason); - - if (wasConnecting) { - [self _disconnect]; - return; - } - - size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize]; - NSData *payload = mutablePayload; - - ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(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]; - - 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 _performDelegateBlock:^{ - [self closeWithCode:SRStatusCodeProtocolError reason:message]; - dispatch_async(_workQueue, ^{ - [self _disconnect]; - }); - }]; -} - -- (void)_failWithError:(NSError *)error; -{ - dispatch_async(_workQueue, ^{ - if (self.readyState != SR_CLOSED) { - _failed = YES; - [self _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) { - [self.delegate webSocket:self didFailWithError:error]; - } - }]; - - self.readyState = SR_CLOSED; - _selfRetain = nil; - - SRFastLog(@"Failing with error %@", error.localizedDescription); - - [self _disconnect]; - } - }); -} - -- (void)_writeData:(NSData *)data; -{ - [self assertOnWorkQueue]; - - if (_closeWhenFinishedWriting) { - return; - } - [_outputBuffer appendData:data]; - [self _pumpWriting]; -} - -- (void)send:(id)data; -{ - NSAssert(self.readyState != SR_CONNECTING, @"Invalid State: Cannot call send: until connection is open"); - // TODO: maybe not copy this for performance - data = [data copy]; - dispatch_async(_workQueue, ^{ - if ([data isKindOfClass:[NSString class]]) { - [self _sendFrameWithOpcode:SROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]]; - } else if ([data isKindOfClass:[NSData class]]) { - [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data]; - } else if (data == nil) { - [self _sendFrameWithOpcode:SROpCodeTextFrame data:data]; - } else { - assert(NO); - } - }); -} - -- (void)sendPing:(NSData *)data; -{ - NSAssert(self.readyState == SR_OPEN, @"Invalid State: Cannot call send: until connection is open"); - // TODO: maybe not copy this for performance - data = [data copy] ?: [NSData data]; // It's okay for a ping to be empty - dispatch_async(_workQueue, ^{ - [self _sendFrameWithOpcode:SROpCodePing data:data]; - }); -} - -- (void)handlePing:(NSData *)pingData; -{ - // Need to pingpong this off _callbackQueue first to make sure messages happen in order - [self _performDelegateBlock:^{ - dispatch_async(_workQueue, ^{ - [self _sendFrameWithOpcode:SROpCodePong data:pingData]; - }); - }]; -} - -- (void)handlePong:(NSData *)pongData; -{ - SRFastLog(@"Received pong"); - [self _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocket:didReceivePong:)]) { - [self.delegate webSocket:self didReceivePong:pongData]; - } - }]; -} - -- (void)_handleMessage:(id)message -{ - SRFastLog(@"Received message"); - [self _performDelegateBlock:^{ - [self.delegate webSocket:self didReceiveMessage:message]; - }]; -} - - -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; - - SRFastLog(@"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 = EndianU16_BtoN(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 _disconnect]; - }); -} - -- (void)_disconnect; -{ - [self assertOnWorkQueue]; - SRFastLog(@"Trying to disconnect"); - _closeWhenFinishedWriting = YES; - [self _pumpWriting]; -} - -- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode; -{ - // Check that the current data is valid UTF8 - - BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose); - if (!isControlFrame) { - [self _readFrameNew]; - } else { - dispatch_async(_workQueue, ^{ - [self _readFrameContinue]; - }); - } - - switch (opcode) { - case SROpCodeTextFrame: { - NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding]; - if (str == nil && frameData) { - [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; - dispatch_async(_workQueue, ^{ - [self _disconnect]; - }); - - return; - } - [self _handleMessage:str]; - break; - } - case SROpCodeBinaryFrame: - [self _handleMessage:[frameData copy]]; - break; - case SROpCodeConnectionClose: - [self handleCloseWithData:frameData]; - break; - case SROpCodePing: - [self handlePing: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_OPEN) { - 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 *self, NSData *newData) { - if (isControlFrame) { - [self _handleFrameWithData:newData opCode:frame_header.opcode]; - } else { - if (frame_header.fin) { - [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode]; - } else { - // TODO add assert that opcode is not a control; - [self _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 *self, NSData *data) { - __block frame_header header = {0}; - - const uint8_t *headerBuffer = data.bytes; - assert(data.length >= 2); - - if (headerBuffer[0] & SRRsvMask) { - [self _closeWithProtocolError:@"Server used RSV bits"]; - return; - } - - uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]); - - BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose); - - if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) { - [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"]; - return; - } - - if (receivedOpcode == 0 && self->_currentFrameCount == 0) { - [self _closeWithProtocolError:@"cannot continue a message"]; - return; - } - - header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode; - - header.fin = !!(SRFinMask & headerBuffer[0]); - - - header.masked = !!(SRMaskMask & headerBuffer[1]); - header.payload_length = SRPayloadLenMask & headerBuffer[1]; - - headerBuffer = NULL; - - if (header.masked) { - [self _closeWithProtocolError:@"Client must receive unmasked data"]; - } - - 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) { - [self _handleFrameHeader:header curData:self->_currentFrameData]; - } else { - [self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) { - size_t mapped_size = data.length; - const void *mapped_buffer = data.bytes; - size_t offset = 0; - - if (header.payload_length == 126) { - assert(mapped_size >= sizeof(uint16_t)); - uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer)); - header.payload_length = newLen; - offset += sizeof(uint16_t); - } else if (header.payload_length == 127) { - assert(mapped_size >= sizeof(uint64_t)); - header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer)); - offset += sizeof(uint64_t); - } else { - assert(header.payload_length < 126 && header.payload_length >= 0); - } - - - if (header.masked) { - assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset); - memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey)); - } - - [self _handleFrameHeader:header curData:self->_currentFrameData]; - } readToCurrentFrame:NO unmaskBytes:NO]; - } - } readToCurrentFrame:NO unmaskBytes:NO]; -} - -- (void)_readFrameNew; -{ - dispatch_async(_workQueue, ^{ - [_currentFrameData setLength:0]; - - _currentFrameOpcode = 0; - _currentFrameCount = 0; - _readOpCount = 0; - _currentStringScanPosition = 0; - - [self _readFrameContinue]; - }); -} - -- (void)_pumpWriting; -{ - [self assertOnWorkQueue]; - - NSUInteger dataLength = _outputBuffer.length; - if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) { - NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset]; - if (bytesWritten == -1) { - [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]]; - return; - } - - _outputBufferOffset += bytesWritten; - - if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) { - _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset]; - _outputBufferOffset = 0; - } - } - - if (_closeWhenFinishedWriting && - _outputBuffer.length - _outputBufferOffset == 0 && - (_inputStream.streamStatus != NSStreamStatusNotOpen && - _inputStream.streamStatus != NSStreamStatusClosed) && - !_sentClose) { - _sentClose = YES; - - [_outputStream close]; - [_inputStream close]; - - - for (NSArray *runLoop in [_scheduledRunloops copy]) { - [self unscheduleFromRunLoop:[runLoop objectAtIndex:0] forMode:[runLoop objectAtIndex:1]]; - } - - if (!_failed) { - [self _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { - [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES]; - } - }]; - } - - _selfRetain = nil; - } -} - -- (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]; -} - - -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_CLOSING) { - return didWork; - } - - if (!_consumers.count) { - return didWork; - } - - size_t curSize = _readBuffer.length - _readBufferOffset; - if (!curSize) { - return didWork; - } - - SRIOConsumer *consumer = [_consumers objectAtIndex:0]; - - size_t bytesNeeded = consumer.bytesNeeded; - - size_t foundSize = 0; - if (consumer.consumer) { - NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO]; - foundSize = consumer.consumer(tempView); - } else { - assert(consumer.bytesNeeded); - if (curSize >= bytesNeeded) { - foundSize = bytesNeeded; - } else if (consumer.readToCurrentFrame) { - foundSize = curSize; - } - } - - NSData *slice = nil; - if (consumer.readToCurrentFrame || foundSize) { - NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize); - slice = [_readBuffer subdataWithRange:sliceRange]; - - _readBufferOffset += foundSize; - - if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) { - _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset]; _readBufferOffset = 0; - } - - if (consumer.unmaskBytes) { - 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 = mutableSlice; - } - - if (consumer.readToCurrentFrame) { - [_currentFrameData appendData:slice]; - - _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 _disconnect]; - }); - 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, 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:(id)data; -{ - [self assertOnWorkQueue]; - - if (nil == data) { - return; - } - - NSAssert([data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"NSString or NSData"); - - size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length]; - - NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead]; - if (!frame) { - [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"]; - return; - } - uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes]; - - // set fin - frame_buffer[0] = SRFinMask | opcode; - - BOOL useMask = YES; -#ifdef NOMASK - useMask = NO; -#endif - - if (useMask) { - // set the mask and header - frame_buffer[1] |= SRMaskMask; - } - - size_t frame_buffer_size = 2; - - const uint8_t *unmasked_payload = NULL; - if ([data isKindOfClass:[NSData class]]) { - unmasked_payload = (uint8_t *)[data bytes]; - } else if ([data isKindOfClass:[NSString class]]) { - unmasked_payload = (const uint8_t *)[data UTF8String]; - } else { - return; - } - - if (payloadLength < 126) { - frame_buffer[1] |= payloadLength; - } else if (payloadLength <= UINT16_MAX) { - frame_buffer[1] |= 126; - *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength); - frame_buffer_size += sizeof(uint16_t); - } else { - frame_buffer[1] |= 127; - *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength); - frame_buffer_size += sizeof(uint64_t); - } - - if (!useMask) { - for (size_t i = 0; i < payloadLength; i++) { - frame_buffer[frame_buffer_size] = unmasked_payload[i]; - frame_buffer_size += 1; - } - } else { - uint8_t *mask_key = frame_buffer + frame_buffer_size; - SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key); - frame_buffer_size += sizeof(uint32_t); - - // TODO: could probably optimize this with SIMD - for (size_t i = 0; i < payloadLength; i++) { - frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)]; - frame_buffer_size += 1; - } - } - - assert(frame_buffer_size <= [frame length]); - frame.length = frame_buffer_size; - - [self _writeData:frame]; -} - -- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode; -{ - if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) { - - NSArray *sslCerts = [_urlRequest SR_SSLPinnedCertificates]; - if (sslCerts) { - SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust]; - if (secTrust) { - NSInteger numCerts = SecTrustGetCertificateCount(secTrust); - for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) { - SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i); - NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert)); - - for (id ref in sslCerts) { - SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref; - NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert)); - - if ([trustedCertData isEqualToData:certData]) { - _pinnedCertFound = YES; - break; - } - } - } - } - - if (!_pinnedCertFound) { - dispatch_async(_workQueue, ^{ - [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:23556 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid server cert"] forKey:NSLocalizedDescriptionKey]]]; - }); - return; - } - } - } - - dispatch_async(_workQueue, ^{ - switch (eventCode) { - case NSStreamEventOpenCompleted: { - SRFastLog(@"NSStreamEventOpenCompleted %@", aStream); - if (self.readyState >= SR_CLOSING) { - return; - } - assert(_readBuffer); - - if (self.readyState == SR_CONNECTING && aStream == _inputStream) { - [self didConnect]; - } - [self _pumpWriting]; - [self _pumpScanner]; - break; - } - - case NSStreamEventErrorOccurred: { - SRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]); - /// TODO specify error better! - [self _failWithError:aStream.streamError]; - _readBufferOffset = 0; - [_readBuffer setLength:0]; - break; - - } - - case NSStreamEventEndEncountered: { - [self _pumpScanner]; - SRFastLog(@"NSStreamEventEndEncountered %@", aStream); - if (aStream.streamError) { - [self _failWithError:aStream.streamError]; - } else { - if (self.readyState != SR_CLOSED) { - self.readyState = SR_CLOSED; - _selfRetain = nil; - } - - 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 _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { - [self.delegate webSocket:self didCloseWithCode:SRStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO]; - } - }]; - } - } - - break; - } - - case NSStreamEventHasBytesAvailable: { - SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream); - const int bufferSize = 2048; - uint8_t buffer[bufferSize]; - - while (_inputStream.hasBytesAvailable) { - NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize]; - - if (bytes_read > 0) { - [_readBuffer appendBytes:buffer length:bytes_read]; - } else if (bytes_read < 0) { - [self _failWithError:_inputStream.streamError]; - } - - if (bytes_read != bufferSize) { - break; - } - }; - [self _pumpScanner]; - break; - } - - case NSStreamEventHasSpaceAvailable: { - SRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream); - [self _pumpWriting]; - break; - } - - default: - SRFastLog(@"(default) %@", aStream); - break; - } - }); -} - -@end - - -@implementation SRIOConsumer - -@synthesize bytesNeeded = _bytesNeeded; -@synthesize consumer = _scanner; -@synthesize handler = _handler; -@synthesize readToCurrentFrame = _readToCurrentFrame; -@synthesize unmaskBytes = _unmaskBytes; - -- (void)setupWithScanner:(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 - - -@implementation SRIOConsumerPool { - NSUInteger _poolSize; - NSMutableArray *_bufferedConsumers; -} - -- (id)initWithBufferCapacity:(NSUInteger)poolSize; -{ - self = [super init]; - if (self) { - _poolSize = poolSize; - _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize]; - } - return self; -} - -- (id)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 setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]; - - return consumer; -} - -- (void)returnConsumer:(SRIOConsumer *)consumer; -{ - if (_bufferedConsumers.count < _poolSize) { - [_bufferedConsumers addObject:consumer]; - } -} - -@end - - -@implementation NSURLRequest (CertificateAdditions) - -- (NSArray *)SR_SSLPinnedCertificates; -{ - return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; -} - -@end - -@implementation NSMutableURLRequest (CertificateAdditions) - -- (NSArray *)SR_SSLPinnedCertificates; -{ - return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; -} - -- (void)setSR_SSLPinnedCertificates:(NSArray *)SR_SSLPinnedCertificates; -{ - [NSURLProtocol setProperty:SR_SSLPinnedCertificates forKey:@"SR_SSLPinnedCertificates" inRequest:self]; -} - -@end - -@implementation NSURL (SRWebSocket) - -- (NSString *)SR_origin; -{ - NSString *scheme = [self.scheme lowercaseString]; - - if ([scheme isEqualToString:@"wss"]) { - scheme = @"https"; - } else if ([scheme isEqualToString:@"ws"]) { - scheme = @"http"; - } - - if (self.port) { - return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port]; - } else { - return [NSString stringWithFormat:@"%@://%@/", scheme, self.host]; - } -} - -@end - -//#define SR_ENABLE_LOG - -static inline void SRFastLog(NSString *format, ...) { -#ifdef SR_ENABLE_LOG - __block va_list arg_list; - va_start (arg_list, format); - - NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; - - va_end(arg_list); - - NSLog(@"[SR] %@", formattedString); -#endif -} - - -#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 data.length - i; - } - } - - return -1; -} - -#endif - -static _SRRunLoopThread *networkThread = nil; -static NSRunLoop *networkRunLoop = nil; - -@implementation NSRunLoop (SRWebSocket) - -+ (NSRunLoop *)SR_networkRunLoop { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - networkThread = [[_SRRunLoopThread alloc] init]; - networkThread.name = @"com.squareup.SocketRocket.NetworkThread"; - [networkThread start]; - networkRunLoop = networkThread.runLoop; - }); - - return networkRunLoop; -} - -@end - - -@implementation _SRRunLoopThread { - dispatch_group_t _waitGroup; -} - -@synthesize runLoop = _runLoop; - -- (void)dealloc -{ - sr_dispatch_release(_waitGroup); -} - -- (id)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); - - NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:nil selector:nil userInfo:nil repeats:NO]; - [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode]; - - while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { - - } - assert(NO); - } -} - -- (NSRunLoop *)runLoop; -{ - dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER); - return _runLoop; -} - -@end From 9aa046db655a988a5476651243919c976fa1068e Mon Sep 17 00:00:00 2001 From: Harrison Harnisch Date: Mon, 20 Apr 2015 23:42:19 -0700 Subject: [PATCH 4/4] Break out SRWebSocket into a completely seperate project --- .../SampleApp.xcodeproj/project.pbxproj | 30 +++++++++++++++++++ .../UIExplorer.xcodeproj/project.pbxproj | 30 +++++++++++++++++++ .../project.pbxproj | 6 ---- .../RCTWebSocket.xcodeproj/project.pbxproj | 6 ++-- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj index d2fc3eadedee94..8fbf8c6dfcce6f 100644 --- a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; + 3C8FABC01AE6270300CA70CA /* libSRWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C8FABBF1AE626D100CA70CA /* libSRWebSocket.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; /* End PBXBuildFile section */ @@ -89,6 +90,13 @@ remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = React; }; + 3C8FABBE1AE626D100CA70CA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3C8FABB01AE626D100CA70CA /* SRWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3C8FAB001AE6175200CA70CA; + remoteInfo = SRWebSocket; + }; 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; @@ -125,6 +133,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = iOS/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = iOS/main.m; sourceTree = ""; }; 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../../React/React.xcodeproj; sourceTree = ""; }; + 3C8FABB01AE626D100CA70CA /* SRWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SRWebSocket.xcodeproj; path = "/Users/hharnisch/Documents/ReactNative/react-native/Libraries/Vibration/../SRWebSocket/SRWebSocket.xcodeproj"; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../../Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ @@ -142,6 +151,7 @@ buildActionMask = 2147483647; files = ( 146834051AC3E58100842450 /* libReact.a in Frameworks */, + 3C8FABC01AE6270300CA70CA /* libSRWebSocket.a in Frameworks */, 00481BE81AC0C86700671115 /* libRCTWebSocketDebugger.a in Frameworks */, 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, 00C302E61ABCBA2D00DB3ED1 /* libRCTAdSupport.a in Frameworks */, @@ -252,6 +262,14 @@ name = Products; sourceTree = ""; }; + 3C8FABB11AE626D100CA70CA /* Products */ = { + isa = PBXGroup; + children = ( + 3C8FABBF1AE626D100CA70CA /* libSRWebSocket.a */, + ); + name = Products; + sourceTree = ""; + }; 78C398B11ACF4ADC00677621 /* Products */ = { isa = PBXGroup; children = ( @@ -272,6 +290,7 @@ 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, + 3C8FABB01AE626D100CA70CA /* SRWebSocket.xcodeproj */, 00481BDB1AC0C7FA00671115 /* RCTWebSocketDebugger.xcodeproj */, ); name = Libraries; @@ -411,6 +430,10 @@ ProductGroup = 146834001AC3E56700842450 /* Products */; ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; }, + { + ProductGroup = 3C8FABB11AE626D100CA70CA /* Products */; + ProjectRef = 3C8FABB01AE626D100CA70CA /* SRWebSocket.xcodeproj */; + }, ); projectRoot = ""; targets = ( @@ -477,6 +500,13 @@ remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 3C8FABBF1AE626D100CA70CA /* libSRWebSocket.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libSRWebSocket.a; + remoteRef = 3C8FABBE1AE626D100CA70CA /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 348d04f0d03999..9ac6d5514c5cda 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14AADF041AC3DB95002390C9 /* libReact.a */; }; 14DC67F41AB71881001358AB /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14DC67F11AB71876001358AB /* libRCTPushNotification.a */; }; + 3C8FABF51AE6285B00CA70CA /* libSRWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C8FABE61AE627FC00CA70CA /* libSRWebSocket.a */; }; 58005BF21ABA80A60062E044 /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58005BEE1ABA80530062E044 /* libRCTTest.a */; }; D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */; }; /* End PBXBuildFile section */ @@ -96,6 +97,13 @@ remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = RCTPushNotification; }; + 3C8FABE51AE627FC00CA70CA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3C8FABE11AE627FC00CA70CA /* SRWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3C8FAB001AE6175200CA70CA; + remoteInfo = SRWebSocket; + }; 58005BED1ABA80530062E044 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */; @@ -132,6 +140,7 @@ 14AADEFF1AC3DB95002390C9 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../../React/React.xcodeproj; sourceTree = ""; }; 14DC67E71AB71876001358AB /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = ../../Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj; sourceTree = ""; }; 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; + 3C8FABE11AE627FC00CA70CA /* SRWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SRWebSocket.xcodeproj; path = ../../Libraries/SRWebSocket/SRWebSocket.xcodeproj; sourceTree = ""; }; 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTTest.xcodeproj; path = ../../Libraries/RCTTest/RCTTest.xcodeproj; sourceTree = ""; }; D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ @@ -149,6 +158,7 @@ buildActionMask = 2147483647; files = ( 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */, + 3C8FABF51AE6285B00CA70CA /* libSRWebSocket.a in Frameworks */, 00D2771A1AB8C3E100DC1E48 /* libRCTWebSocketDebugger.a in Frameworks */, 58005BF21ABA80A60062E044 /* libRCTTest.a in Frameworks */, D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */, @@ -202,6 +212,7 @@ 134180261AA91779003F314A /* RCTNetwork.xcodeproj */, 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */, 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */, + 3C8FABE11AE627FC00CA70CA /* SRWebSocket.xcodeproj */, 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */, D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */, ); @@ -285,6 +296,14 @@ name = Products; sourceTree = ""; }; + 3C8FABE21AE627FC00CA70CA /* Products */ = { + isa = PBXGroup; + children = ( + 3C8FABE61AE627FC00CA70CA /* libSRWebSocket.a */, + ); + name = Products; + sourceTree = ""; + }; 58005BE51ABA80530062E044 /* Products */ = { isa = PBXGroup; children = ( @@ -431,6 +450,10 @@ ProductGroup = 14AADF001AC3DB95002390C9 /* Products */; ProjectRef = 14AADEFF1AC3DB95002390C9 /* React.xcodeproj */; }, + { + ProductGroup = 3C8FABE21AE627FC00CA70CA /* Products */; + ProjectRef = 3C8FABE11AE627FC00CA70CA /* SRWebSocket.xcodeproj */; + }, ); projectRoot = ""; targets = ( @@ -504,6 +527,13 @@ remoteRef = 14DC67F01AB71876001358AB /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 3C8FABE61AE627FC00CA70CA /* libSRWebSocket.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libSRWebSocket.a; + remoteRef = 3C8FABE51AE627FC00CA70CA /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 58005BEE1ABA80530062E044 /* libRCTTest.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj b/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj index 911918cfb65151..f9404566192166 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 00D277161AB8C32C00DC1E48 /* RCTWebSocketExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */; }; - 3C8FAB551AE61E3700CA70CA /* libSRWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C8FAB331AE61C8C00CA70CA /* libSRWebSocket.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,8 +35,6 @@ /* Begin PBXFileReference section */ 00D277141AB8C32C00DC1E48 /* RCTWebSocketExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketExecutor.h; sourceTree = ""; }; 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketExecutor.m; sourceTree = ""; }; - 00D277171AB8C35800DC1E48 /* SRWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRWebSocket.h; sourceTree = ""; }; - 00D277181AB8C35800DC1E48 /* SRWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRWebSocket.m; sourceTree = ""; }; 3C8FAB2E1AE61C8C00CA70CA /* SRWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SRWebSocket.xcodeproj; path = ../SRWebSocket/SRWebSocket.xcodeproj; sourceTree = ""; }; 832C81801AAF6DEF007FA2F7 /* libRCTWebSocketDebugger.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTWebSocketDebugger.a; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -47,7 +44,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3C8FAB551AE61E3700CA70CA /* libSRWebSocket.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -66,8 +62,6 @@ isa = PBXGroup; children = ( 3C8FAB2E1AE61C8C00CA70CA /* SRWebSocket.xcodeproj */, - 00D277171AB8C35800DC1E48 /* SRWebSocket.h */, - 00D277181AB8C35800DC1E48 /* SRWebSocket.m */, 00D277141AB8C32C00DC1E48 /* RCTWebSocketExecutor.h */, 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */, 832C81811AAF6DEF007FA2F7 /* Products */, diff --git a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj index aab59b032b895e..03d91039ef0c97 100644 --- a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj +++ b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj @@ -17,8 +17,6 @@ 3C86DF781ADF646A0047B81A /* WebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebSocket.m; sourceTree = ""; }; 3C86DF7A1ADF695F0047B81A /* WebSocketFactory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebSocketFactory.h; sourceTree = ""; }; 3C86DF7B1ADF695F0047B81A /* WebSocketFactory.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = WebSocketFactory.m; sourceTree = ""; tabWidth = 2; }; - 3CC421A21AE0684B00240844 /* SRWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRWebSocket.h; sourceTree = ""; }; - 3CC421A31AE0684B00240844 /* SRWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRWebSocket.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -39,11 +37,11 @@ 3C86DF7B1ADF695F0047B81A /* WebSocketFactory.m */, 3C86DF771ADF646A0047B81A /* WebSocket.h */, 3C86DF781ADF646A0047B81A /* WebSocket.m */, - 3CC421A21AE0684B00240844 /* SRWebSocket.h */, - 3CC421A31AE0684B00240844 /* SRWebSocket.m */, 3C86DF471ADF2C930047B81A /* Products */, ); + indentWidth = 2; sourceTree = ""; + tabWidth = 2; }; 3C86DF471ADF2C930047B81A /* Products */ = { isa = PBXGroup;