├── .swift-version ├── Package.swift ├── Socket.IO-Client-Swift.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── SocketIO-Mac.xcscheme │ ├── SocketIO-tvOS.xcscheme │ └── SocketIO-iOS.xcscheme ├── Socket.IO-Test-Server ├── acknowledgementEvents.js ├── package.json ├── socketEventRegister.js ├── emitEvents.js ├── main.js └── TestCases.js ├── .travis.yml ├── SocketIO-iOS ├── SocketIO.h └── Info.plist ├── SocketIO-Mac ├── SocketIO-Mac.h └── Info.plist ├── SocketIO-MacTests ├── SocketAckManagerTest.swift ├── Info.plist ├── SocketIOClientConfigurationTest.swift ├── SocketObjectiveCTest.m ├── SocketEngineTest.swift ├── SocketParserTest.swift ├── SocketNamespacePacketTest.swift ├── SocketSideEffectTest.swift └── SocketBasicPacketTest.swift ├── .gitignore ├── Socket.IO-Client-Swift.podspec ├── Source ├── SocketEnginePacketType.swift ├── SocketIOClientStatus.swift ├── SocketEngineClient.swift ├── SocketEventHandler.swift ├── SocketAnyEvent.swift ├── SocketIOClientSpec.swift ├── SocketTypes.swift ├── SocketLogger.swift ├── SocketAckManager.swift ├── SocketEngineWebsocket.swift ├── SocketStringReader.swift ├── SocketClientManager.swift ├── SocketAckEmitter.swift ├── SocketIOClientConfiguration.swift ├── SocketEngineSpec.swift ├── SocketExtensions.swift ├── SocketIOClientOption.swift ├── SocketParsable.swift ├── SocketPacket.swift ├── SocketEnginePollable.swift ├── SSLSecurity.swift ├── SocketIOClient.swift └── SocketEngine.swift ├── README.md └── LICENSE /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "SocketIO" 5 | ) 6 | -------------------------------------------------------------------------------- /Socket.IO-Client-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Socket.IO-Test-Server/acknowledgementEvents.js: -------------------------------------------------------------------------------- 1 | function socketCallback(testKey, socket, testCase) { 2 | return function() { 3 | testCase.assert.apply(undefined , arguments) 4 | var emitArguments = testCase.returnData; 5 | var ack = arguments[arguments.length - 1] 6 | ack.apply(socket, emitArguments) 7 | } 8 | } 9 | 10 | module.exports.socketCallback = socketCallback 11 | -------------------------------------------------------------------------------- /Socket.IO-Test-Server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket.io-client-swift-test-server", 3 | "version": "0.0.1", 4 | "description": "A simple server to test aginst", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Lukas Schmidt", 10 | "license": "MIT", 11 | "dependencies": { 12 | "socket.io": "^1.3.6" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Socket.IO-Test-Server/socketEventRegister.js: -------------------------------------------------------------------------------- 1 | var testCases = require("./TestCases.js") 2 | 3 | function registerSocketForEvents(ioSocket, socketCallback, testKind) { 4 | ioSocket.on('connection', function(socket) { 5 | var testCase; 6 | for(testKey in testCases) { 7 | testCase = testCases[testKey] 8 | socket.on((testKey + testKind), socketCallback(testKey, socket, testCase)) 9 | } 10 | }) 11 | } 12 | 13 | module.exports.register = registerSocketForEvents -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | xcode_project: Socket.IO-Client-Swift.xcodeproj # path to your xcodeproj folder 3 | xcode_scheme: SocketIO-iOS 4 | osx_image: xcode8 5 | branches: 6 | only: 7 | - master 8 | - development 9 | before_install: 10 | - brew update 11 | - brew outdated xctool || brew upgrade xctool 12 | # script: xctool -project Socket.IO-Client-Swift.xcodeproj -scheme SocketIO-Mac build test -parallelize 13 | script: xcodebuild -project Socket.IO-Client-Swift.xcodeproj -scheme SocketIO-Mac build test 14 | -------------------------------------------------------------------------------- /SocketIO-iOS/SocketIO.h: -------------------------------------------------------------------------------- 1 | // 2 | // SocketIO-iOS.h 3 | // SocketIO-iOS 4 | // 5 | // Created by Nacho Soto on 7/11/15. 6 | // 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SocketIO-iOS. 12 | FOUNDATION_EXPORT double SocketIO_iOSVersionNumber; 13 | 14 | //! Project version string for SocketIO-iOS. 15 | FOUNDATION_EXPORT const unsigned char SocketIO_iOSVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /SocketIO-Mac/SocketIO-Mac.h: -------------------------------------------------------------------------------- 1 | // 2 | // SocketIO-Mac.h 3 | // SocketIO-Mac 4 | // 5 | // Created by Nacho Soto on 7/11/15. 6 | // 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SocketIO-Mac. 12 | FOUNDATION_EXPORT double SocketIO_MacVersionNumber; 13 | 14 | //! Project version string for SocketIO-Mac. 15 | FOUNDATION_EXPORT const unsigned char SocketIO_MacVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Socket.IO-Test-Server/emitEvents.js: -------------------------------------------------------------------------------- 1 | function socketCallback(testKey, socket, testCase) { 2 | return function() { 3 | testCase.assert.apply(undefined , arguments) 4 | 5 | var emitArguments = addArrays([testKey + "EmitReturn"], testCase.returnData) 6 | socket.emit.apply(socket, emitArguments) 7 | } 8 | } 9 | 10 | function addArrays(firstArray, secondArray) { 11 | var length = secondArray.length 12 | var i; 13 | for(i = 0; i < length; i++) { 14 | firstArray.push(secondArray[i]) 15 | } 16 | 17 | return firstArray; 18 | } 19 | 20 | module.exports.socketCallback = socketCallback -------------------------------------------------------------------------------- /Socket.IO-Test-Server/main.js: -------------------------------------------------------------------------------- 1 | var app = require('http').createServer() 2 | var io = require('socket.io')(app); 3 | app.listen(6979) 4 | 5 | 6 | var acknowledgementsEvents = require("./acknowledgementEvents.js") 7 | var emitEvents = require("./emitEvents.js") 8 | var socketEventRegister = require("./socketEventRegister.js") 9 | 10 | socketEventRegister.register(io, emitEvents.socketCallback, "Emit") 11 | socketEventRegister.register(io, acknowledgementsEvents.socketCallback, "Acknowledgement") 12 | 13 | var nsp = io.of("/swift") 14 | socketEventRegister.register(nsp, emitEvents.socketCallback, "Emit") 15 | socketEventRegister.register(nsp, acknowledgementsEvents.socketCallback, "Acknowledgement") 16 | -------------------------------------------------------------------------------- /SocketIO-MacTests/SocketAckManagerTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketAckManagerTest.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Lukas Schmidt on 04.09.15. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SocketIO 11 | 12 | class SocketAckManagerTest: XCTestCase { 13 | var ackManager = SocketAckManager() 14 | 15 | func testAddAcks() { 16 | let callbackExpection = self.expectation(description: "callbackExpection") 17 | let itemsArray = ["Hi", "ho"] 18 | func callback(_ items: [Any]) { 19 | callbackExpection.fulfill() 20 | } 21 | ackManager.addAck(1, callback: callback) 22 | ackManager.executeAck(1, with: itemsArray, onQueue: DispatchQueue.main) 23 | 24 | waitForExpectations(timeout: 3.0, handler: nil) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SocketIO-MacTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .AppleDouble 3 | .LSOverride 4 | *.xcodeproj 5 | .build/* 6 | Packages/* 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | # Xcode 31 | build/ 32 | *.pbxuser 33 | !default.pbxuser 34 | *.mode1v3 35 | !default.mode1v3 36 | *.mode2v3 37 | !default.mode2v3 38 | *.perspectivev3 39 | !default.perspectivev3 40 | xcuserdata 41 | *.xccheckout 42 | *.moved-aside 43 | DerivedData 44 | *.hmap 45 | *.ipa 46 | *.xcuserstate 47 | 48 | Socket.IO-Test-Server/node_modules/* 49 | -------------------------------------------------------------------------------- /SocketIO-Mac/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SocketIO-iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Socket.IO-Client-Swift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Socket.IO-Client-Swift" 3 | s.module_name = "SocketIO" 4 | s.version = "8.1.2" 5 | s.summary = "Socket.IO-client for iOS and OS X" 6 | s.description = <<-DESC 7 | Socket.IO-client for iOS and OS X. 8 | Supports ws/wss/polling connections and binary. 9 | For socket.io 1.0+ and Swift. 10 | DESC 11 | s.homepage = "https://github.com/socketio/socket.io-client-swift" 12 | s.license = { :type => 'MIT' } 13 | s.author = { "Erik" => "nuclear.ace@gmail.com" } 14 | s.ios.deployment_target = '8.0' 15 | s.osx.deployment_target = '10.10' 16 | s.tvos.deployment_target = '9.0' 17 | s.source = { :git => "https://github.com/socketio/socket.io-client-swift.git", :tag => 'v8.1.2' } 18 | s.source_files = "Source/**/*.swift" 19 | s.requires_arc = true 20 | s.pod_target_xcconfig = {'SWIFT_VERSION' => '3.0'} 21 | # s.dependency 'Starscream', '~> 0.9' # currently this repo includes Starscream swift files 22 | end 23 | -------------------------------------------------------------------------------- /SocketIO-MacTests/SocketIOClientConfigurationTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestSocketIOClientConfiguration.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | import SocketIO 11 | 12 | class TestSocketIOClientConfiguration: XCTestCase { 13 | var config = [] as SocketIOClientConfiguration 14 | 15 | override func setUp() { 16 | super.setUp() 17 | 18 | config = [.log(false), .forceNew(true)] 19 | } 20 | 21 | func testReplaceSameOption() { 22 | config.insert(.log(true)) 23 | 24 | XCTAssertEqual(config.count, 2) 25 | 26 | switch config[0] { 27 | case let .log(log): 28 | XCTAssertTrue(log) 29 | default: 30 | XCTFail() 31 | } 32 | } 33 | 34 | func testIgnoreIfExisting() { 35 | config.insert(.forceNew(false), replacing: false) 36 | 37 | XCTAssertEqual(config.count, 2) 38 | 39 | switch config[1] { 40 | case let .forceNew(new): 41 | XCTAssertTrue(new) 42 | default: 43 | XCTFail() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SocketIO-MacTests/SocketObjectiveCTest.m: -------------------------------------------------------------------------------- 1 | // 2 | // SocketObjectiveCTest.m 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 3/25/16. 6 | // 7 | // Merely tests whether the Objective-C api breaks 8 | // 9 | 10 | #import 11 | @import SocketIO; 12 | 13 | @interface SocketObjectiveCTest : XCTestCase 14 | 15 | @property SocketIOClient* socket; 16 | 17 | @end 18 | 19 | @implementation SocketObjectiveCTest 20 | 21 | - (void)setUp { 22 | [super setUp]; 23 | NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"]; 24 | self.socket = [[SocketIOClient alloc] initWithSocketURL:url config:@{@"log": @YES, @"forcePolling": @YES}]; 25 | } 26 | 27 | - (void)testOnSyntax { 28 | [self.socket on:@"someCallback" callback:^(NSArray* data, SocketAckEmitter* ack) { 29 | [ack with:@[@1]]; 30 | }]; 31 | } 32 | 33 | - (void)testEmitSyntax { 34 | [self.socket emit:@"testEmit" with:@[@YES]]; 35 | } 36 | 37 | - (void)testEmitWithAckSyntax { 38 | [[self.socket emitWithAck:@"testAckEmit" with:@[@YES]] timingOutAfter:0 callback:^(NSArray* data) { 39 | 40 | }]; 41 | } 42 | 43 | - (void)testOffSyntax { 44 | [self.socket off:@"test"]; 45 | } 46 | 47 | - (void)testSocketManager { 48 | SocketClientManager* manager = [SocketClientManager sharedManager]; 49 | [manager addSocket:self.socket labeledAs:@"test"]; 50 | [manager removeSocketWithLabel:@"test"]; 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /Source/SocketEnginePacketType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketEnginePacketType.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 10/7/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | @objc public enum SocketEnginePacketType : Int { 29 | case open, close, ping, pong, message, upgrade, noop 30 | } -------------------------------------------------------------------------------- /Source/SocketIOClientStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketIOClientStatus.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 8/14/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | /// **NotConnected**: initial state 28 | /// 29 | /// **Disconnected**: connected before 30 | @objc public enum SocketIOClientStatus : Int { 31 | case notConnected, disconnected, connecting, connected 32 | } 33 | -------------------------------------------------------------------------------- /Source/SocketEngineClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketEngineClient.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 3/19/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | @objc public protocol SocketEngineClient { 29 | func engineDidError(reason: String) 30 | func engineDidClose(reason: String) 31 | func engineDidOpen(reason: String) 32 | func parseEngineMessage(_ msg: String) 33 | func parseEngineBinaryData(_ data: Data) 34 | } 35 | -------------------------------------------------------------------------------- /Source/SocketEventHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventHandler.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 1/18/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | struct SocketEventHandler { 28 | let event: String 29 | let id: UUID 30 | let callback: NormalCallback 31 | 32 | func executeCallback(with items: [Any], withAck ack: Int, withSocket socket: SocketIOClient) { 33 | callback(items, SocketAckEmitter(socket: socket, ackNum: ack)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Source/SocketAnyEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketAnyEvent.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 3/28/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | public final class SocketAnyEvent : NSObject { 28 | public let event: String 29 | public let items: [Any]? 30 | override public var description: String { 31 | return "SocketAnyEvent: Event: \(event) items: \(items ?? nil)" 32 | } 33 | 34 | init(event: String, items: [Any]?) { 35 | self.event = event 36 | self.items = items 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Source/SocketIOClientSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketIOClientSpec.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 1/3/16. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | protocol SocketIOClientSpec : class { 26 | var nsp: String { get set } 27 | var waitingPackets: [SocketPacket] { get set } 28 | 29 | func didConnect() 30 | func didDisconnect(reason: String) 31 | func didError(reason: String) 32 | func handleAck(_ ack: Int, data: [Any]) 33 | func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int) 34 | func joinNamespace(_ namespace: String) 35 | } 36 | 37 | extension SocketIOClientSpec { 38 | func didError(reason: String) { 39 | DefaultSocketLogger.Logger.error("%@", type: "SocketIOClient", args: reason) 40 | 41 | handleEvent("error", data: [reason], isInternalMessage: true, withAck: -1) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Source/SocketTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketTypes.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 4/8/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | public protocol SocketData {} 28 | 29 | extension Array : SocketData {} 30 | extension Bool : SocketData {} 31 | extension Dictionary : SocketData {} 32 | extension Double : SocketData {} 33 | extension Int : SocketData {} 34 | extension NSArray : SocketData {} 35 | extension Data : SocketData {} 36 | extension NSData : SocketData {} 37 | extension NSDictionary : SocketData {} 38 | extension NSString : SocketData {} 39 | extension NSNull : SocketData {} 40 | extension String : SocketData {} 41 | 42 | public typealias AckCallback = ([Any]) -> Void 43 | public typealias NormalCallback = ([Any], SocketAckEmitter) -> Void 44 | 45 | typealias JSON = [String: Any] 46 | typealias Probe = (msg: String, type: SocketEnginePacketType, data: [Data]) 47 | typealias ProbeWaitQueue = [Probe] 48 | 49 | enum Either { 50 | case left(E) 51 | case right(V) 52 | } 53 | -------------------------------------------------------------------------------- /Source/SocketLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketLogger.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 4/11/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | public protocol SocketLogger : class { 28 | /// Whether to log or not 29 | var log: Bool { get set } 30 | 31 | /// Normal log messages 32 | func log(_ message: String, type: String, args: Any...) 33 | 34 | /// Error Messages 35 | func error(_ message: String, type: String, args: Any...) 36 | } 37 | 38 | public extension SocketLogger { 39 | func log(_ message: String, type: String, args: Any...) { 40 | abstractLog("LOG", message: message, type: type, args: args) 41 | } 42 | 43 | func error(_ message: String, type: String, args: Any...) { 44 | abstractLog("ERROR", message: message, type: type, args: args) 45 | } 46 | 47 | private func abstractLog(_ logType: String, message: String, type: String, args: [Any]) { 48 | guard log else { return } 49 | 50 | let newArgs = args.map({arg -> CVarArg in String(describing: arg)}) 51 | let messageFormat = String(format: message, arguments: newArgs) 52 | 53 | NSLog("\(logType) \(type): %@", messageFormat) 54 | } 55 | } 56 | 57 | class DefaultSocketLogger : SocketLogger { 58 | static var Logger: SocketLogger = DefaultSocketLogger() 59 | 60 | var log = false 61 | } 62 | -------------------------------------------------------------------------------- /Source/SocketAckManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketAckManager.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 4/3/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | private struct SocketAck : Hashable { 28 | let ack: Int 29 | var callback: AckCallback! 30 | var hashValue: Int { 31 | return ack.hashValue 32 | } 33 | 34 | init(ack: Int) { 35 | self.ack = ack 36 | } 37 | 38 | init(ack: Int, callback: @escaping AckCallback) { 39 | self.ack = ack 40 | self.callback = callback 41 | } 42 | } 43 | 44 | private func <(lhs: SocketAck, rhs: SocketAck) -> Bool { 45 | return lhs.ack < rhs.ack 46 | } 47 | 48 | private func ==(lhs: SocketAck, rhs: SocketAck) -> Bool { 49 | return lhs.ack == rhs.ack 50 | } 51 | 52 | struct SocketAckManager { 53 | private var acks = Set(minimumCapacity: 1) 54 | 55 | mutating func addAck(_ ack: Int, callback: @escaping AckCallback) { 56 | acks.insert(SocketAck(ack: ack, callback: callback)) 57 | } 58 | 59 | /// Should be called on handle queue 60 | mutating func executeAck(_ ack: Int, with items: [Any], onQueue: DispatchQueue) { 61 | let ack = acks.remove(SocketAck(ack: ack)) 62 | 63 | onQueue.async() { ack?.callback(items) } 64 | } 65 | 66 | /// Should be called on handle queue 67 | mutating func timeoutAck(_ ack: Int, onQueue: DispatchQueue) { 68 | let ack = acks.remove(SocketAck(ack: ack)) 69 | 70 | onQueue.async() { ack?.callback?(["NO ACK"]) } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Source/SocketEngineWebsocket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketEngineWebsocket.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 1/15/16. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | /// Protocol that is used to implement socket.io WebSocket support 29 | public protocol SocketEngineWebsocket : SocketEngineSpec, WebSocketDelegate { 30 | func sendWebSocketMessage(_ str: String, withType type: SocketEnginePacketType, withData datas: [Data]) 31 | } 32 | 33 | // WebSocket methods 34 | extension SocketEngineWebsocket { 35 | func probeWebSocket() { 36 | if ws?.isConnected ?? false { 37 | sendWebSocketMessage("probe", withType: .ping, withData: []) 38 | } 39 | } 40 | 41 | /// Send message on WebSockets 42 | /// Only call on emitQueue 43 | public func sendWebSocketMessage(_ str: String, withType type: SocketEnginePacketType, withData datas: [Data]) { 44 | DefaultSocketLogger.Logger.log("Sending ws: %@ as type: %@", type: "SocketEngine", args: str, type.rawValue) 45 | 46 | ws?.write(string: "\(type.rawValue)\(str)") 47 | 48 | for data in datas { 49 | if case let .left(bin) = createBinaryDataForSend(using: data) { 50 | ws?.write(data: bin) 51 | } 52 | } 53 | } 54 | 55 | public func websocketDidReceiveMessage(socket: WebSocket, text: String) { 56 | parseEngineMessage(text, fromPolling: false) 57 | } 58 | 59 | public func websocketDidReceiveData(socket: WebSocket, data: Data) { 60 | parseEngineData(data) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Socket.IO-Test-Server/TestCases.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert") 2 | 3 | module.exports = { 4 | basicTest: { 5 | assert: function(inputData) { 6 | 7 | }, 8 | returnData: [] 9 | }, 10 | testNull: { 11 | assert: function(inputData) { 12 | assert(!inputData) 13 | }, 14 | returnData: [null] 15 | }, 16 | testBinary: { 17 | assert: function(inputData) { 18 | assert.equal(inputData.toString(), "gakgakgak2") 19 | }, 20 | returnData: [new Buffer("gakgakgak2", "utf-8")] 21 | }, 22 | testArray: { 23 | assert: function(inputData) { 24 | assert.equal(inputData.length, 2) 25 | assert.equal(inputData[0], "test1") 26 | assert.equal(inputData[1], "test2") 27 | }, 28 | returnData: [["test3", "test4"]] 29 | }, 30 | testString: { 31 | assert: function(inputData) { 32 | assert.equal(inputData, "marco") 33 | }, 34 | returnData: ["polo"] 35 | }, 36 | testBool: { 37 | assert: function(inputData) { 38 | assert(!inputData) 39 | }, 40 | returnData: [true] 41 | }, 42 | testInteger: { 43 | assert: function(inputData) { 44 | assert.equal(inputData, 10) 45 | }, 46 | returnData: [20] 47 | }, 48 | testDouble: { 49 | assert: function(inputData) { 50 | assert.equal(inputData, 1.1) 51 | }, 52 | returnData: [1.2] 53 | }, 54 | testJSON: { 55 | assert: function(inputData) { 56 | assert.equal(inputData.name, "test") 57 | assert.equal(inputData.nestedTest.test, "test") 58 | assert.equal(inputData.testArray.length, 1) 59 | }, 60 | returnData: [{testString: "test", testNumber: 15, nestedTest: {test: "test"}, testArray: [1, 1]}] 61 | }, 62 | testJSONWithBuffer: { 63 | assert: function(inputData) { 64 | assert.equal(inputData.name, "test") 65 | assert.equal(inputData.nestedTest.test, "test") 66 | assert.equal(inputData.testArray.length, 1) 67 | }, 68 | returnData: [{testString: "test", testNumber: 15, nestedTest: {test: "test"}, testArray: [new Buffer("gakgakgak2", "utf-8"), 1]}] 69 | },testUnicode: { 70 | assert: function(inputData) { 71 | assert.equal(inputData, "🚀") 72 | }, 73 | returnData: ["🚄"] 74 | },testMultipleItems: { 75 | assert: function(array, object, number, string, bool) { 76 | assert.equal(array.length, 2) 77 | assert.equal(array[0], "test1") 78 | assert.equal(array[1], "test2") 79 | assert.equal(number, 15) 80 | assert.equal(string, "marco") 81 | assert.equal(bool, false) 82 | }, 83 | returnData: [[1, 2], {test: "bob"}, 25, "polo", false] 84 | },testMultipleItemsWithBuffer: { 85 | assert: function(array, object, number, string, binary) { 86 | assert.equal(array.length, 2) 87 | assert.equal(array[0], "test1") 88 | assert.equal(array[1], "test2") 89 | assert.equal(number, 15) 90 | assert.equal(string, "marco") 91 | assert.equal(binary.toString(), "gakgakgak2") 92 | }, 93 | returnData: [[1, 2], {test: "bob"}, 25, "polo", new Buffer("gakgakgak2")] 94 | } 95 | } -------------------------------------------------------------------------------- /Source/SocketStringReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketStringReader.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Lukas Schmidt on 07.09.15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | struct SocketStringReader { 26 | let message: String 27 | var currentIndex: String.Index 28 | var hasNext: Bool { 29 | return currentIndex != message.endIndex 30 | } 31 | 32 | var currentCharacter: String { 33 | return String(message[currentIndex]) 34 | } 35 | 36 | init(message: String) { 37 | self.message = message 38 | currentIndex = message.startIndex 39 | } 40 | 41 | @discardableResult 42 | mutating func advance(by: Int) -> String.Index { 43 | currentIndex = message.characters.index(currentIndex, offsetBy: by) 44 | 45 | return currentIndex 46 | } 47 | 48 | mutating func read(count: Int) -> String { 49 | let readString = message[currentIndex.. String { 57 | let substring = message[currentIndex.. String { 71 | return read(count: message.characters.distance(from: currentIndex, to: message.endIndex)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Source/SocketClientManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketClientManager.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 6/11/16. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | /** 28 | Experimental socket manager. 29 | 30 | API subject to change. 31 | 32 | Can be used to persist sockets across ViewControllers. 33 | 34 | Sockets are strongly stored, so be sure to remove them once they are no 35 | longer needed. 36 | 37 | Example usage: 38 | ``` 39 | let manager = SocketClientManager.sharedManager 40 | manager["room1"] = socket1 41 | manager["room2"] = socket2 42 | manager.removeSocket(socket: socket2) 43 | manager["room1"]?.emit("hello") 44 | ``` 45 | */ 46 | open class SocketClientManager : NSObject { 47 | open static let sharedManager = SocketClientManager() 48 | 49 | private var sockets = [String: SocketIOClient]() 50 | 51 | open subscript(string: String) -> SocketIOClient? { 52 | get { 53 | return sockets[string] 54 | } 55 | 56 | set(socket) { 57 | sockets[string] = socket 58 | } 59 | } 60 | 61 | open func addSocket(_ socket: SocketIOClient, labeledAs label: String) { 62 | sockets[label] = socket 63 | } 64 | 65 | open func removeSocket(withLabel label: String) -> SocketIOClient? { 66 | return sockets.removeValue(forKey: label) 67 | } 68 | 69 | open func removeSocket(_ socket: SocketIOClient) -> SocketIOClient? { 70 | var returnSocket: SocketIOClient? 71 | 72 | for (label, dictSocket) in sockets where dictSocket === socket { 73 | returnSocket = sockets.removeValue(forKey: label) 74 | } 75 | 76 | return returnSocket 77 | } 78 | 79 | open func removeSockets() { 80 | sockets.removeAll() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Source/SocketAckEmitter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketAckEmitter.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 9/16/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | public final class SocketAckEmitter : NSObject { 28 | let socket: SocketIOClient 29 | let ackNum: Int 30 | 31 | public var expected: Bool { 32 | return ackNum != -1 33 | } 34 | 35 | init(socket: SocketIOClient, ackNum: Int) { 36 | self.socket = socket 37 | self.ackNum = ackNum 38 | } 39 | 40 | public func with(_ items: SocketData...) { 41 | guard ackNum != -1 else { return } 42 | 43 | socket.emitAck(ackNum, with: items) 44 | } 45 | 46 | public func with(_ items: [Any]) { 47 | guard ackNum != -1 else { return } 48 | 49 | socket.emitAck(ackNum, with: items) 50 | } 51 | 52 | } 53 | 54 | public final class OnAckCallback : NSObject { 55 | private let ackNumber: Int 56 | private let items: [Any] 57 | private weak var socket: SocketIOClient? 58 | 59 | init(ackNumber: Int, items: [Any], socket: SocketIOClient) { 60 | self.ackNumber = ackNumber 61 | self.items = items 62 | self.socket = socket 63 | } 64 | 65 | deinit { 66 | DefaultSocketLogger.Logger.log("OnAckCallback for \(ackNumber) being released", type: "OnAckCallback") 67 | } 68 | 69 | public func timingOut(after seconds: Int, callback: @escaping AckCallback) { 70 | guard let socket = self.socket else { return } 71 | 72 | socket.ackQueue.sync() { 73 | socket.ackHandlers.addAck(ackNumber, callback: callback) 74 | } 75 | 76 | socket._emit(items, ack: ackNumber) 77 | 78 | guard seconds != 0 else { return } 79 | 80 | let time = DispatchTime.now() + Double(UInt64(seconds) * NSEC_PER_SEC) / Double(NSEC_PER_SEC) 81 | 82 | socket.handleQueue.asyncAfter(deadline: time) { 83 | socket.ackHandlers.timeoutAck(self.ackNumber, onQueue: socket.handleQueue) 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /Source/SocketIOClientConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketIOClientConfiguration.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 8/13/16. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | public struct SocketIOClientConfiguration : ExpressibleByArrayLiteral, Collection, MutableCollection { 26 | public typealias Element = SocketIOClientOption 27 | public typealias Index = Array.Index 28 | public typealias Generator = Array.Generator 29 | public typealias SubSequence = Array.SubSequence 30 | 31 | private var backingArray = [SocketIOClientOption]() 32 | 33 | public var startIndex: Index { 34 | return backingArray.startIndex 35 | } 36 | 37 | public var endIndex: Index { 38 | return backingArray.endIndex 39 | } 40 | 41 | public var isEmpty: Bool { 42 | return backingArray.isEmpty 43 | } 44 | 45 | public var count: Index.Stride { 46 | return backingArray.count 47 | } 48 | 49 | public var first: Generator.Element? { 50 | return backingArray.first 51 | } 52 | 53 | public subscript(position: Index) -> Generator.Element { 54 | get { 55 | return backingArray[position] 56 | } 57 | 58 | set { 59 | backingArray[position] = newValue 60 | } 61 | } 62 | 63 | public subscript(bounds: Range) -> SubSequence { 64 | get { 65 | return backingArray[bounds] 66 | } 67 | 68 | set { 69 | backingArray[bounds] = newValue 70 | } 71 | } 72 | 73 | public init(arrayLiteral elements: Element...) { 74 | backingArray = elements 75 | } 76 | 77 | public func generate() -> Generator { 78 | return backingArray.makeIterator() 79 | } 80 | 81 | public func index(after i: Index) -> Index { 82 | return backingArray.index(after: i) 83 | } 84 | 85 | public mutating func insert(_ element: Element, replacing replace: Bool = true) { 86 | for i in 0.. SubSequence { 98 | return backingArray.prefix(upTo: end) 99 | } 100 | 101 | public func prefix(through position: Index) -> SubSequence { 102 | return backingArray.prefix(through: position) 103 | } 104 | 105 | public func suffix(from start: Index) -> SubSequence { 106 | return backingArray.suffix(from: start) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Source/SocketEngineSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketEngineSpec.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 10/7/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | @objc public protocol SocketEngineSpec { 29 | weak var client: SocketEngineClient? { get set } 30 | var closed: Bool { get } 31 | var connected: Bool { get } 32 | var connectParams: [String: Any]? { get set } 33 | var doubleEncodeUTF8: Bool { get } 34 | var cookies: [HTTPCookie]? { get } 35 | var extraHeaders: [String: String]? { get } 36 | var fastUpgrade: Bool { get } 37 | var forcePolling: Bool { get } 38 | var forceWebsockets: Bool { get } 39 | var parseQueue: DispatchQueue { get } 40 | var polling: Bool { get } 41 | var probing: Bool { get } 42 | var emitQueue: DispatchQueue { get } 43 | var handleQueue: DispatchQueue { get } 44 | var sid: String { get } 45 | var socketPath: String { get } 46 | var urlPolling: URL { get } 47 | var urlWebSocket: URL { get } 48 | var websocket: Bool { get } 49 | var ws: WebSocket? { get } 50 | 51 | init(client: SocketEngineClient, url: URL, options: NSDictionary?) 52 | 53 | func connect() 54 | func didError(reason: String) 55 | func disconnect(reason: String) 56 | func doFastUpgrade() 57 | func flushWaitingForPostToWebSocket() 58 | func parseEngineData(_ data: Data) 59 | func parseEngineMessage(_ message: String, fromPolling: Bool) 60 | func write(_ msg: String, withType type: SocketEnginePacketType, withData data: [Data]) 61 | } 62 | 63 | extension SocketEngineSpec { 64 | var urlPollingWithSid: URL { 65 | var com = URLComponents(url: urlPolling, resolvingAgainstBaseURL: false)! 66 | com.percentEncodedQuery = com.percentEncodedQuery! + "&sid=\(sid.urlEncode()!)" 67 | 68 | return com.url! 69 | } 70 | 71 | var urlWebSocketWithSid: URL { 72 | var com = URLComponents(url: urlWebSocket, resolvingAgainstBaseURL: false)! 73 | com.percentEncodedQuery = com.percentEncodedQuery! + (sid == "" ? "" : "&sid=\(sid.urlEncode()!)") 74 | 75 | return com.url! 76 | } 77 | 78 | func createBinaryDataForSend(using data: Data) -> Either { 79 | if websocket { 80 | var byteArray = [UInt8](repeating: 0x4, count: 1) 81 | let mutData = NSMutableData(bytes: &byteArray, length: 1) 82 | 83 | mutData.append(data) 84 | 85 | return .left(mutData as Data) 86 | } else { 87 | let str = "b4" + data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) 88 | 89 | return .right(str) 90 | } 91 | } 92 | 93 | func doubleEncodeUTF8(_ string: String) -> String { 94 | if let latin1 = string.data(using: String.Encoding.utf8), 95 | let utf8 = NSString(data: latin1, encoding: String.Encoding.isoLatin1.rawValue) { 96 | return utf8 as String 97 | } else { 98 | return string 99 | } 100 | } 101 | 102 | func fixDoubleUTF8(_ string: String) -> String { 103 | if let utf8 = string.data(using: String.Encoding.isoLatin1), 104 | let latin1 = NSString(data: utf8, encoding: String.Encoding.utf8.rawValue) { 105 | return latin1 as String 106 | } else { 107 | return string 108 | } 109 | } 110 | 111 | /// Send an engine message (4) 112 | func send(_ msg: String, withData datas: [Data]) { 113 | write(msg, withType: .message, withData: datas) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /SocketIO-MacTests/SocketEngineTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketEngineTest.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 10/15/15. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SocketIO 11 | 12 | class SocketEngineTest: XCTestCase { 13 | var client: SocketIOClient! 14 | var engine: SocketEngine! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | client = SocketIOClient(socketURL: URL(string: "http://localhost")!) 19 | engine = SocketEngine(client: client, url: URL(string: "http://localhost")!, options: nil) 20 | 21 | client.setTestable() 22 | } 23 | 24 | func testBasicPollingMessage() { 25 | let expect = expectation(description: "Basic polling test") 26 | client.on("blankTest") {data, ack in 27 | expect.fulfill() 28 | } 29 | 30 | engine.parsePollingMessage("15:42[\"blankTest\"]") 31 | waitForExpectations(timeout: 3, handler: nil) 32 | } 33 | 34 | func testTwoPacketsInOnePollTest() { 35 | let finalExpectation = expectation(description: "Final packet in poll test") 36 | var gotBlank = false 37 | 38 | client.on("blankTest") {data, ack in 39 | gotBlank = true 40 | } 41 | 42 | client.on("stringTest") {data, ack in 43 | if let str = data[0] as? String, gotBlank { 44 | if str == "hello" { 45 | finalExpectation.fulfill() 46 | } 47 | } 48 | } 49 | 50 | engine.parsePollingMessage("15:42[\"blankTest\"]24:42[\"stringTest\",\"hello\"]") 51 | waitForExpectations(timeout: 3, handler: nil) 52 | } 53 | 54 | func testEngineDoesErrorOnUnknownTransport() { 55 | let finalExpectation = expectation(description: "Unknown Transport") 56 | 57 | client.on("error") {data, ack in 58 | if let error = data[0] as? String, error == "Unknown transport" { 59 | finalExpectation.fulfill() 60 | } 61 | } 62 | 63 | engine.parseEngineMessage("{\"code\": 0, \"message\": \"Unknown transport\"}", fromPolling: false) 64 | waitForExpectations(timeout: 3, handler: nil) 65 | } 66 | 67 | func testEngineDoesErrorOnUnknownMessage() { 68 | let finalExpectation = expectation(description: "Engine Errors") 69 | 70 | client.on("error") {data, ack in 71 | finalExpectation.fulfill() 72 | } 73 | 74 | engine.parseEngineMessage("afafafda", fromPolling: false) 75 | waitForExpectations(timeout: 3, handler: nil) 76 | } 77 | 78 | func testEngineDecodesUTF8Properly() { 79 | let expect = expectation(description: "Engine Decodes utf8") 80 | 81 | client.on("stringTest") {data, ack in 82 | XCTAssertEqual(data[0] as? String, "lïne one\nlīne \rtwo", "Failed string test") 83 | expect.fulfill() 84 | } 85 | 86 | engine.parsePollingMessage("41:42[\"stringTest\",\"lïne one\\nlÄ«ne \\rtwo\"]") 87 | waitForExpectations(timeout: 3, handler: nil) 88 | } 89 | 90 | func testEncodeURLProperly() { 91 | engine.connectParams = [ 92 | "created": "2016-05-04T18:31:15+0200" 93 | ] 94 | 95 | XCTAssertEqual(engine.urlPolling.query, "transport=polling&b64=1&created=2016-05-04T18%3A31%3A15%2B0200") 96 | XCTAssertEqual(engine.urlWebSocket.query, "transport=websocket&created=2016-05-04T18%3A31%3A15%2B0200") 97 | 98 | engine.connectParams = [ 99 | "forbidden": "!*'();:@&=+$,/?%#[]\" {}" 100 | ] 101 | 102 | XCTAssertEqual(engine.urlPolling.query, "transport=polling&b64=1&forbidden=%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23%5B%5D%22%20%7B%7D") 103 | XCTAssertEqual(engine.urlWebSocket.query, "transport=websocket&forbidden=%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23%5B%5D%22%20%7B%7D") 104 | } 105 | 106 | func testBase64Data() { 107 | let expect = expectation(description: "Engine Decodes base64 data") 108 | let b64String = "b4aGVsbG8NCg==" 109 | let packetString = "451-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0}}]" 110 | 111 | client.on("test") {data, ack in 112 | if let data = data[0] as? Data, let string = String(data: data, encoding: .utf8) { 113 | XCTAssertEqual(string, "hello") 114 | } 115 | 116 | expect.fulfill() 117 | } 118 | 119 | engine.parseEngineMessage(packetString, fromPolling: false) 120 | engine.parseEngineMessage(b64String, fromPolling: false) 121 | 122 | waitForExpectations(timeout: 3, handler: nil) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-Mac.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 79 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | 98 | 104 | 105 | 106 | 107 | 109 | 110 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /Source/SocketExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketExtensions.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 7/1/2016. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | enum JSONError : Error { 28 | case notArray 29 | case notNSDictionary 30 | } 31 | 32 | extension Array { 33 | func toJSON() throws -> Data { 34 | return try JSONSerialization.data(withJSONObject: self, options: JSONSerialization.WritingOptions(rawValue: 0)) 35 | } 36 | } 37 | 38 | extension CharacterSet { 39 | static var allowedURLCharacterSet: CharacterSet { 40 | return CharacterSet(charactersIn: "!*'();:@&=+$,/?%#[]\" {}").inverted 41 | } 42 | } 43 | 44 | extension NSDictionary { 45 | private static func keyValueToSocketIOClientOption(key: String, value: Any) -> SocketIOClientOption? { 46 | switch (key, value) { 47 | case let ("connectParams", params as [String: Any]): 48 | return .connectParams(params) 49 | case let ("cookies", cookies as [HTTPCookie]): 50 | return .cookies(cookies) 51 | case let ("doubleEncodeUTF8", encode as Bool): 52 | return .doubleEncodeUTF8(encode) 53 | case let ("extraHeaders", headers as [String: String]): 54 | return .extraHeaders(headers) 55 | case let ("forceNew", force as Bool): 56 | return .forceNew(force) 57 | case let ("forcePolling", force as Bool): 58 | return .forcePolling(force) 59 | case let ("forceWebsockets", force as Bool): 60 | return .forceWebsockets(force) 61 | case let ("handleQueue", queue as DispatchQueue): 62 | return .handleQueue(queue) 63 | case let ("log", log as Bool): 64 | return .log(log) 65 | case let ("logger", logger as SocketLogger): 66 | return .logger(logger) 67 | case let ("nsp", nsp as String): 68 | return .nsp(nsp) 69 | case let ("path", path as String): 70 | return .path(path) 71 | case let ("reconnects", reconnects as Bool): 72 | return .reconnects(reconnects) 73 | case let ("reconnectAttempts", attempts as Int): 74 | return .reconnectAttempts(attempts) 75 | case let ("reconnectWait", wait as Int): 76 | return .reconnectWait(wait) 77 | case let ("secure", secure as Bool): 78 | return .secure(secure) 79 | case let ("security", security as SSLSecurity): 80 | return .security(security) 81 | case let ("selfSigned", selfSigned as Bool): 82 | return .selfSigned(selfSigned) 83 | case let ("sessionDelegate", delegate as URLSessionDelegate): 84 | return .sessionDelegate(delegate) 85 | case let ("voipEnabled", enable as Bool): 86 | return .voipEnabled(enable) 87 | default: 88 | return nil 89 | } 90 | } 91 | 92 | func toSocketConfiguration() -> SocketIOClientConfiguration { 93 | var options = [] as SocketIOClientConfiguration 94 | 95 | for (rawKey, value) in self { 96 | if let key = rawKey as? String, let opt = NSDictionary.keyValueToSocketIOClientOption(key: key, value: value) { 97 | options.insert(opt) 98 | } 99 | } 100 | 101 | return options 102 | } 103 | } 104 | 105 | extension String { 106 | func toArray() throws -> [Any] { 107 | guard let stringData = data(using: .utf8, allowLossyConversion: false) else { return [] } 108 | guard let array = try JSONSerialization.jsonObject(with: stringData, options: .mutableContainers) as? [Any] else { 109 | throw JSONError.notArray 110 | } 111 | 112 | return array 113 | } 114 | 115 | func toNSDictionary() throws -> NSDictionary { 116 | guard let binData = data(using: .utf8, allowLossyConversion: false) else { return [:] } 117 | guard let json = try JSONSerialization.jsonObject(with: binData, options: .allowFragments) as? NSDictionary else { 118 | throw JSONError.notNSDictionary 119 | } 120 | 121 | return json 122 | } 123 | 124 | func urlEncode() -> String? { 125 | return addingPercentEncoding(withAllowedCharacters: .allowedURLCharacterSet) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Source/SocketIOClientOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketIOClientOption .swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 10/17/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | protocol ClientOption : CustomStringConvertible, Equatable { 28 | func getSocketIOOptionValue() -> Any 29 | } 30 | 31 | public enum SocketIOClientOption : ClientOption { 32 | case connectParams([String: Any]) 33 | case cookies([HTTPCookie]) 34 | case doubleEncodeUTF8(Bool) 35 | case extraHeaders([String: String]) 36 | case forceNew(Bool) 37 | case forcePolling(Bool) 38 | case forceWebsockets(Bool) 39 | case handleQueue(DispatchQueue) 40 | case log(Bool) 41 | case logger(SocketLogger) 42 | case nsp(String) 43 | case path(String) 44 | case reconnects(Bool) 45 | case reconnectAttempts(Int) 46 | case reconnectWait(Int) 47 | case secure(Bool) 48 | case security(SSLSecurity) 49 | case selfSigned(Bool) 50 | case sessionDelegate(URLSessionDelegate) 51 | case voipEnabled(Bool) 52 | 53 | public var description: String { 54 | let description: String 55 | 56 | switch self { 57 | case .connectParams: 58 | description = "connectParams" 59 | case .cookies: 60 | description = "cookies" 61 | case .doubleEncodeUTF8: 62 | description = "doubleEncodeUTF8" 63 | case .extraHeaders: 64 | description = "extraHeaders" 65 | case .forceNew: 66 | description = "forceNew" 67 | case .forcePolling: 68 | description = "forcePolling" 69 | case .forceWebsockets: 70 | description = "forceWebsockets" 71 | case .handleQueue: 72 | description = "handleQueue" 73 | case .log: 74 | description = "log" 75 | case .logger: 76 | description = "logger" 77 | case .nsp: 78 | description = "nsp" 79 | case .path: 80 | description = "path" 81 | case .reconnects: 82 | description = "reconnects" 83 | case .reconnectAttempts: 84 | description = "reconnectAttempts" 85 | case .reconnectWait: 86 | description = "reconnectWait" 87 | case .secure: 88 | description = "secure" 89 | case .selfSigned: 90 | description = "selfSigned" 91 | case .security: 92 | description = "security" 93 | case .sessionDelegate: 94 | description = "sessionDelegate" 95 | case .voipEnabled: 96 | description = "voipEnabled" 97 | } 98 | 99 | return description 100 | } 101 | 102 | func getSocketIOOptionValue() -> Any { 103 | let value: Any 104 | 105 | switch self { 106 | case let .connectParams(params): 107 | value = params 108 | case let .cookies(cookies): 109 | value = cookies 110 | case let .doubleEncodeUTF8(encode): 111 | value = encode 112 | case let .extraHeaders(headers): 113 | value = headers 114 | case let .forceNew(force): 115 | value = force 116 | case let .forcePolling(force): 117 | value = force 118 | case let .forceWebsockets(force): 119 | value = force 120 | case let .handleQueue(queue): 121 | value = queue 122 | case let .log(log): 123 | value = log 124 | case let .logger(logger): 125 | value = logger 126 | case let .nsp(nsp): 127 | value = nsp 128 | case let .path(path): 129 | value = path 130 | case let .reconnects(reconnects): 131 | value = reconnects 132 | case let .reconnectAttempts(attempts): 133 | value = attempts 134 | case let .reconnectWait(wait): 135 | value = wait 136 | case let .secure(secure): 137 | value = secure 138 | case let .security(security): 139 | value = security 140 | case let .selfSigned(signed): 141 | value = signed 142 | case let .sessionDelegate(delegate): 143 | value = delegate 144 | case let .voipEnabled(enabled): 145 | value = enabled 146 | } 147 | 148 | return value 149 | } 150 | } 151 | 152 | public func ==(lhs: SocketIOClientOption, rhs: SocketIOClientOption) -> Bool { 153 | return lhs.description == rhs.description 154 | } 155 | -------------------------------------------------------------------------------- /SocketIO-MacTests/SocketParserTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketParserTest.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Lukas Schmidt on 05.09.15. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SocketIO 11 | 12 | class SocketParserTest: XCTestCase { 13 | let testSocket = SocketIOClient(socketURL: URL(string: "http://localhost/")!) 14 | 15 | //Format key: message; namespace-data-binary-id 16 | static let packetTypes: [String: (String, [Any], [Data], Int)] = [ 17 | "0": ("/", [], [], -1), "1": ("/", [], [], -1), 18 | "25[\"test\"]": ("/", ["test"], [], 5), 19 | "2[\"test\",\"~~0\"]": ("/", ["test", "~~0"], [], -1), 20 | "2/swift,[\"testArrayEmitReturn\",[\"test3\",\"test4\"]]": ("/swift", ["testArrayEmitReturn", ["test3", "test4"] as NSArray], [], -1), 21 | "51-/swift,[\"testMultipleItemsWithBufferEmitReturn\",[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]": ("/swift", ["testMultipleItemsWithBufferEmitReturn", [1, 2] as NSArray, ["test": "bob"] as NSDictionary, 25, "polo", ["_placeholder": true, "num": 0] as NSDictionary], [], -1), 22 | "3/swift,0[[\"test3\",\"test4\"]]": ("/swift", [["test3", "test4"] as NSArray], [], 0), 23 | "61-/swift,19[[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]": 24 | ("/swift", [ [1, 2] as NSArray, ["test": "bob"] as NSDictionary, 25, "polo", ["_placeholder": true, "num": 0] as NSDictionary], [], 19), 25 | "4/swift,": ("/swift", [], [], -1), 26 | "0/swift": ("/swift", [], [], -1), 27 | "1/swift": ("/swift", [], [], -1), 28 | "4\"ERROR\"": ("/", ["ERROR"], [], -1), 29 | "4{\"test\":2}": ("/", [["test": 2]], [], -1), 30 | "41": ("/", [1], [], -1), 31 | "4[1, \"hello\"]": ("/", [1, "hello"], [], -1)] 32 | 33 | func testDisconnect() { 34 | let message = "1" 35 | validateParseResult(message) 36 | } 37 | 38 | func testConnect() { 39 | let message = "0" 40 | validateParseResult(message) 41 | } 42 | 43 | func testDisconnectNameSpace() { 44 | let message = "1/swift" 45 | validateParseResult(message) 46 | } 47 | 48 | func testConnecttNameSpace() { 49 | let message = "0/swift" 50 | validateParseResult(message) 51 | } 52 | 53 | func testIdEvent() { 54 | let message = "25[\"test\"]" 55 | validateParseResult(message) 56 | } 57 | 58 | func testBinaryPlaceholderAsString() { 59 | let message = "2[\"test\",\"~~0\"]" 60 | validateParseResult(message) 61 | } 62 | 63 | func testNameSpaceArrayParse() { 64 | let message = "2/swift,[\"testArrayEmitReturn\",[\"test3\",\"test4\"]]" 65 | validateParseResult(message) 66 | } 67 | 68 | func testNameSpaceArrayAckParse() { 69 | let message = "3/swift,0[[\"test3\",\"test4\"]]" 70 | validateParseResult(message) 71 | } 72 | 73 | func testNameSpaceBinaryEventParse() { 74 | let message = "51-/swift,[\"testMultipleItemsWithBufferEmitReturn\",[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]" 75 | validateParseResult(message) 76 | } 77 | 78 | func testNameSpaceBinaryAckParse() { 79 | let message = "61-/swift,19[[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]" 80 | validateParseResult(message) 81 | } 82 | 83 | func testNamespaceErrorParse() { 84 | let message = "4/swift," 85 | validateParseResult(message) 86 | } 87 | 88 | func testErrorTypeString() { 89 | let message = "4\"ERROR\"" 90 | validateParseResult(message) 91 | } 92 | 93 | func testErrorTypeDictionary() { 94 | let message = "4{\"test\":2}" 95 | validateParseResult(message) 96 | } 97 | 98 | func testErrorTypeInt() { 99 | let message = "41" 100 | validateParseResult(message) 101 | } 102 | 103 | func testErrorTypeArray() { 104 | let message = "4[1, \"hello\"]" 105 | validateParseResult(message) 106 | } 107 | 108 | func testInvalidInput() { 109 | let message = "8" 110 | switch testSocket.parseString(message) { 111 | case .left(_): 112 | return 113 | case .right(_): 114 | XCTFail("Created packet when shouldn't have") 115 | } 116 | } 117 | 118 | func testGenericParser() { 119 | var parser = SocketStringReader(message: "61-/swift,") 120 | XCTAssertEqual(parser.read(count: 1), "6") 121 | XCTAssertEqual(parser.currentCharacter, "1") 122 | XCTAssertEqual(parser.readUntilOccurence(of: "-"), "1") 123 | XCTAssertEqual(parser.currentCharacter, "/") 124 | } 125 | 126 | func validateParseResult(_ message: String) { 127 | let validValues = SocketParserTest.packetTypes[message]! 128 | let packet = testSocket.parseString(message) 129 | let type = String(message.characters.prefix(1)) 130 | if case let .right(packet) = packet { 131 | XCTAssertEqual(packet.type, SocketPacket.PacketType(rawValue: Int(type) ?? -1)!) 132 | XCTAssertEqual(packet.nsp, validValues.0) 133 | XCTAssertTrue((packet.data as NSArray).isEqual(to: validValues.1), "\(packet.data)") 134 | XCTAssertTrue((packet.binary as NSArray).isEqual(to: validValues.2), "\(packet.binary)") 135 | XCTAssertEqual(packet.id, validValues.3) 136 | } else { 137 | XCTFail() 138 | } 139 | } 140 | 141 | func testParsePerformance() { 142 | let keys = Array(SocketParserTest.packetTypes.keys) 143 | measure { 144 | for item in keys.enumerated() { 145 | _ = self.testSocket.parseString(item.element) 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /SocketIO-MacTests/SocketNamespacePacketTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketNamespacePacketTest.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 10/11/15. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SocketIO 11 | 12 | class SocketNamespacePacketTest: XCTestCase { 13 | let data = "test".data(using: String.Encoding.utf8)! 14 | let data2 = "test2".data(using: String.Encoding.utf8)! 15 | 16 | func testEmpyEmit() { 17 | let expectedSendString = "2/swift,[\"test\"]" 18 | let sendData: [Any] = ["test"] 19 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/swift", ack: false) 20 | 21 | XCTAssertEqual(packet.packetString, expectedSendString) 22 | } 23 | 24 | func testNullEmit() { 25 | let expectedSendString = "2/swift,[\"test\",null]" 26 | let sendData: [Any] = ["test", NSNull()] 27 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/swift", ack: false) 28 | 29 | XCTAssertEqual(packet.packetString, expectedSendString) 30 | } 31 | 32 | func testStringEmit() { 33 | let expectedSendString = "2/swift,[\"test\",\"foo bar\"]" 34 | let sendData: [Any] = ["test", "foo bar"] 35 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/swift", ack: false) 36 | 37 | XCTAssertEqual(packet.packetString, expectedSendString) 38 | } 39 | 40 | func testJSONEmit() { 41 | let expectedSendString = "2/swift,[\"test\",{\"null\":null,\"test\":\"hello\",\"hello\":1,\"foobar\":true}]" 42 | let sendData: [Any] = ["test", ["foobar": true, "hello": 1, "test": "hello", "null": NSNull()] as NSDictionary] 43 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/swift", ack: false) 44 | 45 | XCTAssertEqual(packet.packetString, expectedSendString) 46 | } 47 | 48 | func testArrayEmit() { 49 | let expectedSendString = "2/swift,[\"test\",[\"hello\",1,{\"test\":\"test\"},true]]" 50 | let sendData: [Any] = ["test", ["hello", 1, ["test": "test"], true]] 51 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/swift", ack: false) 52 | 53 | 54 | XCTAssertEqual(packet.packetString, expectedSendString) 55 | } 56 | 57 | func testBinaryEmit() { 58 | let expectedSendString = "51-/swift,[\"test\",{\"_placeholder\":true,\"num\":0}]" 59 | let sendData: [Any] = ["test", data] 60 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/swift", ack: false) 61 | 62 | XCTAssertEqual(packet.packetString, expectedSendString) 63 | XCTAssertEqual(packet.binary, [data]) 64 | } 65 | 66 | func testMultipleBinaryEmit() { 67 | let expectedSendString = "52-/swift,[\"test\",{\"data2\":{\"_placeholder\":true,\"num\":0},\"data1\":{\"_placeholder\":true,\"num\":1}}]" 68 | let sendData: [Any] = ["test", ["data1": data, "data2": data2] as NSDictionary] 69 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/swift", ack: false) 70 | 71 | XCTAssertEqual(packet.packetString, expectedSendString) 72 | XCTAssertEqual(packet.binary, [data2, data]) 73 | } 74 | 75 | func testEmitWithAck() { 76 | let expectedSendString = "2/swift,0[\"test\"]" 77 | let sendData = ["test"] 78 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/swift", ack: false) 79 | 80 | XCTAssertEqual(packet.packetString, expectedSendString) 81 | } 82 | 83 | func testEmitDataWithAck() { 84 | let expectedSendString = "51-/swift,0[\"test\",{\"_placeholder\":true,\"num\":0}]" 85 | let sendData: [Any] = ["test", data] 86 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/swift", ack: false) 87 | 88 | XCTAssertEqual(packet.packetString, expectedSendString) 89 | XCTAssertEqual(packet.binary, [data]) 90 | } 91 | 92 | // Acks 93 | func testEmptyAck() { 94 | let expectedSendString = "3/swift,0[]" 95 | let packet = SocketPacket.packetFromEmit([], id: 0, nsp: "/swift", ack: true) 96 | 97 | XCTAssertEqual(packet.packetString, expectedSendString) 98 | } 99 | 100 | func testNullAck() { 101 | let expectedSendString = "3/swift,0[null]" 102 | let sendData = [NSNull()] 103 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/swift", ack: true) 104 | 105 | XCTAssertEqual(packet.packetString, expectedSendString) 106 | } 107 | 108 | func testStringAck() { 109 | let expectedSendString = "3/swift,0[\"test\"]" 110 | let sendData = ["test"] 111 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/swift", ack: true) 112 | 113 | XCTAssertEqual(packet.packetString, expectedSendString) 114 | } 115 | 116 | func testJSONAck() { 117 | let expectedSendString = "3/swift,0[{\"null\":null,\"hello\":1,\"test\":\"hello\",\"foobar\":true}]" 118 | let sendData = [["foobar": true, "hello": 1, "test": "hello", "null": NSNull()]] 119 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/swift", ack: true) 120 | 121 | XCTAssertEqual(packet.packetString, expectedSendString) 122 | } 123 | 124 | func testBinaryAck() { 125 | let expectedSendString = "61-/swift,0[{\"_placeholder\":true,\"num\":0}]" 126 | let sendData = [data] 127 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/swift", ack: true) 128 | 129 | XCTAssertEqual(packet.packetString, expectedSendString) 130 | XCTAssertEqual(packet.binary, [data]) 131 | } 132 | 133 | func testMultipleBinaryAck() { 134 | let expectedSendString = "62-/swift,0[{\"data2\":{\"_placeholder\":true,\"num\":0},\"data1\":{\"_placeholder\":true,\"num\":1}}]" 135 | let sendData = [["data1": data, "data2": data2]] 136 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/swift", ack: true) 137 | 138 | XCTAssertEqual(packet.packetString, expectedSendString) 139 | XCTAssertEqual(packet.binary, [data2, data]) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /SocketIO-MacTests/SocketSideEffectTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketSideEffectTest.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 10/11/15. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SocketIO 11 | 12 | class SocketSideEffectTest: XCTestCase { 13 | let data = "test".data(using: String.Encoding.utf8)! 14 | let data2 = "test2".data(using: String.Encoding.utf8)! 15 | private var socket: SocketIOClient! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | socket = SocketIOClient(socketURL: URL(string: "http://localhost/")!) 20 | socket.setTestable() 21 | } 22 | 23 | func testInitialCurrentAck() { 24 | XCTAssertEqual(socket.currentAck, -1) 25 | } 26 | 27 | func testFirstAck() { 28 | socket.emitWithAck("test").timingOut(after: 0) {data in} 29 | XCTAssertEqual(socket.currentAck, 0) 30 | } 31 | 32 | func testSecondAck() { 33 | socket.emitWithAck("test").timingOut(after: 0) {data in} 34 | socket.emitWithAck("test").timingOut(after: 0) {data in} 35 | 36 | XCTAssertEqual(socket.currentAck, 1) 37 | } 38 | 39 | func testHandleAck() { 40 | let expect = expectation(description: "handled ack") 41 | socket.emitWithAck("test").timingOut(after: 0) {data in 42 | XCTAssertEqual(data[0] as? String, "hello world") 43 | expect.fulfill() 44 | } 45 | 46 | socket.parseSocketMessage("30[\"hello world\"]") 47 | waitForExpectations(timeout: 3, handler: nil) 48 | } 49 | 50 | func testHandleAck2() { 51 | let expect = expectation(description: "handled ack2") 52 | socket.emitWithAck("test").timingOut(after: 0) {data in 53 | XCTAssertTrue(data.count == 2, "Wrong number of ack items") 54 | expect.fulfill() 55 | } 56 | 57 | socket.parseSocketMessage("61-0[{\"_placeholder\":true,\"num\":0},{\"test\":true}]") 58 | socket.parseBinaryData(Data()) 59 | waitForExpectations(timeout: 3, handler: nil) 60 | } 61 | 62 | func testHandleEvent() { 63 | let expect = expectation(description: "handled event") 64 | socket.on("test") {data, ack in 65 | XCTAssertEqual(data[0] as? String, "hello world") 66 | expect.fulfill() 67 | } 68 | 69 | socket.parseSocketMessage("2[\"test\",\"hello world\"]") 70 | waitForExpectations(timeout: 3, handler: nil) 71 | } 72 | 73 | func testHandleStringEventWithQuotes() { 74 | let expect = expectation(description: "handled event") 75 | socket.on("test") {data, ack in 76 | XCTAssertEqual(data[0] as? String, "\"hello world\"") 77 | expect.fulfill() 78 | } 79 | 80 | socket.parseSocketMessage("2[\"test\",\"\\\"hello world\\\"\"]") 81 | waitForExpectations(timeout: 3, handler: nil) 82 | } 83 | 84 | func testHandleOnceEvent() { 85 | let expect = expectation(description: "handled event") 86 | socket.once("test") {data, ack in 87 | XCTAssertEqual(data[0] as? String, "hello world") 88 | XCTAssertEqual(self.socket.testHandlers.count, 0) 89 | expect.fulfill() 90 | } 91 | 92 | socket.parseSocketMessage("2[\"test\",\"hello world\"]") 93 | waitForExpectations(timeout: 3, handler: nil) 94 | } 95 | 96 | func testOffWithEvent() { 97 | socket.on("test") {data, ack in } 98 | XCTAssertEqual(socket.testHandlers.count, 1) 99 | socket.on("test") {data, ack in } 100 | XCTAssertEqual(socket.testHandlers.count, 2) 101 | socket.off("test") 102 | XCTAssertEqual(socket.testHandlers.count, 0) 103 | } 104 | 105 | func testOffWithId() { 106 | let handler = socket.on("test") {data, ack in } 107 | XCTAssertEqual(socket.testHandlers.count, 1) 108 | socket.on("test") {data, ack in } 109 | XCTAssertEqual(socket.testHandlers.count, 2) 110 | socket.off(id: handler) 111 | XCTAssertEqual(socket.testHandlers.count, 1) 112 | } 113 | 114 | func testHandlesErrorPacket() { 115 | let expect = expectation(description: "Handled error") 116 | socket.on("error") {data, ack in 117 | if let error = data[0] as? String, error == "test error" { 118 | expect.fulfill() 119 | } 120 | } 121 | 122 | socket.parseSocketMessage("4\"test error\"") 123 | waitForExpectations(timeout: 3, handler: nil) 124 | } 125 | 126 | func testHandleBinaryEvent() { 127 | let expect = expectation(description: "handled binary event") 128 | socket.on("test") {data, ack in 129 | if let dict = data[0] as? NSDictionary, let data = dict["test"] as? NSData { 130 | XCTAssertEqual(data as Data, self.data) 131 | expect.fulfill() 132 | } 133 | } 134 | 135 | socket.parseSocketMessage("51-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0}}]") 136 | socket.parseBinaryData(data) 137 | waitForExpectations(timeout: 3, handler: nil) 138 | } 139 | 140 | func testHandleMultipleBinaryEvent() { 141 | let expect = expectation(description: "handled multiple binary event") 142 | socket.on("test") {data, ack in 143 | if let dict = data[0] as? NSDictionary, let data = dict["test"] as? NSData, 144 | let data2 = dict["test2"] as? NSData { 145 | XCTAssertEqual(data as Data, self.data) 146 | XCTAssertEqual(data2 as Data, self.data2) 147 | expect.fulfill() 148 | } 149 | } 150 | 151 | socket.parseSocketMessage("52-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0},\"test2\":{\"_placeholder\":true,\"num\":1}}]") 152 | socket.parseBinaryData(data) 153 | socket.parseBinaryData(data2) 154 | waitForExpectations(timeout: 3, handler: nil) 155 | } 156 | 157 | func testSocketManager() { 158 | let manager = SocketClientManager.sharedManager 159 | manager["test"] = socket 160 | 161 | XCTAssert(manager["test"] === socket, "failed to get socket") 162 | 163 | manager["test"] = nil 164 | 165 | XCTAssert(manager["test"] == nil, "socket not removed") 166 | 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Source/SocketParsable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketParsable.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | protocol SocketParsable : SocketIOClientSpec { 26 | func parseBinaryData(_ data: Data) 27 | func parseSocketMessage(_ message: String) 28 | } 29 | 30 | extension SocketParsable { 31 | private func isCorrectNamespace(_ nsp: String) -> Bool { 32 | return nsp == self.nsp 33 | } 34 | 35 | private func handleConnect(_ packetNamespace: String) { 36 | if packetNamespace == "/" && nsp != "/" { 37 | joinNamespace(nsp) 38 | } else { 39 | didConnect() 40 | } 41 | } 42 | 43 | private func handlePacket(_ pack: SocketPacket) { 44 | switch pack.type { 45 | case .event where isCorrectNamespace(pack.nsp): 46 | handleEvent(pack.event, data: pack.args, isInternalMessage: false, withAck: pack.id) 47 | case .ack where isCorrectNamespace(pack.nsp): 48 | handleAck(pack.id, data: pack.data) 49 | case .binaryEvent where isCorrectNamespace(pack.nsp): 50 | waitingPackets.append(pack) 51 | case .binaryAck where isCorrectNamespace(pack.nsp): 52 | waitingPackets.append(pack) 53 | case .connect: 54 | handleConnect(pack.nsp) 55 | case .disconnect: 56 | didDisconnect(reason: "Got Disconnect") 57 | case .error: 58 | handleEvent("error", data: pack.data, isInternalMessage: true, withAck: pack.id) 59 | default: 60 | DefaultSocketLogger.Logger.log("Got invalid packet: %@", type: "SocketParser", args: pack.description) 61 | } 62 | } 63 | 64 | /// Parses a messsage from the engine. Returning either a string error or a complete SocketPacket 65 | func parseString(_ message: String) -> Either { 66 | var reader = SocketStringReader(message: message) 67 | 68 | guard let type = Int(reader.read(count: 1)).flatMap({ SocketPacket.PacketType(rawValue: $0) }) else { 69 | return .left("Invalid packet type") 70 | } 71 | 72 | if !reader.hasNext { 73 | return .right(SocketPacket(type: type, nsp: "/")) 74 | } 75 | 76 | var namespace = "/" 77 | var placeholders = -1 78 | 79 | if type == .binaryEvent || type == .binaryAck { 80 | if let holders = Int(reader.readUntilOccurence(of: "-")) { 81 | placeholders = holders 82 | } else { 83 | return .left("Invalid packet") 84 | } 85 | } 86 | 87 | if reader.currentCharacter == "/" { 88 | namespace = reader.readUntilOccurence(of: ",") 89 | } 90 | 91 | if !reader.hasNext { 92 | return .right(SocketPacket(type: type, nsp: namespace, placeholders: placeholders)) 93 | } 94 | 95 | var idString = "" 96 | 97 | if type == .error { 98 | reader.advance(by: -1) 99 | } else { 100 | while reader.hasNext { 101 | if let int = Int(reader.read(count: 1)) { 102 | idString += String(int) 103 | } else { 104 | reader.advance(by: -2) 105 | break 106 | } 107 | } 108 | } 109 | 110 | 111 | 112 | var dataArray = message[message.characters.index(reader.currentIndex, offsetBy: 1).. Either { 129 | do { 130 | return .right(try data.toArray()) 131 | } catch { 132 | return .left("Error parsing data for packet") 133 | } 134 | } 135 | 136 | // Parses messages recieved 137 | func parseSocketMessage(_ message: String) { 138 | guard !message.isEmpty else { return } 139 | 140 | DefaultSocketLogger.Logger.log("Parsing %@", type: "SocketParser", args: message) 141 | 142 | switch parseString(message) { 143 | case let .left(err): 144 | DefaultSocketLogger.Logger.error("\(err): %@", type: "SocketParser", args: message) 145 | case let .right(pack): 146 | DefaultSocketLogger.Logger.log("Decoded packet as: %@", type: "SocketParser", args: pack.description) 147 | handlePacket(pack) 148 | } 149 | } 150 | 151 | func parseBinaryData(_ data: Data) { 152 | guard !waitingPackets.isEmpty else { 153 | DefaultSocketLogger.Logger.error("Got data when not remaking packet", type: "SocketParser") 154 | return 155 | } 156 | 157 | // Should execute event? 158 | guard waitingPackets[waitingPackets.count - 1].addData(data) else { return } 159 | 160 | let packet = waitingPackets.removeLast() 161 | 162 | if packet.type != .binaryAck { 163 | handleEvent(packet.event, data: packet.args, isInternalMessage: false, withAck: packet.id) 164 | } else { 165 | handleAck(packet.id, data: packet.args) 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /SocketIO-MacTests/SocketBasicPacketTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketBasicPacketTest.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 10/7/15. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SocketIO 11 | 12 | class SocketBasicPacketTest: XCTestCase { 13 | let data = "test".data(using: String.Encoding.utf8)! 14 | let data2 = "test2".data(using: String.Encoding.utf8)! 15 | 16 | func testEmpyEmit() { 17 | let expectedSendString = "2[\"test\"]" 18 | let sendData = ["test"] 19 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/", ack: false) 20 | 21 | XCTAssertEqual(packet.packetString, expectedSendString) 22 | } 23 | 24 | func testNullEmit() { 25 | let expectedSendString = "2[\"test\",null]" 26 | let sendData: [Any] = ["test", NSNull()] 27 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/", ack: false) 28 | 29 | XCTAssertEqual(packet.packetString, expectedSendString) 30 | } 31 | 32 | func testStringEmit() { 33 | let expectedSendString = "2[\"test\",\"foo bar\"]" 34 | let sendData = ["test", "foo bar"] 35 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/", ack: false) 36 | 37 | XCTAssertEqual(packet.packetString, expectedSendString) 38 | } 39 | 40 | func testStringEmitWithQuotes() { 41 | let expectedSendString = "2[\"test\",\"\\\"he\\\"llo world\\\"\"]" 42 | let sendData = ["test", "\"he\"llo world\""] 43 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/", ack: false) 44 | 45 | XCTAssertEqual(packet.packetString, expectedSendString) 46 | } 47 | 48 | func testJSONEmit() { 49 | let expectedSendString = "2[\"test\",{\"null\":null,\"hello\":1,\"test\":\"hello\",\"foobar\":true}]" 50 | let sendData: [Any] = ["test", ["foobar": true, "hello": 1, "test": "hello", "null": NSNull()]] 51 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/", ack: false) 52 | 53 | XCTAssertEqual(packet.packetString, expectedSendString) 54 | } 55 | 56 | func testArrayEmit() { 57 | let expectedSendString = "2[\"test\",[\"hello\",1,{\"test\":\"test\"}]]" 58 | let sendData: [Any] = ["test", ["hello", 1, ["test": "test"]]] 59 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/", ack: false) 60 | 61 | XCTAssertEqual(packet.packetString, expectedSendString) 62 | } 63 | 64 | func testBinaryEmit() { 65 | let expectedSendString = "51-[\"test\",{\"_placeholder\":true,\"num\":0}]" 66 | let sendData: [Any] = ["test", data] 67 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/", ack: false) 68 | 69 | XCTAssertEqual(packet.packetString, expectedSendString) 70 | XCTAssertEqual(packet.binary, [data]) 71 | } 72 | 73 | func testMultipleBinaryEmit() { 74 | let expectedSendString = "52-[\"test\",{\"data2\":{\"_placeholder\":true,\"num\":0},\"data1\":{\"_placeholder\":true,\"num\":1}}]" 75 | let sendData: [Any] = ["test", ["data1": data, "data2": data2] as NSDictionary] 76 | let packet = SocketPacket.packetFromEmit(sendData, id: -1, nsp: "/", ack: false) 77 | 78 | XCTAssertEqual(packet.packetString, expectedSendString) 79 | XCTAssertEqual(packet.binary, [data2, data]) 80 | } 81 | 82 | func testEmitWithAck() { 83 | let expectedSendString = "20[\"test\"]" 84 | let sendData = ["test"] 85 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/", ack: false) 86 | 87 | XCTAssertEqual(packet.packetString, 88 | 89 | expectedSendString) 90 | } 91 | 92 | func testEmitDataWithAck() { 93 | let expectedSendString = "51-0[\"test\",{\"_placeholder\":true,\"num\":0}]" 94 | let sendData: [Any] = ["test", data] 95 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/", ack: false) 96 | 97 | XCTAssertEqual(packet.packetString, expectedSendString) 98 | XCTAssertEqual(packet.binary, [data]) 99 | } 100 | 101 | // Acks 102 | func testEmptyAck() { 103 | let expectedSendString = "30[]" 104 | let packet = SocketPacket.packetFromEmit([], id: 0, nsp: "/", ack: true) 105 | 106 | XCTAssertEqual(packet.packetString, expectedSendString) 107 | } 108 | 109 | func testNullAck() { 110 | let expectedSendString = "30[null]" 111 | let sendData = [NSNull()] 112 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/", ack: true) 113 | 114 | XCTAssertEqual(packet.packetString, expectedSendString) 115 | } 116 | 117 | func testStringAck() { 118 | let expectedSendString = "30[\"test\"]" 119 | let sendData = ["test"] 120 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/", ack: true) 121 | 122 | XCTAssertEqual(packet.packetString, expectedSendString) 123 | } 124 | 125 | func testJSONAck() { 126 | let expectedSendString = "30[{\"null\":null,\"hello\":1,\"test\":\"hello\",\"foobar\":true}]" 127 | let sendData = [["foobar": true, "hello": 1, "test": "hello", "null": NSNull()]] 128 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/", ack: true) 129 | 130 | XCTAssertEqual(packet.packetString, expectedSendString) 131 | } 132 | 133 | func testBinaryAck() { 134 | let expectedSendString = "61-0[{\"_placeholder\":true,\"num\":0}]" 135 | let sendData = [data] 136 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/", ack: true) 137 | 138 | XCTAssertEqual(packet.packetString, expectedSendString) 139 | XCTAssertEqual(packet.binary, [data]) 140 | } 141 | 142 | func testMultipleBinaryAck() { 143 | let expectedSendString = "62-0[{\"data2\":{\"_placeholder\":true,\"num\":0},\"data1\":{\"_placeholder\":true,\"num\":1}}]" 144 | let sendData = [["data1": data, "data2": data2]] 145 | let packet = SocketPacket.packetFromEmit(sendData, id: 0, nsp: "/", ack: true) 146 | 147 | XCTAssertEqual(packet.packetString, expectedSendString) 148 | XCTAssertEqual(packet.binary, [data2, data]) 149 | } 150 | 151 | func testBinaryStringPlaceholderInMessage() { 152 | let engineString = "52-[\"test\",\"~~0\",{\"num\":0,\"_placeholder\":true},{\"_placeholder\":true,\"num\":1}]" 153 | let socket = SocketIOClient(socketURL: URL(string: "http://localhost/")!) 154 | socket.setTestable() 155 | 156 | if case let .right(packet) = socket.parseString(engineString) { 157 | var packet = packet 158 | XCTAssertEqual(packet.event, "test") 159 | _ = packet.addData(data) 160 | _ = packet.addData(data2) 161 | XCTAssertEqual(packet.args[0] as? String, "~~0") 162 | } else { 163 | XCTFail() 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Source/SocketPacket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketPacket.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 1/18/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | struct SocketPacket { 29 | enum PacketType: Int { 30 | case connect, disconnect, event, ack, error, binaryEvent, binaryAck 31 | } 32 | 33 | private let placeholders: Int 34 | 35 | private static let logType = "SocketPacket" 36 | 37 | let nsp: String 38 | let id: Int 39 | let type: PacketType 40 | 41 | var binary: [Data] 42 | var data: [Any] 43 | var args: [Any] { 44 | if type == .event || type == .binaryEvent && data.count != 0 { 45 | return Array(data.dropFirst()) 46 | } else { 47 | return data 48 | } 49 | } 50 | 51 | var description: String { 52 | return "SocketPacket {type: \(String(type.rawValue)); data: " + 53 | "\(String(describing: data)); id: \(id); placeholders: \(placeholders); nsp: \(nsp)}" 54 | } 55 | 56 | var event: String { 57 | return String(describing: data[0]) 58 | } 59 | 60 | var packetString: String { 61 | return createPacketString() 62 | } 63 | 64 | init(type: PacketType, data: [Any] = [Any](), id: Int = -1, nsp: String, placeholders: Int = 0, 65 | binary: [Data] = [Data]()) { 66 | self.data = data 67 | self.id = id 68 | self.nsp = nsp 69 | self.type = type 70 | self.placeholders = placeholders 71 | self.binary = binary 72 | } 73 | 74 | mutating func addData(_ data: Data) -> Bool { 75 | if placeholders == binary.count { 76 | return true 77 | } 78 | 79 | binary.append(data) 80 | 81 | if placeholders == binary.count { 82 | fillInPlaceholders() 83 | return true 84 | } else { 85 | return false 86 | } 87 | } 88 | 89 | private func completeMessage(_ message: String) -> String { 90 | if data.count == 0 { 91 | return message + "[]" 92 | } 93 | 94 | guard let jsonSend = try? data.toJSON(), let jsonString = String(data: jsonSend, encoding: .utf8) else { 95 | DefaultSocketLogger.Logger.error("Error creating JSON object in SocketPacket.completeMessage", 96 | type: SocketPacket.logType) 97 | 98 | return message + "[]" 99 | } 100 | 101 | return message + jsonString 102 | } 103 | 104 | private func createPacketString() -> String { 105 | let typeString = String(type.rawValue) 106 | // Binary count? 107 | let binaryCountString = typeString + (type == .binaryEvent || type == .binaryAck ? "\(String(binary.count))-" : "") 108 | // Namespace? 109 | let nspString = binaryCountString + (nsp != "/" ? "\(nsp)," : "") 110 | // Ack number? 111 | let idString = nspString + (id != -1 ? String(id) : "") 112 | 113 | return completeMessage(idString) 114 | } 115 | 116 | // Called when we have all the binary data for a packet 117 | // calls _fillInPlaceholders, which replaces placeholders with the 118 | // corresponding binary 119 | private mutating func fillInPlaceholders() { 120 | data = data.map(_fillInPlaceholders) 121 | } 122 | 123 | // Helper method that looks for placeholders 124 | // If object is a collection it will recurse 125 | // Returns the object if it is not a placeholder or the corresponding 126 | // binary data 127 | private func _fillInPlaceholders(_ object: Any) -> Any { 128 | switch object { 129 | case let dict as JSON: 130 | if dict["_placeholder"] as? Bool ?? false { 131 | return binary[dict["num"] as! Int] 132 | } else { 133 | return dict.reduce(JSON(), {cur, keyValue in 134 | var cur = cur 135 | 136 | cur[keyValue.0] = _fillInPlaceholders(keyValue.1) 137 | 138 | return cur 139 | }) 140 | } 141 | case let arr as [Any]: 142 | return arr.map(_fillInPlaceholders) 143 | default: 144 | return object 145 | } 146 | } 147 | } 148 | 149 | extension SocketPacket { 150 | private static func findType(_ binCount: Int, ack: Bool) -> PacketType { 151 | switch binCount { 152 | case 0 where !ack: 153 | return .event 154 | case 0 where ack: 155 | return .ack 156 | case _ where !ack: 157 | return .binaryEvent 158 | case _ where ack: 159 | return .binaryAck 160 | default: 161 | return .error 162 | } 163 | } 164 | 165 | static func packetFromEmit(_ items: [Any], id: Int, nsp: String, ack: Bool) -> SocketPacket { 166 | let (parsedData, binary) = deconstructData(items) 167 | let packet = SocketPacket(type: findType(binary.count, ack: ack), data: parsedData, 168 | id: id, nsp: nsp, binary: binary) 169 | 170 | return packet 171 | } 172 | } 173 | 174 | private extension SocketPacket { 175 | // Recursive function that looks for NSData in collections 176 | static func shred(_ data: Any, binary: inout [Data]) -> Any { 177 | let placeholder = ["_placeholder": true, "num": binary.count] as JSON 178 | 179 | switch data { 180 | case let bin as Data: 181 | binary.append(bin) 182 | 183 | return placeholder 184 | case let arr as [Any]: 185 | return arr.map({shred($0, binary: &binary)}) 186 | case let dict as JSON: 187 | return dict.reduce(JSON(), {cur, keyValue in 188 | var mutCur = cur 189 | 190 | mutCur[keyValue.0] = shred(keyValue.1, binary: &binary) 191 | 192 | return mutCur 193 | }) 194 | default: 195 | return data 196 | } 197 | } 198 | 199 | // Removes binary data from emit data 200 | // Returns a type containing the de-binaryed data and the binary 201 | static func deconstructData(_ data: [Any]) -> ([Any], [Data]) { 202 | var binary = [Data]() 203 | 204 | return (data.map({shred($0, binary: &binary)}), binary) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Source/SocketEnginePollable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketEnginePollable.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 1/15/16. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | /// Protocol that is used to implement socket.io polling support 28 | public protocol SocketEnginePollable : SocketEngineSpec { 29 | var invalidated: Bool { get } 30 | /// Holds strings waiting to be sent over polling. 31 | /// You shouldn't need to mess with this. 32 | var postWait: [String] { get set } 33 | var session: URLSession? { get } 34 | /// Because socket.io doesn't let you send two polling request at the same time 35 | /// we have to keep track if there's an outstanding poll 36 | var waitingForPoll: Bool { get set } 37 | /// Because socket.io doesn't let you send two post request at the same time 38 | /// we have to keep track if there's an outstanding post 39 | var waitingForPost: Bool { get set } 40 | 41 | func doPoll() 42 | func sendPollMessage(_ message: String, withType type: SocketEnginePacketType, withData datas: [Data]) 43 | func stopPolling() 44 | } 45 | 46 | // Default polling methods 47 | extension SocketEnginePollable { 48 | private func addHeaders(for req: URLRequest) -> URLRequest { 49 | var req = req 50 | 51 | if cookies != nil { 52 | let headers = HTTPCookie.requestHeaderFields(with: cookies!) 53 | req.allHTTPHeaderFields = headers 54 | } 55 | 56 | if extraHeaders != nil { 57 | for (headerName, value) in extraHeaders! { 58 | req.setValue(value, forHTTPHeaderField: headerName) 59 | } 60 | } 61 | 62 | return req 63 | } 64 | 65 | func createRequestForPostWithPostWait() -> URLRequest { 66 | defer { postWait.removeAll(keepingCapacity: true) } 67 | 68 | var postStr = "" 69 | 70 | for packet in postWait { 71 | let len = packet.characters.count 72 | 73 | postStr += "\(len):\(packet)" 74 | } 75 | 76 | DefaultSocketLogger.Logger.log("Created POST string: %@", type: "SocketEnginePolling", args: postStr) 77 | 78 | var req = URLRequest(url: urlPollingWithSid) 79 | let postData = postStr.data(using: .utf8, allowLossyConversion: false)! 80 | 81 | req = addHeaders(for: req) 82 | 83 | req.httpMethod = "POST" 84 | req.setValue("text/plain; charset=UTF-8", forHTTPHeaderField: "Content-Type") 85 | 86 | req.httpBody = postData 87 | req.setValue(String(postData.count), forHTTPHeaderField: "Content-Length") 88 | 89 | return req as URLRequest 90 | } 91 | 92 | public func doPoll() { 93 | if websocket || waitingForPoll || !connected || closed { 94 | return 95 | } 96 | 97 | waitingForPoll = true 98 | 99 | var req = URLRequest(url: urlPollingWithSid) 100 | 101 | req = addHeaders(for: req) 102 | doLongPoll(for: req ) 103 | } 104 | 105 | func doRequest(for req: URLRequest, callbackWith callback: @escaping (Data?, URLResponse?, Error?) -> Void) { 106 | if !polling || closed || invalidated || fastUpgrade { 107 | return 108 | } 109 | 110 | DefaultSocketLogger.Logger.log("Doing polling request", type: "SocketEnginePolling") 111 | 112 | session?.dataTask(with: req, completionHandler: callback).resume() 113 | } 114 | 115 | func doLongPoll(for req: URLRequest) { 116 | doRequest(for: req) {[weak self] data, res, err in 117 | guard let this = self, this.polling else { return } 118 | 119 | if err != nil || data == nil { 120 | DefaultSocketLogger.Logger.error(err?.localizedDescription ?? "Error", type: "SocketEnginePolling") 121 | 122 | if this.polling { 123 | this.didError(reason: err?.localizedDescription ?? "Error") 124 | } 125 | 126 | return 127 | } 128 | 129 | DefaultSocketLogger.Logger.log("Got polling response", type: "SocketEnginePolling") 130 | 131 | if let str = String(data: data!, encoding: String.Encoding.utf8) { 132 | this.parseQueue.async { 133 | this.parsePollingMessage(str) 134 | } 135 | } 136 | 137 | this.waitingForPoll = false 138 | 139 | if this.fastUpgrade { 140 | this.doFastUpgrade() 141 | } else if !this.closed && this.polling { 142 | this.doPoll() 143 | } 144 | } 145 | } 146 | 147 | private func flushWaitingForPost() { 148 | if postWait.count == 0 || !connected { 149 | return 150 | } else if websocket { 151 | flushWaitingForPostToWebSocket() 152 | return 153 | } 154 | 155 | let req = createRequestForPostWithPostWait() 156 | 157 | waitingForPost = true 158 | 159 | DefaultSocketLogger.Logger.log("POSTing", type: "SocketEnginePolling") 160 | 161 | doRequest(for: req) {[weak self] data, res, err in 162 | guard let this = self else { return } 163 | 164 | if err != nil { 165 | DefaultSocketLogger.Logger.error(err?.localizedDescription ?? "Error", type: "SocketEnginePolling") 166 | 167 | if this.polling { 168 | this.didError(reason: err?.localizedDescription ?? "Error") 169 | } 170 | 171 | return 172 | } 173 | 174 | this.waitingForPost = false 175 | 176 | this.emitQueue.async { 177 | if !this.fastUpgrade { 178 | this.flushWaitingForPost() 179 | this.doPoll() 180 | } 181 | } 182 | } 183 | } 184 | 185 | func parsePollingMessage(_ str: String) { 186 | guard str.characters.count != 1 else { return } 187 | 188 | var reader = SocketStringReader(message: str) 189 | 190 | while reader.hasNext { 191 | if let n = Int(reader.readUntilOccurence(of: ":")) { 192 | let str = reader.read(count: n) 193 | 194 | handleQueue.async { self.parseEngineMessage(str, fromPolling: true) } 195 | } else { 196 | handleQueue.async { self.parseEngineMessage(str, fromPolling: true) } 197 | break 198 | } 199 | } 200 | } 201 | 202 | /// Send polling message. 203 | /// Only call on emitQueue 204 | public func sendPollMessage(_ message: String, withType type: SocketEnginePacketType, withData datas: [Data]) { 205 | DefaultSocketLogger.Logger.log("Sending poll: %@ as type: %@", type: "SocketEnginePolling", args: message, type.rawValue) 206 | let fixedMessage: String 207 | 208 | if doubleEncodeUTF8 { 209 | fixedMessage = doubleEncodeUTF8(message) 210 | } else { 211 | fixedMessage = message 212 | } 213 | 214 | postWait.append(String(type.rawValue) + fixedMessage) 215 | 216 | for data in datas { 217 | if case let .right(bin) = createBinaryDataForSend(using: data) { 218 | postWait.append(bin) 219 | } 220 | } 221 | 222 | if !waitingForPost { 223 | flushWaitingForPost() 224 | } 225 | } 226 | 227 | public func stopPolling() { 228 | waitingForPoll = false 229 | waitingForPost = false 230 | session?.finishTasksAndInvalidate() 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/socketio/socket.io-client-swift.svg?branch=master)](https://travis-ci.org/socketio/socket.io-client-swift) 2 | 3 | #Socket.IO-Client-Swift 4 | Socket.IO-client for iOS/OS X. 5 | 6 | ##Example 7 | ```swift 8 | import SocketIO 9 | 10 | let socket = SocketIOClient(socketURL: URL(string: "http://localhost:8080")!, config: [.log(true), .forcePolling(true)]) 11 | 12 | socket.on("connect") {data, ack in 13 | print("socket connected") 14 | } 15 | 16 | socket.on("currentAmount") {data, ack in 17 | if let cur = data[0] as? Double { 18 | socket.emitWithAck("canUpdate", cur).timingOut(after: 0) {data in 19 | socket.emit("update", ["amount": cur + 2.50]) 20 | } 21 | 22 | ack.with("Got your currentAmount", "dude") 23 | } 24 | } 25 | 26 | socket.connect() 27 | ``` 28 | 29 | ##Objective-C Example 30 | ```objective-c 31 | @import SocketIO; 32 | NSURL* url = [[NSURL alloc] initWithString:@"http://localhost:8080"]; 33 | SocketIOClient* socket = [[SocketIOClient alloc] initWithSocketURL:url config:@{@"log": @YES, @"forcePolling": @YES}]; 34 | 35 | [socket on:@"connect" callback:^(NSArray* data, SocketAckEmitter* ack) { 36 | NSLog(@"socket connected"); 37 | }]; 38 | 39 | [socket on:@"currentAmount" callback:^(NSArray* data, SocketAckEmitter* ack) { 40 | double cur = [[data objectAtIndex:0] floatValue]; 41 | 42 | [[socket emitWithAck:@"canUpdate" with:@[@(cur)]] timingOutAfter:0 callback:^(NSArray* data) { 43 | [socket emit:@"update" withItems:@[@{@"amount": @(cur + 2.50)}]]; 44 | }]; 45 | 46 | [ack with:@[@"Got your currentAmount, ", @"dude"]]; 47 | }]; 48 | 49 | [socket connect]; 50 | 51 | ``` 52 | 53 | ##Features 54 | - Supports socket.io 1.0+ 55 | - Supports binary 56 | - Supports Polling and WebSockets 57 | - Supports TLS/SSL 58 | - Can be used from Objective-C 59 | 60 | ##Installation 61 | Requires Swift 3/Xcode 8.x 62 | 63 | If you need swift 2.3 use the swift2.3 branch (Pre-Swift 3 support is no longer maintained) 64 | 65 | If you need swift 2.2 use 7.x (Pre-Swift 3 support is no longer maintained) 66 | 67 | If you need Swift 2.1 use v5.5.0 (Pre-Swift 2.2 support is no longer maintained) 68 | 69 | If you need Swift 1.2 use v2.4.5 (Pre-Swift 2 support is no longer maintained) 70 | 71 | If you need Swift 1.1 use v1.5.2. (Pre-Swift 1.2 support is no longer maintained) 72 | 73 | Manually (iOS 7+) 74 | ----------------- 75 | 1. Copy the Source folder into your Xcode project. (Make sure you add the files to your target(s)) 76 | 2. If you plan on using this from Objective-C, read [this](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html) on exposing Swift code to Objective-C. 77 | 78 | Swift Package Manager 79 | --------------------- 80 | Add the project as a dependency to your Package.swift: 81 | ```swift 82 | import PackageDescription 83 | 84 | let package = Package( 85 | name: "YourSocketIOProject", 86 | dependencies: [ 87 | .Package(url: "https://github.com/socketio/socket.io-client-swift", majorVersion: 8) 88 | ] 89 | ) 90 | ``` 91 | 92 | Then import `import SocketIO`. 93 | 94 | Carthage 95 | ----------------- 96 | Add this line to your `Cartfile`: 97 | ``` 98 | github "socketio/socket.io-client-swift" ~> 8.1.2 # Or latest version 99 | ``` 100 | 101 | Run `carthage update --platform ios,macosx`. 102 | 103 | CocoaPods 1.0.0 or later 104 | ------------------ 105 | Create `Podfile` and add `pod 'Socket.IO-Client-Swift'`: 106 | 107 | ```ruby 108 | use_frameworks! 109 | 110 | target 'YourApp' do 111 | pod 'Socket.IO-Client-Swift', '~> 8.1.2' # Or latest version 112 | end 113 | ``` 114 | 115 | Install pods: 116 | 117 | ``` 118 | $ pod install 119 | ``` 120 | 121 | Import the module: 122 | 123 | Swift: 124 | ```swift 125 | import SocketIO 126 | ``` 127 | 128 | Objective-C: 129 | 130 | ```Objective-C 131 | @import SocketIO; 132 | ``` 133 | 134 | CocoaSeeds 135 | ----------------- 136 | 137 | Add this line to your `Seedfile`: 138 | 139 | ``` 140 | github "socketio/socket.io-client-swift", "v8.1.2", :files => "Source/*.swift" # Or latest version 141 | ``` 142 | 143 | Run `seed install`. 144 | 145 | 146 | ##API 147 | Constructors 148 | ----------- 149 | `init(var socketURL: NSURL, config: SocketIOClientConfiguration = [])` - Creates a new SocketIOClient. If your socket.io server is secure, you need to specify `https` in your socketURL. 150 | 151 | `convenience init(socketURL: NSURL, options: NSDictionary?)` - Same as above, but meant for Objective-C. See Options on how convert between SocketIOClientOptions and dictionary keys. 152 | 153 | Options 154 | ------- 155 | All options are a case of SocketIOClientOption. To get the Objective-C Option, convert the name to lowerCamelCase. 156 | 157 | ```swift 158 | case connectParams([String: AnyObject]) // Dictionary whose contents will be passed with the connection. 159 | case cookies([NSHTTPCookie]) // An array of NSHTTPCookies. Passed during the handshake. Default is nil. 160 | case doubleEncodeUTF8(Bool) // Whether or not to double encode utf8. If using the node based server this should be true. Default is true. 161 | case extraHeaders([String: String]) // Adds custom headers to the initial request. Default is nil. 162 | case forcePolling(Bool) // `true` forces the client to use xhr-polling. Default is `false` 163 | case forceNew(Bool) // Will a create a new engine for each connect. Useful if you find a bug in the engine related to reconnects 164 | case forceWebsockets(Bool) // `true` forces the client to use WebSockets. Default is `false` 165 | case handleQueue(dispatch_queue_t) // The dispatch queue that handlers are run on. Default is the main queue. 166 | case log(Bool) // If `true` socket will log debug messages. Default is false. 167 | case logger(SocketLogger) // Custom logger that conforms to SocketLogger. Will use the default logging otherwise. 168 | case nsp(String) // The namespace to connect to. Must begin with /. Default is `/` 169 | case path(String) // If the server uses a custom path. ex: `"/swift/"`. Default is `""` 170 | case reconnects(Bool) // Whether to reconnect on server lose. Default is `true` 171 | case reconnectAttempts(Int) // How many times to reconnect. Default is `-1` (infinite tries) 172 | case reconnectWait(Int) // Amount of time to wait between reconnects. Default is `10` 173 | case sessionDelegate(NSURLSessionDelegate) // Sets an NSURLSessionDelegate for the underlying engine. Useful if you need to handle self-signed certs. Default is nil. 174 | case secure(Bool) // If the connection should use TLS. Default is false. 175 | case security(SSLSecurity) // Allows you to set which certs are valid. Useful for SSL pinning. 176 | case selfSigned(Bool) // Sets WebSocket.selfSignedSSL. Use this if you're using self-signed certs. 177 | case voipEnabled(Bool) // Only use this option if you're using the client with VoIP services. Changes the way the WebSocket is created. Default is false 178 | ``` 179 | Methods 180 | ------- 181 | 1. `on(_ event: String, callback: NormalCallback) -> NSUUID` - Adds a handler for an event. Items are passed by an array. `ack` can be used to send an ack when one is requested. See example. Returns a unique id for the handler. 182 | 2. `once(_ event: String, callback: NormalCallback) -> NSUUID` - Adds a handler that will only be executed once. Returns a unique id for the handler. 183 | 3. `onAny(callback:((event: String, items: AnyObject?)) -> Void)` - Adds a handler for all events. It will be called on any received event. 184 | 4. `emit(_ event: String, _ items: AnyObject...)` - Sends a message. Can send multiple items. 185 | 5. `emit(_ event: String, withItems items: [AnyObject])` - `emit` for Objective-C 186 | 6. `emitWithAck(_ event: String, _ items: AnyObject...) -> OnAckCallback` - Sends a message that requests an acknowledgement from the server. Returns an object which you can use to add a handler. See example. Note: The message is not sent until you call timingOut(after:) on the returned object. 187 | 7. `emitWithAck(_ event: String, withItems items: [AnyObject]) -> OnAckCallback` - `emitWithAck` for Objective-C. Note: The message is not sent until you call timingOutAfter on the returned object. 188 | 8. `connect()` - Establishes a connection to the server. A "connect" event is fired upon successful connection. 189 | 9. `connect(timeoutAfter timeoutAfter: Int, withTimeoutHandler handler: (() -> Void)?)` - Connect to the server. If it isn't connected after timeoutAfter seconds, the handler is called. 190 | 10. `disconnect()` - Closes the socket. Reopening a disconnected socket is not fully tested. 191 | 11. `reconnect()` - Causes the client to reconnect to the server. 192 | 12. `joinNamespace(_ namespace: String)` - Causes the client to join namespace. Shouldn't need to be called unless you change namespaces manually. 193 | 13. `leaveNamespace()` - Causes the client to leave the nsp and go back to / 194 | 14. `off(_ event: String)` - Removes all event handlers for event. 195 | 15. `off(id id: NSUUID)` - Removes the event that corresponds to id. 196 | 16. `removeAllHandlers()` - Removes all handlers. 197 | 198 | Client Events 199 | ------ 200 | 1. `connect` - Emitted when on a successful connection. 201 | 2. `disconnect` - Emitted when the connection is closed. 202 | 3. `error` - Emitted on an error. 203 | 4. `reconnect` - Emitted when the connection is starting to reconnect. 204 | 5. `reconnectAttempt` - Emitted when attempting to reconnect. 205 | 206 | ##Detailed Example 207 | A more detailed example can be found [here](https://github.com/nuclearace/socket.io-client-swift-example) 208 | 209 | An example using the Swift Package Manager can be found [here](https://github.com/nuclearace/socket.io-client-swift-spm-example) 210 | 211 | ##License 212 | MIT 213 | -------------------------------------------------------------------------------- /Source/SSLSecurity.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // SSLSecurity.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 5/16/15. 7 | // Copyright (c) 2014-2016 Dalton Cherry. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | import Foundation 23 | import Security 24 | 25 | public protocol SSLTrustValidator { 26 | func isValid(_ trust: SecTrust, domain: String?) -> Bool 27 | } 28 | 29 | open class SSLCert { 30 | var certData: Data? 31 | var key: SecKey? 32 | 33 | /** 34 | Designated init for certificates 35 | 36 | - parameter data: is the binary data of the certificate 37 | 38 | - returns: a representation security object to be used with 39 | */ 40 | public init(data: Data) { 41 | self.certData = data 42 | } 43 | 44 | /** 45 | Designated init for public keys 46 | 47 | - parameter key: is the public key to be used 48 | 49 | - returns: a representation security object to be used with 50 | */ 51 | public init(key: SecKey) { 52 | self.key = key 53 | } 54 | } 55 | 56 | open class SSLSecurity : SSLTrustValidator { 57 | public var validatedDN = true //should the domain name be validated? 58 | 59 | var isReady = false //is the key processing done? 60 | var certificates: [Data]? //the certificates 61 | @nonobjc var pubKeys: [SecKey]? //the public keys 62 | var usePublicKeys = false //use public keys or certificate validation? 63 | 64 | /** 65 | Use certs from main app bundle 66 | 67 | - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning validation 68 | 69 | - returns: a representation security object to be used with 70 | */ 71 | public convenience init(usePublicKeys: Bool = false) { 72 | let paths = Bundle.main.paths(forResourcesOfType: "cer", inDirectory: ".") 73 | 74 | let certs = paths.reduce([SSLCert]()) { (certs: [SSLCert], path: String) -> [SSLCert] in 75 | var certs = certs 76 | if let data = NSData(contentsOfFile: path) { 77 | certs.append(SSLCert(data: data as Data)) 78 | } 79 | return certs 80 | } 81 | 82 | self.init(certs: certs, usePublicKeys: usePublicKeys) 83 | } 84 | 85 | /** 86 | Designated init 87 | 88 | - parameter certs: is the certificates or public keys to use 89 | - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning validation 90 | 91 | - returns: a representation security object to be used with 92 | */ 93 | public init(certs: [SSLCert], usePublicKeys: Bool) { 94 | self.usePublicKeys = usePublicKeys 95 | 96 | if self.usePublicKeys { 97 | DispatchQueue.global(qos: .default).async { 98 | let pubKeys = certs.reduce([SecKey]()) { (pubKeys: [SecKey], cert: SSLCert) -> [SecKey] in 99 | var pubKeys = pubKeys 100 | if let data = cert.certData, cert.key == nil { 101 | cert.key = self.extractPublicKey(data) 102 | } 103 | if let key = cert.key { 104 | pubKeys.append(key) 105 | } 106 | return pubKeys 107 | } 108 | 109 | self.pubKeys = pubKeys 110 | self.isReady = true 111 | } 112 | } else { 113 | let certificates = certs.reduce([Data]()) { (certificates: [Data], cert: SSLCert) -> [Data] in 114 | var certificates = certificates 115 | if let data = cert.certData { 116 | certificates.append(data) 117 | } 118 | return certificates 119 | } 120 | self.certificates = certificates 121 | self.isReady = true 122 | } 123 | } 124 | 125 | /** 126 | Valid the trust and domain name. 127 | 128 | - parameter trust: is the serverTrust to validate 129 | - parameter domain: is the CN domain to validate 130 | 131 | - returns: if the key was successfully validated 132 | */ 133 | public func isValid(_ trust: SecTrust, domain: String?) -> Bool { 134 | 135 | var tries = 0 136 | while !self.isReady { 137 | usleep(1000) 138 | tries += 1 139 | if tries > 5 { 140 | return false //doesn't appear it is going to ever be ready... 141 | } 142 | } 143 | var policy: SecPolicy 144 | if self.validatedDN { 145 | policy = SecPolicyCreateSSL(true, domain as NSString?) 146 | } else { 147 | policy = SecPolicyCreateBasicX509() 148 | } 149 | SecTrustSetPolicies(trust,policy) 150 | if self.usePublicKeys { 151 | if let keys = self.pubKeys { 152 | let serverPubKeys = publicKeyChain(trust) 153 | for serverKey in serverPubKeys as [AnyObject] { 154 | for key in keys as [AnyObject] { 155 | if serverKey.isEqual(key) { 156 | return true 157 | } 158 | } 159 | } 160 | } 161 | } else if let certs = self.certificates { 162 | let serverCerts = certificateChain(trust) 163 | var collect = [SecCertificate]() 164 | for cert in certs { 165 | collect.append(SecCertificateCreateWithData(nil,cert as CFData)!) 166 | } 167 | SecTrustSetAnchorCertificates(trust,collect as NSArray) 168 | var result: SecTrustResultType = .unspecified 169 | SecTrustEvaluate(trust,&result) 170 | if result == .unspecified || result == .proceed { 171 | var trustedCount = 0 172 | for serverCert in serverCerts { 173 | for cert in certs { 174 | if cert == serverCert { 175 | trustedCount += 1 176 | break 177 | } 178 | } 179 | } 180 | if trustedCount == serverCerts.count { 181 | return true 182 | } 183 | } 184 | } 185 | return false 186 | } 187 | 188 | /** 189 | Get the public key from a certificate data 190 | 191 | - parameter data: is the certificate to pull the public key from 192 | 193 | - returns: a public key 194 | */ 195 | func extractPublicKey(_ data: Data) -> SecKey? { 196 | guard let cert = SecCertificateCreateWithData(nil, data as CFData) else { return nil } 197 | 198 | return extractPublicKey(cert, policy: SecPolicyCreateBasicX509()) 199 | } 200 | 201 | /** 202 | Get the public key from a certificate 203 | 204 | - parameter data: is the certificate to pull the public key from 205 | 206 | - returns: a public key 207 | */ 208 | func extractPublicKey(_ cert: SecCertificate, policy: SecPolicy) -> SecKey? { 209 | var possibleTrust: SecTrust? 210 | SecTrustCreateWithCertificates(cert, policy, &possibleTrust) 211 | 212 | guard let trust = possibleTrust else { return nil } 213 | var result: SecTrustResultType = .unspecified 214 | SecTrustEvaluate(trust, &result) 215 | return SecTrustCopyPublicKey(trust) 216 | } 217 | 218 | /** 219 | Get the certificate chain for the trust 220 | 221 | - parameter trust: is the trust to lookup the certificate chain for 222 | 223 | - returns: the certificate chain for the trust 224 | */ 225 | func certificateChain(_ trust: SecTrust) -> [Data] { 226 | let certificates = (0.. [Data] in 227 | var certificates = certificates 228 | let cert = SecTrustGetCertificateAtIndex(trust, index) 229 | certificates.append(SecCertificateCopyData(cert!) as Data) 230 | return certificates 231 | } 232 | 233 | return certificates 234 | } 235 | 236 | /** 237 | Get the public key chain for the trust 238 | 239 | - parameter trust: is the trust to lookup the certificate chain and extract the public keys 240 | 241 | - returns: the public keys from the certifcate chain for the trust 242 | */ 243 | @nonobjc func publicKeyChain(_ trust: SecTrust) -> [SecKey] { 244 | let policy = SecPolicyCreateBasicX509() 245 | let keys = (0.. [SecKey] in 246 | var keys = keys 247 | let cert = SecTrustGetCertificateAtIndex(trust, index) 248 | if let key = extractPublicKey(cert!, policy: policy) { 249 | keys.append(key) 250 | } 251 | 252 | return keys 253 | } 254 | 255 | return keys 256 | } 257 | 258 | 259 | } 260 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Erik Little 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | 24 | 25 | This library makes use of the following third party libraries: 26 | 27 | Starscream 28 | ---------- 29 | 30 | Apache License 31 | Version 2.0, January 2004 32 | http://www.apache.org/licenses/ 33 | 34 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 35 | 36 | 1. Definitions. 37 | 38 | "License" shall mean the terms and conditions for use, reproduction, 39 | and distribution as defined by Sections 1 through 9 of this document. 40 | 41 | "Licensor" shall mean the copyright owner or entity authorized by 42 | the copyright owner that is granting the License. 43 | 44 | "Legal Entity" shall mean the union of the acting entity and all 45 | other entities that control, are controlled by, or are under common 46 | control with that entity. For the purposes of this definition, 47 | "control" means (i) the power, direct or indirect, to cause the 48 | direction or management of such entity, whether by contract or 49 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 50 | outstanding shares, or (iii) beneficial ownership of such entity. 51 | 52 | "You" (or "Your") shall mean an individual or Legal Entity 53 | exercising permissions granted by this License. 54 | 55 | "Source" form shall mean the preferred form for making modifications, 56 | including but not limited to software source code, documentation 57 | source, and configuration files. 58 | 59 | "Object" form shall mean any form resulting from mechanical 60 | transformation or translation of a Source form, including but 61 | not limited to compiled object code, generated documentation, 62 | and conversions to other media types. 63 | 64 | "Work" shall mean the work of authorship, whether in Source or 65 | Object form, made available under the License, as indicated by a 66 | copyright notice that is included in or attached to the work 67 | (an example is provided in the Appendix below). 68 | 69 | "Derivative Works" shall mean any work, whether in Source or Object 70 | form, that is based on (or derived from) the Work and for which the 71 | editorial revisions, annotations, elaborations, or other modifications 72 | represent, as a whole, an original work of authorship. For the purposes 73 | of this License, Derivative Works shall not include works that remain 74 | separable from, or merely link (or bind by name) to the interfaces of, 75 | the Work and Derivative Works thereof. 76 | 77 | "Contribution" shall mean any work of authorship, including 78 | the original version of the Work and any modifications or additions 79 | to that Work or Derivative Works thereof, that is intentionally 80 | submitted to Licensor for inclusion in the Work by the copyright owner 81 | or by an individual or Legal Entity authorized to submit on behalf of 82 | the copyright owner. For the purposes of this definition, "submitted" 83 | means any form of electronic, verbal, or written communication sent 84 | to the Licensor or its representatives, including but not limited to 85 | communication on electronic mailing lists, source code control systems, 86 | and issue tracking systems that are managed by, or on behalf of, the 87 | Licensor for the purpose of discussing and improving the Work, but 88 | excluding communication that is conspicuously marked or otherwise 89 | designated in writing by the copyright owner as "Not a Contribution." 90 | 91 | "Contributor" shall mean Licensor and any individual or Legal Entity 92 | on behalf of whom a Contribution has been received by Licensor and 93 | subsequently incorporated within the Work. 94 | 95 | 2. Grant of Copyright License. Subject to the terms and conditions of 96 | this License, each Contributor hereby grants to You a perpetual, 97 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 98 | copyright license to reproduce, prepare Derivative Works of, 99 | publicly display, publicly perform, sublicense, and distribute the 100 | Work and such Derivative Works in Source or Object form. 101 | 102 | 3. Grant of Patent License. Subject to the terms and conditions of 103 | this License, each Contributor hereby grants to You a perpetual, 104 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 105 | (except as stated in this section) patent license to make, have made, 106 | use, offer to sell, sell, import, and otherwise transfer the Work, 107 | where such license applies only to those patent claims licensable 108 | by such Contributor that are necessarily infringed by their 109 | Contribution(s) alone or by combination of their Contribution(s) 110 | with the Work to which such Contribution(s) was submitted. If You 111 | institute patent litigation against any entity (including a 112 | cross-claim or counterclaim in a lawsuit) alleging that the Work 113 | or a Contribution incorporated within the Work constitutes direct 114 | or contributory patent infringement, then any patent licenses 115 | granted to You under this License for that Work shall terminate 116 | as of the date such litigation is filed. 117 | 118 | 4. Redistribution. You may reproduce and distribute copies of the 119 | Work or Derivative Works thereof in any medium, with or without 120 | modifications, and in Source or Object form, provided that You 121 | meet the following conditions: 122 | 123 | (a) You must give any other recipients of the Work or 124 | Derivative Works a copy of this License; and 125 | 126 | (b) You must cause any modified files to carry prominent notices 127 | stating that You changed the files; and 128 | 129 | (c) You must retain, in the Source form of any Derivative Works 130 | that You distribute, all copyright, patent, trademark, and 131 | attribution notices from the Source form of the Work, 132 | excluding those notices that do not pertain to any part of 133 | the Derivative Works; and 134 | 135 | (d) If the Work includes a "NOTICE" text file as part of its 136 | distribution, then any Derivative Works that You distribute must 137 | include a readable copy of the attribution notices contained 138 | within such NOTICE file, excluding those notices that do not 139 | pertain to any part of the Derivative Works, in at least one 140 | of the following places: within a NOTICE text file distributed 141 | as part of the Derivative Works; within the Source form or 142 | documentation, if provided along with the Derivative Works; or, 143 | within a display generated by the Derivative Works, if and 144 | wherever such third-party notices normally appear. The contents 145 | of the NOTICE file are for informational purposes only and 146 | do not modify the License. You may add Your own attribution 147 | notices within Derivative Works that You distribute, alongside 148 | or as an addendum to the NOTICE text from the Work, provided 149 | that such additional attribution notices cannot be construed 150 | as modifying the License. 151 | 152 | You may add Your own copyright statement to Your modifications and 153 | may provide additional or different license terms and conditions 154 | for use, reproduction, or distribution of Your modifications, or 155 | for any such Derivative Works as a whole, provided Your use, 156 | reproduction, and distribution of the Work otherwise complies with 157 | the conditions stated in this License. 158 | 159 | 5. Submission of Contributions. Unless You explicitly state otherwise, 160 | any Contribution intentionally submitted for inclusion in the Work 161 | by You to the Licensor shall be under the terms and conditions of 162 | this License, without any additional terms or conditions. 163 | Notwithstanding the above, nothing herein shall supersede or modify 164 | the terms of any separate license agreement you may have executed 165 | with Licensor regarding such Contributions. 166 | 167 | 6. Trademarks. This License does not grant permission to use the trade 168 | names, trademarks, service marks, or product names of the Licensor, 169 | except as required for reasonable and customary use in describing the 170 | origin of the Work and reproducing the content of the NOTICE file. 171 | 172 | 7. Disclaimer of Warranty. Unless required by applicable law or 173 | agreed to in writing, Licensor provides the Work (and each 174 | Contributor provides its Contributions) on an "AS IS" BASIS, 175 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 176 | implied, including, without limitation, any warranties or conditions 177 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 178 | PARTICULAR PURPOSE. You are solely responsible for determining the 179 | appropriateness of using or redistributing the Work and assume any 180 | risks associated with Your exercise of permissions under this License. 181 | 182 | 8. Limitation of Liability. In no event and under no legal theory, 183 | whether in tort (including negligence), contract, or otherwise, 184 | unless required by applicable law (such as deliberate and grossly 185 | negligent acts) or agreed to in writing, shall any Contributor be 186 | liable to You for damages, including any direct, indirect, special, 187 | incidental, or consequential damages of any character arising as a 188 | result of this License or out of the use or inability to use the 189 | Work (including but not limited to damages for loss of goodwill, 190 | work stoppage, computer failure or malfunction, or any and all 191 | other commercial damages or losses), even if such Contributor 192 | has been advised of the possibility of such damages. 193 | 194 | 9. Accepting Warranty or Additional Liability. While redistributing 195 | the Work or Derivative Works thereof, You may choose to offer, 196 | and charge a fee for, acceptance of support, warranty, indemnity, 197 | or other liability obligations and/or rights consistent with this 198 | License. However, in accepting such obligations, You may act only 199 | on Your own behalf and on Your sole responsibility, not on behalf 200 | of any other Contributor, and only if You agree to indemnify, 201 | defend, and hold each Contributor harmless for any liability 202 | incurred by, or claims asserted against, such Contributor by reason 203 | of your accepting any such warranty or additional liability. 204 | 205 | END OF TERMS AND CONDITIONS 206 | 207 | APPENDIX: How to apply the Apache License to your work. 208 | 209 | To apply the Apache License to your work, attach the following 210 | boilerplate notice, with the fields enclosed by brackets "{}" 211 | replaced with your own identifying information. (Don't include 212 | the brackets!) The text should be enclosed in the appropriate 213 | comment syntax for the file format. We also recommend that a 214 | file or class name and description of purpose be included on the 215 | same "printed page" as the copyright notice for easier 216 | identification within third-party archives. 217 | 218 | Copyright {yyyy} {name of copyright owner} 219 | 220 | Licensed under the Apache License, Version 2.0 (the "License"); 221 | you may not use this file except in compliance with the License. 222 | You may obtain a copy of the License at 223 | 224 | http://www.apache.org/licenses/LICENSE-2.0 225 | 226 | Unless required by applicable law or agreed to in writing, software 227 | distributed under the License is distributed on an "AS IS" BASIS, 228 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 229 | See the License for the specific language governing permissions and 230 | limitations under the License. 231 | -------------------------------------------------------------------------------- /Source/SocketIOClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketIOClient.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 11/23/14. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable { 28 | public let socketURL: URL 29 | 30 | public private(set) var engine: SocketEngineSpec? 31 | public private(set) var status = SocketIOClientStatus.notConnected { 32 | didSet { 33 | switch status { 34 | case .connected: 35 | reconnecting = false 36 | currentReconnectAttempt = 0 37 | default: 38 | break 39 | } 40 | } 41 | } 42 | 43 | public var forceNew = false 44 | public var nsp = "/" 45 | public var config: SocketIOClientConfiguration 46 | public var reconnects = true 47 | public var reconnectWait = 10 48 | 49 | private let logType = "SocketIOClient" 50 | private let parseQueue = DispatchQueue(label: "com.socketio.parseQueue") 51 | 52 | private var anyHandler: ((SocketAnyEvent) -> Void)? 53 | private var currentReconnectAttempt = 0 54 | private var handlers = [SocketEventHandler]() 55 | private var reconnecting = false 56 | 57 | private(set) var currentAck = -1 58 | private(set) var handleQueue = DispatchQueue.main 59 | private(set) var reconnectAttempts = -1 60 | 61 | let ackQueue = DispatchQueue(label: "com.socketio.ackQueue") 62 | let emitQueue = DispatchQueue(label: "com.socketio.emitQueue") 63 | 64 | var ackHandlers = SocketAckManager() 65 | var waitingPackets = [SocketPacket]() 66 | 67 | public var sid: String? { 68 | return nsp + "#" + (engine?.sid ?? "") 69 | } 70 | 71 | /// Type safe way to create a new SocketIOClient. opts can be omitted 72 | public init(socketURL: URL, config: SocketIOClientConfiguration = []) { 73 | self.config = config 74 | self.socketURL = socketURL 75 | 76 | if socketURL.absoluteString.hasPrefix("https://") { 77 | self.config.insert(.secure(true)) 78 | } 79 | 80 | for option in config { 81 | switch option { 82 | case let .reconnects(reconnects): 83 | self.reconnects = reconnects 84 | case let .reconnectAttempts(attempts): 85 | reconnectAttempts = attempts 86 | case let .reconnectWait(wait): 87 | reconnectWait = abs(wait) 88 | case let .nsp(nsp): 89 | self.nsp = nsp 90 | case let .log(log): 91 | DefaultSocketLogger.Logger.log = log 92 | case let .logger(logger): 93 | DefaultSocketLogger.Logger = logger 94 | case let .handleQueue(queue): 95 | handleQueue = queue 96 | case let .forceNew(force): 97 | forceNew = force 98 | default: 99 | continue 100 | } 101 | } 102 | 103 | self.config.insert(.path("/socket.io/"), replacing: false) 104 | 105 | super.init() 106 | } 107 | 108 | /// Not so type safe way to create a SocketIOClient, meant for Objective-C compatiblity. 109 | /// If using Swift it's recommended to use `init(socketURL: NSURL, options: Set)` 110 | public convenience init(socketURL: NSURL, config: NSDictionary?) { 111 | self.init(socketURL: socketURL as URL, config: config?.toSocketConfiguration() ?? []) 112 | } 113 | 114 | deinit { 115 | DefaultSocketLogger.Logger.log("Client is being released", type: logType) 116 | engine?.disconnect(reason: "Client Deinit") 117 | } 118 | 119 | private func addEngine() -> SocketEngineSpec { 120 | DefaultSocketLogger.Logger.log("Adding engine", type: logType, args: "") 121 | 122 | engine = SocketEngine(client: self, url: socketURL, config: config) 123 | 124 | return engine! 125 | } 126 | 127 | /// Connect to the server. 128 | public func connect() { 129 | connect(timeoutAfter: 0, withHandler: nil) 130 | } 131 | 132 | /// Connect to the server. If we aren't connected after timeoutAfter, call withHandler 133 | /// 0 Never times out 134 | public func connect(timeoutAfter: Int, withHandler handler: (() -> Void)?) { 135 | assert(timeoutAfter >= 0, "Invalid timeout: \(timeoutAfter)") 136 | 137 | guard status != .connected else { 138 | DefaultSocketLogger.Logger.log("Tried connecting on an already connected socket", type: logType) 139 | return 140 | } 141 | 142 | status = .connecting 143 | 144 | if engine == nil || forceNew { 145 | addEngine().connect() 146 | } else { 147 | engine?.connect() 148 | } 149 | 150 | guard timeoutAfter != 0 else { return } 151 | 152 | let time = DispatchTime.now() + Double(UInt64(timeoutAfter) * NSEC_PER_SEC) / Double(NSEC_PER_SEC) 153 | 154 | handleQueue.asyncAfter(deadline: time) {[weak self] in 155 | guard let this = self, this.status != .connected && this.status != .disconnected else { return } 156 | 157 | this.status = .disconnected 158 | this.engine?.disconnect(reason: "Connect timeout") 159 | 160 | handler?() 161 | } 162 | } 163 | 164 | private func createOnAck(_ items: [Any]) -> OnAckCallback { 165 | currentAck += 1 166 | 167 | return OnAckCallback(ackNumber: currentAck, items: items, socket: self) 168 | } 169 | 170 | func didConnect() { 171 | DefaultSocketLogger.Logger.log("Socket connected", type: logType) 172 | status = .connected 173 | 174 | // Don't handle as internal because something crazy could happen where 175 | // we disconnect before it's handled 176 | handleEvent("connect", data: [], isInternalMessage: false) 177 | } 178 | 179 | func didDisconnect(reason: String) { 180 | guard status != .disconnected else { return } 181 | 182 | DefaultSocketLogger.Logger.log("Disconnected: %@", type: logType, args: reason) 183 | 184 | reconnecting = false 185 | status = .disconnected 186 | 187 | // Make sure the engine is actually dead. 188 | engine?.disconnect(reason: reason) 189 | handleEvent("disconnect", data: [reason], isInternalMessage: true) 190 | } 191 | 192 | /// Disconnects the socket. 193 | public func disconnect() { 194 | DefaultSocketLogger.Logger.log("Closing socket", type: logType) 195 | 196 | didDisconnect(reason: "Disconnect") 197 | } 198 | 199 | /// Send a message to the server 200 | public func emit(_ event: String, _ items: SocketData...) { 201 | emit(event, with: items) 202 | } 203 | 204 | /// Same as emit, but meant for Objective-C 205 | public func emit(_ event: String, with items: [Any]) { 206 | guard status == .connected else { 207 | handleEvent("error", data: ["Tried emitting \(event) when not connected"], isInternalMessage: true) 208 | return 209 | } 210 | 211 | _emit([event] + items) 212 | } 213 | 214 | /// Sends a message to the server, requesting an ack. Use the onAck method of SocketAckHandler to add 215 | /// an ack. 216 | public func emitWithAck(_ event: String, _ items: SocketData...) -> OnAckCallback { 217 | return emitWithAck(event, with: items) 218 | } 219 | 220 | /// Same as emitWithAck, but for Objective-C 221 | public func emitWithAck(_ event: String, with items: [Any]) -> OnAckCallback { 222 | return createOnAck([event] + items) 223 | } 224 | 225 | func _emit(_ data: [Any], ack: Int? = nil) { 226 | emitQueue.async { 227 | guard self.status == .connected else { 228 | self.handleEvent("error", data: ["Tried emitting when not connected"], isInternalMessage: true) 229 | return 230 | } 231 | 232 | let packet = SocketPacket.packetFromEmit(data, id: ack ?? -1, nsp: self.nsp, ack: false) 233 | let str = packet.packetString 234 | 235 | DefaultSocketLogger.Logger.log("Emitting: %@", type: self.logType, args: str) 236 | 237 | self.engine?.send(str, withData: packet.binary) 238 | } 239 | } 240 | 241 | // If the server wants to know that the client received data 242 | func emitAck(_ ack: Int, with items: [Any]) { 243 | emitQueue.async { 244 | guard self.status == .connected else { return } 245 | 246 | let packet = SocketPacket.packetFromEmit(items, id: ack, nsp: self.nsp, ack: true) 247 | let str = packet.packetString 248 | 249 | DefaultSocketLogger.Logger.log("Emitting Ack: %@", type: self.logType, args: str) 250 | 251 | self.engine?.send(str, withData: packet.binary) 252 | } 253 | } 254 | 255 | public func engineDidClose(reason: String) { 256 | parseQueue.async { 257 | self.waitingPackets.removeAll() 258 | } 259 | 260 | if status != .disconnected { 261 | status = .notConnected 262 | } 263 | 264 | if status == .disconnected || !reconnects { 265 | didDisconnect(reason: reason) 266 | } else if !reconnecting { 267 | reconnecting = true 268 | tryReconnect(reason: reason) 269 | } 270 | } 271 | 272 | /// error 273 | public func engineDidError(reason: String) { 274 | DefaultSocketLogger.Logger.error("%@", type: logType, args: reason) 275 | 276 | handleEvent("error", data: [reason], isInternalMessage: true) 277 | } 278 | 279 | public func engineDidOpen(reason: String) { 280 | DefaultSocketLogger.Logger.log(reason, type: "SocketEngineClient") 281 | } 282 | 283 | // Called when the socket gets an ack for something it sent 284 | func handleAck(_ ack: Int, data: [Any]) { 285 | guard status == .connected else { return } 286 | 287 | DefaultSocketLogger.Logger.log("Handling ack: %@ with data: %@", type: logType, args: ack, data) 288 | 289 | handleQueue.async() { 290 | self.ackHandlers.executeAck(ack, with: data, onQueue: self.handleQueue) 291 | } 292 | } 293 | 294 | /// Causes an event to be handled. Only use if you know what you're doing. 295 | public func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) { 296 | guard status == .connected || isInternalMessage else { return } 297 | 298 | DefaultSocketLogger.Logger.log("Handling event: %@ with data: %@", type: logType, args: event, data) 299 | 300 | handleQueue.async { 301 | self.anyHandler?(SocketAnyEvent(event: event, items: data)) 302 | 303 | for handler in self.handlers where handler.event == event { 304 | handler.executeCallback(with: data, withAck: ack, withSocket: self) 305 | } 306 | } 307 | } 308 | 309 | /// Leaves nsp and goes back to / 310 | public func leaveNamespace() { 311 | if nsp != "/" { 312 | engine?.send("1\(nsp)", withData: []) 313 | nsp = "/" 314 | } 315 | } 316 | 317 | /// Joins namespace 318 | public func joinNamespace(_ namespace: String) { 319 | nsp = namespace 320 | 321 | if nsp != "/" { 322 | DefaultSocketLogger.Logger.log("Joining namespace", type: logType) 323 | engine?.send("0\(nsp)", withData: []) 324 | } 325 | } 326 | 327 | /// Removes handler(s) based on name 328 | public func off(_ event: String) { 329 | DefaultSocketLogger.Logger.log("Removing handler for event: %@", type: logType, args: event) 330 | 331 | handlers = handlers.filter({ $0.event != event }) 332 | } 333 | 334 | /// Removes a handler with the specified UUID gotten from an `on` or `once` 335 | public func off(id: UUID) { 336 | DefaultSocketLogger.Logger.log("Removing handler with id: %@", type: logType, args: id) 337 | 338 | handlers = handlers.filter({ $0.id != id }) 339 | } 340 | 341 | /// Adds a handler for an event. 342 | /// Returns: A unique id for the handler 343 | @discardableResult 344 | public func on(_ event: String, callback: @escaping NormalCallback) -> UUID { 345 | DefaultSocketLogger.Logger.log("Adding handler for event: %@", type: logType, args: event) 346 | 347 | let handler = SocketEventHandler(event: event, id: UUID(), callback: callback) 348 | handlers.append(handler) 349 | 350 | return handler.id 351 | } 352 | 353 | /// Adds a single-use handler for an event. 354 | /// Returns: A unique id for the handler 355 | @discardableResult 356 | public func once(_ event: String, callback: @escaping NormalCallback) -> UUID { 357 | DefaultSocketLogger.Logger.log("Adding once handler for event: %@", type: logType, args: event) 358 | 359 | let id = UUID() 360 | 361 | let handler = SocketEventHandler(event: event, id: id) {[weak self] data, ack in 362 | guard let this = self else { return } 363 | this.off(id: id) 364 | callback(data, ack) 365 | } 366 | 367 | handlers.append(handler) 368 | 369 | return handler.id 370 | } 371 | 372 | /// Adds a handler that will be called on every event. 373 | public func onAny(_ handler: @escaping (SocketAnyEvent) -> Void) { 374 | anyHandler = handler 375 | } 376 | 377 | public func parseEngineMessage(_ msg: String) { 378 | DefaultSocketLogger.Logger.log("Should parse message: %@", type: "SocketIOClient", args: msg) 379 | 380 | parseQueue.async { self.parseSocketMessage(msg) } 381 | } 382 | 383 | public func parseEngineBinaryData(_ data: Data) { 384 | parseQueue.async { self.parseBinaryData(data) } 385 | } 386 | 387 | /// Tries to reconnect to the server. 388 | public func reconnect() { 389 | guard !reconnecting else { return } 390 | 391 | engine?.disconnect(reason: "manual reconnect") 392 | } 393 | 394 | /// Removes all handlers. 395 | /// Can be used after disconnecting to break any potential remaining retain cycles. 396 | public func removeAllHandlers() { 397 | handlers.removeAll(keepingCapacity: false) 398 | } 399 | 400 | private func tryReconnect(reason: String) { 401 | guard reconnecting else { return } 402 | 403 | DefaultSocketLogger.Logger.log("Starting reconnect", type: logType) 404 | handleEvent("reconnect", data: [reason], isInternalMessage: true) 405 | 406 | _tryReconnect() 407 | } 408 | 409 | private func _tryReconnect() { 410 | guard reconnecting else { return } 411 | 412 | if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts || !reconnects { 413 | return didDisconnect(reason: "Reconnect Failed") 414 | } 415 | 416 | DefaultSocketLogger.Logger.log("Trying to reconnect", type: logType) 417 | handleEvent("reconnectAttempt", data: [(reconnectAttempts - currentReconnectAttempt)], isInternalMessage: true) 418 | 419 | currentReconnectAttempt += 1 420 | connect() 421 | 422 | let deadline = DispatchTime.now() + Double(Int64(UInt64(reconnectWait) * NSEC_PER_SEC)) / Double(NSEC_PER_SEC) 423 | 424 | DispatchQueue.main.asyncAfter(deadline: deadline, execute: _tryReconnect) 425 | } 426 | 427 | // Test properties 428 | 429 | var testHandlers: [SocketEventHandler] { 430 | return handlers 431 | } 432 | 433 | func setTestable() { 434 | status = .connected 435 | } 436 | 437 | func setTestEngine(_ engine: SocketEngineSpec?) { 438 | self.engine = engine 439 | } 440 | 441 | func emitTest(event: String, _ data: Any...) { 442 | _emit([event] + data) 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /Source/SocketEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketEngine.swift 3 | // Socket.IO-Client-Swift 4 | // 5 | // Created by Erik Little on 3/3/15. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, SocketEngineWebsocket { 28 | public let emitQueue = DispatchQueue(label: "com.socketio.engineEmitQueue", attributes: []) 29 | public let handleQueue = DispatchQueue(label: "com.socketio.engineHandleQueue", attributes: []) 30 | public let parseQueue = DispatchQueue(label: "com.socketio.engineParseQueue", attributes: []) 31 | 32 | public var connectParams: [String: Any]? { 33 | didSet { 34 | (urlPolling, urlWebSocket) = createURLs() 35 | } 36 | } 37 | 38 | public var postWait = [String]() 39 | public var waitingForPoll = false 40 | public var waitingForPost = false 41 | 42 | public private(set) var closed = false 43 | public private(set) var connected = false 44 | public private(set) var cookies: [HTTPCookie]? 45 | public private(set) var doubleEncodeUTF8 = true 46 | public private(set) var extraHeaders: [String: String]? 47 | public private(set) var fastUpgrade = false 48 | public private(set) var forcePolling = false 49 | public private(set) var forceWebsockets = false 50 | public private(set) var invalidated = false 51 | public private(set) var polling = true 52 | public private(set) var probing = false 53 | public private(set) var session: URLSession? 54 | public private(set) var sid = "" 55 | public private(set) var socketPath = "/engine.io/" 56 | public private(set) var urlPolling = URL(string: "http://localhost/")! 57 | public private(set) var urlWebSocket = URL(string: "http://localhost/")! 58 | public private(set) var websocket = false 59 | public private(set) var ws: WebSocket? 60 | 61 | public weak var client: SocketEngineClient? 62 | 63 | private weak var sessionDelegate: URLSessionDelegate? 64 | 65 | private let logType = "SocketEngine" 66 | private let url: URL 67 | 68 | private var pingInterval: Double? 69 | private var pingTimeout = 0.0 { 70 | didSet { 71 | pongsMissedMax = Int(pingTimeout / (pingInterval ?? 25)) 72 | } 73 | } 74 | 75 | private var pongsMissed = 0 76 | private var pongsMissedMax = 0 77 | private var probeWait = ProbeWaitQueue() 78 | private var secure = false 79 | private var security: SSLSecurity? 80 | private var selfSigned = false 81 | private var voipEnabled = false 82 | 83 | public init(client: SocketEngineClient, url: URL, config: SocketIOClientConfiguration) { 84 | self.client = client 85 | self.url = url 86 | for option in config { 87 | switch option { 88 | case let .connectParams(params): 89 | connectParams = params 90 | case let .cookies(cookies): 91 | self.cookies = cookies 92 | case let .doubleEncodeUTF8(encode): 93 | doubleEncodeUTF8 = encode 94 | case let .extraHeaders(headers): 95 | extraHeaders = headers 96 | case let .sessionDelegate(delegate): 97 | sessionDelegate = delegate 98 | case let .forcePolling(force): 99 | forcePolling = force 100 | case let .forceWebsockets(force): 101 | forceWebsockets = force 102 | case let .path(path): 103 | socketPath = path 104 | 105 | if !socketPath.hasSuffix("/") { 106 | socketPath += "/" 107 | } 108 | case let .voipEnabled(enable): 109 | voipEnabled = enable 110 | case let .secure(secure): 111 | self.secure = secure 112 | case let .selfSigned(selfSigned): 113 | self.selfSigned = selfSigned 114 | case let .security(security): 115 | self.security = security 116 | default: 117 | continue 118 | } 119 | } 120 | 121 | super.init() 122 | 123 | sessionDelegate = sessionDelegate ?? self 124 | 125 | (urlPolling, urlWebSocket) = createURLs() 126 | } 127 | 128 | public convenience init(client: SocketEngineClient, url: URL, options: NSDictionary?) { 129 | self.init(client: client, url: url, config: options?.toSocketConfiguration() ?? []) 130 | } 131 | 132 | deinit { 133 | DefaultSocketLogger.Logger.log("Engine is being released", type: logType) 134 | closed = true 135 | stopPolling() 136 | } 137 | 138 | private func checkAndHandleEngineError(_ msg: String) { 139 | do { 140 | let dict = try msg.toNSDictionary() 141 | guard let error = dict["message"] as? String else { return } 142 | 143 | /* 144 | 0: Unknown transport 145 | 1: Unknown sid 146 | 2: Bad handshake request 147 | 3: Bad request 148 | */ 149 | didError(reason: error) 150 | } catch { 151 | client?.engineDidError(reason: "Got unknown error from server \(msg)") 152 | } 153 | } 154 | 155 | private func handleBase64(message: String) { 156 | // binary in base64 string 157 | let noPrefix = message[message.index(message.startIndex, offsetBy: 2).. (URL, URL) { 211 | if client == nil { 212 | return (URL(string: "http://localhost/")!, URL(string: "http://localhost/")!) 213 | } 214 | 215 | var urlPolling = URLComponents(string: url.absoluteString)! 216 | var urlWebSocket = URLComponents(string: url.absoluteString)! 217 | var queryString = "" 218 | 219 | urlWebSocket.path = socketPath 220 | urlPolling.path = socketPath 221 | 222 | if secure { 223 | urlPolling.scheme = "https" 224 | urlWebSocket.scheme = "wss" 225 | } else { 226 | urlPolling.scheme = "http" 227 | urlWebSocket.scheme = "ws" 228 | } 229 | 230 | if connectParams != nil { 231 | for (key, value) in connectParams! { 232 | let keyEsc = key.urlEncode()! 233 | let valueEsc = "\(value)".urlEncode()! 234 | 235 | queryString += "&\(keyEsc)=\(valueEsc)" 236 | } 237 | } 238 | 239 | urlWebSocket.percentEncodedQuery = "transport=websocket" + queryString 240 | urlPolling.percentEncodedQuery = "transport=polling&b64=1" + queryString 241 | 242 | return (urlPolling.url!, urlWebSocket.url!) 243 | } 244 | 245 | private func createWebsocketAndConnect() { 246 | ws = WebSocket(url: urlWebSocketWithSid as URL) 247 | 248 | if cookies != nil { 249 | let headers = HTTPCookie.requestHeaderFields(with: cookies!) 250 | for (key, value) in headers { 251 | ws?.headers[key] = value 252 | } 253 | } 254 | 255 | if extraHeaders != nil { 256 | for (headerName, value) in extraHeaders! { 257 | ws?.headers[headerName] = value 258 | } 259 | } 260 | 261 | ws?.callbackQueue = handleQueue 262 | ws?.voipEnabled = voipEnabled 263 | ws?.delegate = self 264 | ws?.disableSSLCertValidation = selfSigned 265 | ws?.security = security 266 | 267 | ws?.connect() 268 | } 269 | 270 | public func didError(reason: String) { 271 | DefaultSocketLogger.Logger.error("%@", type: logType, args: reason) 272 | client?.engineDidError(reason: reason) 273 | disconnect(reason: reason) 274 | } 275 | 276 | public func disconnect(reason: String) { 277 | guard connected else { return closeOutEngine(reason: reason) } 278 | 279 | DefaultSocketLogger.Logger.log("Engine is being closed.", type: logType) 280 | 281 | if closed { 282 | return closeOutEngine(reason: reason) 283 | } 284 | 285 | if websocket { 286 | sendWebSocketMessage("", withType: .close, withData: []) 287 | closeOutEngine(reason: reason) 288 | } else { 289 | disconnectPolling(reason: reason) 290 | } 291 | } 292 | 293 | // We need to take special care when we're polling that we send it ASAP 294 | // Also make sure we're on the emitQueue since we're touching postWait 295 | private func disconnectPolling(reason: String) { 296 | emitQueue.sync { 297 | self.postWait.append(String(SocketEnginePacketType.close.rawValue)) 298 | let req = self.createRequestForPostWithPostWait() 299 | self.doRequest(for: req) {_, _, _ in } 300 | self.closeOutEngine(reason: reason) 301 | } 302 | } 303 | 304 | public func doFastUpgrade() { 305 | if waitingForPoll { 306 | DefaultSocketLogger.Logger.error("Outstanding poll when switched to WebSockets," + 307 | "we'll probably disconnect soon. You should report this.", type: logType) 308 | } 309 | 310 | sendWebSocketMessage("", withType: .upgrade, withData: []) 311 | websocket = true 312 | polling = false 313 | fastUpgrade = false 314 | probing = false 315 | flushProbeWait() 316 | } 317 | 318 | private func flushProbeWait() { 319 | DefaultSocketLogger.Logger.log("Flushing probe wait", type: logType) 320 | 321 | emitQueue.async { 322 | for waiter in self.probeWait { 323 | self.write(waiter.msg, withType: waiter.type, withData: waiter.data) 324 | } 325 | 326 | self.probeWait.removeAll(keepingCapacity: false) 327 | 328 | if self.postWait.count != 0 { 329 | self.flushWaitingForPostToWebSocket() 330 | } 331 | } 332 | } 333 | 334 | // We had packets waiting for send when we upgraded 335 | // Send them raw 336 | public func flushWaitingForPostToWebSocket() { 337 | guard let ws = self.ws else { return } 338 | 339 | for msg in postWait { 340 | ws.write(string: msg) 341 | } 342 | 343 | postWait.removeAll(keepingCapacity: false) 344 | } 345 | 346 | private func handleClose(_ reason: String) { 347 | client?.engineDidClose(reason: reason) 348 | } 349 | 350 | private func handleMessage(_ message: String) { 351 | client?.parseEngineMessage(message) 352 | } 353 | 354 | private func handleNOOP() { 355 | doPoll() 356 | } 357 | 358 | private func handleOpen(openData: String) { 359 | guard let json = try? openData.toNSDictionary() else { 360 | didError(reason: "Error parsing open packet") 361 | 362 | return 363 | } 364 | 365 | guard let sid = json["sid"] as? String else { 366 | didError(reason: "Open packet contained no sid") 367 | 368 | return 369 | } 370 | 371 | let upgradeWs: Bool 372 | 373 | self.sid = sid 374 | connected = true 375 | 376 | if let upgrades = json["upgrades"] as? [String] { 377 | upgradeWs = upgrades.contains("websocket") 378 | } else { 379 | upgradeWs = false 380 | } 381 | 382 | if let pingInterval = json["pingInterval"] as? Double, let pingTimeout = json["pingTimeout"] as? Double { 383 | self.pingInterval = pingInterval / 1000.0 384 | self.pingTimeout = pingTimeout / 1000.0 385 | } 386 | 387 | if !forcePolling && !forceWebsockets && upgradeWs { 388 | createWebsocketAndConnect() 389 | } 390 | 391 | sendPing() 392 | 393 | if !forceWebsockets { 394 | doPoll() 395 | } 396 | 397 | client?.engineDidOpen(reason: "Connect") 398 | } 399 | 400 | private func handlePong(with message: String) { 401 | pongsMissed = 0 402 | 403 | // We should upgrade 404 | if message == "3probe" { 405 | upgradeTransport() 406 | } 407 | } 408 | 409 | public func parseEngineData(_ data: Data) { 410 | DefaultSocketLogger.Logger.log("Got binary data: %@", type: "SocketEngine", args: data) 411 | 412 | client?.parseEngineBinaryData(data.subdata(in: 1.. pongsMissedMax { 473 | client?.engineDidClose(reason: "Ping timeout") 474 | 475 | return 476 | } 477 | 478 | guard let pingInterval = pingInterval else { return } 479 | 480 | pongsMissed += 1 481 | write("", withType: .ping, withData: []) 482 | 483 | let time = DispatchTime.now() + Double(Int64(pingInterval * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) 484 | DispatchQueue.main.asyncAfter(deadline: time) {[weak self] in self?.sendPing() } 485 | } 486 | 487 | // Moves from long-polling to websockets 488 | private func upgradeTransport() { 489 | if ws?.isConnected ?? false { 490 | DefaultSocketLogger.Logger.log("Upgrading transport to WebSockets", type: logType) 491 | 492 | fastUpgrade = true 493 | sendPollMessage("", withType: .noop, withData: []) 494 | // After this point, we should not send anymore polling messages 495 | } 496 | } 497 | 498 | /// Write a message, independent of transport. 499 | public func write(_ msg: String, withType type: SocketEnginePacketType, withData data: [Data]) { 500 | emitQueue.async { 501 | guard self.connected else { return } 502 | 503 | if self.websocket { 504 | DefaultSocketLogger.Logger.log("Writing ws: %@ has data: %@", 505 | type: self.logType, args: msg, data.count != 0) 506 | self.sendWebSocketMessage(msg, withType: type, withData: data) 507 | } else if !self.probing { 508 | DefaultSocketLogger.Logger.log("Writing poll: %@ has data: %@", 509 | type: self.logType, args: msg, data.count != 0) 510 | self.sendPollMessage(msg, withType: type, withData: data) 511 | } else { 512 | self.probeWait.append((msg, type, data)) 513 | } 514 | } 515 | } 516 | 517 | // Delegate methods 518 | public func websocketDidConnect(socket: WebSocket) { 519 | if !forceWebsockets { 520 | probing = true 521 | probeWebSocket() 522 | } else { 523 | connected = true 524 | probing = false 525 | polling = false 526 | } 527 | } 528 | 529 | public func websocketDidDisconnect(socket: WebSocket, error: NSError?) { 530 | probing = false 531 | 532 | if closed { 533 | client?.engineDidClose(reason: "Disconnect") 534 | 535 | return 536 | } 537 | 538 | if websocket { 539 | connected = false 540 | websocket = false 541 | 542 | if let reason = error?.localizedDescription { 543 | didError(reason: reason) 544 | } else { 545 | client?.engineDidClose(reason: "Socket Disconnected") 546 | } 547 | } else { 548 | flushProbeWait() 549 | } 550 | } 551 | } 552 | 553 | extension SocketEngine { 554 | public func URLSession(session: URLSession, didBecomeInvalidWithError error: NSError?) { 555 | DefaultSocketLogger.Logger.error("Engine URLSession became invalid", type: "SocketEngine") 556 | 557 | didError(reason: "Engine URLSession became invalid") 558 | } 559 | } 560 | --------------------------------------------------------------------------------