diff --git a/android/src/main/java/com/gcrabtree/rctsocketio/SocketIoModule.java b/android/src/main/java/com/gcrabtree/rctsocketio/SocketIoModule.java index ae5ee80..cf902f2 100644 --- a/android/src/main/java/com/gcrabtree/rctsocketio/SocketIoModule.java +++ b/android/src/main/java/com/gcrabtree/rctsocketio/SocketIoModule.java @@ -15,12 +15,14 @@ import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.Callback; import org.json.JSONObject; import java.net.URISyntaxException; import java.util.HashMap; +import io.socket.client.Ack; import io.socket.client.IO; import io.socket.client.Socket; import io.socket.emitter.Emitter; @@ -63,12 +65,18 @@ public void initialize(String connection, ReadableMap options) { * Emit event to server * @param event The name of the event. * @param items The data to pass through the SocketIo engine to the server endpoint. + * @param Ack callback */ @ReactMethod - public void emit(String event, ReadableMap items) { + public void emit(String event, ReadableMap items, final Callback ack) { HashMap map = SocketIoReadableNativeMap.toHashMap((ReadableNativeMap) items); if (mSocket != null) { - mSocket.emit(event, new JSONObject(map)); + mSocket.emit(event, new JSONObject(map), new Ack() { + @Override + public void call(Object... args) { + ack.invoke(SocketIoJSONUtil.objectsFromJSON(args)); + } + }); } else { Log.e(TAG, "Cannot execute emit. mSocket is null. Initialize socket first!!!"); diff --git a/android/src/main/java/com/gcrabtree/rctsocketio/SocketIoReadableNativeMap.java b/android/src/main/java/com/gcrabtree/rctsocketio/SocketIoReadableNativeMap.java index c9a48fc..972016a 100644 --- a/android/src/main/java/com/gcrabtree/rctsocketio/SocketIoReadableNativeMap.java +++ b/android/src/main/java/com/gcrabtree/rctsocketio/SocketIoReadableNativeMap.java @@ -2,6 +2,7 @@ import android.util.Log; +import com.facebook.jni.HybridData; import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableNativeMap; @@ -15,6 +16,11 @@ public class SocketIoReadableNativeMap extends ReadableNativeMap { private static final String TAG = "SIOReadableNativeMap"; + + protected SocketIoReadableNativeMap(HybridData hybridData) { + super(hybridData); + } + /** * Note: This will only be necessary until RN version 0.26 goes live * It will be deprecated from the project, as this is just included in that version of RN. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..13372ae Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..04e285f --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Dec 28 10:00:20 PST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/index.js b/index.js index ff7f5fb..8433ca4 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ class Socket { this.handlers = {}; this.onAnyHandler = null; + if(Platform.OS === "ios") this.sockets.addListener("socketEvent"); this.deviceEventSubscription = DeviceEventEmitter.addListener( 'socketEvent', this._handleEvent.bind(this) ); @@ -63,8 +64,8 @@ class Socket { this.onAnyHandler = handler; } - emit (event, data) { - this.sockets.emit(event, data); + emit (event, data, ack = () => console.log(`ACK ${event}`)) { + this.sockets.emit(event, data, ack); } joinNamespace (namespace) { diff --git a/ios/RNSwiftSocketIO/Socket.swift b/ios/RNSwiftSocketIO/Socket.swift index f13c035..a15dcbb 100644 --- a/ios/RNSwiftSocketIO/Socket.swift +++ b/ios/RNSwiftSocketIO/Socket.swift @@ -9,33 +9,33 @@ import Foundation @objc(SocketIO) -class SocketIO: NSObject { +class SocketIO : RCTEventEmitter { var socket: SocketIOClient! var connectionSocket: NSURL! - var bridge: RCTBridge! /** * Construct and expose RCTBridge to module */ - @objc func initWithBridge(_bridge: RCTBridge) { + @objc(initWithBridge:) + func initWithBridge(_bridge: RCTBridge) { self.bridge = _bridge } /** * Initialize and configure socket */ - - @objc func initialize(connection: String, config: NSDictionary) -> Void { - connectionSocket = NSURL(string: connection); - + + @objc(initialize:config:) + func initialize(connection: NSString, config: NSDictionary) -> Void { + connectionSocket = NSURL(string: String(connection)); // Connect to socket with config self.socket = SocketIOClient( - socketURL: self.connectionSocket, - options:config as? [String : AnyObject] + socketURL: self.connectionSocket!, + config: config as NSDictionary? ) - + // Initialize onAny events self.onAnyEvent() } @@ -44,15 +44,17 @@ class SocketIO: NSObject { * Manually join the namespace */ - @objc func joinNamespace(namespace: String) -> Void { - self.socket.joinNamespace(namespace); + @objc(joinNamespace:) + func joinNamespace(namespace: String) -> Void { + self.socket.joinNamespace(namespace); } /** * Leave namespace back to '/' */ - @objc func leaveNamespace() { + @objc(leaveNamespace) + func leaveNamespace() { self.socket.leaveNamespace(); } @@ -61,11 +63,11 @@ class SocketIO: NSObject { * add NSDictionary of handler events */ - @objc func addHandlers(handlers: NSDictionary) -> Void { + @objc(addHandlers:) + func addHandlers(handlers: NSDictionary) -> Void { for handler in handlers { - self.socket.on(handler.key as! String) { data, ack in - self.bridge.eventDispatcher.sendDeviceEventWithName( - "socketEvent", body: handler.key as! String) + self.socket.on(handler.value as! String) { data, ack in + self.sendEvent(withName: "socketEvent", body: handler.value as! String) } } } @@ -73,9 +75,16 @@ class SocketIO: NSObject { /** * Emit event to server */ - - @objc func emit(event: String, items: AnyObject) -> Void { - self.socket.emit(event, items) + + @objc(emit:items:ack:) + func emit(event: String, items: AnyObject, ack: RCTResponseSenderBlock?) -> Void { + if let ack = ack { + self.socket.emitWithAck(event, items as! SocketData).timingOut(after: 1) { data in + ack(data) + } + } else { + self.socket.emit(event, items as! SocketData) + } } /** @@ -84,11 +93,9 @@ class SocketIO: NSObject { private func onAnyEventHandler (sock: SocketAnyEvent) -> Void { if let items = sock.items { - self.bridge.eventDispatcher.sendDeviceEventWithName("socketEvent", - body: ["name": sock.event, "items": items]) + self.sendEvent(withName: "socketEvent", body: ["name": sock.event, "items": items]) } else { - self.bridge.eventDispatcher.sendDeviceEventWithName("socketEvent", - body: ["name": sock.event]) + self.sendEvent(withName: "socketEvent", body: ["name": sock.event]) } } @@ -97,22 +104,30 @@ class SocketIO: NSObject { * Currently adding handlers to event on the JS layer */ - @objc func onAnyEvent() -> Void { + @objc(onAnyEvent) + func onAnyEvent() -> Void { self.socket.onAny(self.onAnyEventHandler) } // Connect to socket - @objc func connect() -> Void { + @objc(connect) + func connect() -> Void { self.socket.connect() } // Reconnect to socket - @objc func reconnect() -> Void { + @objc(reconnect) + func reconnect() -> Void { self.socket.reconnect() } // Disconnect from socket - @objc func disconnect() -> Void { + @objc(disconnect) + func disconnect() -> Void { self.socket.disconnect() } + + override func supportedEvents() -> [String]! { + return ["socketEvent"] + } } diff --git a/ios/RNSwiftSocketIO/SocketBridge.h b/ios/RNSwiftSocketIO/SocketBridge.h index 22c2a4e..852aa4c 100644 --- a/ios/RNSwiftSocketIO/SocketBridge.h +++ b/ios/RNSwiftSocketIO/SocketBridge.h @@ -9,8 +9,6 @@ #ifndef RNSwiftSocketIO_SocketBridge_h #define RNSwiftSocketIO_SocketBridge_h -#import "RCTBridge.h" -#import "RCTBridgeModule.h" -#import "RCTEventDispatcher.h" +#import "RCTEventEmitter.h" #endif diff --git a/ios/RNSwiftSocketIO/SocketBridge.m b/ios/RNSwiftSocketIO/SocketBridge.m index 8679b09..db0e611 100644 --- a/ios/RNSwiftSocketIO/SocketBridge.m +++ b/ios/RNSwiftSocketIO/SocketBridge.m @@ -6,17 +6,17 @@ // Copyright (c) 2015 Facebook. All rights reserved. // -#import "RCTBridge.h" +#import "RCTEventEmitter.h" -@interface RCT_EXTERN_MODULE(SocketIO, NSObject) +@interface RCT_EXTERN_MODULE(SocketIO, RCTEventEmitter) -RCT_EXTERN_METHOD(initialize:(NSString*)connection config:(NSDictionary*)config) -RCT_EXTERN_METHOD(addHandlers:(NSDictionary*)handlers) -RCT_EXTERN_METHOD(connect) -RCT_EXTERN_METHOD(disconnect) -RCT_EXTERN_METHOD(reconnect) -RCT_EXTERN_METHOD(emit:(NSString*)event items:(id)items) -RCT_EXTERN_METHOD(joinNamespace:(NSString *)namespace) -RCT_EXTERN_METHOD(leaveNamespace) +RCT_EXTERN_METHOD(initialize:(NSString*)connection config:(NSDictionary*)config); +RCT_EXTERN_METHOD(addHandlers:(NSDictionary*)handlers); +RCT_EXTERN_METHOD(connect); +RCT_EXTERN_METHOD(disconnect); +RCT_EXTERN_METHOD(reconnect); +RCT_EXTERN_METHOD(emit:(NSString*)event items:(id)items ack:(RCTResponseSenderBlock)ack); +RCT_EXTERN_METHOD(joinNamespace:(NSString *)namespace); +RCT_EXTERN_METHOD(leaveNamespace); @end diff --git a/ios/RNSwiftSocketIO/SocketIOClient/NSCharacterSet.swift b/ios/RNSwiftSocketIO/SocketIOClient/NSCharacterSet.swift deleted file mode 100644 index 3ccde2c..0000000 --- a/ios/RNSwiftSocketIO/SocketIOClient/NSCharacterSet.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// NSCharacterSet.swift -// Socket.IO-Client-Swift -// -// Created by Yannick Loriot on 5/4/16. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -extension NSCharacterSet { - class var allowedURLCharacterSet: NSCharacterSet { - return NSCharacterSet(charactersInString: "!*'();:@&=+$,/?%#[]\" {}").invertedSet - } -} diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SSLSecurity.swift b/ios/RNSwiftSocketIO/SocketIOClient/SSLSecurity.swift new file mode 100644 index 0000000..4d2c60a --- /dev/null +++ b/ios/RNSwiftSocketIO/SocketIOClient/SSLSecurity.swift @@ -0,0 +1,259 @@ +////////////////////////////////////////////////////////////////////////////////////////////////// +// +// SSLSecurity.swift +// Starscream +// +// Created by Dalton Cherry on 5/16/15. +// Copyright (c) 2014-2016 Dalton Cherry. +// +// 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 Foundation +import Security + +public protocol SSLTrustValidator { + func isValid(_ trust: SecTrust, domain: String?) -> Bool +} + +open class SSLCert : NSObject { + var certData: Data? + var key: SecKey? + + /** + Designated init for certificates + + - parameter data: is the binary data of the certificate + + - returns: a representation security object to be used with + */ + public init(data: Data) { + self.certData = data + } + + /** + Designated init for public keys + + - parameter key: is the public key to be used + + - returns: a representation security object to be used with + */ + public init(key: SecKey) { + self.key = key + } +} + +open class SSLSecurity : SSLTrustValidator { + public var validatedDN = true //should the domain name be validated? + + var isReady = false //is the key processing done? + var certificates: [Data]? //the certificates + @nonobjc var pubKeys: [SecKey]? //the public keys + var usePublicKeys = false //use public keys or certificate validation? + + /** + Use certs from main app bundle + + - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning validation + + - returns: a representation security object to be used with + */ + public convenience init(usePublicKeys: Bool = false) { + let paths = Bundle.main.paths(forResourcesOfType: "cer", inDirectory: ".") + + let certs = paths.reduce([SSLCert]()) { (certs: [SSLCert], path: String) -> [SSLCert] in + var certs = certs + if let data = NSData(contentsOfFile: path) { + certs.append(SSLCert(data: data as Data)) + } + return certs + } + + self.init(certs: certs, usePublicKeys: usePublicKeys) + } + + /** + Designated init + + - parameter certs: is the certificates or public keys to use + - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning validation + + - returns: a representation security object to be used with + */ + public init(certs: [SSLCert], usePublicKeys: Bool) { + self.usePublicKeys = usePublicKeys + + if self.usePublicKeys { + DispatchQueue.global(qos: .default).async { + let pubKeys = certs.reduce([SecKey]()) { (pubKeys: [SecKey], cert: SSLCert) -> [SecKey] in + var pubKeys = pubKeys + if let data = cert.certData, cert.key == nil { + cert.key = self.extractPublicKey(data) + } + if let key = cert.key { + pubKeys.append(key) + } + return pubKeys + } + + self.pubKeys = pubKeys + self.isReady = true + } + } else { + let certificates = certs.reduce([Data]()) { (certificates: [Data], cert: SSLCert) -> [Data] in + var certificates = certificates + if let data = cert.certData { + certificates.append(data) + } + return certificates + } + self.certificates = certificates + self.isReady = true + } + } + + /** + Valid the trust and domain name. + + - parameter trust: is the serverTrust to validate + - parameter domain: is the CN domain to validate + + - returns: if the key was successfully validated + */ + public func isValid(_ trust: SecTrust, domain: String?) -> Bool { + + var tries = 0 + while !self.isReady { + usleep(1000) + tries += 1 + if tries > 5 { + return false //doesn't appear it is going to ever be ready... + } + } + var policy: SecPolicy + if self.validatedDN { + policy = SecPolicyCreateSSL(true, domain as NSString?) + } else { + policy = SecPolicyCreateBasicX509() + } + SecTrustSetPolicies(trust,policy) + if self.usePublicKeys { + if let keys = self.pubKeys { + let serverPubKeys = publicKeyChain(trust) + for serverKey in serverPubKeys as [AnyObject] { + for key in keys as [AnyObject] { + if serverKey.isEqual(key) { + return true + } + } + } + } + } else if let certs = self.certificates { + let serverCerts = certificateChain(trust) + var collect = [SecCertificate]() + for cert in certs { + collect.append(SecCertificateCreateWithData(nil,cert as CFData)!) + } + SecTrustSetAnchorCertificates(trust,collect as NSArray) + var result: SecTrustResultType = .unspecified + SecTrustEvaluate(trust,&result) + if result == .unspecified || result == .proceed { + var trustedCount = 0 + for serverCert in serverCerts { + for cert in certs { + if cert == serverCert { + trustedCount += 1 + break + } + } + } + if trustedCount == serverCerts.count { + return true + } + } + } + return false + } + + /** + Get the public key from a certificate data + + - parameter data: is the certificate to pull the public key from + + - returns: a public key + */ + func extractPublicKey(_ data: Data) -> SecKey? { + guard let cert = SecCertificateCreateWithData(nil, data as CFData) else { return nil } + + return extractPublicKey(cert, policy: SecPolicyCreateBasicX509()) + } + + /** + Get the public key from a certificate + + - parameter data: is the certificate to pull the public key from + + - returns: a public key + */ + func extractPublicKey(_ cert: SecCertificate, policy: SecPolicy) -> SecKey? { + var possibleTrust: SecTrust? + SecTrustCreateWithCertificates(cert, policy, &possibleTrust) + + guard let trust = possibleTrust else { return nil } + var result: SecTrustResultType = .unspecified + SecTrustEvaluate(trust, &result) + return SecTrustCopyPublicKey(trust) + } + + /** + Get the certificate chain for the trust + + - parameter trust: is the trust to lookup the certificate chain for + + - returns: the certificate chain for the trust + */ + func certificateChain(_ trust: SecTrust) -> [Data] { + let certificates = (0.. [Data] in + var certificates = certificates + let cert = SecTrustGetCertificateAtIndex(trust, index) + certificates.append(SecCertificateCopyData(cert!) as Data) + return certificates + } + + return certificates + } + + /** + Get the public key chain for the trust + + - parameter trust: is the trust to lookup the certificate chain and extract the public keys + + - returns: the public keys from the certifcate chain for the trust + */ + @nonobjc func publicKeyChain(_ trust: SecTrust) -> [SecKey] { + let policy = SecPolicyCreateBasicX509() + let keys = (0.. [SecKey] in + var keys = keys + let cert = SecTrustGetCertificateAtIndex(trust, index) + if let key = extractPublicKey(cert!, policy: policy) { + keys.append(key) + } + + return keys + } + + return keys + } + + +} diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketAckEmitter.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketAckEmitter.swift index edb2522..4af43e7 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketAckEmitter.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketAckEmitter.swift @@ -27,21 +27,61 @@ import Foundation public final class SocketAckEmitter : NSObject { let socket: SocketIOClient let ackNum: Int + + public var expected: Bool { + return ackNum != -1 + } init(socket: SocketIOClient, ackNum: Int) { self.socket = socket self.ackNum = ackNum } - public func with(items: AnyObject...) { + public func with(_ items: SocketData...) { guard ackNum != -1 else { return } - socket.emitAck(ackNum, withItems: items) + socket.emitAck(ackNum, with: items) } - public func with(items: [AnyObject]) { + public func with(_ items: [Any]) { guard ackNum != -1 else { return } - socket.emitAck(ackNum, withItems: items) + socket.emitAck(ackNum, with: items) + } + +} + +public final class OnAckCallback : NSObject { + private let ackNumber: Int + private let items: [Any] + private weak var socket: SocketIOClient? + + init(ackNumber: Int, items: [Any], socket: SocketIOClient) { + self.ackNumber = ackNumber + self.items = items + self.socket = socket + } + + deinit { + DefaultSocketLogger.Logger.log("OnAckCallback for \(ackNumber) being released", type: "OnAckCallback") + } + + public func timingOut(after seconds: Int, callback: @escaping AckCallback) { + guard let socket = self.socket else { return } + + socket.ackQueue.sync() { + socket.ackHandlers.addAck(ackNumber, callback: callback) + } + + socket._emit(items, ack: ackNumber) + + guard seconds != 0 else { return } + + let time = DispatchTime.now() + Double(UInt64(seconds) * NSEC_PER_SEC) / Double(NSEC_PER_SEC) + + socket.handleQueue.asyncAfter(deadline: time) { + socket.ackHandlers.timeoutAck(self.ackNumber, onQueue: socket.handleQueue) + } } + } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketAckManager.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketAckManager.swift index 972e1da..eea183b 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketAckManager.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketAckManager.swift @@ -24,7 +24,7 @@ import Foundation -private struct SocketAck : Hashable, Equatable { +private struct SocketAck : Hashable { let ack: Int var callback: AckCallback! var hashValue: Int { @@ -35,7 +35,7 @@ private struct SocketAck : Hashable, Equatable { self.ack = ack } - init(ack: Int, callback: AckCallback) { + init(ack: Int, callback: @escaping AckCallback) { self.ack = ack self.callback = callback } @@ -51,24 +51,29 @@ private func ==(lhs: SocketAck, rhs: SocketAck) -> Bool { struct SocketAckManager { private var acks = Set(minimumCapacity: 1) + private let ackSemaphore = DispatchSemaphore(value: 1) - mutating func addAck(ack: Int, callback: AckCallback) { + mutating func addAck(_ ack: Int, callback: @escaping AckCallback) { acks.insert(SocketAck(ack: ack, callback: callback)) } - mutating func executeAck(ack: Int, items: [AnyObject]) { - let callback = acks.remove(SocketAck(ack: ack)) - - dispatch_async(dispatch_get_main_queue()) { - callback?.callback(items) - } + /// Should be called on handle queue + mutating func executeAck(_ ack: Int, with items: [Any], onQueue: DispatchQueue) { + ackSemaphore.wait() + defer { ackSemaphore.signal() } + let ack = acks.remove(SocketAck(ack: ack)) + + onQueue.async() { ack?.callback(items) } } - mutating func timeoutAck(ack: Int) { - let callback = acks.remove(SocketAck(ack: ack)) + /// Should be called on handle queue + mutating func timeoutAck(_ ack: Int, onQueue: DispatchQueue) { + ackSemaphore.wait() + defer { ackSemaphore.signal() } + let ack = acks.remove(SocketAck(ack: ack)) - dispatch_async(dispatch_get_main_queue()) { - callback?.callback(["NO ACK"]) + onQueue.async() { + ack?.callback?(["NO ACK"]) } } } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketAnyEvent.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketAnyEvent.swift index 4c26f0e..3647e96 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketAnyEvent.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketAnyEvent.swift @@ -26,12 +26,12 @@ import Foundation public final class SocketAnyEvent : NSObject { public let event: String - public let items: NSArray? + public let items: [Any]? override public var description: String { - return "SocketAnyEvent: Event: \(event) items: \(items ?? nil)" + return "SocketAnyEvent: Event: \(event) items: \(String(describing: items))" } - init(event: String, items: NSArray?) { + init(event: String, items: [Any]?) { self.event = event self.items = items } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketClientManager.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketClientManager.swift new file mode 100644 index 0000000..e230272 --- /dev/null +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketClientManager.swift @@ -0,0 +1,82 @@ +// +// SocketClientManager.swift +// Socket.IO-Client-Swift +// +// Created by Erik Little on 6/11/16. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/** + Experimental socket manager. + + API subject to change. + + Can be used to persist sockets across ViewControllers. + + Sockets are strongly stored, so be sure to remove them once they are no + longer needed. + + Example usage: + ``` + let manager = SocketClientManager.sharedManager + manager["room1"] = socket1 + manager["room2"] = socket2 + manager.removeSocket(socket: socket2) + manager["room1"]?.emit("hello") + ``` + */ +open class SocketClientManager : NSObject { + open static let sharedManager = SocketClientManager() + + private var sockets = [String: SocketIOClient]() + + open subscript(string: String) -> SocketIOClient? { + get { + return sockets[string] + } + + set(socket) { + sockets[string] = socket + } + } + + open func addSocket(_ socket: SocketIOClient, labeledAs label: String) { + sockets[label] = socket + } + + open func removeSocket(withLabel label: String) -> SocketIOClient? { + return sockets.removeValue(forKey: label) + } + + open func removeSocket(_ socket: SocketIOClient) -> SocketIOClient? { + var returnSocket: SocketIOClient? + + for (label, dictSocket) in sockets where dictSocket === socket { + returnSocket = sockets.removeValue(forKey: label) + } + + return returnSocket + } + + open func removeSockets() { + sockets.removeAll() + } +} diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketEngine.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketEngine.swift index e06a811..7fa489e 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketEngine.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketEngine.swift @@ -24,24 +24,24 @@ import Foundation -public final class SocketEngine : NSObject, SocketEnginePollable, SocketEngineWebsocket { - public let emitQueue = dispatch_queue_create("com.socketio.engineEmitQueue", DISPATCH_QUEUE_SERIAL) - public let handleQueue = dispatch_queue_create("com.socketio.engineHandleQueue", DISPATCH_QUEUE_SERIAL) - public let parseQueue = dispatch_queue_create("com.socketio.engineParseQueue", DISPATCH_QUEUE_SERIAL) +public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, SocketEngineWebsocket { + public let emitQueue = DispatchQueue(label: "com.socketio.engineEmitQueue", attributes: []) + public let handleQueue = DispatchQueue(label: "com.socketio.engineHandleQueue", attributes: []) + public let parseQueue = DispatchQueue(label: "com.socketio.engineParseQueue", attributes: []) - public var connectParams: [String: AnyObject]? { + public var connectParams: [String: Any]? { didSet { (urlPolling, urlWebSocket) = createURLs() } } - + public var postWait = [String]() public var waitingForPoll = false public var waitingForPost = false - + public private(set) var closed = false public private(set) var connected = false - public private(set) var cookies: [NSHTTPCookie]? + public private(set) var cookies: [HTTPCookie]? public private(set) var doubleEncodeUTF8 = true public private(set) var extraHeaders: [String: String]? public private(set) var fastUpgrade = false @@ -50,166 +50,170 @@ public final class SocketEngine : NSObject, SocketEnginePollable, SocketEngineWe public private(set) var invalidated = false public private(set) var polling = true public private(set) var probing = false - public private(set) var session: NSURLSession? + public private(set) var session: URLSession? public private(set) var sid = "" public private(set) var socketPath = "/engine.io/" - public private(set) var urlPolling = NSURL() - public private(set) var urlWebSocket = NSURL() + public private(set) var urlPolling = URL(string: "http://localhost/")! + public private(set) var urlWebSocket = URL(string: "http://localhost/")! public private(set) var websocket = false public private(set) var ws: WebSocket? public weak var client: SocketEngineClient? - - private weak var sessionDelegate: NSURLSessionDelegate? - private typealias Probe = (msg: String, type: SocketEnginePacketType, data: [NSData]) - private typealias ProbeWaitQueue = [Probe] + private weak var sessionDelegate: URLSessionDelegate? private let logType = "SocketEngine" - private let url: NSURL - + private let url: URL + private var pingInterval: Double? private var pingTimeout = 0.0 { didSet { pongsMissedMax = Int(pingTimeout / (pingInterval ?? 25)) } } + private var pongsMissed = 0 private var pongsMissedMax = 0 private var probeWait = ProbeWaitQueue() private var secure = false + private var security: SSLSecurity? private var selfSigned = false private var voipEnabled = false - public init(client: SocketEngineClient, url: NSURL, options: Set) { + public init(client: SocketEngineClient, url: URL, config: SocketIOClientConfiguration) { self.client = client self.url = url - - for option in options { + for option in config { switch option { - case let .ConnectParams(params): + case let .connectParams(params): connectParams = params - case let .Cookies(cookies): + case let .cookies(cookies): self.cookies = cookies - case let .DoubleEncodeUTF8(encode): + case let .doubleEncodeUTF8(encode): doubleEncodeUTF8 = encode - case let .ExtraHeaders(headers): + case let .extraHeaders(headers): extraHeaders = headers - case let .SessionDelegate(delegate): + case let .sessionDelegate(delegate): sessionDelegate = delegate - case let .ForcePolling(force): + case let .forcePolling(force): forcePolling = force - case let .ForceWebsockets(force): + case let .forceWebsockets(force): forceWebsockets = force - case let .Path(path): + case let .path(path): socketPath = path - case let .VoipEnabled(enable): + + if !socketPath.hasSuffix("/") { + socketPath += "/" + } + case let .voipEnabled(enable): voipEnabled = enable - case let .Secure(secure): + case let .secure(secure): self.secure = secure - case let .SelfSigned(selfSigned): + case let .selfSigned(selfSigned): self.selfSigned = selfSigned + case let .security(security): + self.security = security default: continue } } - + super.init() + sessionDelegate = sessionDelegate ?? self + (urlPolling, urlWebSocket) = createURLs() } - public convenience init(client: SocketEngineClient, url: NSURL, options: NSDictionary?) { - self.init(client: client, url: url, options: options?.toSocketOptionsSet() ?? []) + public convenience init(client: SocketEngineClient, url: URL, options: NSDictionary?) { + self.init(client: client, url: url, config: options?.toSocketConfiguration() ?? []) } - + deinit { DefaultSocketLogger.Logger.log("Engine is being released", type: logType) closed = true stopPolling() } - - private func checkAndHandleEngineError(msg: String) { - guard let stringData = msg.dataUsingEncoding(NSUTF8StringEncoding, - allowLossyConversion: false) else { return } - + + private func checkAndHandleEngineError(_ msg: String) { do { - if let dict = try NSJSONSerialization.JSONObjectWithData(stringData, options: .MutableContainers) as? NSDictionary { - guard let error = dict["message"] as? String else { return } - - /* - 0: Unknown transport - 1: Unknown sid - 2: Bad handshake request - 3: Bad request - */ - didError(error) - } + let dict = try msg.toNSDictionary() + guard let error = dict["message"] as? String else { return } + + /* + 0: Unknown transport + 1: Unknown sid + 2: Bad handshake request + 3: Bad request + */ + didError(reason: error) } catch { - didError("Got unknown error from server \(msg)") + client?.engineDidError(reason: "Got unknown error from server \(msg)") } } - private func checkIfMessageIsBase64Binary(message: String) -> Bool { - if message.hasPrefix("b4") { - // binary in base64 string - let noPrefix = message[message.startIndex.advancedBy(2).. (NSURL, NSURL) { + private func createURLs() -> (URL, URL) { if client == nil { - return (NSURL(), NSURL()) + return (URL(string: "http://localhost/")!, URL(string: "http://localhost/")!) } - let urlPolling = NSURLComponents(string: url.absoluteString)! - let urlWebSocket = NSURLComponents(string: url.absoluteString)! + var urlPolling = URLComponents(string: url.absoluteString)! + var urlWebSocket = URLComponents(string: url.absoluteString)! var queryString = "" urlWebSocket.path = socketPath @@ -234,15 +238,15 @@ public final class SocketEngine : NSObject, SocketEnginePollable, SocketEngineWe urlWebSocket.percentEncodedQuery = "transport=websocket" + queryString urlPolling.percentEncodedQuery = "transport=polling&b64=1" + queryString - - return (urlPolling.URL!, urlWebSocket.URL!) + + return (urlPolling.url!, urlWebSocket.url!) } private func createWebsocketAndConnect() { - ws = WebSocket(url: urlWebSocketWithSid) - + ws = WebSocket(url: urlWebSocketWithSid as URL) + if cookies != nil { - let headers = NSHTTPCookie.requestHeaderFieldsWithCookies(cookies!) + let headers = HTTPCookie.requestHeaderFields(with: cookies!) for (key, value) in headers { ws?.headers[key] = value } @@ -254,50 +258,46 @@ public final class SocketEngine : NSObject, SocketEnginePollable, SocketEngineWe } } - ws?.queue = handleQueue + ws?.callbackQueue = handleQueue ws?.voipEnabled = voipEnabled ws?.delegate = self - ws?.selfSignedSSL = selfSigned + ws?.disableSSLCertValidation = selfSigned + ws?.security = security ws?.connect() } - - public func didError(error: String) { - DefaultSocketLogger.Logger.error(error, type: logType) - client?.engineDidError(error) - disconnect(error) + + public func didError(reason: String) { + DefaultSocketLogger.Logger.error("%@", type: logType, args: reason) + client?.engineDidError(reason: reason) + disconnect(reason: reason) } - + public func disconnect(reason: String) { - func postSendClose(data: NSData?, _ res: NSURLResponse?, _ err: NSError?) { - sid = "" - closed = true - invalidated = true - connected = false - - ws?.disconnect() - stopPolling() - client?.engineDidClose(reason) - } - + guard connected else { return closeOutEngine(reason: reason) } + DefaultSocketLogger.Logger.log("Engine is being closed.", type: logType) - + if closed { - client?.engineDidClose(reason) - return + return closeOutEngine(reason: reason) } - + if websocket { - sendWebSocketMessage("", withType: .Close, withData: []) - postSendClose(nil, nil, nil) + sendWebSocketMessage("", withType: .close, withData: []) + closeOutEngine(reason: reason) } else { - // We need to take special care when we're polling that we send it ASAP - // Also make sure we're on the emitQueue since we're touching postWait - dispatch_sync(emitQueue) { - self.postWait.append(String(SocketEnginePacketType.Close.rawValue)) - let req = self.createRequestForPostWithPostWait() - self.doRequest(req, withCallback: postSendClose) - } + disconnectPolling(reason: reason) + } + } + + // We need to take special care when we're polling that we send it ASAP + // Also make sure we're on the emitQueue since we're touching postWait + private func disconnectPolling(reason: String) { + emitQueue.sync { + self.postWait.append(String(SocketEnginePacketType.close.rawValue)) + let req = self.createRequestForPostWithPostWait() + self.doRequest(for: req) {_, _, _ in } + self.closeOutEngine(reason: reason) } } @@ -307,7 +307,7 @@ public final class SocketEngine : NSObject, SocketEnginePollable, SocketEngineWe "we'll probably disconnect soon. You should report this.", type: logType) } - sendWebSocketMessage("", withType: .Upgrade, withData: []) + sendWebSocketMessage("", withType: .upgrade, withData: []) websocket = true polling = false fastUpgrade = false @@ -318,132 +318,139 @@ public final class SocketEngine : NSObject, SocketEnginePollable, SocketEngineWe private func flushProbeWait() { DefaultSocketLogger.Logger.log("Flushing probe wait", type: logType) - dispatch_async(emitQueue) { + emitQueue.async { for waiter in self.probeWait { self.write(waiter.msg, withType: waiter.type, withData: waiter.data) } - - self.probeWait.removeAll(keepCapacity: false) - + + self.probeWait.removeAll(keepingCapacity: false) + if self.postWait.count != 0 { self.flushWaitingForPostToWebSocket() } } } - + // We had packets waiting for send when we upgraded // Send them raw public func flushWaitingForPostToWebSocket() { guard let ws = self.ws else { return } - + for msg in postWait { - ws.writeString(fixDoubleUTF8(msg)) + ws.write(string: msg) } - - postWait.removeAll(keepCapacity: true) + + postWait.removeAll(keepingCapacity: false) } - private func handleClose(reason: String) { - client?.engineDidClose(reason) + private func handleClose(_ reason: String) { + client?.engineDidClose(reason: reason) } - private func handleMessage(message: String) { + private func handleMessage(_ message: String) { client?.parseEngineMessage(message) } private func handleNOOP() { doPoll() } - + private func handleOpen(openData: String) { - let mesData = openData.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - do { - let json = try NSJSONSerialization.JSONObjectWithData(mesData, - options: NSJSONReadingOptions.AllowFragments) as? NSDictionary - if let sid = json?["sid"] as? String { - let upgradeWs: Bool - - self.sid = sid - connected = true - - if let upgrades = json?["upgrades"] as? [String] { - upgradeWs = upgrades.contains("websocket") - } else { - upgradeWs = false - } - - if let pingInterval = json?["pingInterval"] as? Double, pingTimeout = json?["pingTimeout"] as? Double { - self.pingInterval = pingInterval / 1000.0 - self.pingTimeout = pingTimeout / 1000.0 - } - - if !forcePolling && !forceWebsockets && upgradeWs { - createWebsocketAndConnect() - } - - sendPing() - - if !forceWebsockets { - doPoll() - } - - client?.engineDidOpen?("Connect") - } - } catch { - didError("Error parsing open packet") + guard let json = try? openData.toNSDictionary() else { + didError(reason: "Error parsing open packet") + + return } + + guard let sid = json["sid"] as? String else { + didError(reason: "Open packet contained no sid") + + return + } + + let upgradeWs: Bool + + self.sid = sid + connected = true + pongsMissed = 0 + + if let upgrades = json["upgrades"] as? [String] { + upgradeWs = upgrades.contains("websocket") + } else { + upgradeWs = false + } + + if let pingInterval = json["pingInterval"] as? Double, let pingTimeout = json["pingTimeout"] as? Double { + self.pingInterval = pingInterval / 1000.0 + self.pingTimeout = pingTimeout / 1000.0 + } + + if !forcePolling && !forceWebsockets && upgradeWs { + createWebsocketAndConnect() + } + + sendPing() + + if !forceWebsockets { + doPoll() + } + + client?.engineDidOpen(reason: "Connect") } - private func handlePong(pongMessage: String) { + private func handlePong(with message: String) { pongsMissed = 0 // We should upgrade - if pongMessage == "3probe" { + if message == "3probe" { upgradeTransport() } } - - public func parseEngineData(data: NSData) { + + public func parseEngineData(_ data: Data) { DefaultSocketLogger.Logger.log("Got binary data: %@", type: "SocketEngine", args: data) - client?.parseEngineBinaryData(data.subdataWithRange(NSMakeRange(1, data.length - 1))) + + client?.parseEngineBinaryData(data.subdata(in: 1.. pongsMissedMax { - client?.engineDidClose("Ping timeout") + client?.engineDidClose(reason: "Ping timeout") + return } + + guard let pingInterval = pingInterval else { return } - if let pingInterval = pingInterval { - pongsMissed += 1 - write("", withType: .Ping, withData: []) - - let time = dispatch_time(DISPATCH_TIME_NOW, Int64(pingInterval * Double(NSEC_PER_SEC))) - dispatch_after(time, dispatch_get_main_queue()) {[weak self] in - self?.sendPing() - } - } + pongsMissed += 1 + write("", withType: .ping, withData: []) + + let time = DispatchTime.now() + Double(Int64(pingInterval * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) + DispatchQueue.main.asyncAfter(deadline: time) {[weak self] in self?.sendPing() } } - + // Moves from long-polling to websockets private func upgradeTransport() { if ws?.isConnected ?? false { DefaultSocketLogger.Logger.log("Upgrading transport to WebSockets", type: logType) fastUpgrade = true - sendPollMessage("", withType: .Noop, withData: []) + sendPollMessage("", withType: .noop, withData: []) // After this point, we should not send anymore polling messages } } /// Write a message, independent of transport. - public func write(msg: String, withType type: SocketEnginePacketType, withData data: [NSData]) { - dispatch_async(emitQueue) { + public func write(_ msg: String, withType type: SocketEnginePacketType, withData data: [Data]) { + emitQueue.async { guard self.connected else { return } - + if self.websocket { DefaultSocketLogger.Logger.log("Writing ws: %@ has data: %@", type: self.logType, args: msg, data.count != 0) @@ -512,7 +514,7 @@ public final class SocketEngine : NSObject, SocketEnginePollable, SocketEngineWe } } } - + // Delegate methods public func websocketDidConnect(socket: WebSocket) { if !forceWebsockets { @@ -524,28 +526,35 @@ public final class SocketEngine : NSObject, SocketEnginePollable, SocketEngineWe polling = false } } - + public func websocketDidDisconnect(socket: WebSocket, error: NSError?) { probing = false - + if closed { - client?.engineDidClose("Disconnect") + client?.engineDidClose(reason: "Disconnect") + return } - + if websocket { connected = false websocket = false - - let reason = error?.localizedDescription ?? "Socket Disconnected" - - if error != nil { - didError(reason) + + if let reason = error?.localizedDescription { + didError(reason: reason) + } else { + client?.engineDidClose(reason: "Socket Disconnected") } - - client?.engineDidClose(reason) } else { flushProbeWait() } } } + +extension SocketEngine { + public func URLSession(session: URLSession, didBecomeInvalidWithError error: NSError?) { + DefaultSocketLogger.Logger.error("Engine URLSession became invalid", type: "SocketEngine") + + didError(reason: "Engine URLSession became invalid") + } +} diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineClient.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineClient.swift index a1db7f6..49cac7c 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineClient.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineClient.swift @@ -28,7 +28,7 @@ import Foundation @objc public protocol SocketEngineClient { func engineDidError(reason: String) func engineDidClose(reason: String) - optional func engineDidOpen(reason: String) - func parseEngineMessage(msg: String) - func parseEngineBinaryData(data: NSData) + func engineDidOpen(reason: String) + func parseEngineMessage(_ msg: String) + func parseEngineBinaryData(_ data: Data) } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketEnginePacketType.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketEnginePacketType.swift index 592d79b..763335e 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketEnginePacketType.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketEnginePacketType.swift @@ -26,5 +26,5 @@ import Foundation @objc public enum SocketEnginePacketType : Int { - case Open, Close, Ping, Pong, Message, Upgrade, Noop + case open, close, ping, pong, message, upgrade, noop } \ No newline at end of file diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketEnginePollable.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketEnginePollable.swift index c419e51..b050975 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketEnginePollable.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketEnginePollable.swift @@ -30,7 +30,7 @@ public protocol SocketEnginePollable : SocketEngineSpec { /// Holds strings waiting to be sent over polling. /// You shouldn't need to mess with this. var postWait: [String] { get set } - var session: NSURLSession? { get } + var session: URLSession? { get } /// Because socket.io doesn't let you send two polling request at the same time /// we have to keep track if there's an outstanding poll var waitingForPoll: Bool { get set } @@ -39,15 +39,17 @@ public protocol SocketEnginePollable : SocketEngineSpec { var waitingForPost: Bool { get set } func doPoll() - func sendPollMessage(message: String, withType type: SocketEnginePacketType, withData datas: [NSData]) + func sendPollMessage(_ message: String, withType type: SocketEnginePacketType, withData datas: [Data]) func stopPolling() } // Default polling methods extension SocketEnginePollable { - private func addHeaders(req: NSMutableURLRequest) { + private func addHeaders(for req: URLRequest) -> URLRequest { + var req = req + if cookies != nil { - let headers = NSHTTPCookie.requestHeaderFieldsWithCookies(cookies!) + let headers = HTTPCookie.requestHeaderFields(with: cookies!) req.allHTTPHeaderFields = headers } @@ -56,9 +58,13 @@ extension SocketEnginePollable { req.setValue(value, forHTTPHeaderField: headerName) } } + + return req } - func createRequestForPostWithPostWait() -> NSURLRequest { + func createRequestForPostWithPostWait() -> URLRequest { + defer { postWait.removeAll(keepingCapacity: true) } + var postStr = "" for packet in postWait { @@ -69,22 +75,18 @@ extension SocketEnginePollable { DefaultSocketLogger.Logger.log("Created POST string: %@", type: "SocketEnginePolling", args: postStr) - postWait.removeAll(keepCapacity: false) + var req = URLRequest(url: urlPollingWithSid) + let postData = postStr.data(using: .utf8, allowLossyConversion: false)! - let req = NSMutableURLRequest(URL: urlPollingWithSid) + req = addHeaders(for: req) - addHeaders(req) - - req.HTTPMethod = "POST" + req.httpMethod = "POST" req.setValue("text/plain; charset=UTF-8", forHTTPHeaderField: "Content-Type") + + req.httpBody = postData + req.setValue(String(postData.count), forHTTPHeaderField: "Content-Length") - let postData = postStr.dataUsingEncoding(NSUTF8StringEncoding, - allowLossyConversion: false)! - - req.HTTPBody = postData - req.setValue(String(postData.length), forHTTPHeaderField: "Content-Length") - - return req + return req as URLRequest } public func doPoll() { @@ -93,41 +95,41 @@ extension SocketEnginePollable { } waitingForPoll = true - let req = NSMutableURLRequest(URL: urlPollingWithSid) - addHeaders(req) - doLongPoll(req) + var req = URLRequest(url: urlPollingWithSid) + + req = addHeaders(for: req) + doLongPoll(for: req ) } - func doRequest(req: NSURLRequest, withCallback callback: (NSData?, NSURLResponse?, NSError?) -> Void) { + func doRequest(for req: URLRequest, callbackWith callback: @escaping (Data?, URLResponse?, Error?) -> Void) { if !polling || closed || invalidated || fastUpgrade { - DefaultSocketLogger.Logger.error("Tried to do polling request when not supposed to", type: "SocketEnginePolling") return } DefaultSocketLogger.Logger.log("Doing polling request", type: "SocketEnginePolling") - session?.dataTaskWithRequest(req, completionHandler: callback).resume() + session?.dataTask(with: req, completionHandler: callback).resume() } - func doLongPoll(req: NSURLRequest) { - doRequest(req) {[weak self] data, res, err in - guard let this = self where this.polling else { return } + func doLongPoll(for req: URLRequest) { + doRequest(for: req) {[weak self] data, res, err in + guard let this = self, this.polling else { return } if err != nil || data == nil { DefaultSocketLogger.Logger.error(err?.localizedDescription ?? "Error", type: "SocketEnginePolling") if this.polling { - this.didError(err?.localizedDescription ?? "Error") + this.didError(reason: err?.localizedDescription ?? "Error") } return } - + DefaultSocketLogger.Logger.log("Got polling response", type: "SocketEnginePolling") - if let str = String(data: data!, encoding: NSUTF8StringEncoding) { - dispatch_async(this.parseQueue) { + if let str = String(data: data!, encoding: String.Encoding.utf8) { + this.parseQueue.async { this.parsePollingMessage(str) } } @@ -156,14 +158,14 @@ extension SocketEnginePollable { DefaultSocketLogger.Logger.log("POSTing", type: "SocketEnginePolling") - doRequest(req) {[weak self] data, res, err in + doRequest(for: req) {[weak self] data, res, err in guard let this = self else { return } if err != nil { DefaultSocketLogger.Logger.error(err?.localizedDescription ?? "Error", type: "SocketEnginePolling") if this.polling { - this.didError(err?.localizedDescription ?? "Error") + this.didError(reason: err?.localizedDescription ?? "Error") } return @@ -171,7 +173,7 @@ extension SocketEnginePollable { this.waitingForPost = false - dispatch_async(this.emitQueue) { + this.emitQueue.async { if !this.fastUpgrade { this.flushWaitingForPost() this.doPoll() @@ -180,22 +182,18 @@ extension SocketEnginePollable { } } - func parsePollingMessage(str: String) { + func parsePollingMessage(_ str: String) { guard str.characters.count != 1 else { return } var reader = SocketStringReader(message: str) while reader.hasNext { - if let n = Int(reader.readUntilStringOccurence(":")) { - let str = reader.read(n) + if let n = Int(reader.readUntilOccurence(of: ":")) { + let str = reader.read(count: n) - dispatch_async(handleQueue) { - self.parseEngineMessage(str, fromPolling: true) - } + handleQueue.async { self.parseEngineMessage(str, fromPolling: true) } } else { - dispatch_async(handleQueue) { - self.parseEngineMessage(str, fromPolling: true) - } + handleQueue.async { self.parseEngineMessage(str, fromPolling: true) } break } } @@ -203,7 +201,7 @@ extension SocketEnginePollable { /// Send polling message. /// Only call on emitQueue - public func sendPollMessage(message: String, withType type: SocketEnginePacketType, withData datas: [NSData]) { + public func sendPollMessage(_ message: String, withType type: SocketEnginePacketType, withData datas: [Data]) { DefaultSocketLogger.Logger.log("Sending poll: %@ as type: %@", type: "SocketEnginePolling", args: message, type.rawValue) let fixedMessage: String @@ -213,12 +211,10 @@ extension SocketEnginePollable { fixedMessage = message } - let strMsg = "\(type.rawValue)\(fixedMessage)" - - postWait.append(strMsg) + postWait.append(String(type.rawValue) + fixedMessage) for data in datas { - if case let .Right(bin) = createBinaryDataForSend(data) { + if case let .right(bin) = createBinaryDataForSend(using: data) { postWait.append(bin) } } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineSpec.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineSpec.swift index 7fdd779..f862889 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineSpec.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineSpec.swift @@ -29,79 +29,79 @@ import Foundation weak var client: SocketEngineClient? { get set } var closed: Bool { get } var connected: Bool { get } - var connectParams: [String: AnyObject]? { get set } + var connectParams: [String: Any]? { get set } var doubleEncodeUTF8: Bool { get } - var cookies: [NSHTTPCookie]? { get } + var cookies: [HTTPCookie]? { get } var extraHeaders: [String: String]? { get } var fastUpgrade: Bool { get } var forcePolling: Bool { get } var forceWebsockets: Bool { get } - var parseQueue: dispatch_queue_t! { get } + var parseQueue: DispatchQueue { get } var polling: Bool { get } var probing: Bool { get } - var emitQueue: dispatch_queue_t! { get } - var handleQueue: dispatch_queue_t! { get } + var emitQueue: DispatchQueue { get } + var handleQueue: DispatchQueue { get } var sid: String { get } var socketPath: String { get } - var urlPolling: NSURL { get } - var urlWebSocket: NSURL { get } + var urlPolling: URL { get } + var urlWebSocket: URL { get } var websocket: Bool { get } var ws: WebSocket? { get } - init(client: SocketEngineClient, url: NSURL, options: NSDictionary?) + init(client: SocketEngineClient, url: URL, options: NSDictionary?) func connect() - func didError(error: String) + func didError(reason: String) func disconnect(reason: String) func doFastUpgrade() func flushWaitingForPostToWebSocket() - func parseEngineData(data: NSData) - func parseEngineMessage(message: String, fromPolling: Bool) - func write(msg: String, withType type: SocketEnginePacketType, withData data: [NSData]) + func parseEngineData(_ data: Data) + func parseEngineMessage(_ message: String, fromPolling: Bool) + func write(_ msg: String, withType type: SocketEnginePacketType, withData data: [Data]) } extension SocketEngineSpec { - var urlPollingWithSid: NSURL { - let com = NSURLComponents(URL: urlPolling, resolvingAgainstBaseURL: false)! + var urlPollingWithSid: URL { + var com = URLComponents(url: urlPolling, resolvingAgainstBaseURL: false)! com.percentEncodedQuery = com.percentEncodedQuery! + "&sid=\(sid.urlEncode()!)" - return com.URL! + return com.url! } - var urlWebSocketWithSid: NSURL { - let com = NSURLComponents(URL: urlWebSocket, resolvingAgainstBaseURL: false)! + var urlWebSocketWithSid: URL { + var com = URLComponents(url: urlWebSocket, resolvingAgainstBaseURL: false)! com.percentEncodedQuery = com.percentEncodedQuery! + (sid == "" ? "" : "&sid=\(sid.urlEncode()!)") - return com.URL! + return com.url! } - func createBinaryDataForSend(data: NSData) -> Either { + func createBinaryDataForSend(using data: Data) -> Either { if websocket { - var byteArray = [UInt8](count: 1, repeatedValue: 0x4) + var byteArray = [UInt8](repeating: 0x4, count: 1) let mutData = NSMutableData(bytes: &byteArray, length: 1) - mutData.appendData(data) + mutData.append(data) - return .Left(mutData) + return .left(mutData as Data) } else { - let str = "b4" + data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0)) + let str = "b4" + data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) - return .Right(str) + return .right(str) } } - func doubleEncodeUTF8(string: String) -> String { - if let latin1 = string.dataUsingEncoding(NSUTF8StringEncoding), - utf8 = NSString(data: latin1, encoding: NSISOLatin1StringEncoding) { + func doubleEncodeUTF8(_ string: String) -> String { + if let latin1 = string.data(using: String.Encoding.utf8), + let utf8 = NSString(data: latin1, encoding: String.Encoding.isoLatin1.rawValue) { return utf8 as String } else { return string } } - func fixDoubleUTF8(string: String) -> String { - if let utf8 = string.dataUsingEncoding(NSISOLatin1StringEncoding), - latin1 = NSString(data: utf8, encoding: NSUTF8StringEncoding) { + func fixDoubleUTF8(_ string: String) -> String { + if let utf8 = string.data(using: String.Encoding.isoLatin1), + let latin1 = NSString(data: utf8, encoding: String.Encoding.utf8.rawValue) { return latin1 as String } else { return string @@ -109,7 +109,7 @@ extension SocketEngineSpec { } /// Send an engine message (4) - func send(msg: String, withData datas: [NSData]) { - write(msg, withType: .Message, withData: datas) + func send(_ msg: String, withData datas: [Data]) { + write(msg, withType: .message, withData: datas) } } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineWebsocket.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineWebsocket.swift index e1b0ba8..3c37b2b 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineWebsocket.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketEngineWebsocket.swift @@ -27,36 +27,36 @@ import Foundation /// Protocol that is used to implement socket.io WebSocket support public protocol SocketEngineWebsocket : SocketEngineSpec, WebSocketDelegate { - func sendWebSocketMessage(str: String, withType type: SocketEnginePacketType, withData datas: [NSData]) + func sendWebSocketMessage(_ str: String, withType type: SocketEnginePacketType, withData datas: [Data]) } // WebSocket methods extension SocketEngineWebsocket { func probeWebSocket() { if ws?.isConnected ?? false { - sendWebSocketMessage("probe", withType: .Ping, withData: []) + sendWebSocketMessage("probe", withType: .ping, withData: []) } } /// Send message on WebSockets /// Only call on emitQueue - public func sendWebSocketMessage(str: String, withType type: SocketEnginePacketType, withData datas: [NSData]) { - DefaultSocketLogger.Logger.log("Sending ws: %@ as type: %@", type: "SocketEngine", args: str, type.rawValue) - - ws?.writeString("\(type.rawValue)\(str)") - - for data in datas { - if case let .Left(bin) = createBinaryDataForSend(data) { - ws?.writeData(bin) - } + public func sendWebSocketMessage(_ str: String, withType type: SocketEnginePacketType, withData datas: [Data]) { + DefaultSocketLogger.Logger.log("Sending ws: %@ as type: %@", type: "SocketEngine", args: str, type.rawValue) + + ws?.write(string: "\(type.rawValue)\(str)") + + for data in datas { + if case let .left(bin) = createBinaryDataForSend(using: data) { + ws?.write(data: bin) } + } } public func websocketDidReceiveMessage(socket: WebSocket, text: String) { parseEngineMessage(text, fromPolling: false) } - public func websocketDidReceiveData(socket: WebSocket, data: NSData) { + public func websocketDidReceiveData(socket: WebSocket, data: Data) { parseEngineData(data) } } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketEventHandler.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketEventHandler.swift index 41774a9..5497f7f 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketEventHandler.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketEventHandler.swift @@ -26,10 +26,10 @@ import Foundation struct SocketEventHandler { let event: String - let id: NSUUID + let id: UUID let callback: NormalCallback - func executeCallback(items: [AnyObject], withAck ack: Int, withSocket socket: SocketIOClient) { + func executeCallback(with items: [Any], withAck ack: Int, withSocket socket: SocketIOClient) { callback(items, SocketAckEmitter(socket: socket, ackNum: ack)) } } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketExtensions.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketExtensions.swift new file mode 100644 index 0000000..bf5280a --- /dev/null +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketExtensions.swift @@ -0,0 +1,127 @@ +// +// SocketExtensions.swift +// Socket.IO-Client-Swift +// +// Created by Erik Little on 7/1/2016. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +enum JSONError : Error { + case notArray + case notNSDictionary +} + +extension Array { + func toJSON() throws -> Data { + return try JSONSerialization.data(withJSONObject: self, options: JSONSerialization.WritingOptions(rawValue: 0)) + } +} + +extension CharacterSet { + static var allowedURLCharacterSet: CharacterSet { + return CharacterSet(charactersIn: "!*'();:@&=+$,/?%#[]\" {}").inverted + } +} + +extension NSDictionary { + private static func keyValueToSocketIOClientOption(key: String, value: Any) -> SocketIOClientOption? { + switch (key, value) { + case let ("connectParams", params as [String: Any]): + return .connectParams(params) + case let ("cookies", cookies as [HTTPCookie]): + return .cookies(cookies) + case let ("doubleEncodeUTF8", encode as Bool): + return .doubleEncodeUTF8(encode) + case let ("extraHeaders", headers as [String: String]): + return .extraHeaders(headers) + case let ("forceNew", force as Bool): + return .forceNew(force) + case let ("forcePolling", force as Bool): + return .forcePolling(force) + case let ("forceWebsockets", force as Bool): + return .forceWebsockets(force) + case let ("handleQueue", queue as DispatchQueue): + return .handleQueue(queue) + case let ("log", log as Bool): + return .log(log) + case let ("logger", logger as SocketLogger): + return .logger(logger) + case let ("nsp", nsp as String): + return .nsp(nsp) + case let ("path", path as String): + return .path(path) + case let ("reconnects", reconnects as Bool): + return .reconnects(reconnects) + case let ("reconnectAttempts", attempts as Int): + return .reconnectAttempts(attempts) + case let ("reconnectWait", wait as Int): + return .reconnectWait(wait) + case let ("secure", secure as Bool): + return .secure(secure) + case let ("security", security as SSLSecurity): + return .security(security) + case let ("selfSigned", selfSigned as Bool): + return .selfSigned(selfSigned) + case let ("sessionDelegate", delegate as URLSessionDelegate): + return .sessionDelegate(delegate) + case let ("voipEnabled", enable as Bool): + return .voipEnabled(enable) + default: + return nil + } + } + + func toSocketConfiguration() -> SocketIOClientConfiguration { + var options = [] as SocketIOClientConfiguration + + for (rawKey, value) in self { + if let key = rawKey as? String, let opt = NSDictionary.keyValueToSocketIOClientOption(key: key, value: value) { + options.insert(opt) + } + } + + return options + } +} + +extension String { + func toArray() throws -> [Any] { + guard let stringData = data(using: .utf8, allowLossyConversion: false) else { return [] } + guard let array = try JSONSerialization.jsonObject(with: stringData, options: .mutableContainers) as? [Any] else { + throw JSONError.notArray + } + + return array + } + + func toNSDictionary() throws -> NSDictionary { + guard let binData = data(using: .utf8, allowLossyConversion: false) else { return [:] } + guard let json = try JSONSerialization.jsonObject(with: binData, options: .allowFragments) as? NSDictionary else { + throw JSONError.notNSDictionary + } + + return json + } + + func urlEncode() -> String? { + return addingPercentEncoding(withAllowedCharacters: .allowedURLCharacterSet) + } +} diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClient.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClient.swift index 02cda38..22d1e52 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClient.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClient.swift @@ -25,13 +25,13 @@ import Foundation public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable { - public let socketURL: NSURL + public let socketURL: URL public private(set) var engine: SocketEngineSpec? - public private(set) var status = SocketIOClientStatus.NotConnected { + public private(set) var status = SocketIOClientStatus.notConnected { didSet { switch status { - case .Connected: + case .connected: reconnecting = false currentReconnectAttempt = 0 default: @@ -42,100 +42,105 @@ public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable public var forceNew = false public var nsp = "/" - public var options: Set + public var config: SocketIOClientConfiguration public var reconnects = true public var reconnectWait = 10 - public var sid: String? { - return nsp + "#" + (engine?.sid ?? "") - } - private let emitQueue = dispatch_queue_create("com.socketio.emitQueue", DISPATCH_QUEUE_SERIAL) private let logType = "SocketIOClient" - private let parseQueue = dispatch_queue_create("com.socketio.parseQueue", DISPATCH_QUEUE_SERIAL) + private let parseQueue = DispatchQueue(label: "com.socketio.parseQueue") private var anyHandler: ((SocketAnyEvent) -> Void)? private var currentReconnectAttempt = 0 private var handlers = [SocketEventHandler]() - private var ackHandlers = SocketAckManager() private var reconnecting = false + private let ackSemaphore = DispatchSemaphore(value: 1) private(set) var currentAck = -1 - private(set) var handleQueue = dispatch_get_main_queue() + private(set) var handleQueue = DispatchQueue.main private(set) var reconnectAttempts = -1 + + let ackQueue = DispatchQueue(label: "com.socketio.ackQueue") + let emitQueue = DispatchQueue(label: "com.socketio.emitQueue") + var ackHandlers = SocketAckManager() var waitingPackets = [SocketPacket]() + public var sid: String? { + return engine?.sid + } + /// Type safe way to create a new SocketIOClient. opts can be omitted - public init(socketURL: NSURL, options: Set = []) { - self.options = options + public init(socketURL: URL, config: SocketIOClientConfiguration = []) { + self.config = config self.socketURL = socketURL if socketURL.absoluteString.hasPrefix("https://") { - self.options.insertIgnore(.Secure(true)) + self.config.insert(.secure(true)) } - for option in options { + for option in config { switch option { - case let .Reconnects(reconnects): + case let .reconnects(reconnects): self.reconnects = reconnects - case let .ReconnectAttempts(attempts): + case let .reconnectAttempts(attempts): reconnectAttempts = attempts - case let .ReconnectWait(wait): + case let .reconnectWait(wait): reconnectWait = abs(wait) - case let .Nsp(nsp): + case let .nsp(nsp): self.nsp = nsp - case let .Log(log): + case let .log(log): DefaultSocketLogger.Logger.log = log - case let .Logger(logger): + case let .logger(logger): DefaultSocketLogger.Logger = logger - case let .HandleQueue(queue): + case let .handleQueue(queue): handleQueue = queue - case let .ForceNew(force): + case let .forceNew(force): forceNew = force default: continue } } - - self.options.insertIgnore(.Path("/socket.io/")) + + self.config.insert(.path("/socket.io/"), replacing: false) super.init() } /// Not so type safe way to create a SocketIOClient, meant for Objective-C compatiblity. /// If using Swift it's recommended to use `init(socketURL: NSURL, options: Set)` - public convenience init(socketURL: NSURL, options: NSDictionary?) { - self.init(socketURL: socketURL, options: options?.toSocketOptionsSet() ?? []) + public convenience init(socketURL: NSURL, config: NSDictionary?) { + self.init(socketURL: socketURL as URL, config: config?.toSocketConfiguration() ?? []) } deinit { DefaultSocketLogger.Logger.log("Client is being released", type: logType) - engine?.disconnect("Client Deinit") + engine?.disconnect(reason: "Client Deinit") } private func addEngine() -> SocketEngineSpec { - DefaultSocketLogger.Logger.log("Adding engine", type: logType) - - engine = SocketEngine(client: self, url: socketURL, options: options) + DefaultSocketLogger.Logger.log("Adding engine", type: logType, args: "") + + engine = SocketEngine(client: self, url: socketURL, config: config) return engine! } /// Connect to the server. public func connect() { - connect(timeoutAfter: 0, withTimeoutHandler: nil) + connect(timeoutAfter: 0, withHandler: nil) } - /// Connect to the server. If we aren't connected after timeoutAfter, call handler - public func connect(timeoutAfter timeoutAfter: Int, withTimeoutHandler handler: (() -> Void)?) { + /// Connect to the server. If we aren't connected after timeoutAfter, call withHandler + /// 0 Never times out + public func connect(timeoutAfter: Int, withHandler handler: (() -> Void)?) { assert(timeoutAfter >= 0, "Invalid timeout: \(timeoutAfter)") - guard status != .Connected else { + guard status != .connected else { DefaultSocketLogger.Logger.log("Tried connecting on an already connected socket", type: logType) return } - status = .Connecting + status = .connecting if engine == nil || forceNew { addEngine().connect() @@ -145,40 +150,32 @@ public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable guard timeoutAfter != 0 else { return } - let time = dispatch_time(DISPATCH_TIME_NOW, Int64(timeoutAfter) * Int64(NSEC_PER_SEC)) - - dispatch_after(time, handleQueue) {[weak self] in - if let this = self where this.status != .Connected && this.status != .Disconnected { - this.status = .Disconnected - this.engine?.disconnect("Connect timeout") + let time = DispatchTime.now() + Double(UInt64(timeoutAfter) * NSEC_PER_SEC) / Double(NSEC_PER_SEC) - handler?() - } + handleQueue.asyncAfter(deadline: time) {[weak self] in + guard let this = self, this.status != .connected && this.status != .disconnected else { return } + + this.status = .disconnected + this.engine?.disconnect(reason: "Connect timeout") + + handler?() } } - private func createOnAck(items: [AnyObject]) -> OnAckCallback { + private func nextAck() -> Int { + ackSemaphore.wait() + defer { ackSemaphore.signal() } currentAck += 1 + return currentAck + } - return {[weak self, ack = currentAck] timeout, callback in - if let this = self { - this.ackHandlers.addAck(ack, callback: callback) - this._emit(items, ack: ack) - - if timeout != 0 { - let time = dispatch_time(DISPATCH_TIME_NOW, Int64(timeout * NSEC_PER_SEC)) - - dispatch_after(time, this.handleQueue) { - this.ackHandlers.timeoutAck(ack) - } - } - } - } + private func createOnAck(_ items: [Any]) -> OnAckCallback { + return OnAckCallback(ackNumber: nextAck(), items: items, socket: self) } func didConnect() { DefaultSocketLogger.Logger.log("Socket connected", type: logType) - status = .Connected + status = .connected // Don't handle as internal because something crazy could happen where // we disconnect before it's handled @@ -186,58 +183,55 @@ public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable } func didDisconnect(reason: String) { - guard status != .Disconnected else { return } + guard status != .disconnected else { return } DefaultSocketLogger.Logger.log("Disconnected: %@", type: logType, args: reason) - status = .Disconnected - reconnects = false + reconnecting = false + status = .disconnected // Make sure the engine is actually dead. - engine?.disconnect(reason) + engine?.disconnect(reason: reason) handleEvent("disconnect", data: [reason], isInternalMessage: true) } - /// Disconnects the socket. Only reconnect the same socket if you know what you're doing. - /// Will turn off automatic reconnects. + /// Disconnects the socket. public func disconnect() { - assert(status != .NotConnected, "Tried closing a NotConnected client") - DefaultSocketLogger.Logger.log("Closing socket", type: logType) - reconnects = false - didDisconnect("Disconnect") + didDisconnect(reason: "Disconnect") } /// Send a message to the server - public func emit(event: String, _ items: AnyObject...) { - emit(event, withItems: items) + public func emit(_ event: String, _ items: SocketData...) { + emit(event, with: items) } /// Same as emit, but meant for Objective-C - public func emit(event: String, withItems items: [AnyObject]) { - guard status == .Connected else { + public func emit(_ event: String, with items: [Any]) { + guard status == .connected else { handleEvent("error", data: ["Tried emitting \(event) when not connected"], isInternalMessage: true) return } _emit([event] + items) + } /// Sends a message to the server, requesting an ack. Use the onAck method of SocketAckHandler to add /// an ack. - public func emitWithAck(event: String, _ items: AnyObject...) -> OnAckCallback { - return emitWithAck(event, withItems: items) + public func emitWithAck(_ event: String, _ items: SocketData...) -> OnAckCallback { + return emitWithAck(event, with: items) } /// Same as emitWithAck, but for Objective-C - public func emitWithAck(event: String, withItems items: [AnyObject]) -> OnAckCallback { + public func emitWithAck(_ event: String, with items: [Any]) -> OnAckCallback { return createOnAck([event] + items) } - private func _emit(data: [AnyObject], ack: Int? = nil) { - dispatch_async(emitQueue) { - guard self.status == .Connected else { + func _emit(_ data: [Any], ack: Int? = nil) { + emitQueue.async { + guard self.status == .connected else { self.handleEvent("error", data: ["Tried emitting when not connected"], isInternalMessage: true) return } @@ -252,31 +246,33 @@ public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable } // If the server wants to know that the client received data - func emitAck(ack: Int, withItems items: [AnyObject]) { - dispatch_async(emitQueue) { - if self.status == .Connected { - let packet = SocketPacket.packetFromEmit(items, id: ack ?? -1, nsp: self.nsp, ack: true) - let str = packet.packetString - - DefaultSocketLogger.Logger.log("Emitting Ack: %@", type: self.logType, args: str) - - self.engine?.send(str, withData: packet.binary) - } + func emitAck(_ ack: Int, with items: [Any]) { + emitQueue.async { + guard self.status == .connected else { return } + + let packet = SocketPacket.packetFromEmit(items, id: ack, nsp: self.nsp, ack: true) + let str = packet.packetString + + DefaultSocketLogger.Logger.log("Emitting Ack: %@", type: self.logType, args: str) + + self.engine?.send(str, withData: packet.binary) } } public func engineDidClose(reason: String) { - waitingPackets.removeAll() + parseQueue.async { + self.waitingPackets.removeAll() + } - if status != .Disconnected { - status = .NotConnected + if status != .disconnected { + status = .notConnected } - if status == .Disconnected || !reconnects { - didDisconnect(reason) + if status == .disconnected || !reconnects { + didDisconnect(reason: reason) } else if !reconnecting { reconnecting = true - tryReconnectWithReason(reason) + tryReconnect(reason: reason) } } @@ -286,29 +282,33 @@ public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable handleEvent("error", data: [reason], isInternalMessage: true) } + + public func engineDidOpen(reason: String) { + DefaultSocketLogger.Logger.log(reason, type: "SocketEngineClient") + } // Called when the socket gets an ack for something it sent - func handleAck(ack: Int, data: [AnyObject]) { - guard status == .Connected else { return } + func handleAck(_ ack: Int, data: [Any]) { + guard status == .connected else { return } - DefaultSocketLogger.Logger.log("Handling ack: %@ with data: %@", type: logType, args: ack, data ?? "") + DefaultSocketLogger.Logger.log("Handling ack: %@ with data: %@", type: logType, args: ack, data) - ackHandlers.executeAck(ack, items: data) + handleQueue.async() { + self.ackHandlers.executeAck(ack, with: data, onQueue: self.handleQueue) + } } /// Causes an event to be handled. Only use if you know what you're doing. - public func handleEvent(event: String, data: [AnyObject], isInternalMessage: Bool, withAck ack: Int = -1) { - guard status == .Connected || isInternalMessage else { - return - } + public func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) { + guard status == .connected || isInternalMessage else { return } - DefaultSocketLogger.Logger.log("Handling event: %@ with data: %@", type: logType, args: event, data ?? "") + DefaultSocketLogger.Logger.log("Handling event: %@ with data: %@", type: logType, args: event, data) - dispatch_async(handleQueue) { + handleQueue.async { self.anyHandler?(SocketAnyEvent(event: event, items: data)) for handler in self.handlers where handler.event == event { - handler.executeCallback(data, withAck: ack, withSocket: self) + handler.executeCallback(with: data, withAck: ack, withSocket: self) } } } @@ -322,7 +322,7 @@ public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable } /// Joins namespace - public func joinNamespace(namespace: String) { + public func joinNamespace(_ namespace: String) { nsp = namespace if nsp != "/" { @@ -332,25 +332,26 @@ public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable } /// Removes handler(s) based on name - public func off(event: String) { + public func off(_ event: String) { DefaultSocketLogger.Logger.log("Removing handler for event: %@", type: logType, args: event) - handlers = handlers.filter { $0.event != event } + handlers = handlers.filter({ $0.event != event }) } /// Removes a handler with the specified UUID gotten from an `on` or `once` - public func off(id id: NSUUID) { + public func off(id: UUID) { DefaultSocketLogger.Logger.log("Removing handler with id: %@", type: logType, args: id) - handlers = handlers.filter { $0.id != id } + handlers = handlers.filter({ $0.id != id }) } /// Adds a handler for an event. /// Returns: A unique id for the handler - public func on(event: String, callback: NormalCallback) -> NSUUID { + @discardableResult + public func on(_ event: String, callback: @escaping NormalCallback) -> UUID { DefaultSocketLogger.Logger.log("Adding handler for event: %@", type: logType, args: event) - let handler = SocketEventHandler(event: event, id: NSUUID(), callback: callback) + let handler = SocketEventHandler(event: event, id: UUID(), callback: callback) handlers.append(handler) return handler.id @@ -358,10 +359,11 @@ public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable /// Adds a single-use handler for an event. /// Returns: A unique id for the handler - public func once(event: String, callback: NormalCallback) -> NSUUID { + @discardableResult + public func once(_ event: String, callback: @escaping NormalCallback) -> UUID { DefaultSocketLogger.Logger.log("Adding once handler for event: %@", type: logType, args: event) - let id = NSUUID() + let id = UUID() let handler = SocketEventHandler(event: event, id: id) {[weak self] data, ack in guard let this = self else { return } @@ -375,83 +377,75 @@ public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable } /// Adds a handler that will be called on every event. - public func onAny(handler: (SocketAnyEvent) -> Void) { + public func onAny(_ handler: @escaping (SocketAnyEvent) -> Void) { anyHandler = handler } - public func parseEngineMessage(msg: String) { + public func parseEngineMessage(_ msg: String) { DefaultSocketLogger.Logger.log("Should parse message: %@", type: "SocketIOClient", args: msg) - dispatch_async(parseQueue) { - self.parseSocketMessage(msg) - } + parseQueue.async { self.parseSocketMessage(msg) } } - public func parseEngineBinaryData(data: NSData) { - dispatch_async(parseQueue) { - self.parseBinaryData(data) - } + public func parseEngineBinaryData(_ data: Data) { + parseQueue.async { self.parseBinaryData(data) } } /// Tries to reconnect to the server. public func reconnect() { guard !reconnecting else { return } - engine?.disconnect("manual reconnect") + engine?.disconnect(reason: "manual reconnect") } /// Removes all handlers. /// Can be used after disconnecting to break any potential remaining retain cycles. public func removeAllHandlers() { - handlers.removeAll(keepCapacity: false) + handlers.removeAll(keepingCapacity: false) } - private func tryReconnectWithReason(reason: String) { - if reconnecting { - DefaultSocketLogger.Logger.log("Starting reconnect", type: logType) - handleEvent("reconnect", data: [reason], isInternalMessage: true) - - _tryReconnect() - } + private func tryReconnect(reason: String) { + guard reconnecting else { return } + + DefaultSocketLogger.Logger.log("Starting reconnect", type: logType) + handleEvent("reconnect", data: [reason], isInternalMessage: true) + + _tryReconnect() } private func _tryReconnect() { - if !reconnecting { - return - } + guard reconnecting else { return } if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts || !reconnects { - return didDisconnect("Reconnect Failed") + return didDisconnect(reason: "Reconnect Failed") } DefaultSocketLogger.Logger.log("Trying to reconnect", type: logType) - handleEvent("reconnectAttempt", data: [reconnectAttempts - currentReconnectAttempt], - isInternalMessage: true) + handleEvent("reconnectAttempt", data: [(reconnectAttempts - currentReconnectAttempt)], isInternalMessage: true) currentReconnectAttempt += 1 connect() - let dispatchAfter = dispatch_time(DISPATCH_TIME_NOW, Int64(UInt64(reconnectWait) * NSEC_PER_SEC)) + let deadline = DispatchTime.now() + Double(Int64(UInt64(reconnectWait) * NSEC_PER_SEC)) / Double(NSEC_PER_SEC) - dispatch_after(dispatchAfter, dispatch_get_main_queue(), _tryReconnect) + DispatchQueue.main.asyncAfter(deadline: deadline, execute: _tryReconnect) } -} - -// Test extensions -extension SocketIOClient { + + // Test properties + var testHandlers: [SocketEventHandler] { return handlers } - + func setTestable() { - status = .Connected + status = .connected } - - func setTestEngine(engine: SocketEngineSpec?) { + + func setTestEngine(_ engine: SocketEngineSpec?) { self.engine = engine } - - func emitTest(event: String, _ data: AnyObject...) { - self._emit([event] + data) + + func emitTest(event: String, _ data: Any...) { + _emit([event] + data) } } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientConfiguration.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientConfiguration.swift new file mode 100644 index 0000000..4fc45ba --- /dev/null +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientConfiguration.swift @@ -0,0 +1,108 @@ +// +// SocketIOClientConfiguration.swift +// Socket.IO-Client-Swift +// +// Created by Erik Little on 8/13/16. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +public struct SocketIOClientConfiguration : ExpressibleByArrayLiteral, Collection, MutableCollection { + public typealias Element = SocketIOClientOption + public typealias Index = Array.Index + public typealias Generator = Array.Iterator + public typealias SubSequence = Array.SubSequence + + private var backingArray = [SocketIOClientOption]() + + public var startIndex: Index { + return backingArray.startIndex + } + + public var endIndex: Index { + return backingArray.endIndex + } + + public var isEmpty: Bool { + return backingArray.isEmpty + } + + public var count: Index.Stride { + return backingArray.count + } + + public var first: Generator.Element? { + return backingArray.first + } + + public subscript(position: Index) -> Generator.Element { + get { + return backingArray[position] + } + + set { + backingArray[position] = newValue + } + } + + public subscript(bounds: Range) -> SubSequence { + get { + return backingArray[bounds] + } + + set { + backingArray[bounds] = newValue + } + } + + public init(arrayLiteral elements: Element...) { + backingArray = elements + } + + public func generate() -> Generator { + return backingArray.makeIterator() + } + + public func index(after i: Index) -> Index { + return backingArray.index(after: i) + } + + public mutating func insert(_ element: Element, replacing replace: Bool = true) { + for i in 0.. SubSequence { + return backingArray.prefix(upTo: end) + } + + public func prefix(through position: Index) -> SubSequence { + return backingArray.prefix(through: position) + } + + public func suffix(from start: Index) -> SubSequence { + return backingArray.suffix(from: start) + } +} diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientOption.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientOption.swift index 93626f5..dd723f4 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientOption.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientOption.swift @@ -24,123 +24,124 @@ import Foundation -protocol ClientOption : CustomStringConvertible, Hashable { - func getSocketIOOptionValue() -> AnyObject +protocol ClientOption : CustomStringConvertible, Equatable { + func getSocketIOOptionValue() -> Any } public enum SocketIOClientOption : ClientOption { - case ConnectParams([String: AnyObject]) - case Cookies([NSHTTPCookie]) - case DoubleEncodeUTF8(Bool) - case ExtraHeaders([String: String]) - case ForceNew(Bool) - case ForcePolling(Bool) - case ForceWebsockets(Bool) - case HandleQueue(dispatch_queue_t) - case Log(Bool) - case Logger(SocketLogger) - case Nsp(String) - case Path(String) - case Reconnects(Bool) - case ReconnectAttempts(Int) - case ReconnectWait(Int) - case Secure(Bool) - case SelfSigned(Bool) - case SessionDelegate(NSURLSessionDelegate) - case VoipEnabled(Bool) + case connectParams([String: Any]) + case cookies([HTTPCookie]) + case doubleEncodeUTF8(Bool) + case extraHeaders([String: String]) + case forceNew(Bool) + case forcePolling(Bool) + case forceWebsockets(Bool) + case handleQueue(DispatchQueue) + case log(Bool) + case logger(SocketLogger) + case nsp(String) + case path(String) + case reconnects(Bool) + case reconnectAttempts(Int) + case reconnectWait(Int) + case secure(Bool) + case security(SSLSecurity) + case selfSigned(Bool) + case sessionDelegate(URLSessionDelegate) + case voipEnabled(Bool) public var description: String { let description: String switch self { - case .ConnectParams: + case .connectParams: description = "connectParams" - case .Cookies: + case .cookies: description = "cookies" - case .DoubleEncodeUTF8: + case .doubleEncodeUTF8: description = "doubleEncodeUTF8" - case .ExtraHeaders: + case .extraHeaders: description = "extraHeaders" - case .ForceNew: + case .forceNew: description = "forceNew" - case .ForcePolling: + case .forcePolling: description = "forcePolling" - case .ForceWebsockets: + case .forceWebsockets: description = "forceWebsockets" - case .HandleQueue: + case .handleQueue: description = "handleQueue" - case .Log: + case .log: description = "log" - case .Logger: + case .logger: description = "logger" - case .Nsp: + case .nsp: description = "nsp" - case .Path: + case .path: description = "path" - case .Reconnects: + case .reconnects: description = "reconnects" - case .ReconnectAttempts: + case .reconnectAttempts: description = "reconnectAttempts" - case .ReconnectWait: + case .reconnectWait: description = "reconnectWait" - case .Secure: + case .secure: description = "secure" - case .SelfSigned: + case .selfSigned: description = "selfSigned" - case .SessionDelegate: + case .security: + description = "security" + case .sessionDelegate: description = "sessionDelegate" - case .VoipEnabled: + case .voipEnabled: description = "voipEnabled" } return description } - public var hashValue: Int { - return description.hashValue - } - - func getSocketIOOptionValue() -> AnyObject { - let value: AnyObject + func getSocketIOOptionValue() -> Any { + let value: Any switch self { - case let .ConnectParams(params): + case let .connectParams(params): value = params - case let .Cookies(cookies): + case let .cookies(cookies): value = cookies - case let .DoubleEncodeUTF8(encode): + case let .doubleEncodeUTF8(encode): value = encode - case let .ExtraHeaders(headers): + case let .extraHeaders(headers): value = headers - case let .ForceNew(force): + case let .forceNew(force): value = force - case let .ForcePolling(force): + case let .forcePolling(force): value = force - case let .ForceWebsockets(force): + case let .forceWebsockets(force): value = force - case let .HandleQueue(queue): + case let .handleQueue(queue): value = queue - case let .Log(log): + case let .log(log): value = log - case let .Logger(logger): + case let .logger(logger): value = logger - case let .Nsp(nsp): + case let .nsp(nsp): value = nsp - case let .Path(path): + case let .path(path): value = path - case let .Reconnects(reconnects): + case let .reconnects(reconnects): value = reconnects - case let .ReconnectAttempts(attempts): + case let .reconnectAttempts(attempts): value = attempts - case let .ReconnectWait(wait): + case let .reconnectWait(wait): value = wait - case let .Secure(secure): + case let .secure(secure): value = secure - case let .SelfSigned(signed): + case let .security(security): + value = security + case let .selfSigned(signed): value = signed - case let .SessionDelegate(delegate): + case let .sessionDelegate(delegate): value = delegate - case let .VoipEnabled(enabled): + case let .voipEnabled(enabled): value = enabled } @@ -151,70 +152,3 @@ public enum SocketIOClientOption : ClientOption { public func ==(lhs: SocketIOClientOption, rhs: SocketIOClientOption) -> Bool { return lhs.description == rhs.description } - -extension Set where Element : ClientOption { - mutating func insertIgnore(element: Element) { - if !contains(element) { - insert(element) - } - } -} - -extension NSDictionary { - private static func keyValueToSocketIOClientOption(key: String, value: AnyObject) -> SocketIOClientOption? { - switch (key, value) { - case let ("connectParams", params as [String: AnyObject]): - return .ConnectParams(params) - case let ("cookies", cookies as [NSHTTPCookie]): - return .Cookies(cookies) - case let ("doubleEncodeUTF8", encode as Bool): - return .DoubleEncodeUTF8(encode) - case let ("extraHeaders", headers as [String: String]): - return .ExtraHeaders(headers) - case let ("forceNew", force as Bool): - return .ForceNew(force) - case let ("forcePolling", force as Bool): - return .ForcePolling(force) - case let ("forceWebsockets", force as Bool): - return .ForceWebsockets(force) - case let ("handleQueue", queue as dispatch_queue_t): - return .HandleQueue(queue) - case let ("log", log as Bool): - return .Log(log) - case let ("logger", logger as SocketLogger): - return .Logger(logger) - case let ("nsp", nsp as String): - return .Nsp(nsp) - case let ("path", path as String): - return .Path(path) - case let ("reconnects", reconnects as Bool): - return .Reconnects(reconnects) - case let ("reconnectAttempts", attempts as Int): - return .ReconnectAttempts(attempts) - case let ("reconnectWait", wait as Int): - return .ReconnectWait(wait) - case let ("secure", secure as Bool): - return .Secure(secure) - case let ("selfSigned", selfSigned as Bool): - return .SelfSigned(selfSigned) - case let ("sessionDelegate", delegate as NSURLSessionDelegate): - return .SessionDelegate(delegate) - case let ("voipEnabled", enable as Bool): - return .VoipEnabled(enable) - default: - return nil - } - } - - func toSocketOptionsSet() -> Set { - var options = Set() - - for (rawKey, value) in self { - if let key = rawKey as? String, opt = NSDictionary.keyValueToSocketIOClientOption(key, value: value) { - options.insertIgnore(opt) - } - } - - return options - } -} diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientSpec.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientSpec.swift index 8b33cf9..e91c840 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientSpec.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientSpec.swift @@ -29,9 +29,9 @@ protocol SocketIOClientSpec : class { func didConnect() func didDisconnect(reason: String) func didError(reason: String) - func handleAck(ack: Int, data: [AnyObject]) - func handleEvent(event: String, data: [AnyObject], isInternalMessage: Bool, withAck ack: Int) - func joinNamespace(namespace: String) + func handleAck(_ ack: Int, data: [Any]) + func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int) + func joinNamespace(_ namespace: String) } extension SocketIOClientSpec { diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientStatus.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientStatus.swift index 0a34c2f..27574e6 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientStatus.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketIOClientStatus.swift @@ -28,5 +28,5 @@ import Foundation /// /// **Disconnected**: connected before @objc public enum SocketIOClientStatus : Int { - case NotConnected, Disconnected, Connecting, Connected + case notConnected, disconnected, connecting, connected } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketLogger.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketLogger.swift index bff9d4e..640d344 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketLogger.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketLogger.swift @@ -29,28 +29,28 @@ public protocol SocketLogger : class { var log: Bool { get set } /// Normal log messages - func log(message: String, type: String, args: AnyObject...) + func log(_ message: String, type: String, args: Any...) /// Error Messages - func error(message: String, type: String, args: AnyObject...) + func error(_ message: String, type: String, args: Any...) } public extension SocketLogger { - func log(message: String, type: String, args: AnyObject...) { + func log(_ message: String, type: String, args: Any...) { abstractLog("LOG", message: message, type: type, args: args) } - func error(message: String, type: String, args: AnyObject...) { + func error(_ message: String, type: String, args: Any...) { abstractLog("ERROR", message: message, type: type, args: args) } - private func abstractLog(logType: String, message: String, type: String, args: [AnyObject]) { + private func abstractLog(_ logType: String, message: String, type: String, args: [Any]) { guard log else { return } - let newArgs = args.map({arg -> CVarArgType in String(arg)}) - let replaced = String(format: message, arguments: newArgs) + let newArgs = args.map({arg -> CVarArg in String(describing: arg)}) + let messageFormat = String(format: message, arguments: newArgs) - NSLog("%@ %@: %@", logType, type, replaced) + NSLog("\(logType) \(type): %@", messageFormat) } } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketPacket.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketPacket.swift index 52de38a..d88ef4c 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketPacket.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketPacket.swift @@ -26,6 +26,10 @@ import Foundation struct SocketPacket { + enum PacketType: Int { + case connect, disconnect, event, ack, error, binaryEvent, binaryAck + } + private let placeholders: Int private static let logType = "SocketPacket" @@ -34,35 +38,31 @@ struct SocketPacket { let id: Int let type: PacketType - enum PacketType: Int { - case Connect, Disconnect, Event, Ack, Error, BinaryEvent, BinaryAck - } - - var args: [AnyObject] { - if type == .Event || type == .BinaryEvent && data.count != 0 { + var binary: [Data] + var data: [Any] + var args: [Any] { + if type == .event || type == .binaryEvent && data.count != 0 { return Array(data.dropFirst()) } else { return data } } - var binary: [NSData] - var data: [AnyObject] var description: String { return "SocketPacket {type: \(String(type.rawValue)); data: " + - "\(String(data)); id: \(id); placeholders: \(placeholders); nsp: \(nsp)}" + "\(String(describing: data)); id: \(id); placeholders: \(placeholders); nsp: \(nsp)}" } var event: String { - return String(data[0]) + return String(describing: data[0]) } var packetString: String { return createPacketString() } - init(type: SocketPacket.PacketType, data: [AnyObject] = [AnyObject](), id: Int = -1, - nsp: String, placeholders: Int = 0, binary: [NSData] = [NSData]()) { + init(type: PacketType, data: [Any] = [Any](), id: Int = -1, nsp: String, placeholders: Int = 0, + binary: [Data] = [Data]()) { self.data = data self.id = id self.nsp = nsp @@ -71,7 +71,7 @@ struct SocketPacket { self.binary = binary } - mutating func addData(data: NSData) -> Bool { + mutating func addData(_ data: Data) -> Bool { if placeholders == binary.count { return true } @@ -86,98 +86,31 @@ struct SocketPacket { } } - private func completeMessage(message: String) -> String { - let restOfMessage: String - + private func completeMessage(_ message: String) -> String { if data.count == 0 { return message + "[]" } - do { - let jsonSend = try NSJSONSerialization.dataWithJSONObject(data, - options: NSJSONWritingOptions(rawValue: 0)) - guard let jsonString = String(data: jsonSend, encoding: NSUTF8StringEncoding) else { - return "[]" - } - - restOfMessage = jsonString - } catch { + guard let jsonSend = try? data.toJSON(), let jsonString = String(data: jsonSend, encoding: .utf8) else { DefaultSocketLogger.Logger.error("Error creating JSON object in SocketPacket.completeMessage", - type: SocketPacket.logType) + type: SocketPacket.logType) - restOfMessage = "[]" - } - - return message + restOfMessage - } - - private func createAck() -> String { - let message: String - - if type == .Ack { - if nsp == "/" { - message = "3\(id)" - } else { - message = "3\(nsp),\(id)" - } - } else { - if nsp == "/" { - message = "6\(binary.count)-\(id)" - } else { - message = "6\(binary.count)-\(nsp),\(id)" - } - } - - return completeMessage(message) - } - - - private func createMessageForEvent() -> String { - let message: String - - if type == .Event { - if nsp == "/" { - if id == -1 { - message = "2" - } else { - message = "2\(id)" - } - } else { - if id == -1 { - message = "2\(nsp)," - } else { - message = "2\(nsp),\(id)" - } - } - } else { - if nsp == "/" { - if id == -1 { - message = "5\(binary.count)-" - } else { - message = "5\(binary.count)-\(id)" - } - } else { - if id == -1 { - message = "5\(binary.count)-\(nsp)," - } else { - message = "5\(binary.count)-\(nsp),\(id)" - } - } + return message + "[]" } - return completeMessage(message) + return message + jsonString } private func createPacketString() -> String { - let str: String - - if type == .Event || type == .BinaryEvent { - str = createMessageForEvent() - } else { - str = createAck() - } + let typeString = String(type.rawValue) + // Binary count? + let binaryCountString = typeString + (type == .binaryEvent || type == .binaryAck ? "\(String(binary.count))-" : "") + // Namespace? + let nspString = binaryCountString + (nsp != "/" ? "\(nsp)," : "") + // Ack number? + let idString = nspString + (id != -1 ? String(id) : "") - return str + return completeMessage(idString) } // Called when we have all the binary data for a packet @@ -187,20 +120,25 @@ struct SocketPacket { data = data.map(_fillInPlaceholders) } - // Helper method that looks for placeholder strings + // Helper method that looks for placeholders // If object is a collection it will recurse - // Returns the object if it is not a placeholder string or the corresponding + // Returns the object if it is not a placeholder or the corresponding // binary data - private func _fillInPlaceholders(object: AnyObject) -> AnyObject { + private func _fillInPlaceholders(_ object: Any) -> Any { switch object { - case let string as String where string["~~(\\d)"].groups() != nil: - return binary[Int(string["~~(\\d)"].groups()![1])!] - case let dict as NSDictionary: - return dict.reduce(NSMutableDictionary(), combine: {cur, keyValue in - cur[keyValue.0 as! NSCopying] = _fillInPlaceholders(keyValue.1) - return cur - }) - case let arr as [AnyObject]: + case let dict as JSON: + if dict["_placeholder"] as? Bool ?? false { + return binary[dict["num"] as! Int] + } else { + return dict.reduce(JSON(), {cur, keyValue in + var cur = cur + + cur[keyValue.0] = _fillInPlaceholders(keyValue.1) + + return cur + }) + } + case let arr as [Any]: return arr.map(_fillInPlaceholders) default: return object @@ -209,25 +147,25 @@ struct SocketPacket { } extension SocketPacket { - private static func findType(binCount: Int, ack: Bool) -> PacketType { + private static func findType(_ binCount: Int, ack: Bool) -> PacketType { switch binCount { case 0 where !ack: - return .Event + return .event case 0 where ack: - return .Ack + return .ack case _ where !ack: - return .BinaryEvent + return .binaryEvent case _ where ack: - return .BinaryAck + return .binaryAck default: - return .Error + return .error } } - static func packetFromEmit(items: [AnyObject], id: Int, nsp: String, ack: Bool) -> SocketPacket { + static func packetFromEmit(_ items: [Any], id: Int, nsp: String, ack: Bool) -> SocketPacket { let (parsedData, binary) = deconstructData(items) let packet = SocketPacket(type: findType(binary.count, ack: ack), data: parsedData, - id: id, nsp: nsp, placeholders: -1, binary: binary) + id: id, nsp: nsp, binary: binary) return packet } @@ -235,19 +173,23 @@ extension SocketPacket { private extension SocketPacket { // Recursive function that looks for NSData in collections - static func shred(data: AnyObject, inout binary: [NSData]) -> AnyObject { - let placeholder = ["_placeholder": true, "num": binary.count] + static func shred(_ data: Any, binary: inout [Data]) -> Any { + let placeholder = ["_placeholder": true, "num": binary.count] as JSON switch data { - case let bin as NSData: + case let bin as Data: binary.append(bin) + return placeholder - case let arr as [AnyObject]: + case let arr as [Any]: return arr.map({shred($0, binary: &binary)}) - case let dict as NSDictionary: - return dict.reduce(NSMutableDictionary(), combine: {cur, keyValue in - cur[keyValue.0 as! NSCopying] = shred(keyValue.1, binary: &binary) - return cur + case let dict as JSON: + return dict.reduce(JSON(), {cur, keyValue in + var mutCur = cur + + mutCur[keyValue.0] = shred(keyValue.1, binary: &binary) + + return mutCur }) default: return data @@ -256,8 +198,8 @@ private extension SocketPacket { // Removes binary data from emit data // Returns a type containing the de-binaryed data and the binary - static func deconstructData(data: [AnyObject]) -> ([AnyObject], [NSData]) { - var binary = [NSData]() + static func deconstructData(_ data: [Any]) -> ([Any], [Data]) { + var binary = [Data]() return (data.map({shred($0, binary: &binary)}), binary) } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketParsable.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketParsable.swift index c74b160..7c9ce21 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketParsable.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketParsable.swift @@ -23,40 +23,38 @@ import Foundation protocol SocketParsable : SocketIOClientSpec { - func parseBinaryData(data: NSData) - func parseSocketMessage(message: String) + func parseBinaryData(_ data: Data) + func parseSocketMessage(_ message: String) } extension SocketParsable { - private func isCorrectNamespace(nsp: String) -> Bool { + private func isCorrectNamespace(_ nsp: String) -> Bool { return nsp == self.nsp } - private func handleConnect(p: SocketPacket) { - if p.nsp == "/" && nsp != "/" { + private func handleConnect(_ packetNamespace: String) { + if packetNamespace == "/" && nsp != "/" { joinNamespace(nsp) - } else if p.nsp != "/" && nsp == "/" { - didConnect() } else { didConnect() } } - private func handlePacket(pack: SocketPacket) { + private func handlePacket(_ pack: SocketPacket) { switch pack.type { - case .Event where isCorrectNamespace(pack.nsp): + case .event where isCorrectNamespace(pack.nsp): handleEvent(pack.event, data: pack.args, isInternalMessage: false, withAck: pack.id) - case .Ack where isCorrectNamespace(pack.nsp): + case .ack where isCorrectNamespace(pack.nsp): handleAck(pack.id, data: pack.data) - case .BinaryEvent where isCorrectNamespace(pack.nsp): + case .binaryEvent where isCorrectNamespace(pack.nsp): waitingPackets.append(pack) - case .BinaryAck where isCorrectNamespace(pack.nsp): + case .binaryAck where isCorrectNamespace(pack.nsp): waitingPackets.append(pack) - case .Connect: - handleConnect(pack) - case .Disconnect: - didDisconnect("Got Disconnect") - case .Error: + case .connect: + handleConnect(pack.nsp) + case .disconnect: + didDisconnect(reason: "Got Disconnect") + case .error: handleEvent("error", data: pack.data, isInternalMessage: true, withAck: pack.id) default: DefaultSocketLogger.Logger.log("Got invalid packet: %@", type: "SocketParser", args: pack.description) @@ -64,117 +62,105 @@ extension SocketParsable { } /// Parses a messsage from the engine. Returning either a string error or a complete SocketPacket - func parseString(message: String) -> Either { - var parser = SocketStringReader(message: message) + func parseString(_ message: String) -> Either { + var reader = SocketStringReader(message: message) - guard let type = SocketPacket.PacketType(rawValue: Int(parser.read(1)) ?? -1) else { - return .Left("Invalid packet type") + guard let type = Int(reader.read(count: 1)).flatMap({ SocketPacket.PacketType(rawValue: $0) }) else { + return .left("Invalid packet type") } - if !parser.hasNext { - return .Right(SocketPacket(type: type, nsp: "/")) + if !reader.hasNext { + return .right(SocketPacket(type: type, nsp: "/")) } var namespace = "/" var placeholders = -1 - if type == .BinaryEvent || type == .BinaryAck { - if let holders = Int(parser.readUntilStringOccurence("-")) { + if type == .binaryEvent || type == .binaryAck { + if let holders = Int(reader.readUntilOccurence(of: "-")) { placeholders = holders } else { - return .Left("Invalid packet") + return .left("Invalid packet") } } - if parser.currentCharacter == "/" { - namespace = parser.readUntilStringOccurence(",") ?? parser.readUntilEnd() + if reader.currentCharacter == "/" { + namespace = reader.readUntilOccurence(of: ",") } - if !parser.hasNext { - return .Right(SocketPacket(type: type, id: -1, - nsp: namespace ?? "/", placeholders: placeholders)) + if !reader.hasNext { + return .right(SocketPacket(type: type, nsp: namespace, placeholders: placeholders)) } var idString = "" - if type == .Error { - parser.advanceIndexBy(-1) - } - - while parser.hasNext && type != .Error { - if let int = Int(parser.read(1)) { - idString += String(int) - } else { - parser.advanceIndexBy(-2) - break + if type == .error { + reader.advance(by: -1) + } else { + while reader.hasNext { + if let int = Int(reader.read(count: 1)) { + idString += String(int) + } else { + reader.advance(by: -2) + break + } } } - let d = message[parser.currentIndex.advancedBy(1).. Either { - let stringData = data.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) - + private func parseData(_ data: String) -> Either { do { - if let arr = try NSJSONSerialization.JSONObjectWithData(stringData!, - options: NSJSONReadingOptions.MutableContainers) as? [AnyObject] { - return .Right(arr) - } else { - return .Left("Expected data array") - } + return .right(try data.toArray()) } catch { - return .Left("Error parsing data for packet") + return .left("Error parsing data for packet") } } // Parses messages recieved - func parseSocketMessage(message: String) { + func parseSocketMessage(_ message: String) { guard !message.isEmpty else { return } DefaultSocketLogger.Logger.log("Parsing %@", type: "SocketParser", args: message) switch parseString(message) { - case let .Left(err): + case let .left(err): DefaultSocketLogger.Logger.error("\(err): %@", type: "SocketParser", args: message) - case let .Right(pack): + case let .right(pack): DefaultSocketLogger.Logger.log("Decoded packet as: %@", type: "SocketParser", args: pack.description) handlePacket(pack) } } - func parseBinaryData(data: NSData) { + func parseBinaryData(_ data: Data) { guard !waitingPackets.isEmpty else { DefaultSocketLogger.Logger.error("Got data when not remaking packet", type: "SocketParser") return } // Should execute event? - guard waitingPackets[waitingPackets.count - 1].addData(data) else { - return - } + guard waitingPackets[waitingPackets.count - 1].addData(data) else { return } let packet = waitingPackets.removeLast() - if packet.type != .BinaryAck { - handleEvent(packet.event, data: packet.args ?? [], - isInternalMessage: false, withAck: packet.id) + if packet.type != .binaryAck { + handleEvent(packet.event, data: packet.args, isInternalMessage: false, withAck: packet.id) } else { handleAck(packet.id, data: packet.args) } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketStringReader.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketStringReader.swift index d1e2b59..8bdb4d4 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketStringReader.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketStringReader.swift @@ -38,31 +38,36 @@ struct SocketStringReader { currentIndex = message.startIndex } - mutating func advanceIndexBy(n: Int) { - currentIndex = currentIndex.advancedBy(n) + @discardableResult + mutating func advance(by: Int) -> String.Index { + currentIndex = message.characters.index(currentIndex, offsetBy: by) + + return currentIndex } - mutating func read(readLength: Int) -> String { - let readString = message[currentIndex.. String { + let readString = message[currentIndex.. String { + mutating func readUntilOccurence(of string: String) -> String { let substring = message[currentIndex.. String { - return read(currentIndex.distanceTo(message.endIndex)) + return read(count: message.characters.distance(from: currentIndex, to: message.endIndex)) } } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SocketTypes.swift b/ios/RNSwiftSocketIO/SocketIOClient/SocketTypes.swift index b8840be..cc194a7 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/SocketTypes.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/SocketTypes.swift @@ -24,11 +24,29 @@ import Foundation -public typealias AckCallback = ([AnyObject]) -> Void -public typealias NormalCallback = ([AnyObject], SocketAckEmitter) -> Void -public typealias OnAckCallback = (timeoutAfter: UInt64, callback: AckCallback) -> Void +public protocol SocketData {} + +extension Array : SocketData {} +extension Bool : SocketData {} +extension Dictionary : SocketData {} +extension Double : SocketData {} +extension Int : SocketData {} +extension NSArray : SocketData {} +extension Data : SocketData {} +extension NSData : SocketData {} +extension NSDictionary : SocketData {} +extension NSString : SocketData {} +extension NSNull : SocketData {} +extension String : SocketData {} + +public typealias AckCallback = ([Any]) -> Void +public typealias NormalCallback = ([Any], SocketAckEmitter) -> Void + +typealias JSON = [String: Any] +typealias Probe = (msg: String, type: SocketEnginePacketType, data: [Data]) +typealias ProbeWaitQueue = [Probe] enum Either { - case Left(E) - case Right(V) + case left(E) + case right(V) } diff --git a/ios/RNSwiftSocketIO/SocketIOClient/String.swift b/ios/RNSwiftSocketIO/SocketIOClient/String.swift deleted file mode 100644 index 0e30e8c..0000000 --- a/ios/RNSwiftSocketIO/SocketIOClient/String.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// String.swift -// Socket.IO-Client-Swift -// -// Created by Yannick Loriot on 5/4/16. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -extension String { - func urlEncode() -> String? { - return stringByAddingPercentEncodingWithAllowedCharacters(.allowedURLCharacterSet) - } -} diff --git a/ios/RNSwiftSocketIO/SocketIOClient/SwiftRegex.swift b/ios/RNSwiftSocketIO/SocketIOClient/SwiftRegex.swift deleted file mode 100644 index b704afd..0000000 --- a/ios/RNSwiftSocketIO/SocketIOClient/SwiftRegex.swift +++ /dev/null @@ -1,195 +0,0 @@ -// -// SwiftRegex.swift -// SwiftRegex -// -// Created by John Holdsworth on 26/06/2014. -// Copyright (c) 2014 John Holdsworth. -// -// $Id: //depot/SwiftRegex/SwiftRegex.swift#37 $ -// -// This code is in the public domain from: -// https://github.com/johnno1962/SwiftRegex -// - -import Foundation - -infix operator <~ { associativity none precedence 130 } - -private let lock = dispatch_semaphore_create(1) -private var swiftRegexCache = [String: NSRegularExpression]() - -internal final class SwiftRegex : NSObject, BooleanType { - var target: String - var regex: NSRegularExpression - - init(target:String, pattern:String, options:NSRegularExpressionOptions?) { - self.target = target - - if dispatch_semaphore_wait(lock, dispatch_time(DISPATCH_TIME_NOW, Int64(10 * NSEC_PER_MSEC))) != 0 { - do { - let regex = try NSRegularExpression(pattern: pattern, options: - NSRegularExpressionOptions.DotMatchesLineSeparators) - self.regex = regex - } catch let error as NSError { - SwiftRegex.failure("Error in pattern: \(pattern) - \(error)") - self.regex = NSRegularExpression() - } - - super.init() - return - } - - if let regex = swiftRegexCache[pattern] { - self.regex = regex - } else { - do { - let regex = try NSRegularExpression(pattern: pattern, options: - NSRegularExpressionOptions.DotMatchesLineSeparators) - swiftRegexCache[pattern] = regex - self.regex = regex - } catch let error as NSError { - SwiftRegex.failure("Error in pattern: \(pattern) - \(error)") - self.regex = NSRegularExpression() - } - } - dispatch_semaphore_signal(lock) - super.init() - } - - private static func failure(message: String) { - fatalError("SwiftRegex: \(message)") - } - - private var targetRange: NSRange { - return NSRange(location: 0,length: target.utf16.count) - } - - private func substring(range: NSRange) -> String? { - if range.location != NSNotFound { - return (target as NSString).substringWithRange(range) - } else { - return nil - } - } - - func doesMatch(options: NSMatchingOptions!) -> Bool { - return range(options).location != NSNotFound - } - - func range(options: NSMatchingOptions) -> NSRange { - return regex.rangeOfFirstMatchInString(target as String, options: [], range: targetRange) - } - - func match(options: NSMatchingOptions) -> String? { - return substring(range(options)) - } - - func groups() -> [String]? { - return groupsForMatch(regex.firstMatchInString(target as String, options: - NSMatchingOptions.WithoutAnchoringBounds, range: targetRange)) - } - - private func groupsForMatch(match: NSTextCheckingResult?) -> [String]? { - guard let match = match else { - return nil - } - var groups = [String]() - for groupno in 0...regex.numberOfCaptureGroups { - if let group = substring(match.rangeAtIndex(groupno)) { - groups += [group] - } else { - groups += ["_"] // avoids bridging problems - } - } - return groups - } - - subscript(groupno: Int) -> String? { - get { - return groups()?[groupno] - } - - set(newValue) { - if newValue == nil { - return - } - - for match in Array(matchResults().reverse()) { - let replacement = regex.replacementStringForResult(match, - inString: target as String, offset: 0, template: newValue!) - let mut = NSMutableString(string: target) - mut.replaceCharactersInRange(match.rangeAtIndex(groupno), withString: replacement) - - target = mut as String - } - } - } - - func matchResults() -> [NSTextCheckingResult] { - let matches = regex.matchesInString(target as String, options: - NSMatchingOptions.WithoutAnchoringBounds, range: targetRange) - as [NSTextCheckingResult] - - return matches - } - - func ranges() -> [NSRange] { - return matchResults().map { $0.range } - } - - func matches() -> [String] { - return matchResults().map( { self.substring($0.range)!}) - } - - func allGroups() -> [[String]?] { - return matchResults().map { self.groupsForMatch($0) } - } - - func dictionary(options: NSMatchingOptions!) -> Dictionary { - var out = Dictionary() - for match in matchResults() { - out[substring(match.rangeAtIndex(1))!] = substring(match.rangeAtIndex(2))! - } - return out - } - - func substituteMatches(substitution: ((NSTextCheckingResult, UnsafeMutablePointer) -> String), - options:NSMatchingOptions) -> String { - let out = NSMutableString() - var pos = 0 - - regex.enumerateMatchesInString(target as String, options: options, range: targetRange ) {match, flags, stop in - let matchRange = match!.range - out.appendString( self.substring(NSRange(location:pos, length:matchRange.location-pos))!) - out.appendString( substitution(match!, stop) ) - pos = matchRange.location + matchRange.length - } - - out.appendString(substring(NSRange(location:pos, length:targetRange.length-pos))!) - - return out as String - } - - var boolValue: Bool { - return doesMatch(nil) - } -} - -extension String { - subscript(pattern: String, options: NSRegularExpressionOptions) -> SwiftRegex { - return SwiftRegex(target: self, pattern: pattern, options: options) - } -} - -extension String { - subscript(pattern: String) -> SwiftRegex { - return SwiftRegex(target: self, pattern: pattern, options: nil) - } -} - -func <~ (left: SwiftRegex, right: String) -> String { - return left.substituteMatches({match, stop in - return left.regex.replacementStringForResult( match, - inString: left.target as String, offset: 0, template: right ) - }, options: []) -} diff --git a/ios/RNSwiftSocketIO/SocketIOClient/WebSocket.swift b/ios/RNSwiftSocketIO/SocketIOClient/WebSocket.swift index 833eece..34152af 100644 --- a/ios/RNSwiftSocketIO/SocketIOClient/WebSocket.swift +++ b/ios/RNSwiftSocketIO/SocketIOClient/WebSocket.swift @@ -3,7 +3,7 @@ // Websocket.swift // // Created by Dalton Cherry on 7/16/14. -// Copyright (c) 2014-2015 Dalton Cherry. +// Copyright (c) 2014-2016 Dalton Cherry. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,60 +18,64 @@ // limitations under the License. // ////////////////////////////////////////////////////////////////////////////////////////////////// - import Foundation import CoreFoundation import Security +public let WebsocketDidConnectNotification = "WebsocketDidConnectNotification" +public let WebsocketDidDisconnectNotification = "WebsocketDidDisconnectNotification" +public let WebsocketDisconnectionErrorKeyName = "WebsocketDisconnectionErrorKeyName" + public protocol WebSocketDelegate: class { func websocketDidConnect(socket: WebSocket) func websocketDidDisconnect(socket: WebSocket, error: NSError?) func websocketDidReceiveMessage(socket: WebSocket, text: String) - func websocketDidReceiveData(socket: WebSocket, data: NSData) + func websocketDidReceiveData(socket: WebSocket, data: Data) } public protocol WebSocketPongDelegate: class { - func websocketDidReceivePong(socket: WebSocket) + func websocketDidReceivePong(socket: WebSocket, data: Data?) } -public class WebSocket : NSObject, NSStreamDelegate { +open class WebSocket : NSObject, StreamDelegate { enum OpCode : UInt8 { - case ContinueFrame = 0x0 - case TextFrame = 0x1 - case BinaryFrame = 0x2 - //3-7 are reserved. - case ConnectionClose = 0x8 - case Ping = 0x9 - case Pong = 0xA - //B-F reserved. + case continueFrame = 0x0 + case textFrame = 0x1 + case binaryFrame = 0x2 + // 3-7 are reserved. + case connectionClose = 0x8 + case ping = 0x9 + case pong = 0xA + // B-F reserved. } public enum CloseCode : UInt16 { - case Normal = 1000 - case GoingAway = 1001 - case ProtocolError = 1002 - case ProtocolUnhandledType = 1003 + case normal = 1000 + case goingAway = 1001 + case protocolError = 1002 + case protocolUnhandledType = 1003 // 1004 reserved. - case NoStatusReceived = 1005 + case noStatusReceived = 1005 //1006 reserved. - case Encoding = 1007 - case PolicyViolated = 1008 - case MessageTooBig = 1009 + case encoding = 1007 + case policyViolated = 1008 + case messageTooBig = 1009 } public static let ErrorDomain = "WebSocket" - enum InternalErrorCode : UInt16 { + enum InternalErrorCode: UInt16 { // 0-999 WebSocket status codes not used - case OutputStreamWriteError = 1 + case outputStreamWriteError = 1 } - //Where the callback is executed. It defaults to the main UI thread queue. - public var queue = dispatch_get_main_queue() + // Where the callback is executed. It defaults to the main UI thread queue. + public var callbackQueue = DispatchQueue.main - var optionalProtocols : [String]? - //Constant Values. + var optionalProtocols: [String]? + + // MARK: - Constants let headerWSUpgradeName = "Upgrade" let headerWSUpgradeValue = "websocket" let headerWSHostName = "Host" @@ -90,136 +94,164 @@ public class WebSocket : NSObject, NSStreamDelegate { let MaskMask: UInt8 = 0x80 let PayloadLenMask: UInt8 = 0x7F let MaxFrameSize: Int = 32 + let httpSwitchProtocolCode = 101 + let supportedSSLSchemes = ["wss", "https"] class WSResponse { var isFin = false - var code: OpCode = .ContinueFrame + var code: OpCode = .continueFrame var bytesLeft = 0 var frameCount = 0 var buffer: NSMutableData? } + // MARK: - Delegates + /// Responds to callback about new messages coming in over the WebSocket + /// and also connection/disconnect messages. public weak var delegate: WebSocketDelegate? + + /// Receives a callback for each pong message recived. public weak var pongDelegate: WebSocketPongDelegate? + + + // MARK: - Block based API. public var onConnect: ((Void) -> Void)? public var onDisconnect: ((NSError?) -> Void)? public var onText: ((String) -> Void)? - public var onData: ((NSData) -> Void)? - public var onPong: ((Void) -> Void)? + public var onData: ((Data) -> Void)? + public var onPong: ((Data?) -> Void)? + public var headers = [String: String]() public var voipEnabled = false - public var selfSignedSSL = false - public var security: SSLSecurity? + public var disableSSLCertValidation = false + public var security: SSLTrustValidator? public var enabledSSLCipherSuites: [SSLCipherSuite]? public var origin: String? - public var isConnected :Bool { + public var timeout = 5 + public var isConnected: Bool { return connected } - public var currentURL: NSURL {return url} - private var url: NSURL - private var inputStream: NSInputStream? - private var outputStream: NSOutputStream? + + public var currentURL: URL { return url } + + // MARK: - Private + private var url: URL + private var inputStream: InputStream? + private var outputStream: OutputStream? private var connected = false - private var isCreated = false - private var writeQueue = NSOperationQueue() + private var isConnecting = false + private var writeQueue = OperationQueue() private var readStack = [WSResponse]() - private var inputQueue = [NSData]() - private var fragBuffer: NSData? + private var inputQueue = [Data]() + private var fragBuffer: Data? private var certValidated = false private var didDisconnect = false private var readyToWrite = false private let mutex = NSLock() + private let notificationCenter = NotificationCenter.default private var canDispatch: Bool { mutex.lock() let canWork = readyToWrite mutex.unlock() return canWork } - //the shared processing queue used for all websocket - private static let sharedWorkQueue = dispatch_queue_create("com.vluxe.starscream.websocket", DISPATCH_QUEUE_SERIAL) + /// The shared processing queue used for all WebSocket. + private static let sharedWorkQueue = DispatchQueue(label: "com.vluxe.starscream.websocket", attributes: []) - //used for setting protocols. - public init(url: NSURL, protocols: [String]? = nil) { + /// Used for setting protocols. + public init(url: URL, protocols: [String]? = nil) { self.url = url self.origin = url.absoluteString + if let hostUrl = URL (string: "/", relativeTo: url) { + var origin = hostUrl.absoluteString + origin.remove(at: origin.index(before: origin.endIndex)) + self.origin = origin + } writeQueue.maxConcurrentOperationCount = 1 optionalProtocols = protocols } - ///Connect to the websocket server on a background thread - public func connect() { - guard !isCreated else { return } + // Used for specifically setting the QOS for the write queue. + public convenience init(url: URL, writeQueueQOS: QualityOfService, protocols: [String]? = nil) { + self.init(url: url, protocols: protocols) + writeQueue.qualityOfService = writeQueueQOS + } + + /** + Connect to the WebSocket server on a background thread. + */ + open func connect() { + guard !isConnecting else { return } didDisconnect = false - isCreated = true + isConnecting = true createHTTPRequest() - isCreated = false } /** Disconnect from the server. I send a Close control frame to the server, then expect the server to respond with a Close control frame and close the socket from its end. I notify my delegate once the socket has been closed. - If you supply a non-nil `forceTimeout`, I wait at most that long (in seconds) for the server to close the socket. After the timeout expires, I close the socket and notify my delegate. - If you supply a zero (or negative) `forceTimeout`, I immediately close the socket (without sending a Close control frame) and notify my delegate. - - Parameter forceTimeout: Maximum time to wait for the server to close the socket. + - Parameter closeCode: The code to send on disconnect. The default is the normal close code for cleanly disconnecting a webSocket. */ - public func disconnect(forceTimeout forceTimeout: NSTimeInterval? = nil) { + open func disconnect(forceTimeout: TimeInterval? = nil, closeCode: UInt16 = CloseCode.normal.rawValue) { + guard isConnected else { return } switch forceTimeout { - case .Some(let seconds) where seconds > 0: - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(seconds * Double(NSEC_PER_SEC))), queue) { [weak self] in + case .some(let seconds) where seconds > 0: + let milliseconds = Int(seconds * 1_000) + callbackQueue.asyncAfter(deadline: .now() + .milliseconds(milliseconds)) { [weak self] in self?.disconnectStream(nil) } fallthrough - case .None: - writeError(CloseCode.Normal.rawValue) - + case .none: + writeError(closeCode) default: - self.disconnectStream(nil) + disconnectStream(nil) break } } /** Write a string to the websocket. This sends it as a text frame. - If you supply a non-nil completion block, I will perform it when the write completes. - - parameter str: The string to write. + - parameter string: The string to write. - parameter completion: The (optional) completion handler. */ - public func writeString(str: String, completion: (() -> ())? = nil) { + open func write(string: String, completion: (() -> ())? = nil) { guard isConnected else { return } - dequeueWrite(str.dataUsingEncoding(NSUTF8StringEncoding)!, code: .TextFrame, writeCompletion: completion) + dequeueWrite(string.data(using: String.Encoding.utf8)!, code: .textFrame, writeCompletion: completion) } /** Write binary data to the websocket. This sends it as a binary frame. - If you supply a non-nil completion block, I will perform it when the write completes. - parameter data: The data to write. - parameter completion: The (optional) completion handler. */ - public func writeData(data: NSData, completion: (() -> ())? = nil) { + open func write(data: Data, completion: (() -> ())? = nil) { guard isConnected else { return } - dequeueWrite(data, code: .BinaryFrame, writeCompletion: completion) + dequeueWrite(data, code: .binaryFrame, writeCompletion: completion) } - //write a ping to the websocket. This sends it as a control frame. - //yodel a sound to the planet. This sends it as an astroid. http://youtu.be/Eu5ZJELRiJ8?t=42s - public func writePing(data: NSData, completion: (() -> ())? = nil) { + /** + Write a ping to the websocket. This sends it as a control frame. + Yodel a sound to the planet. This sends it as an astroid. http://youtu.be/Eu5ZJELRiJ8?t=42s + */ + open func write(ping: Data, completion: (() -> ())? = nil) { guard isConnected else { return } - dequeueWrite(data, code: .Ping, writeCompletion: completion) + dequeueWrite(ping, code: .ping, writeCompletion: completion) } - //private method that starts the connection + /** + Private method that starts the connection. + */ private func createHTTPRequest() { - - let urlRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, "GET", - url, kCFHTTPVersion1_1).takeRetainedValue() + let urlRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, "GET" as CFString, + url as CFURL, kCFHTTPVersion1_1).takeRetainedValue() var port = url.port if port == nil { - if ["wss", "https"].contains(url.scheme) { + if supportedSSLSchemes.contains(url.scheme!) { port = 443 } else { port = 80 @@ -228,7 +260,7 @@ public class WebSocket : NSObject, NSStreamDelegate { addHeader(urlRequest, key: headerWSUpgradeName, val: headerWSUpgradeValue) addHeader(urlRequest, key: headerWSConnectionName, val: headerWSConnectionValue) if let protocols = optionalProtocols { - addHeader(urlRequest, key: headerWSProtocolName, val: protocols.joinWithSeparator(",")) + addHeader(urlRequest, key: headerWSProtocolName, val: protocols.joined(separator: ",")) } addHeader(urlRequest, key: headerWSVersionName, val: headerWSVersionValue) addHeader(urlRequest, key: headerWSKeyName, val: generateWebSocketKey()) @@ -236,79 +268,89 @@ public class WebSocket : NSObject, NSStreamDelegate { addHeader(urlRequest, key: headerOriginName, val: origin) } addHeader(urlRequest, key: headerWSHostName, val: "\(url.host!):\(port!)") - for (key,value) in headers { + for (key, value) in headers { addHeader(urlRequest, key: key, val: value) } if let cfHTTPMessage = CFHTTPMessageCopySerializedMessage(urlRequest) { let serializedRequest = cfHTTPMessage.takeRetainedValue() - initStreamsWithData(serializedRequest, Int(port!)) + initStreamsWithData(serializedRequest as Data, Int(port!)) } } - //Add a header to the CFHTTPMessage by using the NSString bridges to CFString - private func addHeader(urlRequest: CFHTTPMessage, key: NSString, val: NSString) { - CFHTTPMessageSetHeaderFieldValue(urlRequest, key, val) + /** + Add a header to the CFHTTPMessage by using the NSString bridges to CFString + */ + private func addHeader(_ urlRequest: CFHTTPMessage, key: String, val: String) { + CFHTTPMessageSetHeaderFieldValue(urlRequest, key as CFString, val as CFString) } - //generate a websocket key as needed in rfc + /** + Generate a WebSocket key as needed in RFC. + */ private func generateWebSocketKey() -> String { var key = "" let seed = 16 for _ in 0..? var writeStream: Unmanaged? - let h: NSString = url.host! + let h = url.host! as NSString CFStreamCreatePairWithSocketToHost(nil, h, UInt32(port), &readStream, &writeStream) inputStream = readStream!.takeRetainedValue() outputStream = writeStream!.takeRetainedValue() guard let inStream = inputStream, let outStream = outputStream else { return } inStream.delegate = self outStream.delegate = self - if ["wss", "https"].contains(url.scheme) { - inStream.setProperty(NSStreamSocketSecurityLevelNegotiatedSSL, forKey: NSStreamSocketSecurityLevelKey) - outStream.setProperty(NSStreamSocketSecurityLevelNegotiatedSSL, forKey: NSStreamSocketSecurityLevelKey) + if supportedSSLSchemes.contains(url.scheme!) { + certValidated = false + inStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey) + outStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: Stream.PropertyKey.socketSecurityLevelKey) + if disableSSLCertValidation { + let settings: [NSObject: NSObject] = [kCFStreamSSLValidatesCertificateChain: NSNumber(value: false), kCFStreamSSLPeerName: kCFNull] + inStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) + outStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) + } + if let cipherSuites = self.enabledSSLCipherSuites { + if let sslContextIn = CFReadStreamCopyProperty(inputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext?, + let sslContextOut = CFWriteStreamCopyProperty(outputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext? { + let resIn = SSLSetEnabledCiphers(sslContextIn, cipherSuites, cipherSuites.count) + let resOut = SSLSetEnabledCiphers(sslContextOut, cipherSuites, cipherSuites.count) + if resIn != errSecSuccess { + let error = self.errorWithDetail("Error setting ingoing cypher suites", code: UInt16(resIn)) + disconnectStream(error) + return + } + if resOut != errSecSuccess { + let error = self.errorWithDetail("Error setting outgoing cypher suites", code: UInt16(resOut)) + disconnectStream(error) + return + } + } + } } else { certValidated = true //not a https session, so no need to check SSL pinning } if voipEnabled { - inStream.setProperty(NSStreamNetworkServiceTypeVoIP, forKey: NSStreamNetworkServiceType) - outStream.setProperty(NSStreamNetworkServiceTypeVoIP, forKey: NSStreamNetworkServiceType) - } - if selfSignedSSL { - let settings: [NSObject: NSObject] = [kCFStreamSSLValidatesCertificateChain: NSNumber(bool:false), kCFStreamSSLPeerName: kCFNull] - inStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as String) - outStream.setProperty(settings, forKey: kCFStreamPropertySSLSettings as String) - } - if let cipherSuites = self.enabledSSLCipherSuites { - if let sslContextIn = CFReadStreamCopyProperty(inputStream, kCFStreamPropertySSLContext) as! SSLContextRef?, - sslContextOut = CFWriteStreamCopyProperty(outputStream, kCFStreamPropertySSLContext) as! SSLContextRef? { - let resIn = SSLSetEnabledCiphers(sslContextIn, cipherSuites, cipherSuites.count) - let resOut = SSLSetEnabledCiphers(sslContextOut, cipherSuites, cipherSuites.count) - if resIn != errSecSuccess { - let error = self.errorWithDetail("Error setting ingoing cypher suites", code: UInt16(resIn)) - disconnectStream(error) - return - } - if resOut != errSecSuccess { - let error = self.errorWithDetail("Error setting outgoing cypher suites", code: UInt16(resOut)) - disconnectStream(error) - return - } - } + inStream.setProperty(StreamNetworkServiceTypeValue.voIP as AnyObject, forKey: Stream.PropertyKey.networkServiceType) + outStream.setProperty(StreamNetworkServiceTypeValue.voIP as AnyObject, forKey: Stream.PropertyKey.networkServiceType) } + CFReadStreamSetDispatchQueue(inStream, WebSocket.sharedWorkQueue) CFWriteStreamSetDispatchQueue(outStream, WebSocket.sharedWorkQueue) inStream.open() @@ -318,60 +360,78 @@ public class WebSocket : NSObject, NSStreamDelegate { self.readyToWrite = true self.mutex.unlock() - let bytes = UnsafePointer(data.bytes) - var timeout = 5000000 //wait 5 seconds before giving up - writeQueue.addOperationWithBlock { [weak self] in - while !outStream.hasSpaceAvailable { - usleep(100) //wait until the socket is ready - timeout -= 100 - if timeout < 0 { - self?.cleanupStream() + let bytes = UnsafeRawPointer((data as NSData).bytes).assumingMemoryBound(to: UInt8.self) + var out = timeout * 1_000_000 // wait 5 seconds before giving up + let operation = BlockOperation() + operation.addExecutionBlock { [weak self, weak operation] in + guard let sOperation = operation else { return } + while !outStream.hasSpaceAvailable && !sOperation.isCancelled { + usleep(100) // wait until the socket is ready + guard !sOperation.isCancelled else { return } + out -= 100 + if out < 0 { + WebSocket.sharedWorkQueue.async { + self?.cleanupStream() + } self?.doDisconnect(self?.errorWithDetail("write wait timed out", code: 2)) return } else if outStream.streamError != nil { - return //disconnectStream will be called. + return // disconnectStream will be called. } } - outStream.write(bytes, maxLength: data.length) - } - } - //delegate for the stream methods. Processes incoming bytes - public func stream(aStream: NSStream, handleEvent eventCode: NSStreamEvent) { - - if let sec = security where !certValidated && [.HasBytesAvailable, .HasSpaceAvailable].contains(eventCode) { - let possibleTrust: AnyObject? = aStream.propertyForKey(kCFStreamPropertySSLPeerTrust as String) - if let trust: AnyObject = possibleTrust { - let domain: AnyObject? = aStream.propertyForKey(kCFStreamSSLPeerName as String) - if sec.isValid(trust as! SecTrustRef, domain: domain as! String?) { - certValidated = true - } else { - let error = errorWithDetail("Invalid SSL certificate", code: 1) - disconnectStream(error) + guard !sOperation.isCancelled, let s = self else { return } + // Do the pinning now if needed + if let sec = s.security, !s.certValidated { + let trust = outStream.property(forKey: kCFStreamPropertySSLPeerTrust as Stream.PropertyKey) as! SecTrust + let domain = outStream.property(forKey: kCFStreamSSLPeerName as Stream.PropertyKey) as? String + s.certValidated = sec.isValid(trust, domain: domain) + if !s.certValidated { + WebSocket.sharedWorkQueue.async { + let error = s.errorWithDetail("Invalid SSL certificate", code: 1) + s.disconnectStream(error) + } return } } + outStream.write(bytes, maxLength: data.count) } - if eventCode == .HasBytesAvailable { + writeQueue.addOperation(operation) + } + + /** + Delegate for the stream methods. Processes incoming bytes + */ + open func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + if eventCode == .hasBytesAvailable { if aStream == inputStream { processInputStream() } - } else if eventCode == .ErrorOccurred { - disconnectStream(aStream.streamError) - } else if eventCode == .EndEncountered { + } else if eventCode == .errorOccurred { + disconnectStream(aStream.streamError as NSError?) + } else if eventCode == .endEncountered { disconnectStream(nil) } } - //disconnect the stream object - private func disconnectStream(error: NSError?) { + + /** + Disconnect the stream object and notifies the delegate. + */ + private func disconnectStream(_ error: NSError?, runDelegate: Bool = true) { if error == nil { writeQueue.waitUntilAllOperationsAreFinished() } else { writeQueue.cancelAllOperations() } cleanupStream() - doDisconnect(error) + connected = false + if runDelegate { + doDisconnect(error) + } } + /** + cleanup the streams. + */ private func cleanupStream() { outputStream?.delegate = nil inputStream?.delegate = nil @@ -385,68 +445,73 @@ public class WebSocket : NSObject, NSStreamDelegate { } outputStream = nil inputStream = nil + fragBuffer = nil } - ///handles the incoming bytes and sending them to the proper processing method + /** + Handles the incoming bytes and sending them to the proper processing method. + */ private func processInputStream() { let buf = NSMutableData(capacity: BUFFER_MAX) - let buffer = UnsafeMutablePointer(buf!.bytes) + let buffer = UnsafeMutableRawPointer(mutating: buf!.bytes).assumingMemoryBound(to: UInt8.self) let length = inputStream!.read(buffer, maxLength: BUFFER_MAX) - guard length > 0 else { return } var process = false if inputQueue.count == 0 { process = true } - inputQueue.append(NSData(bytes: buffer, length: length)) + inputQueue.append(Data(bytes: buffer, count: length)) if process { dequeueInput() } } - ///dequeue the incoming input so it is processed in order + + /** + Dequeue the incoming input so it is processed in order. + */ private func dequeueInput() { - guard !inputQueue.isEmpty else { return } - - let data = inputQueue[0] - var work = data - if let fragBuffer = fragBuffer { - let combine = NSMutableData(data: fragBuffer) - combine.appendData(data) - work = combine - self.fragBuffer = nil - } - let buffer = UnsafePointer(work.bytes) - let length = work.length - if !connected { - processTCPHandshake(buffer, bufferLen: length) - } else { - processRawMessage(buffer, bufferLen: length) + while !inputQueue.isEmpty { + autoreleasepool { + let data = inputQueue[0] + var work = data + if let buffer = fragBuffer { + var combine = NSData(data: buffer) as Data + combine.append(data) + work = combine + fragBuffer = nil + } + let buffer = UnsafeRawPointer((work as NSData).bytes).assumingMemoryBound(to: UInt8.self) + let length = work.count + if !connected { + processTCPHandshake(buffer, bufferLen: length) + } else { + processRawMessagesInBuffer(buffer, bufferLen: length) + } + inputQueue = inputQueue.filter{ $0 != data } + } } - inputQueue = inputQueue.filter{$0 != data} - dequeueInput() } - //handle checking the inital connection status - private func processTCPHandshake(buffer: UnsafePointer, bufferLen: Int) { + /** + Handle checking the inital connection status + */ + private func processTCPHandshake(_ buffer: UnsafePointer, bufferLen: Int) { let code = processHTTP(buffer, bufferLen: bufferLen) switch code { case 0: - connected = true - guard canDispatch else {return} - dispatch_async(queue) { [weak self] in - guard let s = self else { return } - s.onConnect?() - s.delegate?.websocketDidConnect(s) - } + break case -1: - fragBuffer = NSData(bytes: buffer, length: bufferLen) - break //do nothing, we are going to collect more data + fragBuffer = Data(bytes: buffer, count: bufferLen) + break // do nothing, we are going to collect more data default: doDisconnect(errorWithDetail("Invalid HTTP upgrade", code: UInt16(code))) } } - ///Finds the HTTP Packet in the TCP stream, by looking for the CRLF. - private func processHTTP(buffer: UnsafePointer, bufferLen: Int) -> Int { + + /** + Finds the HTTP Packet in the TCP stream, by looking for the CRLF. + */ + private func processHTTP(_ buffer: UnsafePointer, bufferLen: Int) -> Int { let CRLFBytes = [UInt8(ascii: "\r"), UInt8(ascii: "\n"), UInt8(ascii: "\r"), UInt8(ascii: "\n")] var k = 0 var totalSize = 0 @@ -466,27 +531,40 @@ public class WebSocket : NSObject, NSStreamDelegate { if code != 0 { return code } + isConnecting = false + connected = true + didDisconnect = false + if canDispatch { + callbackQueue.async { [weak self] in + guard let s = self else { return } + s.onConnect?() + s.delegate?.websocketDidConnect(socket: s) + s.notificationCenter.post(name: NSNotification.Name(WebsocketDidConnectNotification), object: self) + } + } totalSize += 1 //skip the last \n let restSize = bufferLen - totalSize if restSize > 0 { - processRawMessage((buffer+totalSize),bufferLen: restSize) + processRawMessagesInBuffer(buffer + totalSize, bufferLen: restSize) } return 0 //success } - return -1 //was unable to find the full TCP header + return -1 // Was unable to find the full TCP header. } - ///validates the HTTP is a 101 as per the RFC spec - private func validateResponse(buffer: UnsafePointer, bufferLen: Int) -> Int { + /** + Validates the HTTP is a 101 as per the RFC spec. + */ + private func validateResponse(_ buffer: UnsafePointer, bufferLen: Int) -> Int { let response = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, false).takeRetainedValue() CFHTTPMessageAppendBytes(response, buffer, bufferLen) let code = CFHTTPMessageGetResponseStatusCode(response) - if code != 101 { + if code != httpSwitchProtocolCode { return code } if let cfHeaders = CFHTTPMessageCopyAllHeaderFields(response) { let headers = cfHeaders.takeRetainedValue() as NSDictionary - if let acceptKey = headers[headerWSAcceptName] as? NSString { + if let acceptKey = headers[headerWSAcceptName as NSString] as? NSString { if acceptKey.length > 0 { return 0 } @@ -495,13 +573,17 @@ public class WebSocket : NSObject, NSStreamDelegate { return -1 } - ///read a 16 bit big endian value from a buffer - private static func readUint16(buffer: UnsafePointer, offset: Int) -> UInt16 { + /** + Read a 16 bit big endian value from a buffer + */ + private static func readUint16(_ buffer: UnsafePointer, offset: Int) -> UInt16 { return (UInt16(buffer[offset + 0]) << 8) | UInt16(buffer[offset + 1]) } - ///read a 64 bit big endian value from a buffer - private static func readUint64(buffer: UnsafePointer, offset: Int) -> UInt64 { + /** + Read a 64 bit big endian value from a buffer + */ + private static func readUint64(_ buffer: UnsafePointer, offset: Int) -> UInt64 { var value = UInt64(0) for i in 0...7 { value = (value << 8) | UInt64(buffer[offset + i]) @@ -509,27 +591,35 @@ public class WebSocket : NSObject, NSStreamDelegate { return value } - ///write a 16 bit big endian value to a buffer - private static func writeUint16(buffer: UnsafeMutablePointer, offset: Int, value: UInt16) { + /** + Write a 16-bit big endian value to a buffer. + */ + private static func writeUint16(_ buffer: UnsafeMutablePointer, offset: Int, value: UInt16) { buffer[offset + 0] = UInt8(value >> 8) buffer[offset + 1] = UInt8(value & 0xff) } - ///write a 64 bit big endian value to a buffer - private static func writeUint64(buffer: UnsafeMutablePointer, offset: Int, value: UInt64) { + /** + Write a 64-bit big endian value to a buffer. + */ + private static func writeUint64(_ buffer: UnsafeMutablePointer, offset: Int, value: UInt64) { for i in 0...7 { buffer[offset + i] = UInt8((value >> (8*UInt64(7 - i))) & 0xff) } } - ///process the websocket data - private func processRawMessage(buffer: UnsafePointer, bufferLen: Int) { + /** + Process one message at the start of `buffer`. Return another buffer (sharing storage) that contains the leftover contents of `buffer` that I didn't process. + */ + private func processOneRawMessage(inBuffer buffer: UnsafeBufferPointer) -> UnsafeBufferPointer { let response = readStack.last - if response != nil && bufferLen < 2 { - fragBuffer = NSData(bytes: buffer, length: bufferLen) - return + guard let baseAddress = buffer.baseAddress else {return emptyBuffer} + let bufferLen = buffer.count + if response != nil && bufferLen < 2 { + fragBuffer = Data(buffer: buffer) + return emptyBuffer } - if let response = response where response.bytesLeft > 0 { + if let response = response, response.bytesLeft > 0 { var len = response.bytesLeft var extra = bufferLen - response.bytesLeft if response.bytesLeft > bufferLen { @@ -537,124 +627,118 @@ public class WebSocket : NSObject, NSStreamDelegate { extra = 0 } response.bytesLeft -= len - response.buffer?.appendData(NSData(bytes: buffer, length: len)) - processResponse(response) - let offset = bufferLen - extra - if extra > 0 { - processExtra((buffer+offset), bufferLen: extra) - } - return + response.buffer?.append(Data(bytes: baseAddress, count: len)) + _ = processResponse(response) + return buffer.fromOffset(bufferLen - extra) } else { - let isFin = (FinMask & buffer[0]) - let receivedOpcode = OpCode(rawValue: (OpCodeMask & buffer[0])) - let isMasked = (MaskMask & buffer[1]) - let payloadLen = (PayloadLenMask & buffer[1]) + let isFin = (FinMask & baseAddress[0]) + let receivedOpcodeRawValue = (OpCodeMask & baseAddress[0]) + let receivedOpcode = OpCode(rawValue: receivedOpcodeRawValue) + let isMasked = (MaskMask & baseAddress[1]) + let payloadLen = (PayloadLenMask & baseAddress[1]) var offset = 2 - if (isMasked > 0 || (RSVMask & buffer[0]) > 0) && receivedOpcode != .Pong { - let errCode = CloseCode.ProtocolError.rawValue + if (isMasked > 0 || (RSVMask & baseAddress[0]) > 0) && receivedOpcode != .pong { + let errCode = CloseCode.protocolError.rawValue doDisconnect(errorWithDetail("masked and rsv data is not currently supported", code: errCode)) writeError(errCode) - return + return emptyBuffer } - let isControlFrame = (receivedOpcode == .ConnectionClose || receivedOpcode == .Ping) - if !isControlFrame && (receivedOpcode != .BinaryFrame && receivedOpcode != .ContinueFrame && - receivedOpcode != .TextFrame && receivedOpcode != .Pong) { - let errCode = CloseCode.ProtocolError.rawValue - doDisconnect(errorWithDetail("unknown opcode: \(receivedOpcode)", code: errCode)) + let isControlFrame = (receivedOpcode == .connectionClose || receivedOpcode == .ping) + if !isControlFrame && (receivedOpcode != .binaryFrame && receivedOpcode != .continueFrame && + receivedOpcode != .textFrame && receivedOpcode != .pong) { + let errCode = CloseCode.protocolError.rawValue + doDisconnect(errorWithDetail("unknown opcode: \(receivedOpcodeRawValue)", code: errCode)) writeError(errCode) - return + return emptyBuffer } if isControlFrame && isFin == 0 { - let errCode = CloseCode.ProtocolError.rawValue + let errCode = CloseCode.protocolError.rawValue doDisconnect(errorWithDetail("control frames can't be fragmented", code: errCode)) writeError(errCode) - return + return emptyBuffer } - if receivedOpcode == .ConnectionClose { - var code = CloseCode.Normal.rawValue + var closeCode = CloseCode.normal.rawValue + if receivedOpcode == .connectionClose { if payloadLen == 1 { - code = CloseCode.ProtocolError.rawValue + closeCode = CloseCode.protocolError.rawValue } else if payloadLen > 1 { - code = WebSocket.readUint16(buffer, offset: offset) - if code < 1000 || (code > 1003 && code < 1007) || (code > 1011 && code < 3000) { - code = CloseCode.ProtocolError.rawValue + closeCode = WebSocket.readUint16(baseAddress, offset: offset) + if closeCode < 1000 || (closeCode > 1003 && closeCode < 1007) || (closeCode > 1011 && closeCode < 3000) { + closeCode = CloseCode.protocolError.rawValue } - offset += 2 } - if payloadLen > 2 { - let len = Int(payloadLen-2) - if len > 0 { - let bytes = UnsafePointer((buffer+offset)) - let str: NSString? = NSString(data: NSData(bytes: bytes, length: len), encoding: NSUTF8StringEncoding) - if str == nil { - code = CloseCode.ProtocolError.rawValue - } - } + if payloadLen < 2 { + doDisconnect(errorWithDetail("connection closed by server", code: closeCode)) + writeError(closeCode) + return emptyBuffer } - doDisconnect(errorWithDetail("connection closed by server", code: code)) - writeError(code) - return - } - if isControlFrame && payloadLen > 125 { - writeError(CloseCode.ProtocolError.rawValue) - return + } else if isControlFrame && payloadLen > 125 { + writeError(CloseCode.protocolError.rawValue) + return emptyBuffer } var dataLength = UInt64(payloadLen) if dataLength == 127 { - dataLength = WebSocket.readUint64(buffer, offset: offset) - offset += sizeof(UInt64) + dataLength = WebSocket.readUint64(baseAddress, offset: offset) + offset += MemoryLayout.size } else if dataLength == 126 { - dataLength = UInt64(WebSocket.readUint16(buffer, offset: offset)) - offset += sizeof(UInt16) + dataLength = UInt64(WebSocket.readUint16(baseAddress, offset: offset)) + offset += MemoryLayout.size } if bufferLen < offset || UInt64(bufferLen - offset) < dataLength { - fragBuffer = NSData(bytes: buffer, length: bufferLen) - return + fragBuffer = Data(bytes: baseAddress, count: bufferLen) + return emptyBuffer } var len = dataLength if dataLength > UInt64(bufferLen) { len = UInt64(bufferLen-offset) } - let data: NSData - if len < 0 { - len = 0 - data = NSData() - } else { - data = NSData(bytes: UnsafePointer((buffer+offset)), length: Int(len)) + if receivedOpcode == .connectionClose && len > 0 { + let size = MemoryLayout.size + offset += size + len -= UInt64(size) } - if receivedOpcode == .Pong { + let data = Data(bytes: baseAddress+offset, count: Int(len)) + + if receivedOpcode == .connectionClose { + var closeReason = "connection closed by server" + if let customCloseReason = String(data: data, encoding: .utf8) { + closeReason = customCloseReason + } else { + closeCode = CloseCode.protocolError.rawValue + } + doDisconnect(errorWithDetail(closeReason, code: closeCode)) + writeError(closeCode) + return emptyBuffer + } + if receivedOpcode == .pong { if canDispatch { - dispatch_async(queue) { [weak self] in + callbackQueue.async { [weak self] in guard let s = self else { return } - s.onPong?() - s.pongDelegate?.websocketDidReceivePong(s) + let pongData: Data? = data.count > 0 ? data : nil + s.onPong?(pongData) + s.pongDelegate?.websocketDidReceivePong(socket: s, data: pongData) } } - let step = Int(offset+numericCast(len)) - let extra = bufferLen-step - if extra > 0 { - processRawMessage((buffer+step), bufferLen: extra) - } - return + return buffer.fromOffset(offset + Int(len)) } var response = readStack.last if isControlFrame { - response = nil //don't append pings + response = nil // Don't append pings. } - if isFin == 0 && receivedOpcode == .ContinueFrame && response == nil { - let errCode = CloseCode.ProtocolError.rawValue + if isFin == 0 && receivedOpcode == .continueFrame && response == nil { + let errCode = CloseCode.protocolError.rawValue doDisconnect(errorWithDetail("continue frame before a binary or text frame", code: errCode)) writeError(errCode) - return + return emptyBuffer } var isNew = false if response == nil { - if receivedOpcode == .ContinueFrame { - let errCode = CloseCode.ProtocolError.rawValue + if receivedOpcode == .continueFrame { + let errCode = CloseCode.protocolError.rawValue doDisconnect(errorWithDetail("first frame can't be a continue frame", - code: errCode)) + code: errCode)) writeError(errCode) - return + return emptyBuffer } isNew = true response = WSResponse() @@ -662,16 +746,16 @@ public class WebSocket : NSObject, NSStreamDelegate { response!.bytesLeft = Int(dataLength) response!.buffer = NSMutableData(data: data) } else { - if receivedOpcode == .ContinueFrame { + if receivedOpcode == .continueFrame { response!.bytesLeft = Int(dataLength) } else { - let errCode = CloseCode.ProtocolError.rawValue + let errCode = CloseCode.protocolError.rawValue doDisconnect(errorWithDetail("second and beyond of fragment message must be a continue frame", - code: errCode)) + code: errCode)) writeError(errCode) - return + return emptyBuffer } - response!.buffer!.appendData(data) + response!.buffer!.append(data) } if let response = response { response.bytesLeft -= Int(len) @@ -680,53 +764,55 @@ public class WebSocket : NSObject, NSStreamDelegate { if isNew { readStack.append(response) } - processResponse(response) + _ = processResponse(response) } - let step = Int(offset+numericCast(len)) - let extra = bufferLen-step - if extra > 0 { - processExtra((buffer+step), bufferLen: extra) - } + let step = Int(offset + numericCast(len)) + return buffer.fromOffset(step) } - } - ///process the extra of a buffer - private func processExtra(buffer: UnsafePointer, bufferLen: Int) { - if bufferLen < 2 { - fragBuffer = NSData(bytes: buffer, length: bufferLen) - } else { - processRawMessage(buffer, bufferLen: bufferLen) + /** + Process all messages in the buffer if possible. + */ + private func processRawMessagesInBuffer(_ pointer: UnsafePointer, bufferLen: Int) { + var buffer = UnsafeBufferPointer(start: pointer, count: bufferLen) + repeat { + buffer = processOneRawMessage(inBuffer: buffer) + } while buffer.count >= 2 + if buffer.count > 0 { + fragBuffer = Data(buffer: buffer) } } - ///process the finished response of a buffer - private func processResponse(response: WSResponse) -> Bool { + /** + Process the finished response of a buffer. + */ + private func processResponse(_ response: WSResponse) -> Bool { if response.isFin && response.bytesLeft <= 0 { - if response.code == .Ping { - let data = response.buffer! //local copy so it is perverse for writing - dequeueWrite(data, code: OpCode.Pong) - } else if response.code == .TextFrame { - let str: NSString? = NSString(data: response.buffer!, encoding: NSUTF8StringEncoding) + if response.code == .ping { + let data = response.buffer! // local copy so it is perverse for writing + dequeueWrite(data as Data, code: .pong) + } else if response.code == .textFrame { + let str: NSString? = NSString(data: response.buffer! as Data, encoding: String.Encoding.utf8.rawValue) if str == nil { - writeError(CloseCode.Encoding.rawValue) + writeError(CloseCode.encoding.rawValue) return false } if canDispatch { - dispatch_async(queue) { [weak self] in + callbackQueue.async { [weak self] in guard let s = self else { return } s.onText?(str! as String) - s.delegate?.websocketDidReceiveMessage(s, text: str! as String) + s.delegate?.websocketDidReceiveMessage(socket: s, text: str! as String) } } - } else if response.code == .BinaryFrame { + } else if response.code == .binaryFrame { if canDispatch { - let data = response.buffer! //local copy so it is perverse for writing - dispatch_async(queue) { [weak self] in + let data = response.buffer! // local copy so it is perverse for writing + callbackQueue.async { [weak self] in guard let s = self else { return } - s.onData?(data) - s.delegate?.websocketDidReceiveData(s, data: data) + s.onData?(data as Data) + s.delegate?.websocketDidReceiveData(socket: s, data: data as Data) } } } @@ -736,72 +822,80 @@ public class WebSocket : NSObject, NSStreamDelegate { return false } - ///Create an error - private func errorWithDetail(detail: String, code: UInt16) -> NSError { + /** + Create an error + */ + private func errorWithDetail(_ detail: String, code: UInt16) -> NSError { var details = [String: String]() details[NSLocalizedDescriptionKey] = detail return NSError(domain: WebSocket.ErrorDomain, code: Int(code), userInfo: details) } - ///write a an error to the socket - private func writeError(code: UInt16) { - let buf = NSMutableData(capacity: sizeof(UInt16)) - let buffer = UnsafeMutablePointer(buf!.bytes) + /** + Write an error to the socket + */ + private func writeError(_ code: UInt16) { + let buf = NSMutableData(capacity: MemoryLayout.size) + let buffer = UnsafeMutableRawPointer(mutating: buf!.bytes).assumingMemoryBound(to: UInt8.self) WebSocket.writeUint16(buffer, offset: 0, value: code) - dequeueWrite(NSData(bytes: buffer, length: sizeof(UInt16)), code: .ConnectionClose) + dequeueWrite(Data(bytes: buffer, count: MemoryLayout.size), code: .connectionClose) } - ///used to write things to the stream - private func dequeueWrite(data: NSData, code: OpCode, writeCompletion: (() -> ())? = nil) { - writeQueue.addOperationWithBlock { [weak self] in + + /** + Used to write things to the stream + */ + private func dequeueWrite(_ data: Data, code: OpCode, writeCompletion: (() -> ())? = nil) { + let operation = BlockOperation() + operation.addExecutionBlock { [weak self, weak operation] in //stream isn't ready, let's wait guard let s = self else { return } + guard let sOperation = operation else { return } var offset = 2 - let bytes = UnsafeMutablePointer(data.bytes) - let dataLength = data.length + let dataLength = data.count let frame = NSMutableData(capacity: dataLength + s.MaxFrameSize) - let buffer = UnsafeMutablePointer(frame!.mutableBytes) + let buffer = UnsafeMutableRawPointer(frame!.mutableBytes).assumingMemoryBound(to: UInt8.self) buffer[0] = s.FinMask | code.rawValue if dataLength < 126 { buffer[1] = CUnsignedChar(dataLength) } else if dataLength <= Int(UInt16.max) { buffer[1] = 126 WebSocket.writeUint16(buffer, offset: offset, value: UInt16(dataLength)) - offset += sizeof(UInt16) + offset += MemoryLayout.size } else { buffer[1] = 127 WebSocket.writeUint64(buffer, offset: offset, value: UInt64(dataLength)) - offset += sizeof(UInt64) + offset += MemoryLayout.size } buffer[1] |= s.MaskMask let maskKey = UnsafeMutablePointer(buffer + offset) - SecRandomCopyBytes(kSecRandomDefault, Int(sizeof(UInt32)), maskKey) - offset += sizeof(UInt32) + _ = SecRandomCopyBytes(kSecRandomDefault, Int(MemoryLayout.size), maskKey) + offset += MemoryLayout.size for i in 0...size] offset += 1 } var total = 0 - while true { + while !sOperation.isCancelled { guard let outStream = s.outputStream else { break } - let writeBuffer = UnsafePointer(frame!.bytes+total) + let writeBuffer = UnsafeRawPointer(frame!.bytes+total).assumingMemoryBound(to: UInt8.self) let len = outStream.write(writeBuffer, maxLength: offset-total) if len < 0 { - var error: NSError? + var error: Error? if let streamError = outStream.streamError { error = streamError } else { - let errCode = InternalErrorCode.OutputStreamWriteError.rawValue + let errCode = InternalErrorCode.outputStreamWriteError.rawValue error = s.errorWithDetail("output stream error during write", code: errCode) } - s.doDisconnect(error) + s.doDisconnect(error as NSError?) break } else { total += len } if total >= offset { - if let queue = self?.queue, callback = writeCompletion { - dispatch_async(queue) { + if let queue = self?.callbackQueue, let callback = writeCompletion { + queue.async { callback() } } @@ -809,262 +903,53 @@ public class WebSocket : NSObject, NSStreamDelegate { break } } - } + writeQueue.addOperation(operation) } - ///used to preform the disconnect delegate - private func doDisconnect(error: NSError?) { + /** + Used to preform the disconnect delegate + */ + private func doDisconnect(_ error: NSError?) { guard !didDisconnect else { return } didDisconnect = true + isConnecting = false connected = false guard canDispatch else {return} - dispatch_async(queue) { [weak self] in + callbackQueue.async { [weak self] in guard let s = self else { return } s.onDisconnect?(error) - s.delegate?.websocketDidDisconnect(s, error: error) + s.delegate?.websocketDidDisconnect(socket: s, error: error) + let userInfo = error.map{ [WebsocketDisconnectionErrorKeyName: $0] } + s.notificationCenter.post(name: NSNotification.Name(WebsocketDidDisconnectNotification), object: self, userInfo: userInfo) } } + // MARK: - Deinit deinit { mutex.lock() readyToWrite = false mutex.unlock() cleanupStream() + writeQueue.cancelAllOperations() } } -public class SSLCert { - var certData: NSData? - var key: SecKeyRef? +private extension Data { - /** - Designated init for certificates - - - parameter data: is the binary data of the certificate - - - returns: a representation security object to be used with - */ - public init(data: NSData) { - self.certData = data + init(buffer: UnsafeBufferPointer) { + self.init(bytes: buffer.baseAddress!, count: buffer.count) } - /** - Designated init for public keys - - - parameter key: is the public key to be used - - - returns: a representation security object to be used with - */ - public init(key: SecKeyRef) { - self.key = key - } } -public class SSLSecurity { - public var validatedDN = true //should the domain name be validated? - - var isReady = false //is the key processing done? - var certificates: [NSData]? //the certificates - var pubKeys: [SecKeyRef]? //the public keys - var usePublicKeys = false //use public keys or certificate validation? - - /** - Use certs from main app bundle - - - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning validation - - - returns: a representation security object to be used with - */ - public convenience init(usePublicKeys: Bool = false) { - let paths = NSBundle.mainBundle().pathsForResourcesOfType("cer", inDirectory: ".") - - let certs = paths.reduce([SSLCert]()) { (certs: [SSLCert], path: String) -> [SSLCert] in - var certs = certs - if let data = NSData(contentsOfFile: path) { - certs.append(SSLCert(data: data)) - } - return certs - } - - self.init(certs: certs, usePublicKeys: usePublicKeys) - } - - /** - Designated init - - - parameter keys: is the certificates or public keys to use - - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning validation - - - returns: a representation security object to be used with - */ - public init(certs: [SSLCert], usePublicKeys: Bool) { - self.usePublicKeys = usePublicKeys - - if self.usePublicKeys { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)) { - let pubKeys = certs.reduce([SecKeyRef]()) { (pubKeys: [SecKeyRef], cert: SSLCert) -> [SecKeyRef] in - var pubKeys = pubKeys - if let data = cert.certData where cert.key == nil { - cert.key = self.extractPublicKey(data) - } - if let key = cert.key { - pubKeys.append(key) - } - return pubKeys - } - - self.pubKeys = pubKeys - self.isReady = true - } - } else { - let certificates = certs.reduce([NSData]()) { (certificates: [NSData], cert: SSLCert) -> [NSData] in - var certificates = certificates - if let data = cert.certData { - certificates.append(data) - } - return certificates - } - self.certificates = certificates - self.isReady = true - } - } +private extension UnsafeBufferPointer { - /** - Valid the trust and domain name. - - - parameter trust: is the serverTrust to validate - - parameter domain: is the CN domain to validate - - - returns: if the key was successfully validated - */ - public func isValid(trust: SecTrustRef, domain: String?) -> Bool { - - var tries = 0 - while(!self.isReady) { - usleep(1000) - tries += 1 - if tries > 5 { - return false //doesn't appear it is going to ever be ready... - } - } - var policy: SecPolicyRef - if self.validatedDN { - policy = SecPolicyCreateSSL(true, domain) - } else { - policy = SecPolicyCreateBasicX509() - } - SecTrustSetPolicies(trust,policy) - if self.usePublicKeys { - if let keys = self.pubKeys { - let serverPubKeys = publicKeyChainForTrust(trust) - for serverKey in serverPubKeys as [AnyObject] { - for key in keys as [AnyObject] { - if serverKey.isEqual(key) { - return true - } - } - } - } - } else if let certs = self.certificates { - let serverCerts = certificateChainForTrust(trust) - var collect = [SecCertificate]() - for cert in certs { - collect.append(SecCertificateCreateWithData(nil,cert)!) - } - SecTrustSetAnchorCertificates(trust,collect) - var result: SecTrustResultType = 0 - SecTrustEvaluate(trust,&result) - let r = Int(result) - if r == kSecTrustResultUnspecified || r == kSecTrustResultProceed { - var trustedCount = 0 - for serverCert in serverCerts { - for cert in certs { - if cert == serverCert { - trustedCount += 1 - break - } - } - } - if trustedCount == serverCerts.count { - return true - } - } - } - return false + func fromOffset(_ offset: Int) -> UnsafeBufferPointer { + return UnsafeBufferPointer(start: baseAddress?.advanced(by: offset), count: count - offset) } - /** - Get the public key from a certificate data - - - parameter data: is the certificate to pull the public key from - - - returns: a public key - */ - func extractPublicKey(data: NSData) -> SecKeyRef? { - guard let cert = SecCertificateCreateWithData(nil, data) else { return nil } - - return extractPublicKeyFromCert(cert, policy: SecPolicyCreateBasicX509()) - } - - /** - Get the public key from a certificate - - - parameter data: is the certificate to pull the public key from - - - returns: a public key - */ - func extractPublicKeyFromCert(cert: SecCertificate, policy: SecPolicy) -> SecKeyRef? { - var possibleTrust: SecTrust? - SecTrustCreateWithCertificates(cert, policy, &possibleTrust) - - guard let trust = possibleTrust else { return nil } - - var result: SecTrustResultType = 0 - SecTrustEvaluate(trust, &result) - return SecTrustCopyPublicKey(trust) - } - - /** - Get the certificate chain for the trust - - - parameter trust: is the trust to lookup the certificate chain for - - - returns: the certificate chain for the trust - */ - func certificateChainForTrust(trust: SecTrustRef) -> [NSData] { - let certificates = (0.. [NSData] in - var certificates = certificates - let cert = SecTrustGetCertificateAtIndex(trust, index) - certificates.append(SecCertificateCopyData(cert!)) - return certificates - } - - return certificates - } - - /** - Get the public key chain for the trust - - - parameter trust: is the trust to lookup the certificate chain and extract the public keys - - - returns: the public keys from the certifcate chain for the trust - */ - func publicKeyChainForTrust(trust: SecTrustRef) -> [SecKeyRef] { - let policy = SecPolicyCreateBasicX509() - let keys = (0.. [SecKeyRef] in - var keys = keys - let cert = SecTrustGetCertificateAtIndex(trust, index) - if let key = extractPublicKeyFromCert(cert!, policy: policy) { - keys.append(key) - } - - return keys - } - - return keys - } - - -} \ No newline at end of file +} + +private let emptyBuffer = UnsafeBufferPointer(start: nil, count: 0)