Skip to content

Support socket.io 3 + starscream 4 #1309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 1, 2021
Prev Previous commit
Next Next commit
Support both v2 and v3
  • Loading branch information
nuclearace committed Jan 27, 2021
commit fde88c10c505eaad36241b731beddc837b597c44
12 changes: 12 additions & 0 deletions Source/SocketIO/Client/SocketIOClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ open class SocketIOClient: NSObject, SocketIOClientSpec {

joinNamespace(withPayload: payload)

switch manager.version {
case .three:
break
case .two where manager.status == .connected && nsp == "/":
// We might not get a connect event for the default nsp, fire immediately
didConnect(toNamespace: nsp, payload: nil)

return
case _:
break
}

guard timeoutAfter != 0 else { return }

manager.handleQueue.asyncAfter(deadline: DispatchTime.now() + timeoutAfter) {[weak self] in
Expand Down
13 changes: 13 additions & 0 deletions Source/SocketIO/Client/SocketIOClientOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
import Foundation
import Starscream

/// The socket.io version being used.
public enum SocketIOVersion: Int {
case two = 2
case three = 3
}

protocol ClientOption : CustomStringConvertible, Equatable {
func getSocketIOOptionValue() -> Any
}
Expand Down Expand Up @@ -99,6 +105,9 @@ public enum SocketIOClientOption : ClientOption {
/// Sets an NSURLSessionDelegate for the underlying engine. Useful if you need to handle self-signed certs.
case sessionDelegate(URLSessionDelegate)

/// The version of socket.io being used. This should match the server version. Default is 3.
case version(SocketIOVersion)

// MARK: Properties

/// The description of this option.
Expand Down Expand Up @@ -148,6 +157,8 @@ public enum SocketIOClientOption : ClientOption {
description = "sessionDelegate"
case .enableSOCKSProxy:
description = "enableSOCKSProxy"
case .version:
description = "version"
}

return description
Expand Down Expand Up @@ -199,6 +210,8 @@ public enum SocketIOClientOption : ClientOption {
value = delegate
case let .enableSOCKSProxy(enable):
value = enable
case let.version(versionNum):
value = versionNum
}

return value
Expand Down
78 changes: 69 additions & 9 deletions Source/SocketIO/Engine/SocketEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ open class SocketEngine:
/// The url for WebSockets.
public private(set) var urlWebSocket = URL(string: "http://localhost/")!

/// The version of engine.io being used. Default is three.
public private(set) var version: SocketIOVersion = .three

/// If `true`, then the engine is currently in WebSockets mode.
@available(*, deprecated, message: "No longer needed, if we're not polling, then we must be doing websockets")
public private(set) var websocket = false
Expand All @@ -133,8 +136,14 @@ open class SocketEngine:

private var lastCommunication: Date?
private var pingInterval: Int?
private var pingTimeout = 0
private var pingTimeout = 0 {
didSet {
pongsMissedMax = Int(pingTimeout / (pingInterval ?? 25000))
}
}

private var pongsMissed = 0
private var pongsMissedMax = 0
private var probeWait = ProbeWaitQueue()
private var secure = false
private var certPinner: CertificatePinning?
Expand Down Expand Up @@ -196,8 +205,9 @@ open class SocketEngine:
}

private func handleBase64(message: String) {
let offset = version.rawValue >= 3 ? 1 : 2
// binary in base64 string
let noPrefix = String(message[message.index(message.startIndex, offsetBy: 1)..<message.endIndex])
let noPrefix = String(message[message.index(message.startIndex, offsetBy: offset)..<message.endIndex])

if let data = Data(base64Encoded: noPrefix, options: .ignoreUnknownCharacters) {
client?.parseEngineBinaryData(data)
Expand Down Expand Up @@ -278,6 +288,14 @@ open class SocketEngine:
urlWebSocket.percentEncodedQuery = "transport=websocket" + queryString
urlPolling.percentEncodedQuery = "transport=polling&b64=1" + queryString

if !urlWebSocket.percentEncodedQuery!.contains("EIO") {
urlWebSocket.percentEncodedQuery = urlWebSocket.percentEncodedQuery! + engineIOParam
}

if !urlPolling.percentEncodedQuery!.contains("EIO") {
urlPolling.percentEncodedQuery = urlPolling.percentEncodedQuery! + engineIOParam
}

return (urlPolling.url!, urlWebSocket.url!)
}

Expand All @@ -289,6 +307,8 @@ open class SocketEngine:
includingCookies: session?.configuration.httpCookieStorage?.cookies(for: urlPollingWithSid)
)

print("ws req: \(req)")

ws = WebSocket(request: req, certPinner: certPinner, compressionHandler: compress ? WSCompression() : nil)
ws?.callbackQueue = engineQueue
ws?.delegate = self
Expand Down Expand Up @@ -413,6 +433,7 @@ open class SocketEngine:

self.sid = sid
connected = true
pongsMissed = 0

if let upgrades = json["upgrades"] as? [String] {
upgradeWs = upgrades.contains("websocket")
Expand All @@ -429,26 +450,37 @@ open class SocketEngine:
createWebSocketAndConnect()
}

if version.rawValue >= 3 {
checkPings()
} else {
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",
type: SocketEngine.logType)

upgradeTransport()
}

client?.engineDidReceivePong()
}

