| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550 |
- //
- // SocketEngine.swift
- // Socket.IO-Client-Swift
- //
- // Created by Erik Little on 3/3/15.
- //
- // 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
- public final class SocketEngine : NSObject, SocketEnginePollable, SocketEngineWebsocket {
- public let emitQueue = dispatch_queue_create("com.socketio.engineEmitQueue", DISPATCH_QUEUE_SERIAL)
- public let handleQueue = dispatch_queue_create("com.socketio.engineHandleQueue", DISPATCH_QUEUE_SERIAL)
- public let parseQueue = dispatch_queue_create("com.socketio.engineParseQueue", DISPATCH_QUEUE_SERIAL)
- public var connectParams: [String: AnyObject]? {
- didSet {
- (urlPolling, urlWebSocket) = createURLs()
- }
- }
-
- public var postWait = [String]()
- public var waitingForPoll = false
- public var waitingForPost = false
-
- public private(set) var closed = false
- public private(set) var connected = false
- public private(set) var cookies: [NSHTTPCookie]?
- public private(set) var doubleEncodeUTF8 = true
- public private(set) var extraHeaders: [String: String]?
- public private(set) var fastUpgrade = false
- public private(set) var forcePolling = false
- public private(set) var forceWebsockets = false
- public private(set) var invalidated = false
- public private(set) var polling = true
- public private(set) var probing = false
- public private(set) var session: NSURLSession?
- public private(set) var sid = ""
- public private(set) var socketPath = "/engine.io/"
- public private(set) var urlPolling = NSURL()
- public private(set) var urlWebSocket = NSURL()
- public private(set) var websocket = false
- public private(set) var ws: WebSocket?
- public weak var client: SocketEngineClient?
-
- private weak var sessionDelegate: NSURLSessionDelegate?
- private let logType = "SocketEngine"
- private let url: NSURL
-
- private var pingInterval: Double?
- private var pingTimeout = 0.0 {
- didSet {
- pongsMissedMax = Int(pingTimeout / (pingInterval ?? 25))
- }
- }
-
- private var pongsMissed = 0
- private var pongsMissedMax = 0
- private var probeWait = ProbeWaitQueue()
- private var secure = false
- private var selfSigned = false
- private var voipEnabled = false
- public init(client: SocketEngineClient, url: NSURL, options: Set<SocketIOClientOption>) {
- self.client = client
- self.url = url
-
- for option in options {
- switch option {
- case let .ConnectParams(params):
- connectParams = params
- case let .Cookies(cookies):
- self.cookies = cookies
- case let .DoubleEncodeUTF8(encode):
- doubleEncodeUTF8 = encode
- case let .ExtraHeaders(headers):
- extraHeaders = headers
- case let .SessionDelegate(delegate):
- sessionDelegate = delegate
- case let .ForcePolling(force):
- forcePolling = force
- case let .ForceWebsockets(force):
- forceWebsockets = force
- case let .Path(path):
- socketPath = path
- case let .VoipEnabled(enable):
- voipEnabled = enable
- case let .Secure(secure):
- self.secure = secure
- case let .SelfSigned(selfSigned):
- self.selfSigned = selfSigned
- default:
- continue
- }
- }
-
- super.init()
-
- (urlPolling, urlWebSocket) = createURLs()
- }
-
- public convenience init(client: SocketEngineClient, url: NSURL, options: NSDictionary?) {
- self.init(client: client, url: url, options: options?.toSocketOptionsSet() ?? [])
- }
-
- deinit {
- DefaultSocketLogger.Logger.log("Engine is being released", type: logType)
- closed = true
- stopPolling()
- }
-
- private func checkAndHandleEngineError(msg: String) {
- guard let stringData = msg.dataUsingEncoding(NSUTF8StringEncoding,
- allowLossyConversion: false) else { return }
-
- do {
- if let dict = try NSJSONSerialization.JSONObjectWithData(stringData, options: .MutableContainers) as? NSDictionary {
- guard let error = dict["message"] as? String else { return }
-
- /*
- 0: Unknown transport
- 1: Unknown sid
- 2: Bad handshake request
- 3: Bad request
- */
- didError(error)
- }
- } catch {
- didError("Got unknown error from server \(msg)")
- }
- }
- private func checkIfMessageIsBase64Binary(message: String) -> Bool {
- if message.hasPrefix("b4") {
- // binary in base64 string
- let noPrefix = message[message.startIndex.advancedBy(2)..<message.endIndex]
- if let data = NSData(base64EncodedString: noPrefix,
- options: .IgnoreUnknownCharacters) {
- client?.parseEngineBinaryData(data)
- }
-
- return true
- } else {
- return false
- }
- }
-
- /// Starts the connection to the server
- public func connect() {
- if connected {
- DefaultSocketLogger.Logger.error("Engine tried opening while connected. Assuming this was a reconnect", type: logType)
- disconnect("reconnect")
- }
-
- DefaultSocketLogger.Logger.log("Starting engine", type: logType)
- DefaultSocketLogger.Logger.log("Handshaking", type: logType)
-
- resetEngine()
-
- if forceWebsockets {
- polling = false
- websocket = true
- createWebsocketAndConnect()
- return
- }
-
- let reqPolling = NSMutableURLRequest(URL: urlPolling)
-
- if cookies != nil {
- let headers = NSHTTPCookie.requestHeaderFieldsWithCookies(cookies!)
- reqPolling.allHTTPHeaderFields = headers
- }
-
- if let extraHeaders = extraHeaders {
- for (headerName, value) in extraHeaders {
- reqPolling.setValue(value, forHTTPHeaderField: headerName)
- }
- }
-
- dispatch_async(emitQueue) {
- self.doLongPoll(reqPolling)
- }
- }
- private func createURLs() -> (NSURL, NSURL) {
- if client == nil {
- return (NSURL(), NSURL())
- }
- let urlPolling = NSURLComponents(string: url.absoluteString)!
- let urlWebSocket = NSURLComponents(string: url.absoluteString)!
- var queryString = ""
-
- urlWebSocket.path = socketPath
- urlPolling.path = socketPath
- if secure {
- urlPolling.scheme = "https"
- urlWebSocket.scheme = "wss"
- } else {
- urlPolling.scheme = "http"
- urlWebSocket.scheme = "ws"
- }
- if connectParams != nil {
- for (key, value) in connectParams! {
- let keyEsc = key.urlEncode()!
- let valueEsc = "\(value)".urlEncode()!
- queryString += "&\(keyEsc)=\(valueEsc)"
- }
- }
- urlWebSocket.percentEncodedQuery = "transport=websocket" + queryString
- urlPolling.percentEncodedQuery = "transport=polling&b64=1" + queryString
-
- return (urlPolling.URL!, urlWebSocket.URL!)
- }
- private func createWebsocketAndConnect() {
- ws = WebSocket(url: urlWebSocketWithSid)
-
- if cookies != nil {
- let headers = NSHTTPCookie.requestHeaderFieldsWithCookies(cookies!)
- for (key, value) in headers {
- ws?.headers[key] = value
- }
- }
- if extraHeaders != nil {
- for (headerName, value) in extraHeaders! {
- ws?.headers[headerName] = value
- }
- }
- ws?.queue = handleQueue
- ws?.voipEnabled = voipEnabled
- ws?.delegate = self
- ws?.selfSignedSSL = selfSigned
- ws?.connect()
- }
-
- public func didError(error: String) {
- DefaultSocketLogger.Logger.error(error, type: logType)
- client?.engineDidError(error)
- disconnect(error)
- }
-
- public func disconnect(reason: String) {
- func postSendClose(data: NSData?, _ res: NSURLResponse?, _ err: NSError?) {
- sid = ""
- closed = true
- invalidated = true
- connected = false
-
- ws?.disconnect()
- stopPolling()
- client?.engineDidClose(reason)
- }
-
- DefaultSocketLogger.Logger.log("Engine is being closed.", type: logType)
-
- if closed {
- client?.engineDidClose(reason)
- return
- }
-
- if websocket {
- sendWebSocketMessage("", withType: .Close, withData: [])
- postSendClose(nil, nil, nil)
- } else {
- // We need to take special care when we're polling that we send it ASAP
- // Also make sure we're on the emitQueue since we're touching postWait
- dispatch_sync(emitQueue) {
- self.postWait.append(String(SocketEnginePacketType.Close.rawValue))
- let req = self.createRequestForPostWithPostWait()
- self.doRequest(req, withCallback: postSendClose)
- }
- }
- }
- public func doFastUpgrade() {
- if waitingForPoll {
- DefaultSocketLogger.Logger.error("Outstanding poll when switched to WebSockets," +
- "we'll probably disconnect soon. You should report this.", type: logType)
- }
- sendWebSocketMessage("", withType: .Upgrade, withData: [])
- websocket = true
- polling = false
- fastUpgrade = false
- probing = false
- flushProbeWait()
- }
- private func flushProbeWait() {
- DefaultSocketLogger.Logger.log("Flushing probe wait", type: logType)
- dispatch_async(emitQueue) {
- for waiter in self.probeWait {
- self.write(waiter.msg, withType: waiter.type, withData: waiter.data)
- }
-
- self.probeWait.removeAll(keepCapacity: false)
-
- if self.postWait.count != 0 {
- self.flushWaitingForPostToWebSocket()
- }
- }
- }
-
- // We had packets waiting for send when we upgraded
- // Send them raw
- public func flushWaitingForPostToWebSocket() {
- guard let ws = self.ws else { return }
-
- for msg in postWait {
- ws.writeString(fixDoubleUTF8(msg))
- }
-
- postWait.removeAll(keepCapacity: true)
- }
- private func handleClose(reason: String) {
- client?.engineDidClose(reason)
- }
- private func handleMessage(message: String) {
- client?.parseEngineMessage(message)
- }
- private func handleNOOP() {
- doPoll()
- }
- private func handleOpen(openData: String) {
- let mesData = openData.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
- do {
- let json = try NSJSONSerialization.JSONObjectWithData(mesData,
- options: NSJSONReadingOptions.AllowFragments) as? NSDictionary
- if let sid = json?["sid"] as? String {
- let upgradeWs: Bool
- self.sid = sid
- connected = true
- if let upgrades = json?["upgrades"] as? [String] {
- upgradeWs = upgrades.contains("websocket")
- } else {
- upgradeWs = false
- }
- if let pingInterval = json?["pingInterval"] as? Double, pingTimeout = json?["pingTimeout"] as? Double {
- self.pingInterval = pingInterval / 1000.0
- self.pingTimeout = pingTimeout / 1000.0
- }
- if !forcePolling && !forceWebsockets && upgradeWs {
- createWebsocketAndConnect()
- }
-
- sendPing()
-
- if !forceWebsockets {
- doPoll()
- }
-
- client?.engineDidOpen("Connect")
- }
- } catch {
- didError("Error parsing open packet")
- }
- }
- private func handlePong(pongMessage: String) {
- pongsMissed = 0
- // We should upgrade
- if pongMessage == "3probe" {
- upgradeTransport()
- }
- }
-
- public func parseEngineData(data: NSData) {
- DefaultSocketLogger.Logger.log("Got binary data: %@", type: "SocketEngine", args: data)
- client?.parseEngineBinaryData(data.subdataWithRange(NSMakeRange(1, data.length - 1)))
- }
- public func parseEngineMessage(message: String, fromPolling: Bool) {
- DefaultSocketLogger.Logger.log("Got message: %@", type: logType, args: message)
-
- let reader = SocketStringReader(message: message)
- let fixedString: String
- guard let type = SocketEnginePacketType(rawValue: Int(reader.currentCharacter) ?? -1) else {
- if !checkIfMessageIsBase64Binary(message) {
- checkAndHandleEngineError(message)
- }
-
- return
- }
- if fromPolling && type != .Noop && doubleEncodeUTF8 {
- fixedString = fixDoubleUTF8(message)
- } else {
- fixedString = message
- }
- switch type {
- case .Message:
- handleMessage(fixedString[fixedString.startIndex.successor()..<fixedString.endIndex])
- case .Noop:
- handleNOOP()
- case .Pong:
- handlePong(fixedString)
- case .Open:
- handleOpen(fixedString[fixedString.startIndex.successor()..<fixedString.endIndex])
- case .Close:
- handleClose(fixedString)
- default:
- DefaultSocketLogger.Logger.log("Got unknown packet type", type: logType)
- }
- }
-
- // Puts the engine back in its default state
- private func resetEngine() {
- closed = false
- connected = false
- fastUpgrade = false
- polling = true
- probing = false
- invalidated = false
- session = NSURLSession(configuration: .defaultSessionConfiguration(),
- delegate: sessionDelegate,
- delegateQueue: NSOperationQueue())
- sid = ""
- waitingForPoll = false
- waitingForPost = false
- websocket = false
- }
- private func sendPing() {
- if !connected {
- return
- }
-
- //Server is not responding
- if pongsMissed > pongsMissedMax {
- client?.engineDidClose("Ping timeout")
- return
- }
-
- if let pingInterval = pingInterval {
- pongsMissed += 1
- write("", withType: .Ping, withData: [])
-
- let time = dispatch_time(DISPATCH_TIME_NOW, Int64(pingInterval * Double(NSEC_PER_SEC)))
- dispatch_after(time, dispatch_get_main_queue()) {[weak self] in
- self?.sendPing()
- }
- }
- }
-
- // Moves from long-polling to websockets
- private func upgradeTransport() {
- if ws?.isConnected ?? false {
- DefaultSocketLogger.Logger.log("Upgrading transport to WebSockets", type: logType)
- fastUpgrade = true
- sendPollMessage("", withType: .Noop, withData: [])
- // After this point, we should not send anymore polling messages
- }
- }
- /// Write a message, independent of transport.
- public func write(msg: String, withType type: SocketEnginePacketType, withData data: [NSData]) {
- dispatch_async(emitQueue) {
- guard self.connected else { return }
-
- if self.websocket {
- DefaultSocketLogger.Logger.log("Writing ws: %@ has data: %@",
- type: self.logType, args: msg, data.count != 0)
- self.sendWebSocketMessage(msg, withType: type, withData: data)
- } else if !self.probing {
- DefaultSocketLogger.Logger.log("Writing poll: %@ has data: %@",
- type: self.logType, args: msg, data.count != 0)
- self.sendPollMessage(msg, withType: type, withData: data)
- } else {
- self.probeWait.append((msg, type, data))
- }
- }
- }
-
- // Delegate methods
- public func websocketDidConnect(socket: WebSocket) {
- if !forceWebsockets {
- probing = true
- probeWebSocket()
- } else {
- connected = true
- probing = false
- polling = false
- }
- }
-
- public func websocketDidDisconnect(socket: WebSocket, error: NSError?) {
- probing = false
-
- if closed {
- client?.engineDidClose("Disconnect")
- return
- }
-
- if websocket {
- connected = false
- websocket = false
-
- let reason = error?.localizedDescription ?? "Socket Disconnected"
-
- if error != nil {
- didError(reason)
- }
-
- client?.engineDidClose(reason)
- } else {
- flushProbeWait()
- }
- }
- }
|