diff --git a/README.md b/README.md index 439b513..ff92b65 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,11 @@ import Socket_IO_Client_Swift API === -Constructor +Constructors ----------- -`init(socketURL: String, opts:[String: AnyObject]? = nil)` - Constructs a new client for the given URL. opts can be omitted (will use default values. See example) +`init(socketURL: String, opts:NSDictionary? = nil)` - Constructs a new client for the given URL. opts can be omitted (will use default values. See example) + +`convenience init(socketURL: String, options:NSDictionary?)` - Same as above, but meant for Objective-C. See Objective-C Example. Methods ------- 1. `socket.on(name:String, callback:((data:NSArray?, ack:AckEmitter?) -> Void))` - Adds a handler for an event. Items are passed by an array. `ack` can be used to send an ack when one is requested. See example. @@ -124,12 +126,12 @@ socket.connect() Objective-C Example =================== ```objective-c -SocketIOClient* socket = [[SocketIOClient alloc] initWithSocketURL:@"localhost:8080" opts:nil]; +SocketIOClient* socket = [[SocketIOClient alloc] initWithSocketURL:@"localhost:8080" options:nil]; [socket on: @"connect" callback: ^(NSArray* data, void (^ack)(NSArray*)) { NSLog(@"connected"); [socket emitObjc:@"echo" :@[@"echo test"]]; - [[socket emitWithAckObjc:@"ackack" :@[@"test"]] onAck:^(NSArray* data) { + [[socket emitWithAckObjc:@"ackack" :@[@"test"]] onAck:0 withCallback:^(NSArray* data) { NSLog(@"Got data"); }]; }]; diff --git a/Socket.IO-Client-Swift.podspec b/Socket.IO-Client-Swift.podspec index e689e1a..67b1fa5 100644 --- a/Socket.IO-Client-Swift.podspec +++ b/Socket.IO-Client-Swift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Socket.IO-Client-Swift" - s.version = "1.1.4" + s.version = "1.2.0" s.summary = "Socket.IO-client for Swift" s.description = <<-DESC Socket.IO-client for Swift. @@ -12,7 +12,7 @@ Pod::Spec.new do |s| s.author = { "Erik" => "nuclear.ace@gmail.com" } s.ios.deployment_target = '8.0' s.osx.deployment_target = '10.10' - s.source = { :git => "https://github.com/socketio/socket.io-client-swift.git", :tag => 'v1.1.4' } + s.source = { :git => "https://github.com/socketio/socket.io-client-swift.git", :tag => 'v1.2.0' } s.source_files = "SwiftIO/**/*.swift" s.requires_arc = true # s.dependency 'Starscream', '~> 0.9' # currently this repo includes Starscream swift files diff --git a/SwiftIO/SocketEngine.swift b/SwiftIO/SocketEngine.swift index 20a54e7..f92c857 100644 --- a/SwiftIO/SocketEngine.swift +++ b/SwiftIO/SocketEngine.swift @@ -78,19 +78,19 @@ public class SocketEngine: NSObject, WebSocketDelegate { return self._websocket } var ws:WebSocket? - + init(client:SocketIOClient, forcePolling:Bool = false) { self.client = client self.forcePolling = forcePolling self.session = NSURLSession(configuration: NSURLSessionConfiguration.ephemeralSessionConfiguration(), delegate: nil, delegateQueue: self.workQueue) } - + func close() { self.pingTimer?.invalidate() self.send(PacketType.CLOSE.rawValue) } - + private func createBinaryDataForSend(data:NSData) -> (NSData?, String?) { if self.websocket { var byteArray = [UInt8](count: 1, repeatedValue: 0x0) @@ -102,16 +102,16 @@ public class SocketEngine: NSObject, WebSocketDelegate { var str = "b4" str += data.base64EncodedStringWithOptions( NSDataBase64EncodingOptions.Encoding64CharacterLineLength) - + return (nil, str) } } - + private func createURLs(params:[String: AnyObject]? = nil) -> (String, String) { var url = "\(self.client.socketURL)/socket.io/?transport=" var urlPolling:String var urlWebSocket:String - + if self.client.secure { urlPolling = "https://" + url + "polling" urlWebSocket = "wss://" + url + "websocket" @@ -119,14 +119,14 @@ public class SocketEngine: NSObject, WebSocketDelegate { urlPolling = "http://" + url + "polling" urlWebSocket = "ws://" + url + "websocket" } - + if params != nil { for (key, value) in params! { let keyEsc = key.stringByAddingPercentEncodingWithAllowedCharacters( NSCharacterSet.URLHostAllowedCharacterSet())! urlPolling += "&\(keyEsc)=" urlWebSocket += "&\(keyEsc)=" - + if value is String { let valueEsc = (value as! String).stringByAddingPercentEncodingWithAllowedCharacters( NSCharacterSet.URLHostAllowedCharacterSet())! @@ -138,10 +138,10 @@ public class SocketEngine: NSObject, WebSocketDelegate { } } } - + return (urlPolling, urlWebSocket) } - + private func doFastUpgrade() { self.sendWebSocketMessage("", withType: PacketType.UPGRADE) self._websocket = true @@ -149,23 +149,23 @@ public class SocketEngine: NSObject, WebSocketDelegate { self.fastUpgrade = false self.flushProbeWait() } - + private func doPoll() { if self.websocket || self.waitingForPoll || !self.connected { return } - + self.waitingForPoll = true self.doRequest(self.parsePollingMessage) } - + private func doRequest(callback:(String) -> Void) { if !self.polling { return } - + let req = NSURLRequest(URL: NSURL(string: self.urlPolling! + "&sid=\(self.sid)")!) - + self.session.dataTaskWithRequest(req) {[weak self] data, res, err in if self == nil { return @@ -173,18 +173,19 @@ public class SocketEngine: NSObject, WebSocketDelegate { if self!.polling { self?.handlePollingFailed(err) } - + return } - + // println(data) - - if let str = NSString(data: data, encoding: NSUTF8StringEncoding) { - dispatch_async(self!.parseQueue) {callback(str as String)} + + + if let str = NSString(data: data, encoding: NSUTF8StringEncoding) as? String { + dispatch_async(self!.parseQueue) {callback(str)} } - + self?.waitingForPoll = false - + if self!.fastUpgrade { self?.doFastUpgrade() return @@ -193,22 +194,22 @@ public class SocketEngine: NSObject, WebSocketDelegate { } }.resume() } - + private func flushProbeWait() { // println("flushing probe wait") dispatch_async(self.emitQueue) {[weak self] in if self == nil { return } - + for waiter in self!.probeWait { waiter() } - + self?.probeWait.removeAll(keepCapacity: false) } } - + private func flushWaitingForPost() { if self.postWait.count == 0 || !self.connected { return @@ -216,29 +217,29 @@ public class SocketEngine: NSObject, WebSocketDelegate { self.flushWaitingForPostToWebSocket() return } - + var postStr = "" - + for packet in self.postWait { let len = count(packet) - + postStr += "\(len):\(packet)" } - + self.postWait.removeAll(keepCapacity: false) - + let req = NSMutableURLRequest(URL: NSURL(string: self.urlPolling! + "&sid=\(self.sid)")!) - + req.HTTPMethod = "POST" req.setValue("text/plain; charset=UTF-8", forHTTPHeaderField: "Content-Type") - + let postData = postStr.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! - + // NSLog("posting: \(postStr)") req.HTTPBody = postData req.setValue(String(postData.length), forHTTPHeaderField: "Content-Length") - + self.waitingForPost = true self.session.dataTaskWithRequest(req) {[weak self] data, res, err in if self == nil { @@ -247,7 +248,7 @@ public class SocketEngine: NSObject, WebSocketDelegate { self?.handlePollingFailed(err) return } - + self?.waitingForPost = false dispatch_async(self!.emitQueue) { self?.flushWaitingForPost() @@ -255,21 +256,21 @@ public class SocketEngine: NSObject, WebSocketDelegate { return }}.resume() } - + // We had packets waiting for send when we upgraded // Send them raw private func flushWaitingForPostToWebSocket() { for msg in self.postWait { self.ws?.writeString(msg) } - + self.postWait.removeAll(keepCapacity: true) } - + // A poll failed, tell the client about it private func handlePollingFailed(reason:NSError?) { assert(self.polling, "Polling failed when we're not polling") - + if !self.client.reconnecting { self._connected = false self.ws?.disconnect() @@ -279,17 +280,17 @@ public class SocketEngine: NSObject, WebSocketDelegate { self.client.pollingDidFail(reason) } } - + func open(opts:[String: AnyObject]? = nil) { if self.connected { assert(false, "We're in a bad state, this shouldn't happen.") } - + let (urlPolling, urlWebSocket) = self.createURLs(params: opts) self.urlPolling = urlPolling self.urlWebSocket = urlWebSocket let reqPolling = NSURLRequest(URL: NSURL(string: urlPolling + "&b64=1")!) - + self.session.dataTaskWithRequest(reqPolling) {[weak self] data, res, err in var err2:NSError? if self == nil { @@ -298,30 +299,30 @@ public class SocketEngine: NSObject, WebSocketDelegate { self?.handlePollingFailed(err) return } - + if let dataString = NSString(data: data, encoding: NSUTF8StringEncoding) as? String { let parsed:[String]? = dataString["(\\d*):(\\d)(\\{.*\\})?"].groups() - + if parsed == nil || parsed?.count != 4 { return } - + let length = parsed![1] let type = parsed![2] let jsonData = parsed![3].dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) - + if type != "0" { NSLog("Error handshaking") return } - + if let json = NSJSONSerialization.JSONObjectWithData(jsonData!, options: NSJSONReadingOptions.AllowFragments, error: &err2) as? NSDictionary { if let sid = json["sid"] as? String { // println(json) self?.sid = sid self?._connected = true - + if !self!.forcePolling { self?.ws = WebSocket(url: NSURL(string: urlWebSocket + "&sid=\(self!.sid)")!) self?.ws?.queue = self?.handleQueue @@ -332,43 +333,43 @@ public class SocketEngine: NSObject, WebSocketDelegate { NSLog("Error handshaking") return } - + if let pingInterval = json["pingInterval"] as? Int { self?.pingInterval = pingInterval / 1000 } } - + self?.doPoll() self?.startPingTimer() }}.resume() } - + // Translatation of engine.io-parser#decodePayload private func parsePollingMessage(str:String) { if str.length == 1 { return } - + // println(str) - + let strArray = Array(str) var length = "" var n = 0 var msg = "" - + func testLength(length:String, inout n:Int) -> Bool { if let num = length.toInt() { n = num } else { return true } - + return false } - + for var i = 0, l = str.length; i < l; i = i &+ 1 { let chr = String(strArray[i]) - + if chr != ":" { length += chr } else { @@ -376,16 +377,16 @@ public class SocketEngine: NSObject, WebSocketDelegate { println("failure in parsePollingMessage") return } - + msg = String(strArray[i&+1...i&+n]) - + if let lengthInt = length.toInt() { if lengthInt != msg.length { println("parsing error") return } } - + if msg.length != 0 { // Be sure to capture the value of the msg dispatch_async(self.handleQueue) {[weak self, msg] in @@ -393,36 +394,37 @@ public class SocketEngine: NSObject, WebSocketDelegate { return } } - + i += n length = "" } } } - + private func parseEngineData(data:NSData) { self.client.parseBinaryData(data.subdataWithRange(NSMakeRange(1, data.length - 1))) } - + private func parseEngineMessage(var message:String) { // NSLog("Engine got message: \(message)") - + fixDoubleUTF8(&message) + // We should upgrade if message == "3probe" { self.upgradeTransport() return } - + let type = message["^(\\d)"].groups()?[1] - + if type != PacketType.MESSAGE.rawValue { // TODO Handle other packets if message.hasPrefix("b4") { // binary in base64 string - + message.removeRange(Range(start: message.startIndex, end: advance(message.startIndex, 2))) - + if let data = NSData(base64EncodedString: message, options: NSDataBase64DecodingOptions.IgnoreUnknownCharacters) { // println("sending \(data)") @@ -431,13 +433,13 @@ public class SocketEngine: NSObject, WebSocketDelegate { return } } - + return } else if type == PacketType.NOOP.rawValue { self.doPoll() return } - + if message == PacketType.CLOSE.rawValue { // do nothing return @@ -445,29 +447,29 @@ public class SocketEngine: NSObject, WebSocketDelegate { // println("Got something idk what to do with") // println(messageString) } - + // Remove message type message.removeAtIndex(message.startIndex) - + dispatch_async(self.client.handleQueue) {[weak self] in self?.client.parseSocketMessage(message) return } } - + private func probeWebSocket() { if self.websocketConnected { self.sendWebSocketMessage("probe", withType: PacketType.PING) } } - + public func send(msg:String, datas:[NSData]? = nil) { let _send = {[weak self] (msg:String, datas:[NSData]?) -> () -> Void in return { if self == nil || !self!.connected { return } - + if self!.websocket { // println("sending ws: \(msg):\(datas)") self?.sendWebSocketMessage(msg, withType: PacketType.MESSAGE, datas: datas) @@ -477,12 +479,12 @@ public class SocketEngine: NSObject, WebSocketDelegate { } } } - + dispatch_async(self.emitQueue) {[weak self] in if self == nil { return } - + if self!.probing { self?.probeWait.append(_send(msg, datas)) } else { @@ -490,7 +492,7 @@ public class SocketEngine: NSObject, WebSocketDelegate { } } } - + func sendPing() { if self.websocket { self.sendWebSocketMessage("", withType: PacketType.PING) @@ -498,31 +500,31 @@ public class SocketEngine: NSObject, WebSocketDelegate { self.sendPollMessage("", withType: PacketType.PING) } } - + private func sendPollMessage(msg:String, withType type:PacketType, datas:[NSData]? = nil) { // println("Sending poll: \(msg) as type: \(type.rawValue)") let strMsg = "\(type.rawValue)\(msg)" - + self.postWait.append(strMsg) - + if datas != nil { for data in datas! { let (nilData, b64Data) = self.createBinaryDataForSend(data) - + self.postWait.append(b64Data!) } } - + if !self.waitingForPost { self.flushWaitingForPost() } } - + private func sendWebSocketMessage(str:String, withType type:PacketType, datas:[NSData]? = nil) { // println("Sending ws: \(str) as type: \(type.rawValue)") self.ws?.writeString("\(type.rawValue)\(str)") - + if datas != nil { for data in datas! { let (data, nilString) = self.createBinaryDataForSend(data) @@ -532,20 +534,20 @@ public class SocketEngine: NSObject, WebSocketDelegate { } } } - + // Starts the ping timer private func startPingTimer() { if self.pingInterval == nil { return } - + self.pingTimer?.invalidate() dispatch_async(dispatch_get_main_queue()) { self.pingTimer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(self.pingInterval!), target: self, selector: Selector("sendPing"), userInfo: nil, repeats: true) } } - + private func upgradeTransport() { if self.websocketConnected { // Do a fast upgrade @@ -554,17 +556,17 @@ public class SocketEngine: NSObject, WebSocketDelegate { self.sendPollMessage("", withType: PacketType.NOOP) } } - + public func websocketDidConnect(socket:WebSocket) { self.websocketConnected = true self.probing = true self.probeWebSocket() } - + public func websocketDidDisconnect(socket:WebSocket, error:NSError?) { self.websocketConnected = false self.probing = false - + if self.websocket { self.pingTimer?.invalidate() self._connected = false @@ -575,11 +577,11 @@ public class SocketEngine: NSObject, WebSocketDelegate { self.flushProbeWait() } } - + public func websocketDidReceiveMessage(socket:WebSocket, text:String) { self.parseEngineMessage(text) } - + public func websocketDidReceiveData(socket:WebSocket, data:NSData) { self.parseEngineData(data) } diff --git a/SwiftIO/SocketFixUTF8.swift b/SwiftIO/SocketFixUTF8.swift new file mode 100644 index 0000000..0d10fcc --- /dev/null +++ b/SwiftIO/SocketFixUTF8.swift @@ -0,0 +1,100 @@ +// +// SocketFixUTF8.swift +// Socket.IO-Swift +// +// Created by Erik Little on 3/16/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. +// +// Adapted from: https://github.com/durbrow/fix-double-utf8.swift + +import Foundation + +var memoizer = [String: UnicodeScalar]() + +func lookup(base:UnicodeScalar, combi:UnicodeScalar) -> UnicodeScalar { + let combined = "\(base)\(combi)" + + if let y = memoizer[combined] { + return y + } + + for i in 0x80...0xFF { + let ch = UnicodeScalar(i) + + if String(ch) == combined { + memoizer[combined] = ch + return ch + } + } + let ch = UnicodeScalar(0xFFFD) // Unicode replacement character � + + memoizer[combined] = ch + return ch +} + +func fixDoubleUTF8(inout name:String) { + var isASCII = true + var y = [UInt8]() + + for ch in name.unicodeScalars { + if ch.value < 0x80 { + y.append(UInt8(ch)) + continue + } + isASCII = false + + if ch.value < 0x100 { + y.append(UInt8(ch)) + continue + } + // might be a combining character that when combined with the + // preceeding character maps to a codepoint in the UTF8 range + if y.count == 0 { + return + } + + let last = y.removeLast() + let repl = lookup(UnicodeScalar(last), ch) + + // the replacement needs to be in the UTF8 range + if repl.value >= 0x100 { + return + } + + y.append(UInt8(repl)) + } + + if isASCII { + return + } + + y.append(0) // null terminator + + return y.withUnsafeBufferPointer { + let cstr = UnsafePointer($0.baseAddress) // typecase from uint8_t * to char * + let rslt = String.fromCStringRepairingIllFormedUTF8(cstr) // -> (String, Bool) + if let str = rslt.0 { + if !rslt.hadError { + name = str + } + } + + return + } +} \ No newline at end of file diff --git a/SwiftIO/SocketIOClient.swift b/SwiftIO/SocketIOClient.swift index 9ec61bc..330ffbf 100644 --- a/SwiftIO/SocketIOClient.swift +++ b/SwiftIO/SocketIOClient.swift @@ -74,7 +74,7 @@ public class SocketIOClient: NSObject { return self._sid } - public init(var socketURL:String, opts:[String: AnyObject]? = nil) { + public init(var socketURL:String, opts:NSDictionary? = nil) { if socketURL["https://"].matches().count != 0 { self._secure = true } @@ -116,6 +116,10 @@ public class SocketIOClient: NSObject { self.engine = SocketEngine(client: self, forcePolling: self.forcePolling) } + public convenience init(socketURL:String, options:NSDictionary?) { + self.init(socketURL: socketURL, opts: options) + } + // Closes the socket public func close() { self._closed = true diff --git a/SwiftIO/SocketParser.swift b/SwiftIO/SocketParser.swift index 1e2b6ab..e094f26 100644 --- a/SwiftIO/SocketParser.swift +++ b/SwiftIO/SocketParser.swift @@ -217,20 +217,16 @@ class SocketParser { return } - /** Begin check for message **/ - let messageGroups = stringMessage["(\\d*)\\/?(\\w*)?,?(\\d*)?\\[\"(.*?)\",?(.*?)?\\]$"].groups() + let messageGroups = stringMessage["(\\d*)\\/?(\\w*)?,?(\\d*)?\\[\"(.*?)\",?(.*?)?\\]$", + NSRegularExpressionOptions.DotMatchesLineSeparators].groups() if messageGroups == nil { NSLog("Error in groups") return } - // let messageGroups = SwiftRegex(target: stringMessage as NSString, - // pattern: "(\\d*)\\/?(\\w*)?,?(\\d*)?\\[\"(.*?)\",?(.*?)?\\]$", - // options: NSRegularExpressionOptions.DotMatchesLineSeparators).groups() - if messageGroups![1].hasPrefix("2") { var mesNum = messageGroups![1] var ackNum:String @@ -343,10 +339,8 @@ class SocketParser { /** Begin check for binary placeholders **/ - let binaryGroup = message["^(\\d*)-\\/?(\\w*)?,?(\\d*)?\\[(\".*?\")?,?(.*)?\\]$"].groups() - // let binaryGroup = SwiftRegex(target: message, - // pattern: "^(\\d*)-\\/?(\\w*)?,?(\\d*)?\\[(\".*?\")?,?(.*)?\\]$", - // options: NSRegularExpressionOptions.DotMatchesLineSeparators).groups() + let binaryGroup = message["^(\\d*)-\\/?(\\w*)?,?(\\d*)?\\[(\".*?\")?,?(.*)?\\]$", + NSRegularExpressionOptions.DotMatchesLineSeparators].groups() if binaryGroup == nil { return