private func handlePing(with message: String) {
write("", withType: .pong, withData: [])
if version.rawValue >= 3 {
write("", withType: .pong, withData: [])
}

client?.engineDidReceivePing()
}
Expand Down Expand Up @@ -478,7 +510,7 @@ open class SocketEngine:

lastCommunication = Date()

client?.parseEngineBinaryData(data)
client?.parseEngineBinaryData(version.rawValue >= 3 ? data : data.subdata(in: 1..<data.endIndex))
}

/// Parses a raw engine.io packet.
Expand All @@ -489,13 +521,11 @@ open class SocketEngine:

DefaultSocketLogger.Logger.log("Got message: \(message)", type: SocketEngine.logType)

let reader = SocketStringReader(message: message)

if message.hasPrefix("b") {
if message.hasPrefix(version.rawValue >= 3 ? "b" : "b4") {
return handleBase64(message: message)
}

guard let type = SocketEnginePacketType(rawValue: Int(reader.currentCharacter) ?? -1) else {
guard let type = SocketEnginePacketType(rawValue: message.first?.wholeNumberValue ?? -1) else {
checkAndHandleEngineError(message)

return
Expand Down Expand Up @@ -536,6 +566,34 @@ open class SocketEngine:
waitingForPost = false
}

private func sendPing() {
guard connected, let pingInterval = pingInterval else {
print("not connected \(self.connected) or no ping interval \(self.pingInterval ?? -222)")
return
}

// Server is not responding
if pongsMissed > pongsMissedMax {
closeOutEngine(reason: "Ping timeout")
return
}

pongsMissed += 1
write("", withType: .ping, withData: [], completion: nil)

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 {
print("wrong ping?")
return
}

this.sendPing()
}

client?.engineDidSendPing()
}

/// Called when the engine should set/update its configs from a given configuration.
///
/// parameter config: The `SocketIOClientConfiguration` that should be used to set/update configs.
Expand Down Expand Up @@ -570,6 +628,8 @@ open class SocketEngine:
self.compress = true
case .enableSOCKSProxy:
self.enableSOCKSProxy = true
case let .version(num):
version = num
default:
continue
}
Expand Down
10 changes: 8 additions & 2 deletions Source/SocketIO/Engine/SocketEngineClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,16 @@ import Foundation
/// - parameter reason: The reason the engine opened.
func engineDidOpen(reason: String)

/// Called when the engine receives a ping message.
/// Called when the engine receives a ping message. Only called in socket.io >3.
func engineDidReceivePing()

