├── .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 | [](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 |
--------------------------------------------------------------------------------