Support socket.io 3 + starscream 4

This commit is contained in:
Erik Little 2020-11-07 15:06:22 -05:00
parent 6992ea5250
commit 6cd8f79e8b
No known key found for this signature in database
GPG Key ID: 62F837E56F4E9320
25 changed files with 164 additions and 579 deletions

View File

@ -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

View File

@ -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`

View File

@ -1 +1 @@
github "daltoniam/Starscream" ~> 3.1
github "daltoniam/Starscream" ~> 4.0

View File

@ -1 +1 @@
github "daltoniam/Starscream" "3.1.0"
github "daltoniam/Starscream" "4.0.4"

View File

@ -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"
}
}
]

View File

@ -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"]),

View File

@ -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

View File

@ -46,17 +46,8 @@ open class SocketIOClient : NSObject, SocketIOClientSpec {
/// 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

View File

@ -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)

View File

@ -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.
///

View File

@ -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)..<message.endIndex])
let noPrefix = String(message[message.index(message.startIndex, offsetBy: 1)..<message.endIndex])
if let data = Data(base64Encoded: noPrefix, options: .ignoreUnknownCharacters) {
client?.parseEngineBinaryData(data)
@ -285,45 +289,14 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So
private func createWebSocketAndConnect() {
var req = URLRequest(url: urlWebSocketWithSid)
addHeaders(to: &req, includingCookies: session?.configuration.httpCookieStorage?.cookies(for: urlPollingWithSid))
addHeaders(
to: &req,
includingCookies: session?.configuration.httpCookieStorage?.cookies(for: urlPollingWithSid)
)
let stream = FoundationStream()
stream.enableSOCKSProxy = enableSOCKSProxy
ws = WebSocket(request: req, stream: stream)
ws = WebSocket(request: req, certPinner: certPinner, compressionHandler: compress ? WSCompression() : nil)
ws?.callbackQueue = engineQueue
ws?.enableCompression = compress
ws?.disableSSLCertValidation = selfSigned
ws?.security = security?.security
ws?.onConnect = {[weak self] in
guard let this = self else { return }
this.websocketDidConnect()
}
ws?.onDisconnect = {[weak self] error in
guard let this = self else { return }
this.websocketDidDisconnect(error: error)
}
ws?.onData = {[weak self] data in
guard let this = self else { return }
this.parseEngineData(data)
}
ws?.onText = {[weak self] message in
guard let this = self else { return }
this.parseEngineMessage(message)
}
ws?.onHttpResponseHeaders = {[weak self] headers in
guard let this = self else { return }
this.client?.engineDidWebsocketUpgrade(headers: headers)
}
ws?.delegate = self
ws?.connect()
}
@ -445,7 +418,7 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So
self.sid = sid
connected = true
pongsMissed = 0
pingsMissed = 0
if let upgrades = json["upgrades"] as? [String] {
upgradeWs = upgrades.contains("websocket")
@ -462,18 +435,15 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So
createWebSocketAndConnect()
}
sendPing()
if !forceWebsockets {
doPoll()
}
checkPings()
client?.engineDidOpen(reason: "Connect")
}
private func handlePong(with message: String) {
pongsMissed = 0
// We should upgrade
if message == "3probe" {
DefaultSocketLogger.Logger.log("Received probe response, should upgrade to WebSockets",
@ -481,8 +451,31 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So
upgradeTransport()
}
}
client?.engineDidReceivePong()
private func handlePing(with message: String) {
pingsMissed = 0
write("", withType: .pong, withData: [])
client?.engineDidReceivePing()
}
private func checkPings() {
let pingInterval = self.pingInterval ?? 25_000
engineQueue.asyncAfter(deadline: .now() + .milliseconds(pingInterval)) {[weak self, id = self.sid] in
// Make sure not to ping old connections
guard let this = self, this.sid == id else { return }
if this.pingsMissed > 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..<data.endIndex))
client?.parseEngineBinaryData(data)
}
/// Parses a raw engine.io packet.
@ -502,7 +495,7 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So
let reader = SocketStringReader(message: message)
if message.hasPrefix("b4") {
if message.hasPrefix("b") {
return handleBase64(message: message)
}
@ -517,6 +510,8 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So
handleMessage(String(message.dropFirst()))
case .noop:
handleNOOP()
case .ping:
handlePing(with: message)
case .pong:
handlePong(with: message)
case .open:
@ -549,12 +544,12 @@ open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, So
guard connected, let pingInterval = pingInterval else { return }
// Server is not responding
if pongsMissed > 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
}
}
}

View File

@ -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.
///

View File

@ -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)
}
}

View File

@ -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<Data, String> {
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)
}
}

View File

@ -28,6 +28,11 @@ import Starscream
/// Protocol that is used to implement socket.io WebSocket support
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?()
}
}
}

View File

@ -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 {

View File

@ -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
@ -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.
///

View File

@ -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 + "]"
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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