/// Called when the engine sends a pong to the server.
/// Called when the engine receives a pong message. Only called in socket.io 2.
func engineDidReceivePong()

/// Called when the engine sends a ping to the server. Only called in socket.io 2.
func engineDidSendPing()

/// Called when the engine sends a pong to the server. Only called in socket.io >3.
func engineDidSendPong()

/// Called when the engine has a message that must be parsed.
Expand Down
35 changes: 31 additions & 4 deletions Source/SocketIO/Engine/SocketEnginePollable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,15 @@ extension SocketEnginePollable {
postWait.removeAll(keepingCapacity: true)
}

let postStr = postWait.lazy.map({ $0.msg }).joined(separator: "\u{1e}")
var postStr = ""

if version.rawValue >= 3 {
postStr = postWait.lazy.map({ $0.msg }).joined(separator: "\u{1e}")
} else {
for packet in postWait {
postStr += "\(packet.msg.utf16.count):\(packet.msg)"
}
}

DefaultSocketLogger.Logger.log("Created POST string: \(postStr)", type: "SocketEnginePolling")

Expand Down Expand Up @@ -195,10 +203,29 @@ extension SocketEnginePollable {

DefaultSocketLogger.Logger.log("Got poll message: \(str)", type: "SocketEnginePolling")

let records = str.components(separatedBy: "\u{1e}")
if version.rawValue >= 3 {
let records = str.components(separatedBy: "\u{1e}")

for record in records {
parseEngineMessage(record)
}
} else {
guard str.count != 1 else {
parseEngineMessage(str)

return
}

var reader = SocketStringReader(message: str)

for record in records {
parseEngineMessage(record)
while reader.hasNext {
if let n = Int(reader.readUntilOccurence(of: ":")) {
parseEngineMessage(reader.read(count: n))
} else {
parseEngineMessage(str)
break
}
}
}
}

Expand Down
27 changes: 25 additions & 2 deletions Source/SocketIO/Engine/SocketEngineSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public protocol SocketEngineSpec: class {
/// The url for WebSockets.
var urlWebSocket: URL { get }

/// The version of engine.io being used. Default is three.
var version: SocketIOVersion { get }

/// If `true`, then the engine is currently in WebSockets mode.
@available(*, deprecated, message: "No longer needed, if we're not polling, then we must be doing websockets")
var websocket: Bool { get }
Expand Down Expand Up @@ -142,17 +145,35 @@ public protocol SocketEngineSpec: class {
}

extension SocketEngineSpec {
var engineIOParam: String {
switch version {
case .two:
return "&EIO=3"
case .three:
return "&EIO=4"
}
}

var urlPollingWithSid: URL {
var com = URLComponents(url: urlPolling, resolvingAgainstBaseURL: false)!
com.percentEncodedQuery = com.percentEncodedQuery! + "&sid=\(sid.urlEncode()!)"

if !com.percentEncodedQuery!.contains("EIO") {
com.percentEncodedQuery = com.percentEncodedQuery! + engineIOParam
}

return com.url!
}

var urlWebSocketWithSid: URL {
var com = URLComponents(url: urlWebSocket, resolvingAgainstBaseURL: false)!
com.percentEncodedQuery = com.percentEncodedQuery! + (sid == "" ? "" : "&sid=\(sid.urlEncode()!)")

if !com.percentEncodedQuery!.contains("EIO") {
com.percentEncodedQuery = com.percentEncodedQuery! + engineIOParam
}


return com.url!
}

Expand All @@ -172,10 +193,12 @@ extension SocketEngineSpec {
}

func createBinaryDataForSend(using data: Data) -> Either<Data, String> {
let prefixB64 = version.rawValue >= 3 ? "b" : "b4"

if polling {
return .right("b" + data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)))
return .right(prefixB64 + data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)))
} else {
return .left(data)
return .left(version.rawValue >= 3 ? data : Data([0x4]) + data)
}
}

Expand Down
Loading