diff --git a/.travis.yml b/.travis.yml index ff931c3..b90dafc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: objective-c xcode_project: Socket.IO-Client-Swift.xcodeproj # path to your xcodeproj folder xcode_scheme: SocketIO-Mac -osx_image: xcode11.2 +osx_image: xcode12.2 branches: only: - master diff --git a/CHANGELOG.md b/CHANGELOG.md index fa85a85..7b496c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# v16.0.0 + +- Removed Objective-C support. It's time for you to embrace Swift. +- Socket.io 3 support. + # v15.3.0 - Add `==` operators for `SocketAckStatus` and `String` diff --git a/Cartfile b/Cartfile index 4f886c2..6c2bffe 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "daltoniam/Starscream" ~> 3.1 +github "daltoniam/Starscream" ~> 4.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index c76f727..f9b2a1a 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1 +1 @@ -github "daltoniam/Starscream" "3.1.0" +github "daltoniam/Starscream" "4.0.4" diff --git a/Package.resolved b/Package.resolved index 6f383f4..a9c6ce6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/daltoniam/Starscream", "state": { "branch": null, - "revision": "9c03ef715d1bc9334b446c90df53586dd38cf849", - "version": "3.1.0" + "revision": "df8d82047f6654d8e4b655d1b1525c64e1059d21", + "version": "4.0.4" } } ] diff --git a/Package.swift b/Package.swift index 001312c..90c4b1f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.3 import PackageDescription @@ -8,7 +8,7 @@ let package = Package( .library(name: "SocketIO", targets: ["SocketIO"]) ], dependencies: [ - .package(url: "https://github.com/daltoniam/Starscream", .upToNextMinor(from: "3.1.0")), + .package(url: "https://github.com/daltoniam/Starscream", .upToNextMinor(from: "4.0.0")), ], targets: [ .target(name: "SocketIO", dependencies: ["Starscream"]), diff --git a/Socket.IO-Client-Swift.podspec b/Socket.IO-Client-Swift.podspec index e9c9c8c..843c744 100644 --- a/Socket.IO-Client-Swift.podspec +++ b/Socket.IO-Client-Swift.podspec @@ -1,12 +1,12 @@ Pod::Spec.new do |s| s.name = "Socket.IO-Client-Swift" s.module_name = "SocketIO" - s.version = "15.2.0" + s.version = "16.0.0" s.summary = "Socket.IO-client for iOS and OS X" s.description = <<-DESC Socket.IO-client for iOS and OS X. Supports ws/wss/polling connections and binary. - For socket.io 2.0+ and Swift. + For socket.io 3.0+ and Swift. DESC s.homepage = "https://github.com/socketio/socket.io-client-swift" s.license = { :type => 'MIT' } @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.requires_arc = true s.source = { :git => "https://github.com/socketio/socket.io-client-swift.git", - :tag => 'v15.2.0', + :tag => 'v16.0.0', :submodules => true } @@ -27,5 +27,5 @@ Pod::Spec.new do |s| 'SWIFT_VERSION' => '5.0' } s.source_files = "Source/SocketIO/**/*.swift", "Source/SocketIO/*.swift" - s.dependency "Starscream", "~> 3.1" + s.dependency "Starscream", "~> 4.0" end diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index 355fd21..955e813 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -40,23 +40,14 @@ import Foundation /// /// **NOTE**: The client is not thread/queue safe, all interaction with the socket should be done on the `manager.handleQueue` /// -open class SocketIOClient : NSObject, SocketIOClientSpec { +open class SocketIOClient: NSObject, SocketIOClientSpec { // MARK: Properties /// The namespace that this socket is currently connected to. /// /// **Must** start with a `/`. - @objc public let nsp: String - /// The session id of this client. - @objc - public var sid: String { - guard let engine = manager?.engine else { return "" } - - return nsp == "/" ? engine.sid : "\(nsp)#\(engine.sid)" - } - /// A handler that will be called on any event. public private(set) var anyHandler: ((SocketAnyEvent) -> ())? @@ -64,7 +55,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { public private(set) var handlers = [SocketEventHandler]() /// The manager for this socket. - @objc public private(set) weak var manager: SocketManagerSpec? /// A view into this socket where emits do not check for binary data. @@ -76,17 +66,18 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// ``` /// /// **NOTE**: It is not safe to hold on to this view beyond the life of the socket. - @objc public private(set) lazy var rawEmitView = SocketRawView(socket: self) /// The status of this client. - @objc public private(set) var status = SocketIOStatus.notConnected { didSet { handleClientEvent(.statusChange, data: [status, status.rawValue]) } } + /// The id of this socket.io connect. This is different from the sid of the engine.io connection. + public private(set) var sid: String? + let ackHandlers = SocketAckManager() private(set) var currentAck = -1 @@ -99,7 +90,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// /// - parameter manager: The manager for this socket. /// - parameter nsp: The namespace of the socket. - @objc public init(manager: SocketManagerSpec, nsp: String) { self.manager = manager self.nsp = nsp @@ -117,7 +107,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// Connect to the server. The same as calling `connect(timeoutAfter:withHandler:)` with a timeout of 0. /// /// Only call after adding your event listeners, unless you know what you're doing. - @objc open func connect() { connect(timeoutAfter: 0, withHandler: nil) } @@ -129,7 +118,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// - parameter timeoutAfter: The number of seconds after which if we are not connected we assume the connection /// has failed. Pass 0 to never timeout. /// - parameter handler: The handler to call when the client fails to connect. - @objc open func connect(timeoutAfter: Double, withHandler handler: (() -> ())?) { assert(timeoutAfter >= 0, "Invalid timeout: \(timeoutAfter)") @@ -142,13 +130,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { joinNamespace() - if manager.status == .connected && nsp == "/" { - // We might not get a connect event for the default nsp, fire immediately - didConnect(toNamespace: nsp) - - return - } - guard timeoutAfter != 0 else { return } manager.handleQueue.asyncAfter(deadline: DispatchTime.now() + timeoutAfter) {[weak self] in @@ -171,14 +152,15 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// then this is only called when the client connects to that namespace. /// /// - parameter toNamespace: The namespace that was connected to. - open func didConnect(toNamespace namespace: String) { + open func didConnect(toNamespace namespace: String, payload: [String: Any]?) { guard status != .connected else { return } DefaultSocketLogger.Logger.log("Socket connected", type: logType) status = .connected + sid = payload?["sid"] as? String - handleClientEvent(.connect, data: [namespace]) + handleClientEvent(.connect, data: payload == nil ? [namespace] : [namespace, payload!]) } /// Called when the client has disconnected from socket.io. @@ -190,6 +172,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { DefaultSocketLogger.Logger.log("Disconnected: \(reason)", type: logType) status = .disconnected + sid = "" handleClientEvent(.disconnect, data: [reason]) } @@ -198,7 +181,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// /// This will cause the socket to leave the namespace it is associated to, as well as remove itself from the /// `manager`. - @objc open func disconnect() { DefaultSocketLogger.Logger.log("Closing socket", type: logType) @@ -215,7 +197,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// - parameter completion: Callback called on transport write completion. open func emit(_ event: String, _ items: SocketData..., completion: (() -> ())? = nil) { do { - try emit(event, with: items.map({ try $0.socketRepresentation() }), completion: completion) + emit([event] + (try items.map({ try $0.socketRepresentation() })), completion: completion) } catch { DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)", type: logType) @@ -224,25 +206,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { } } - /// Same as emit, but meant for Objective-C - /// - /// - parameter event: The event to send. - /// - parameter items: The items to send with this event. Send an empty array to send no data. - @objc - open func emit(_ event: String, with items: [Any]) { - emit([event] + items) - } - - /// Same as emit, but meant for Objective-C - /// - /// - parameter event: The event to send. - /// - parameter items: The items to send with this event. Send an empty array to send no data. - /// - parameter completion: Callback called on transport write completion. - @objc - open func emit(_ event: String, with items: [Any], completion: (() -> ())? = nil) { - emit([event] + items, completion: completion) - } - /// Sends a message to the server, requesting an ack. /// /// **NOTE**: It is up to the server send an ack back, just calling this method does not mean the server will ack. @@ -264,7 +227,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// - returns: An `OnAckCallback`. You must call the `timingOut(after:)` method before the event will be sent. open func emitWithAck(_ event: String, _ items: SocketData...) -> OnAckCallback { do { - return emitWithAck(event, with: try items.map({ try $0.socketRepresentation() })) + return createOnAck([event] + (try items.map({ try $0.socketRepresentation() }))) } catch { DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)", type: logType) @@ -275,27 +238,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { } } - /// Same as emitWithAck, but for Objective-C - /// - /// **NOTE**: It is up to the server send an ack back, just calling this method does not mean the server will ack. - /// Check that your server's api will ack the event being sent. - /// - /// Example: - /// - /// ```swift - /// socket.emitWithAck("myEvent", with: [1]).timingOut(after: 1) {data in - /// ... - /// } - /// ``` - /// - /// - parameter event: The event to send. - /// - parameter items: The items to send with this event. Use `[]` to send nothing. - /// - returns: An `OnAckCallback`. You must call the `timingOut(after:)` method before the event will be sent. - @objc - open func emitWithAck(_ event: String, with items: [Any]) -> OnAckCallback { - return createOnAck([event] + items) - } - func emit(_ data: [Any], ack: Int? = nil, binary: Bool = true, @@ -338,7 +280,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// /// - parameter ack: The number for this ack. /// - parameter data: The data sent back with this ack. - @objc open func handleAck(_ ack: Int, data: [Any]) { guard status == .connected else { return } @@ -361,7 +302,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// - parameter data: The data that was sent with this event. /// - parameter isInternalMessage: Whether this event was sent internally. If `true` it is always sent to handlers. /// - parameter ack: If > 0 then this event expects to get an ack back from the client. - @objc open func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) { guard status == .connected || isInternalMessage else { return } @@ -387,7 +327,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { case .ack, .binaryAck: handleAck(packet.id, data: packet.data) case .connect: - didConnect(toNamespace: nsp) + didConnect(toNamespace: nsp, payload: packet.data.isEmpty ? nil : packet.data[0] as? [String: Any]) case .disconnect: didDisconnect(reason: "Got Disconnect") case .error: @@ -396,13 +336,11 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { } /// Call when you wish to leave a namespace and disconnect this socket. - @objc open func leaveNamespace() { manager?.disconnectSocket(self) } /// Joins `nsp`. - @objc open func joinNamespace() { DefaultSocketLogger.Logger.log("Joining namespace \(nsp)", type: logType) @@ -423,7 +361,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// If you wish to remove a specific event, call the `off(id:)` with the UUID received from its `on` call. /// /// - parameter event: The event to remove handlers for. - @objc open func off(_ event: String) { DefaultSocketLogger.Logger.log("Removing handler for event: \(event)", type: logType) @@ -435,7 +372,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// If you want to remove all events for an event, call the off `off(_:)` method with the event name. /// /// - parameter id: The UUID of the handler you wish to remove. - @objc open func off(id: UUID) { DefaultSocketLogger.Logger.log("Removing handler with id: \(id)", type: logType) @@ -447,7 +383,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// - parameter event: The event name for this handler. /// - parameter callback: The callback that will execute when this event is received. /// - returns: A unique id for the handler that can be used to remove it. - @objc @discardableResult open func on(_ event: String, callback: @escaping NormalCallback) -> UUID { DefaultSocketLogger.Logger.log("Adding handler for event: \(event)", type: logType) @@ -491,7 +426,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// - parameter event: The event name for this handler. /// - parameter callback: The callback that will execute when this event is received. /// - returns: A unique id for the handler that can be used to remove it. - @objc @discardableResult open func once(_ event: String, callback: @escaping NormalCallback) -> UUID { DefaultSocketLogger.Logger.log("Adding once handler for event: \(event)", type: logType) @@ -512,20 +446,17 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// Adds a handler that will be called on every event. /// /// - parameter handler: The callback that will execute whenever an event is received. - @objc open func onAny(_ handler: @escaping (SocketAnyEvent) -> ()) { anyHandler = handler } /// Tries to reconnect to the server. - @objc @available(*, unavailable, message: "Call the manager's reconnect method") open func reconnect() { } /// Removes all handlers. /// /// Can be used after disconnecting to break any potential remaining retain cycles. - @objc open func removeAllHandlers() { handlers.removeAll(keepingCapacity: false) } @@ -534,7 +465,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { /// Called when the manager detects a broken connection, or when a manual reconnect is triggered. /// /// - parameter reason: The reason this socket is reconnecting. - @objc open func setReconnecting(reason: String) { status = .connecting diff --git a/Source/SocketIO/Client/SocketIOClientOption.swift b/Source/SocketIO/Client/SocketIOClientOption.swift index bda846d..c4effb8 100644 --- a/Source/SocketIO/Client/SocketIOClientOption.swift +++ b/Source/SocketIO/Client/SocketIOClientOption.swift @@ -52,7 +52,7 @@ public enum SocketIOClientOption : ClientOption { /// If passed `true`, the only transport that will be used will be WebSockets. case forceWebsockets(Bool) - + /// If passed `true`, the WebSocket stream will be configured with the enableSOCKSProxy `true`. case enableSOCKSProxy(Bool) @@ -80,10 +80,10 @@ public enum SocketIOClientOption : ClientOption { /// The minimum number of seconds to wait before reconnect attempts. case reconnectWait(Int) - + /// The maximum number of seconds to wait before reconnect attempts. case reconnectWaitMax(Int) - + /// The randomization factor for calculating reconnect jitter. case randomizationFactor(Double) @@ -91,7 +91,7 @@ public enum SocketIOClientOption : ClientOption { case secure(Bool) /// Allows you to set which certs are valid. Useful for SSL pinning. - case security(SSLSecurity) + case security(CertificatePinning) /// If you're using a self-signed set. Only use for development. case selfSigned(Bool) diff --git a/Source/SocketIO/Client/SocketIOClientSpec.swift b/Source/SocketIO/Client/SocketIOClientSpec.swift index 06c67e6..18b3d64 100644 --- a/Source/SocketIO/Client/SocketIOClientSpec.swift +++ b/Source/SocketIO/Client/SocketIOClientSpec.swift @@ -54,6 +54,9 @@ public protocol SocketIOClientSpec : AnyObject { /// **NOTE**: It is not safe to hold on to this view beyond the life of the socket. var rawEmitView: SocketRawView { get } + /// The id of this socket.io connect. This is different from the sid of the engine.io connection. + var sid: String? { get } + /// The status of this client. var status: SocketIOStatus { get } @@ -77,7 +80,7 @@ public protocol SocketIOClientSpec : AnyObject { /// then this is only called when the client connects to that namespace. /// /// - parameter toNamespace: The namespace that was connected to. - func didConnect(toNamespace namespace: String) + func didConnect(toNamespace namespace: String, payload: [String: Any]?) /// Called when the client has disconnected from socket.io. /// diff --git a/Source/SocketIO/Engine/SocketEngine.swift b/Source/SocketIO/Engine/SocketEngine.swift index a22965f..8b82300 100644 --- a/Source/SocketIO/Engine/SocketEngine.swift +++ b/Source/SocketIO/Engine/SocketEngine.swift @@ -28,7 +28,8 @@ import Starscream /// The class that handles the engine.io protocol and transports. /// See `SocketEnginePollable` and `SocketEngineWebsocket` for transport specific methods. -open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, SocketEngineWebsocket, ConfigSettable { +open class SocketEngine: + NSObject, WebSocketDelegate, URLSessionDelegate, SocketEnginePollable, SocketEngineWebsocket, ConfigSettable { // MARK: Properties private static let logType = "SocketEngine" @@ -120,6 +121,9 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So /// The WebSocket for this engine. public private(set) var ws: WebSocket? + /// Whether or not the WebSocket is currently connected. + public private(set) var wsConnected = false + /// The client for this engine. public weak var client: SocketEngineClient? @@ -130,15 +134,15 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So private var pingInterval: Int? private var pingTimeout = 0 { didSet { - pongsMissedMax = Int(pingTimeout / (pingInterval ?? 25000)) + pingsMissedMax = Int(pingTimeout / (pingInterval ?? 25000)) } } - private var pongsMissed = 0 - private var pongsMissedMax = 0 + private var pingsMissed = 0 + private var pingsMissedMax = 0 private var probeWait = ProbeWaitQueue() private var secure = false - private var security: SocketIO.SSLSecurity? + private var certPinner: CertificatePinning? private var selfSigned = false // MARK: Initializers @@ -198,7 +202,7 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So private func handleBase64(message: String) { // binary in base64 string - let noPrefix = String(message[message.index(message.startIndex, offsetBy: 2).. this.pingsMissedMax { + this.closeOutEngine(reason: "Ping timeout") + } else { + this.pingsMissed += 1 + + this.checkPings() + } + } } /// Parses raw binary received from engine.io. @@ -491,7 +484,7 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So open func parseEngineData(_ data: Data) { DefaultSocketLogger.Logger.log("Got binary data: \(data)", type: SocketEngine.logType) - client?.parseEngineBinaryData(data.subdata(in: 1.. pongsMissedMax { + if pingsMissed > pingsMissedMax { closeOutEngine(reason: "Ping timeout") return } - pongsMissed += 1 + pingsMissed += 1 write("", withType: .ping, withData: [], completion: nil) engineQueue.asyncAfter(deadline: .now() + .milliseconds(pingInterval)) {[weak self, id = self.sid] in @@ -564,7 +559,7 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So this.sendPing() } - client?.engineDidSendPing() + client?.engineDidSendPong() } /// Called when the engine should set/update its configs from a given configuration. @@ -595,8 +590,8 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So self.secure = secure case let .selfSigned(selfSigned): self.selfSigned = selfSigned - case let .security(security): - self.security = security + case let .security(pinner): + self.certPinner = pinner case .compress: self.compress = true case .enableSOCKSProxy: @@ -609,7 +604,7 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So // Moves from long-polling to websockets private func upgradeTransport() { - if ws?.isConnected ?? false { + if wsConnected { DefaultSocketLogger.Logger.log("Upgrading transport to WebSockets", type: SocketEngine.logType) fastUpgrade = true @@ -630,6 +625,7 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So completion?() return } + guard !self.probing else { self.probeWait.append((msg, type, data, completion)) @@ -705,3 +701,32 @@ extension SocketEngine { didError(reason: "Engine URLSession became invalid") } } + +enum EngineError: Error { + case canceled +} + +extension SocketEngine { + public func didReceive(event: WebSocketEvent, client _: WebSocket) { + switch event { + case let .connected(headers): + wsConnected = true + client?.engineDidWebsocketUpgrade(headers: headers) + websocketDidConnect() + case let .error(err): + print(err) + case .cancelled: + wsConnected = false + websocketDidDisconnect(error: EngineError.canceled) + case let .disconnected(reason, code): + wsConnected = false + websocketDidDisconnect(error: nil) + case let .text(msg): + parseEngineMessage(msg) + case let .binary(data): + parseEngineData(data) + case _: + break + } + } +} diff --git a/Source/SocketIO/Engine/SocketEngineClient.swift b/Source/SocketIO/Engine/SocketEngineClient.swift index 00d68fa..bd3a3aa 100644 --- a/Source/SocketIO/Engine/SocketEngineClient.swift +++ b/Source/SocketIO/Engine/SocketEngineClient.swift @@ -44,11 +44,11 @@ import Foundation /// - parameter reason: The reason the engine opened. func engineDidOpen(reason: String) - /// Called when the engine receives a pong message. - func engineDidReceivePong() + /// Called when the engine receives a ping message. + func engineDidReceivePing() - /// Called when the engine sends a ping to the server. - func engineDidSendPing() + /// Called when the engine sends a pong to the server. + func engineDidSendPong() /// Called when the engine has a message that must be parsed. /// diff --git a/Source/SocketIO/Engine/SocketEnginePacketType.swift b/Source/SocketIO/Engine/SocketEnginePacketType.swift index b420ff2..a361168 100644 --- a/Source/SocketIO/Engine/SocketEnginePacketType.swift +++ b/Source/SocketIO/Engine/SocketEnginePacketType.swift @@ -26,7 +26,7 @@ import Foundation /// Represents the type of engine.io packet types. -@objc public enum SocketEnginePacketType : Int { +@objc public enum SocketEnginePacketType: Int { /// Open message. case open diff --git a/Source/SocketIO/Engine/SocketEnginePollable.swift b/Source/SocketIO/Engine/SocketEnginePollable.swift index 9e55bd9..9a00a69 100644 --- a/Source/SocketIO/Engine/SocketEnginePollable.swift +++ b/Source/SocketIO/Engine/SocketEnginePollable.swift @@ -25,7 +25,7 @@ import Foundation /// Protocol that is used to implement socket.io polling support -public protocol SocketEnginePollable : SocketEngineSpec { +public protocol SocketEnginePollable: SocketEngineSpec { // MARK: Properties /// `true` If engine's session has been invalidated. @@ -79,11 +79,7 @@ extension SocketEnginePollable { postWait.removeAll(keepingCapacity: true) } - var postStr = "" - - for packet in postWait { - postStr += "\(packet.msg.utf16.count):\(packet.msg)" - } + let postStr = postWait.lazy.map({ $0.msg }).joined(separator: "\u{1e}") DefaultSocketLogger.Logger.log("Created POST string: \(postStr)", type: "SocketEnginePolling") @@ -195,19 +191,14 @@ extension SocketEnginePollable { } func parsePollingMessage(_ str: String) { - guard str.count != 1 else { return } + guard !str.isEmpty else { return } DefaultSocketLogger.Logger.log("Got poll message: \(str)", type: "SocketEnginePolling") - var reader = SocketStringReader(message: str) + let records = str.components(separatedBy: "\u{1e}") - while reader.hasNext { - if let n = Int(reader.readUntilOccurence(of: ":")) { - parseEngineMessage(reader.read(count: n)) - } else { - parseEngineMessage(str) - break - } + for record in records { + parseEngineMessage(record) } } diff --git a/Source/SocketIO/Engine/SocketEngineSpec.swift b/Source/SocketIO/Engine/SocketEngineSpec.swift index a7ccf33..cbbb38c 100644 --- a/Source/SocketIO/Engine/SocketEngineSpec.swift +++ b/Source/SocketIO/Engine/SocketEngineSpec.swift @@ -27,7 +27,7 @@ import Foundation import Starscream /// Specifies a SocketEngine. -@objc public protocol SocketEngineSpec { +public protocol SocketEngineSpec: class { // MARK: Properties /// The client for this engine. @@ -173,9 +173,9 @@ extension SocketEngineSpec { func createBinaryDataForSend(using data: Data) -> Either { if polling { - return .right("b4" + data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0))) + return .right("b" + data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0))) } else { - return .left(Data([0x4]) + data) + return .left(data) } } diff --git a/Source/SocketIO/Engine/SocketEngineWebsocket.swift b/Source/SocketIO/Engine/SocketEngineWebsocket.swift index ea1c53f..1453ce4 100644 --- a/Source/SocketIO/Engine/SocketEngineWebsocket.swift +++ b/Source/SocketIO/Engine/SocketEngineWebsocket.swift @@ -27,7 +27,12 @@ import Foundation import Starscream /// Protocol that is used to implement socket.io WebSocket support -public protocol SocketEngineWebsocket : SocketEngineSpec { +public protocol SocketEngineWebsocket: SocketEngineSpec { + // MARK: Properties + + /// Whether or not the ws is connected + var wsConnected: Bool { get } + // MARK: Methods /// Sends an engine.io message through the WebSocket transport. @@ -47,7 +52,7 @@ public protocol SocketEngineWebsocket : SocketEngineSpec { // WebSocket methods extension SocketEngineWebsocket { func probeWebSocket() { - if ws?.isConnected ?? false { + if wsConnected { sendWebSocketMessage("probe", withType: .ping, withData: [], completion: nil) } } @@ -69,14 +74,14 @@ extension SocketEngineWebsocket { ws?.write(string: "\(type.rawValue)\(str)") - if data.count == 0 { - completion?() - } - for item in data { if case let .left(bin) = createBinaryDataForSend(using: item) { ws?.write(data: bin, completion: completion) } } + + if data.count == 0 { + completion?() + } } } diff --git a/Source/SocketIO/Manager/SocketManager.swift b/Source/SocketIO/Manager/SocketManager.swift index f87fd7c..3eb5168 100644 --- a/Source/SocketIO/Manager/SocketManager.swift +++ b/Source/SocketIO/Manager/SocketManager.swift @@ -45,7 +45,7 @@ import Foundation /// /// **NOTE**: The manager is not thread/queue safe, all interaction with the manager should be done on the `handleQueue` /// -open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDataBufferable, ConfigSettable { +open class SocketManager: NSObject, SocketManagerSpec, SocketParsable, SocketDataBufferable, ConfigSettable { private static let logType = "SocketManager" // MARK: Properties @@ -282,18 +282,8 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa return } - emitAll(event, withItems: emitData) - } - - /// Sends an event to the server on all namespaces in this manager. - /// - /// Same as `emitAll(_:_:)`, but meant for Objective-C. - /// - /// - parameter event: The event to send. - /// - parameter items: The data to send with this event. - open func emitAll(_ event: String, withItems items: [Any]) { forAll {socket in - socket.emit(event, with: items, completion: nil) + socket.emit([event] + emitData, completion: nil) } } @@ -349,33 +339,32 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa DefaultSocketLogger.Logger.log("Engine opened \(reason)", type: SocketManager.logType) status = .connected - nsps["/"]?.didConnect(toNamespace: "/") - for (nsp, socket) in nsps where nsp != "/" && socket.status == .connecting { + for (_, socket) in nsps where socket.status == .connecting { connectSocket(socket) } } /// Called when the engine receives a pong message. - open func engineDidReceivePong() { + open func engineDidReceivePing() { handleQueue.async { - self._engineDidReceivePong() + self._engineDidReceivePing() } } - private func _engineDidReceivePong() { - emitAll(clientEvent: .pong, data: []) + private func _engineDidReceivePing() { + emitAll(clientEvent: .ping, data: []) } /// Called when the sends a ping to the server. - open func engineDidSendPing() { + open func engineDidSendPong() { handleQueue.async { - self._engineDidSendPing() + self._engineDidSendPong() } } - private func _engineDidSendPing() { - emitAll(clientEvent: .ping, data: []) + private func _engineDidSendPong() { + emitAll(clientEvent: .pong, data: []) } private func forAll(do: (SocketIOClient) throws -> ()) rethrows { @@ -476,14 +465,19 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa } DefaultSocketLogger.Logger.log("Trying to reconnect", type: SocketManager.logType) - emitAll(clientEvent: .reconnectAttempt, data: [(reconnectAttempts - currentReconnectAttempt)]) + + forAll {socket in + guard socket.status == .connecting else { return } + + socket.handleClientEvent(.reconnectAttempt, data: [(reconnectAttempts - currentReconnectAttempt)]) + } currentReconnectAttempt += 1 connect() let interval = reconnectInterval(attempts: currentReconnectAttempt) DefaultSocketLogger.Logger.log("Scheduling reconnect in \(interval)s", type: SocketManager.logType) - handleQueue.asyncAfter(deadline: DispatchTime.now() + interval, execute: _tryReconnect) + handleQueue.asyncAfter(deadline: .now() + interval, execute: _tryReconnect) } func reconnectInterval(attempts: Int) -> Double { diff --git a/Source/SocketIO/Manager/SocketManagerSpec.swift b/Source/SocketIO/Manager/SocketManagerSpec.swift index 35d5afc..a5c6251 100644 --- a/Source/SocketIO/Manager/SocketManagerSpec.swift +++ b/Source/SocketIO/Manager/SocketManagerSpec.swift @@ -45,7 +45,6 @@ import Foundation /// To disconnect a socket and remove it from the manager, either call `SocketIOClient.disconnect()` on the socket, /// or call one of the `disconnectSocket` methods on this class. /// -@objc public protocol SocketManagerSpec : AnyObject, SocketEngineClient { // MARK: Properties @@ -71,7 +70,7 @@ public protocol SocketManagerSpec : AnyObject, SocketEngineClient { /// The minimum number of seconds to wait before attempting to reconnect. var reconnectWait: Int { get set } - + /// The maximum number of seconds to wait before attempting to reconnect. var reconnectWaitMax: Int { get set } @@ -116,7 +115,7 @@ public protocol SocketManagerSpec : AnyObject, SocketEngineClient { /// /// - parameter event: The event to send. /// - parameter items: The data to send with this event. - func emitAll(_ event: String, withItems items: [Any]) + func emitAll(_ event: String, _ items: SocketData...) /// Tries to reconnect to the server. /// diff --git a/Source/SocketIO/Parse/SocketParsable.swift b/Source/SocketIO/Parse/SocketParsable.swift index 9be9c60..4462ce2 100644 --- a/Source/SocketIO/Parse/SocketParsable.swift +++ b/Source/SocketIO/Parse/SocketParsable.swift @@ -118,7 +118,7 @@ public extension SocketParsable where Self: SocketManagerSpec & SocketDataBuffer var dataArray = String(message.utf16[message.utf16.index(reader.currentIndex, offsetBy: 1)...])! - if type == .error && !dataArray.hasPrefix("[") && !dataArray.hasSuffix("]") { + if (type == .error || type == .connect) && !dataArray.hasPrefix("[") && !dataArray.hasSuffix("]") { dataArray = "[" + dataArray + "]" } diff --git a/Source/SocketIO/Util/SSLSecurity.swift b/Source/SocketIO/Util/SSLSecurity.swift deleted file mode 100644 index 2035265..0000000 --- a/Source/SocketIO/Util/SSLSecurity.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// SSLSecurity.swift -// SocketIO-iOS -// -// Created by Lukas Schmidt on 24.09.17. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import Starscream - -/// A wrapper around Starscream's SSLSecurity that provides a minimal Objective-C interface. -open class SSLSecurity : NSObject { - // MARK: Properties - - /// The internal Starscream SSLSecurity. - public let security: Starscream.SSLSecurity - - init(security: Starscream.SSLSecurity) { - self.security = security - } - - // MARK: Methods - - /// Creates a new SSLSecurity that specifies whether to use publicKeys or certificates should be used for SSL - /// pinning validation - /// - /// - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning - /// validation - @objc - public convenience init(usePublicKeys: Bool = true) { - let security = Starscream.SSLSecurity(usePublicKeys: usePublicKeys) - self.init(security: security) - } - - - /// Designated init - /// - /// - parameter certs: is the certificates or public keys to use - /// - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning - /// validation - /// - returns: a representation security object to be used with - public convenience init(certs: [SSLCert], usePublicKeys: Bool) { - let security = Starscream.SSLSecurity(certs: certs, usePublicKeys: usePublicKeys) - self.init(security: security) - } - - /// Returns whether or not the given trust is valid. - /// - /// - parameter trust: The trust to validate. - /// - parameter domain: The CN domain to validate. - /// - returns: Whether or not this is valid. - public func isValid(_ trust: SecTrust, domain: String?) -> Bool { - return security.isValid(trust, domain: domain) - } -} diff --git a/Source/SocketIO/Util/SocketExtensions.swift b/Source/SocketIO/Util/SocketExtensions.swift index 23170b7..63b0b99 100644 --- a/Source/SocketIO/Util/SocketExtensions.swift +++ b/Source/SocketIO/Util/SocketExtensions.swift @@ -77,7 +77,7 @@ extension Dictionary where Key == String, Value == Any { return .randomizationFactor(factor) case let ("secure", secure as Bool): return .secure(secure) - case let ("security", security as SSLSecurity): + case let ("security", security as CertificatePinning): return .security(security) case let ("selfSigned", selfSigned as Bool): return .selfSigned(selfSigned) diff --git a/Tests/TestSocketIOObjc/ManagerObjectiveCTest.h b/Tests/TestSocketIOObjc/ManagerObjectiveCTest.h deleted file mode 100644 index e715321..0000000 --- a/Tests/TestSocketIOObjc/ManagerObjectiveCTest.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// Created by Erik Little on 10/21/17. -// - -#import "SocketIO_Tests-Swift.h" - -@import XCTest; -@import SocketIO; - -@interface ManagerObjectiveCTest : XCTestCase - -@property TestSocket* socket; -@property TestSocket* socket2; -@property TestManager* manager; - -@end diff --git a/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m b/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m deleted file mode 100644 index a807eed..0000000 --- a/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m +++ /dev/null @@ -1,141 +0,0 @@ -// -// Created by Erik Little on 10/21/17. -// - -#import "ManagerObjectiveCTest.h" - -@import Dispatch; -@import Foundation; -@import XCTest; -@import SocketIO; - -@implementation ManagerObjectiveCTest - -- (void)testSettingConfig { - NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"]; - NSDictionary* headers = @{@"My Header": @"Some Value"}; - - self.manager = [[TestManager alloc] initWithSocketURL:url config:@{ - @"forceNew": @YES, - @"extraHeaders": headers - }]; - - [self.manager connect]; - - XCTAssertTrue(self.manager.forceNew); - XCTAssertTrue([self.manager.engine.extraHeaders isEqualToDictionary:headers]); - -} - -- (void)testManagerProperties { - XCTAssertNotNil(self.manager.defaultSocket); - XCTAssertNil(self.manager.engine); - XCTAssertFalse(self.manager.forceNew); - XCTAssertEqual(self.manager.handleQueue, dispatch_get_main_queue()); - XCTAssertTrue(self.manager.reconnects); - XCTAssertEqual(self.manager.reconnectWait, 10); - XCTAssertEqual(self.manager.reconnectWaitMax, 30); - XCTAssertEqual(self.manager.randomizationFactor, 0.5); - XCTAssertEqual(self.manager.status, SocketIOStatusNotConnected); -} - -- (void)testConnectSocketSyntax { - [self setUpSockets]; - [self.manager connectSocket:self.socket]; -} - -- (void)testDisconnectSocketSyntax { - [self setUpSockets]; - [self.manager disconnectSocket:self.socket]; -} - -- (void)testSocketForNamespaceSyntax { - SocketIOClient* client = [self.manager socketForNamespace:@"/swift"]; - client = nil; -} - -- (void)testManagerCallsConnect { - [self setUpSockets]; - - XCTestExpectation* expect = [self expectationWithDescription:@"The manager should call connect on the default socket"]; - XCTestExpectation* expect2 = [self expectationWithDescription:@"The manager should call connect on the socket"]; - - self.socket.expects[@"didConnectCalled"] = expect; - self.socket2.expects[@"didConnectCalled"] = expect2; - - [self.socket connect]; - [self.socket2 connect]; - - [self.manager fakeConnecting]; - [self.manager fakeConnectingToNamespace:@"/swift"]; - - [self waitForExpectationsWithTimeout:0.3 handler:nil]; -} - -- (void)testManagerCallsDisconnect { - [self setUpSockets]; - - XCTestExpectation* expect = [self expectationWithDescription:@"The manager should call disconnect on the default socket"]; - XCTestExpectation* expect2 = [self expectationWithDescription:@"The manager should call disconnect on the socket"]; - - self.socket.expects[@"didDisconnectCalled"] = expect; - self.socket2.expects[@"didDisconnectCalled"] = expect2; - - [self.socket2 on:@"connect" callback:^(NSArray* data, SocketAckEmitter* ack) { - [self.manager disconnect]; - [self.manager fakeDisconnecting]; - }]; - - [self.socket connect]; - [self.socket2 connect]; - - [self.manager fakeConnecting]; - [self.manager fakeConnectingToNamespace:@"/swift"]; - - [self waitForExpectationsWithTimeout:0.3 handler:nil]; -} - -- (void)testManagerEmitAll { - [self setUpSockets]; - - XCTestExpectation* expect = [self expectationWithDescription:@"The manager should emit an event to the default socket"]; - XCTestExpectation* expect2 = [self expectationWithDescription:@"The manager should emit an event to the socket"]; - - self.socket.expects[@"emitAllEventCalled"] = expect; - self.socket2.expects[@"emitAllEventCalled"] = expect2; - - [self.socket2 on:@"connect" callback:^(NSArray* data, SocketAckEmitter* ack) { - [self.manager emitAll:@"event" withItems:@[@"testing"]]; - }]; - - [self.socket connect]; - [self.socket2 connect]; - - [self.manager fakeConnecting]; - [self.manager fakeConnectingToNamespace:@"/swift"]; - - [self waitForExpectationsWithTimeout:0.3 handler:nil]; -} - -- (void)testMangerRemoveSocket { - [self setUpSockets]; - - [self.manager removeSocket:self.socket]; - - XCTAssertNil(self.manager.nsps[self.socket.nsp]); -} - -- (void)setUpSockets { - self.socket = [self.manager testSocketForNamespace:@"/"]; - self.socket2 = [self.manager testSocketForNamespace:@"/swift"]; -} - -- (void)setUp { - [super setUp]; - NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"]; - self.manager = [[TestManager alloc] initWithSocketURL:url config:@{@"log": @NO}]; - self.socket = nil; - self.socket2 = nil; -} - -@end diff --git a/Tests/TestSocketIOObjc/SocketObjectiveCTest.h b/Tests/TestSocketIOObjc/SocketObjectiveCTest.h deleted file mode 100644 index 1d0de3e..0000000 --- a/Tests/TestSocketIOObjc/SocketObjectiveCTest.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// Created by Erik Little on 10/21/17. -// - - -@import Dispatch; -@import Foundation; -@import XCTest; -@import SocketIO; - -@interface SocketObjectiveCTest : XCTestCase - -@property SocketIOClient* socket; -@property SocketManager* manager; - -@end diff --git a/Tests/TestSocketIOObjc/SocketObjectiveCTest.m b/Tests/TestSocketIOObjc/SocketObjectiveCTest.m deleted file mode 100644 index 61412c1..0000000 --- a/Tests/TestSocketIOObjc/SocketObjectiveCTest.m +++ /dev/null @@ -1,122 +0,0 @@ -// -// SocketObjectiveCTest.m -// Socket.IO-Client-Swift -// -// Created by Erik Little on 3/25/16. -// -// Merely tests whether the Objective-C api breaks -// - -#import "SocketIO_Tests-Swift.h" -#import "SocketObjectiveCTest.h" - -@import Dispatch; -@import Foundation; -@import XCTest; -@import SocketIO; - -// TODO Manager interface tests - -@implementation SocketObjectiveCTest - -- (void)testProperties { - XCTAssertTrue([self.socket.nsp isEqualToString:@"/"]); - XCTAssertEqual(self.socket.status, SocketIOStatusNotConnected); -} - -- (void)testOnSyntax { - [self.socket on:@"someCallback" callback:^(NSArray* data, SocketAckEmitter* ack) { - [ack with:@[@1]]; - [[ack rawEmitView] with:@[@"hello"]]; - }]; -} - -- (void)testConnectSyntax { - [self.socket connect]; -} - -- (void)testConnectTimeoutAfterSyntax { - [self.socket connectWithTimeoutAfter:1 withHandler: ^() { }]; -} - -- (void)testDisconnectSyntax { - [self.socket disconnect]; -} - -- (void)testLeaveNamespaceSyntax { - [self.socket leaveNamespace]; -} - -- (void)testJoinNamespaceSyntax { - [self.socket joinNamespace]; -} - -- (void)testOnAnySyntax { - [self.socket onAny:^(SocketAnyEvent* any) { - NSString* event = any.event; - NSArray* data = any.items; - - [self.socket emit:event with:data]; - }]; -} - -- (void)testRemoveAllHandlersSyntax { - [self.socket removeAllHandlers]; -} - -- (void)testEmitSyntax { - [self.socket emit:@"testEmit" with:@[@YES]]; -} - -- (void)testEmitWriteCompletionSyntax { - [self.socket emit:@"testEmit" with:@[@YES] completion:^{}]; -} - -- (void)testEmitWriteCompletion { - XCTestExpectation* expect = [self expectationWithDescription:@"Write completion should be called"]; - - [self.socket emit:@"testEmit" with:@[@YES] completion:^{ - [expect fulfill]; - }]; - - [self waitForExpectationsWithTimeout:0.3 handler:nil]; -} - -- (void)testRawEmitSyntax { - [[self.socket rawEmitView] emit:@"myEvent" with:@[@1]]; -} - -- (void)testEmitWithAckSyntax { - [[self.socket emitWithAck:@"testAckEmit" with:@[@YES]] timingOutAfter:0 callback:^(NSArray* data) { }]; -} - -- (void)testOffSyntax { - [self.socket off:@"test"]; -} - -- (void)testSSLSecurity { - SSLSecurity* sec = [[SSLSecurity alloc] initWithUsePublicKeys:0]; - sec = nil; -} - -- (void)testStatusChangeHandler { - XCTestExpectation* expect = [self expectationWithDescription:@"statusChange should be correctly called"]; - - [self.socket on:@"statusChange" callback:^(NSArray* data, SocketAckEmitter* ack) { - XCTAssertTrue([data[1] integerValue] == SocketIOStatusConnecting); - [expect fulfill]; - }]; - - [OBjcUtils setTestStatusWithSocket:self.socket status:SocketIOStatusConnecting]; - - [self waitForExpectationsWithTimeout:0.3 handler:nil]; -} - -- (void)setUp { - [super setUp]; - NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"]; - self.manager = [[SocketManager alloc] initWithSocketURL:url config:@{@"log": @NO}]; - self.socket = [self.manager defaultSocket]; -} - -@end