diff --git a/CHANGELOG.md b/CHANGELOG.md index c934b93c..0a857174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. `Starscream` adheres to [Semantic Versioning](http://semver.org/). +#### [2.1.0](https://github.com/daltoniam/Starscream/tree/2.1.0) + +Adds WebSocket compression. Also adds advance WebSocket delegate for extra control. Bug Fixes. + +[#349](https://github.com/daltoniam/Starscream/pull/349) +[#344](https://github.com/daltoniam/Starscream/pull/344) +[#339](https://github.com/daltoniam/Starscream/pull/339) +[#337](https://github.com/daltoniam/Starscream/pull/337) +[#334](https://github.com/daltoniam/Starscream/issues/334) +[#333](https://github.com/daltoniam/Starscream/pull/333) +[#319](https://github.com/daltoniam/Starscream/issues/319) +[#309](https://github.com/daltoniam/Starscream/issues/309) +[#329](https://github.com/daltoniam/Starscream/issues/329) + #### [2.0.4](https://github.com/daltoniam/Starscream/tree/2.0.4) SSL Pinning fix by Giuliano Galea as reported by Lukas Futera of [Centralway](https://www.centralway.com/de/). diff --git a/Package.swift b/Package.swift index 163515b9..73cc1d6c 100644 --- a/Package.swift +++ b/Package.swift @@ -21,5 +21,12 @@ import PackageDescription let package = Package( - name: "Starscream" + name: "Starscream", + dependencies: [ + .Package(url: "https://github.com/daltoniam/zlib-spm.git", + majorVersion: 1), + .Package(url: "https://github.com/IBM-Swift/CommonCrypto.git", + majorVersion: 0), + ], + exclude: ["Tests", "examples"] ) diff --git a/README.md b/README.md index 7120f775..bce39ab3 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,14 @@ Starscream is a conforming WebSocket ([RFC 6455](http://tools.ietf.org/html/rfc6455)) client library in Swift for iOS and OSX. -It's Objective-C counter part can be found here: [Jetfire](https://github.com/acmacalister/jetfire) +Its Objective-C counterpart can be found here: [Jetfire](https://github.com/acmacalister/jetfire) ## Features - Conforms to all of the base [Autobahn test suite](http://autobahn.ws/testsuite/). - Nonblocking. Everything happens in the background, thanks to GCD. - TLS/WSS support. +- Compression Extensions support ([RFC 7692](https://tools.ietf.org/html/rfc7692)) - Simple concise codebase at just a few hundred LOC. ## Example @@ -160,6 +161,21 @@ socket.headers["Sec-WebSocket-Version"] = "14" socket.headers["My-Awesome-Header"] = "Everything is Awesome!" ``` +### Custom HTTP Method + +Your server may use a different HTTP method when connecting to the websocket: + +```swift +socket.httpMethod = .post +``` +you can use a custom string: + +```swift +socket.httpMethod = .custom(value: "mycustomhttpmethod") +``` + + + ### Protocols If you need to specify a protocol, simple add it to the init: @@ -171,16 +187,11 @@ socket.delegate = self socket.connect() ``` -### Self Signed SSL and VOIP - -There are a couple of other properties that modify the stream: +### Self Signed SSL ```swift socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"]) -//set this if you are planning on using the socket in a VOIP background setting (using the background VOIP service). -socket.voipEnabled = true - //set this you want to ignore SSL cert validation, so a self signed SSL certificate can be used. socket.disableSSLCertValidation = true ``` @@ -197,6 +208,30 @@ socket.security = SSLSecurity(certs: [SSLCert(data: data)], usePublicKeys: true) ``` You load either a `Data` blob of your certificate or you can use a `SecKeyRef` if you have a public key you want to use. The `usePublicKeys` bool is whether to use the certificates for validation or the public keys. The public keys will be extracted from the certificates automatically if `usePublicKeys` is choosen. +### SSL Cipher Suites + +To use an SSL encrypted connection, you need to tell Starscream about the cipher suites your server supports. + +```swift +socket = WebSocket(url: URL(string: "wss://localhost:8080/")!, protocols: ["chat","superchat"]) + +// Set enabled cipher suites to AES 256 and AES 128 +socket.enabledSSLCipherSuites = [TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256] +``` + +If you don't know which cipher suites are supported by your server, you can try pointing [SSL Labs](https://www.ssllabs.com/ssltest/) at it and checking the results. + +### Compression Extensions + +Compression Extensions ([RFC 7692](https://tools.ietf.org/html/rfc7692)) is supported in Starscream. Compression is enabled by default, however compression will only be used if it is supported by the server as well. You may enable or disable compression via the `.enableCompression` property: + +```swift +socket = WebSocket(url: URL(string: "ws://localhost:8080/")!) +socket.enableCompression = false +``` + +Compression should be disabled if your application is transmitting already-compressed, random, or other uncompressable data. + ### Custom Queue A custom queue can be specified when delegate methods are called. By default `DispatchQueue.main` is used, thus making all delegate methods calls run on the main thread. It is important to note that all WebSocket processing is done on a background thread, only the delegate method calls are changed when modifying the queue. The actual processing is always on a background thread and will not pause your app. @@ -286,6 +321,46 @@ Add the `Starscream.xcodeproj` to your Xcode project. Once that is complete, in If you are running this in an OSX app or on a physical iOS device you will need to make sure you add the `Starscream.framework` to be included in your app bundle. To do this, in Xcode, navigate to the target configuration window by clicking on the blue project icon, and selecting the application target under the "Targets" heading in the sidebar. In the tab bar at the top of that window, open the "Build Phases" panel. Expand the "Link Binary with Libraries" group, and add `Starscream.framework`. Click on the + button at the top left of the panel and select "New Copy Files Phase". Rename this new phase to "Copy Frameworks", set the "Destination" to "Frameworks", and add `Starscream.framework` respectively. + +## WebSocketAdvancedDelegate +The advanced delegate acts just like the simpler delegate but provides some additional information on the connection and incoming frames. + +```swift +socket.advancedDelegate = self +``` + +In most cases you do not need the extra info and should use the normal delegate. + +#### websocketDidReceiveMessage +```swift +func websocketDidReceiveMessage(socket: WebSocket, text: String, response: WebSocket.WSResponse { + print("got some text: \(text)") + print("First frame for this message arrived on \(response.firstFrame)") +} +``` + +#### websocketDidReceiveData +```swift +func websocketDidReceiveData(socket: WebSocket, data: Date, response: WebSocket.WSResponse) { + print("got some data it long: \(data.count)") + print("A total of \(response.frameCount) frames were used to send this data") +} +``` + +#### websocketHttpUpgrade +These methods are called when the HTTP upgrade request is sent and when the response returns. +```swift +func websocketHttpUpgrade(socket: WebSocket, request: CFHTTPMessage) { + print("the http request was sent we can check the raw http if we need to") +} +``` + +```swift +func websocketHttpUpgrade(socket: WebSocket, response: CFHTTPMessage) { + print("the http response has returned.") +} +``` + ## TODOs - [ ] WatchOS? diff --git a/Source/Compression.swift b/Source/Compression.swift new file mode 100644 index 00000000..cdfdc7f8 --- /dev/null +++ b/Source/Compression.swift @@ -0,0 +1,177 @@ +////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Compression.swift +// +// Created by Joseph Ross on 7/16/14. +// Copyright © 2017 Joseph Ross. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +////////////////////////////////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Compression implementation is implemented in conformance with RFC 7692 Compression Extensions +// for WebSocket: https://tools.ietf.org/html/rfc7692 +// +////////////////////////////////////////////////////////////////////////////////////////////////// + +import Foundation +import CZLib + +class Decompressor { + private var strm = z_stream() + private var buffer = [UInt8](repeating: 0, count: 0x2000) + private var inflateInitialized = false + private let windowBits:Int + + init?(windowBits:Int) { + self.windowBits = windowBits + guard initInflate() else { return nil } + } + + private func initInflate() -> Bool { + if Z_OK == inflateInit2_(&strm, -CInt(windowBits), + ZLIB_VERSION, CInt(MemoryLayout.size)) + { + inflateInitialized = true + return true + } + return false + } + + func reset() throws { + teardownInflate() + guard initInflate() else { throw NSError() } + } + + func decompress(_ data: Data, finish: Bool) throws -> Data { + return try data.withUnsafeBytes { (bytes:UnsafePointer) -> Data in + return try decompress(bytes: bytes, count: data.count, finish: finish) + } + } + + func decompress(bytes: UnsafePointer, count: Int, finish: Bool) throws -> Data { + var decompressed = Data() + try decompress(bytes: bytes, count: count, out: &decompressed) + + if finish { + let tail:[UInt8] = [0x00, 0x00, 0xFF, 0xFF] + try decompress(bytes: tail, count: tail.count, out: &decompressed) + } + + return decompressed + + } + + private func decompress(bytes: UnsafePointer, count: Int, out:inout Data) throws { + var res:CInt = 0 + strm.next_in = UnsafeMutablePointer(mutating: bytes) + strm.avail_in = CUnsignedInt(count) + + repeat { + strm.next_out = UnsafeMutablePointer(&buffer) + strm.avail_out = CUnsignedInt(buffer.count) + + res = inflate(&strm, 0) + + let byteCount = buffer.count - Int(strm.avail_out) + out.append(buffer, count: byteCount) + } while res == Z_OK && strm.avail_out == 0 + + guard (res == Z_OK && strm.avail_out > 0) + || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) + else { + throw NSError(domain: WebSocket.ErrorDomain, code: Int(WebSocket.InternalErrorCode.compressionError.rawValue), userInfo: nil) + } + } + + private func teardownInflate() { + if inflateInitialized, Z_OK == inflateEnd(&strm) { + inflateInitialized = false + } + } + + deinit { + teardownInflate() + } +} + +class Compressor { + private var strm = z_stream() + private var buffer = [UInt8](repeating: 0, count: 0x2000) + private var deflateInitialized = false + private let windowBits:Int + + init?(windowBits: Int) { + self.windowBits = windowBits + guard initDeflate() else { return nil } + } + + private func initDeflate() -> Bool { + if Z_OK == deflateInit2_(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + -CInt(windowBits), 8, Z_DEFAULT_STRATEGY, + ZLIB_VERSION, CInt(MemoryLayout.size)) + { + deflateInitialized = true + return true + } + return false + } + + func reset() throws { + teardownDeflate() + guard initDeflate() else { throw NSError() } + } + + func compress(_ data: Data) throws -> Data { + var compressed = Data() + var res:CInt = 0 + data.withUnsafeBytes { (ptr:UnsafePointer) -> Void in + strm.next_in = UnsafeMutablePointer(mutating: ptr) + strm.avail_in = CUnsignedInt(data.count) + + repeat { + strm.next_out = UnsafeMutablePointer(&buffer) + strm.avail_out = CUnsignedInt(buffer.count) + + res = deflate(&strm, Z_SYNC_FLUSH) + + let byteCount = buffer.count - Int(strm.avail_out) + compressed.append(buffer, count: byteCount) + } + while res == Z_OK && strm.avail_out == 0 + + } + + guard res == Z_OK && strm.avail_out > 0 + || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) + else { + throw NSError(domain: WebSocket.ErrorDomain, code: Int(WebSocket.InternalErrorCode.compressionError.rawValue), userInfo: nil) + } + + compressed.removeLast(4) + return compressed + } + + private func teardownDeflate() { + if deflateInitialized, Z_OK == deflateEnd(&strm) { + deflateInitialized = false + } + } + + deinit { + teardownDeflate() + } +} + diff --git a/Source/Info-tvOS.plist b/Source/Info-tvOS.plist index 24551b51..a664afcf 100644 --- a/Source/Info-tvOS.plist +++ b/Source/Info-tvOS.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.0.4 + 2.1.0 CFBundleSignature ???? CFBundleVersion diff --git a/Source/Info.plist b/Source/Info.plist index 241ef869..dca568ec 100644 --- a/Source/Info.plist +++ b/Source/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.0.4 + 2.1.0 CFBundleSignature ???? CFBundleVersion diff --git a/Source/WebSocket.swift b/Source/WebSocket.swift index ca164ef2..cd109cbb 100644 --- a/Source/WebSocket.swift +++ b/Source/WebSocket.swift @@ -21,7 +21,7 @@ import Foundation import CoreFoundation -import Security +import CommonCrypto public let WebsocketDidConnectNotification = "WebsocketDidConnectNotification" public let WebsocketDidDisconnectNotification = "WebsocketDidDisconnectNotification" @@ -38,9 +38,19 @@ public protocol WebSocketPongDelegate: class { func websocketDidReceivePong(socket: WebSocket, data: Data?) } +// A Delegate with more advanced info on messages and connection etc. +public protocol WebSocketAdvancedDelegate: class { + func websocketDidConnect(socket: WebSocket) + func websocketDidDisconnect(socket: WebSocket, error: NSError?) + func websocketDidReceiveMessage(socket: WebSocket, text: String, response: WebSocket.WSResponse) + func websocketDidReceiveData(socket: WebSocket, data: Data, response: WebSocket.WSResponse) + func websocketHttpUpgrade(socket: WebSocket, request: CFHTTPMessage) + func websocketHttpUpgrade(socket: WebSocket, response: CFHTTPMessage) +} + open class WebSocket : NSObject, StreamDelegate { - enum OpCode : UInt8 { + public enum OpCode : UInt8 { case continueFrame = 0x0 case textFrame = 0x1 case binaryFrame = 0x2 @@ -69,6 +79,9 @@ open class WebSocket : NSObject, StreamDelegate { enum InternalErrorCode: UInt16 { // 0-999 WebSocket status codes not used case outputStreamWriteError = 1 + case compressionError = 2 + case invalidSSLError = 3 + case writeTimeoutError = 4 } // Where the callback is executed. It defaults to the main UI thread queue. @@ -86,6 +99,7 @@ open class WebSocket : NSObject, StreamDelegate { let headerWSProtocolName = "Sec-WebSocket-Protocol" let headerWSVersionName = "Sec-WebSocket-Version" let headerWSVersionValue = "13" + let headerWSExtensionName = "Sec-WebSocket-Extensions" let headerWSKeyName = "Sec-WebSocket-Key" let headerOriginName = "Origin" let headerWSAcceptName = "Sec-WebSocket-Accept" @@ -93,18 +107,22 @@ open class WebSocket : NSObject, StreamDelegate { let FinMask: UInt8 = 0x80 let OpCodeMask: UInt8 = 0x0F let RSVMask: UInt8 = 0x70 + let RSV1Mask: UInt8 = 0x40 let MaskMask: UInt8 = 0x80 let PayloadLenMask: UInt8 = 0x7F let MaxFrameSize: Int = 32 let httpSwitchProtocolCode = 101 let supportedSSLSchemes = ["wss", "https"] - class WSResponse { + public class WSResponse { var isFin = false - var code: OpCode = .continueFrame + public var code: OpCode = .continueFrame var bytesLeft = 0 - var frameCount = 0 - var buffer: NSMutableData? + public var frameCount = 0 + public var buffer: NSMutableData? + public let firstFrame = { + return Date() + }() } // MARK: - Delegates @@ -113,21 +131,47 @@ open class WebSocket : NSObject, StreamDelegate { /// and also connection/disconnect messages. public weak var delegate: WebSocketDelegate? + /// The optional advanced delegate can be used insteadof of the delegate + public weak var advancedDelegate: WebSocketAdvancedDelegate? + /// Receives a callback for each pong message recived. public weak var pongDelegate: WebSocketPongDelegate? // MARK: - Block based API. - public var onConnect: ((Void) -> Void)? + public enum HTTPMethod { + case get + case post + case put + case connect + case custom(value: String) + var representation: String { + switch self { + case .get: + return "GET" + case .post: + return "POST" + case .put: + return "PUT" + case .connect: + return "CONNECT" + case .custom(let value): + return value.capitalized + } + } + } + + public var onConnect: (() -> Void)? public var onDisconnect: ((NSError?) -> Void)? public var onText: ((String) -> Void)? public var onData: ((Data) -> Void)? public var onPong: ((Data?) -> Void)? + public var httpMethod: HTTPMethod = .get public var headers = [String: String]() - public var voipEnabled = false public var disableSSLCertValidation = false + public var enableCompression = true public var security: SSLTrustValidator? public var enabledSSLCipherSuites: [SSLCipherSuite]? public var origin: String? @@ -139,12 +183,24 @@ open class WebSocket : NSObject, StreamDelegate { public var currentURL: URL { return url } // MARK: - Private + + private struct CompressionState { + var supportsCompression = false + var messageNeedsDecompression = false + var serverMaxWindowBits = 15 + var clientMaxWindowBits = 15 + var clientNoContextTakeover = false + var serverNoContextTakeover = false + var decompressor:Decompressor? = nil + var compressor:Compressor? = nil + } private var url: URL private var inputStream: InputStream? private var outputStream: OutputStream? private var connected = false private var isConnecting = false + private var compressionState = CompressionState() private var writeQueue = OperationQueue() private var readStack = [WSResponse]() private var inputQueue = [Data]() @@ -152,6 +208,7 @@ open class WebSocket : NSObject, StreamDelegate { private var certValidated = false private var didDisconnect = false private var readyToWrite = false + private var headerSecKey = "" private let mutex = NSLock() private let notificationCenter = NotificationCenter.default private var canDispatch: Bool { @@ -258,7 +315,7 @@ open class WebSocket : NSObject, StreamDelegate { Private method that starts the connection. */ private func createHTTPRequest() { - let urlRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, "GET" as CFString, + let urlRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, httpMethod.representation as CFString, url as CFURL, kCFHTTPVersion1_1).takeRetainedValue() var port = url.port @@ -274,11 +331,16 @@ open class WebSocket : NSObject, StreamDelegate { if let protocols = optionalProtocols { addHeader(urlRequest, key: headerWSProtocolName, val: protocols.joined(separator: ",")) } + headerSecKey = generateWebSocketKey() addHeader(urlRequest, key: headerWSVersionName, val: headerWSVersionValue) - addHeader(urlRequest, key: headerWSKeyName, val: generateWebSocketKey()) + addHeader(urlRequest, key: headerWSKeyName, val: headerSecKey) if let origin = origin { addHeader(urlRequest, key: headerOriginName, val: origin) } + if enableCompression { + let val = "permessage-deflate; client_max_window_bits; server_max_window_bits=15" + addHeader(urlRequest, key: headerWSExtensionName, val: val) + } addHeader(urlRequest, key: headerWSHostName, val: "\(url.host!):\(port!)") for (key, value) in headers { addHeader(urlRequest, key: key, val: value) @@ -286,6 +348,7 @@ open class WebSocket : NSObject, StreamDelegate { if let cfHTTPMessage = CFHTTPMessageCopySerializedMessage(urlRequest) { let serializedRequest = cfHTTPMessage.takeRetainedValue() initStreamsWithData(serializedRequest as Data, Int(port!)) + self.advancedDelegate?.websocketHttpUpgrade(socket: self, request: urlRequest) } } @@ -359,10 +422,6 @@ open class WebSocket : NSObject, StreamDelegate { } else { certValidated = true //not a https session, so no need to check SSL pinning } - if voipEnabled { - inStream.setProperty(StreamNetworkServiceTypeValue.voIP as AnyObject, forKey: Stream.PropertyKey.networkServiceType) - outStream.setProperty(StreamNetworkServiceTypeValue.voIP as AnyObject, forKey: Stream.PropertyKey.networkServiceType) - } CFReadStreamSetDispatchQueue(inStream, WebSocket.sharedWorkQueue) CFWriteStreamSetDispatchQueue(outStream, WebSocket.sharedWorkQueue) @@ -386,7 +445,8 @@ open class WebSocket : NSObject, StreamDelegate { WebSocket.sharedWorkQueue.async { self?.cleanupStream() } - self?.doDisconnect(self?.errorWithDetail("write wait timed out", code: 2)) + let errCode = InternalErrorCode.writeTimeoutError.rawValue + self?.doDisconnect(self?.errorWithDetail("write wait timed out", code: errCode)) return } else if outStream.streamError != nil { return // disconnectStream will be called. @@ -395,12 +455,16 @@ open class WebSocket : NSObject, StreamDelegate { guard !sOperation.isCancelled, let s = self else { return } // Do the pinning now if needed if let sec = s.security, !s.certValidated { - let trust = outStream.property(forKey: kCFStreamPropertySSLPeerTrust as Stream.PropertyKey) as! SecTrust - let domain = outStream.property(forKey: kCFStreamSSLPeerName as Stream.PropertyKey) as? String - s.certValidated = sec.isValid(trust, domain: domain) + if let possibleTrust = outStream.property(forKey: kCFStreamPropertySSLPeerTrust as Stream.PropertyKey) { + let domain = outStream.property(forKey: kCFStreamSSLPeerName as Stream.PropertyKey) as? String + s.certValidated = sec.isValid(possibleTrust as! SecTrust, domain: domain) + } else { + s.certValidated = false + } if !s.certValidated { WebSocket.sharedWorkQueue.async { - let error = s.errorWithDetail("Invalid SSL certificate", code: 1) + let errCode = InternalErrorCode.invalidSSLError.rawValue + let error = s.errorWithDetail("Invalid SSL certificate", code: errCode) s.disconnectStream(error) } return @@ -552,6 +616,7 @@ open class WebSocket : NSObject, StreamDelegate { guard let s = self else { return } s.onConnect?() s.delegate?.websocketDidConnect(socket: s) + s.advancedDelegate?.websocketDidConnect(socket: s) s.notificationCenter.post(name: NSNotification.Name(WebsocketDidConnectNotification), object: self) } } @@ -572,13 +637,24 @@ open class WebSocket : NSObject, StreamDelegate { let response = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, false).takeRetainedValue() CFHTTPMessageAppendBytes(response, buffer, bufferLen) let code = CFHTTPMessageGetResponseStatusCode(response) + self.advancedDelegate?.websocketHttpUpgrade(socket: self, response: response) if code != httpSwitchProtocolCode { return code } if let cfHeaders = CFHTTPMessageCopyAllHeaderFields(response) { let headers = cfHeaders.takeRetainedValue() as NSDictionary + if let extensionHeader = headers[headerWSExtensionName as NSString] as? String { + processExtensionHeader(extensionHeader) + } + if let acceptKey = headers[headerWSAcceptName as NSString] as? NSString { if acceptKey.length > 0 { + if headerSecKey.characters.count > 0 { + let sha = "\(headerSecKey)258EAFA5-E914-47DA-95CA-C5AB0DC85B11".sha1Base64() + if sha != acceptKey as String { + return -1 + } + } return 0 } } @@ -586,6 +662,37 @@ open class WebSocket : NSObject, StreamDelegate { return -1 } + /** + Parses the extension header, setting up the compression parameters. + */ + func processExtensionHeader(_ extensionHeader: String) { + let parts = extensionHeader.components(separatedBy: ";") + for p in parts { + let part = p.trimmingCharacters(in: .whitespaces) + if part == "permessage-deflate" { + compressionState.supportsCompression = true + } else if part.hasPrefix("server_max_window_bits=") { + let valString = part.components(separatedBy: "=")[1] + if let val = Int(valString.trimmingCharacters(in: .whitespaces)) { + compressionState.serverMaxWindowBits = val + } + } else if part.hasPrefix("client_max_window_bits=") { + let valString = part.components(separatedBy: "=")[1] + if let val = Int(valString.trimmingCharacters(in: .whitespaces)) { + compressionState.clientMaxWindowBits = val + } + } else if part == "client_no_context_takeover" { + compressionState.clientNoContextTakeover = true + } else if part == "server_no_context_takeover" { + compressionState.serverNoContextTakeover = true + } + } + if compressionState.supportsCompression { + compressionState.decompressor = Decompressor(windowBits: compressionState.serverMaxWindowBits) + compressionState.compressor = Compressor(windowBits: compressionState.clientMaxWindowBits) + } + } + /** Read a 16 bit big endian value from a buffer */ @@ -650,7 +757,10 @@ open class WebSocket : NSObject, StreamDelegate { let isMasked = (MaskMask & baseAddress[1]) let payloadLen = (PayloadLenMask & baseAddress[1]) var offset = 2 - if (isMasked > 0 || (RSVMask & baseAddress[0]) > 0) && receivedOpcode != .pong { + if compressionState.supportsCompression && receivedOpcode != .continueFrame { + compressionState.messageNeedsDecompression = (RSV1Mask & baseAddress[0]) > 0 + } + if (isMasked > 0 || (RSVMask & baseAddress[0]) > 0) && receivedOpcode != .pong && !compressionState.messageNeedsDecompression { let errCode = CloseCode.protocolError.rawValue doDisconnect(errorWithDetail("masked and rsv data is not currently supported", code: errCode)) writeError(errCode) @@ -710,7 +820,23 @@ open class WebSocket : NSObject, StreamDelegate { offset += size len -= UInt64(size) } - let data = Data(bytes: baseAddress+offset, count: Int(len)) + let data: Data + if compressionState.messageNeedsDecompression, let decompressor = compressionState.decompressor { + do { + data = try decompressor.decompress(bytes: baseAddress+offset, count: Int(len), finish: isFin > 0) + if isFin > 0 && compressionState.serverNoContextTakeover{ + try decompressor.reset() + } + } catch { + let closeReason = "Decompression failed: \(error)" + let closeCode = CloseCode.encoding.rawValue + doDisconnect(errorWithDetail(closeReason, code: closeCode)) + writeError(closeCode) + return emptyBuffer + } + } else { + data = Data(bytes: baseAddress+offset, count: Int(len)) + } if receivedOpcode == .connectionClose { var closeReason = "connection closed by server" @@ -817,6 +943,7 @@ open class WebSocket : NSObject, StreamDelegate { guard let s = self else { return } s.onText?(str! as String) s.delegate?.websocketDidReceiveMessage(socket: s, text: str! as String) + s.advancedDelegate?.websocketDidReceiveMessage(socket: s, text: str! as String, response: response) } } } else if response.code == .binaryFrame { @@ -826,6 +953,7 @@ open class WebSocket : NSObject, StreamDelegate { guard let s = self else { return } s.onData?(data as Data) s.delegate?.websocketDidReceiveData(socket: s, data: data as Data) + s.advancedDelegate?.websocketDidReceiveData(socket: s, data: data as Data, response: response) } } } @@ -864,10 +992,23 @@ open class WebSocket : NSObject, StreamDelegate { guard let s = self else { return } guard let sOperation = operation else { return } var offset = 2 + var firstByte:UInt8 = s.FinMask | code.rawValue + var data = data + if [.textFrame, .binaryFrame].contains(code), let compressor = s.compressionState.compressor { + do { + data = try compressor.compress(data) + if s.compressionState.clientNoContextTakeover { + try compressor.reset() + } + firstByte |= s.RSV1Mask + } catch { + // TODO: report error? We can just send the uncompressed frame. + } + } let dataLength = data.count let frame = NSMutableData(capacity: dataLength + s.MaxFrameSize) let buffer = UnsafeMutableRawPointer(frame!.mutableBytes).assumingMemoryBound(to: UInt8.self) - buffer[0] = s.FinMask | code.rawValue + buffer[0] = firstByte if dataLength < 126 { buffer[1] = CUnsignedChar(dataLength) } else if dataLength <= Int(UInt16.max) { @@ -933,6 +1074,7 @@ open class WebSocket : NSObject, StreamDelegate { guard let s = self else { return } s.onDisconnect?(error) s.delegate?.websocketDidDisconnect(socket: s, error: error) + s.advancedDelegate?.websocketDidDisconnect(socket: s, error: error) let userInfo = error.map{ [WebsocketDisconnectionErrorKeyName: $0] } s.notificationCenter.post(name: NSNotification.Name(WebsocketDidDisconnectNotification), object: self, userInfo: userInfo) } @@ -950,6 +1092,15 @@ open class WebSocket : NSObject, StreamDelegate { } +private extension String { + func sha1Base64() -> String { + let data = self.data(using: String.Encoding.utf8)! + var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH)) + data.withUnsafeBytes { _ = CC_SHA1($0, CC_LONG(data.count), &digest) } + return Data(bytes: digest).base64EncodedString() + } +} + private extension Data { init(buffer: UnsafeBufferPointer) { diff --git a/Starscream.podspec b/Starscream.podspec index 03060df6..31f80395 100644 --- a/Starscream.podspec +++ b/Starscream.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Starscream" - s.version = "2.0.4" + s.version = "2.1.0" s.summary = "A conforming WebSocket RFC 6455 client library in Swift for iOS and OSX." s.homepage = "https://github.com/daltoniam/Starscream" s.license = 'Apache License, Version 2.0' @@ -12,4 +12,9 @@ Pod::Spec.new do |s| s.tvos.deployment_target = '9.0' s.source_files = 'Source/*.swift' s.requires_arc = 'true' + s.libraries = 'z' + s.pod_target_xcconfig = { + 'SWIFT_INCLUDE_PATHS' => '$(PODS_ROOT)/Starscream/zlib' + } + s.preserve_paths = 'zlib/*' end diff --git a/Starscream.xcodeproj/project.pbxproj b/Starscream.xcodeproj/project.pbxproj index 3734f5c3..7c6144a4 100644 --- a/Starscream.xcodeproj/project.pbxproj +++ b/Starscream.xcodeproj/project.pbxproj @@ -20,6 +20,29 @@ 742419BC1DC6BDBA003ACE43 /* StarscreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742419BB1DC6BDBA003ACE43 /* StarscreamTests.swift */; }; 742419BD1DC6BDBA003ACE43 /* StarscreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742419BB1DC6BDBA003ACE43 /* StarscreamTests.swift */; }; 742419BE1DC6BDBA003ACE43 /* StarscreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742419BB1DC6BDBA003ACE43 /* StarscreamTests.swift */; }; + D85927D01ED65933003460CB /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1360011C473BEF00AA3A01 /* WebSocket.swift */; }; + D85927D11ED65963003460CB /* SSLSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C135FFF1C473BEF00AA3A01 /* SSLSecurity.swift */; }; + D85927D81ED76F25003460CB /* include.h in Headers */ = {isa = PBXBuildFile; fileRef = D85927D71ED76F25003460CB /* include.h */; }; + D85927D91ED76F25003460CB /* include.h in Headers */ = {isa = PBXBuildFile; fileRef = D85927D71ED76F25003460CB /* include.h */; }; + D85927DA1ED76F25003460CB /* include.h in Headers */ = {isa = PBXBuildFile; fileRef = D85927D71ED76F25003460CB /* include.h */; }; + D85927DB1ED7737C003460CB /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1360011C473BEF00AA3A01 /* WebSocket.swift */; }; + D85927DC1ED7737C003460CB /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1360011C473BEF00AA3A01 /* WebSocket.swift */; }; + D85927DD1ED773AC003460CB /* SSLSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C135FFF1C473BEF00AA3A01 /* SSLSecurity.swift */; }; + D85927DE1ED773AD003460CB /* SSLSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C135FFF1C473BEF00AA3A01 /* SSLSecurity.swift */; }; + D88EAF7F1ED4DFB5004FE2C3 /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88EAF7E1ED4DFB5004FE2C3 /* Compression.swift */; }; + D88EAF821ED4DFD3004FE2C3 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D88EAF811ED4DFD3004FE2C3 /* libz.tbd */; }; + D88EAF841ED4E7D8004FE2C3 /* CompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88EAF831ED4E7D8004FE2C3 /* CompressionTests.swift */; }; + D88EAF851ED4E7D8004FE2C3 /* CompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88EAF831ED4E7D8004FE2C3 /* CompressionTests.swift */; }; + D88EAF861ED4E7D8004FE2C3 /* CompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88EAF831ED4E7D8004FE2C3 /* CompressionTests.swift */; }; + D88EAF871ED4E88C004FE2C3 /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88EAF7E1ED4DFB5004FE2C3 /* Compression.swift */; }; + D88EAF881ED4E88C004FE2C3 /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88EAF7E1ED4DFB5004FE2C3 /* Compression.swift */; }; + D88EAF891ED4E88D004FE2C3 /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88EAF7E1ED4DFB5004FE2C3 /* Compression.swift */; }; + D88EAF8A1ED4E88D004FE2C3 /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88EAF7E1ED4DFB5004FE2C3 /* Compression.swift */; }; + D88EAF8B1ED4E88D004FE2C3 /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88EAF7E1ED4DFB5004FE2C3 /* Compression.swift */; }; + D88EAF8E1ED4E92E004FE2C3 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D88EAF8D1ED4E92E004FE2C3 /* libz.tbd */; }; + D88EAF911ED4E949004FE2C3 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D88EAF901ED4E949004FE2C3 /* libz.tbd */; }; + D88EAF921ED4E95D004FE2C3 /* Starscream.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B3E79E619D48B7F006071F7 /* Starscream.framework */; }; + D88EAF931ED4E975004FE2C3 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D88EAF811ED4DFD3004FE2C3 /* libz.tbd */; }; D9C3E36A19E48FF1009FC285 /* Starscream.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9C3E35F19E48FF1009FC285 /* Starscream.framework */; }; /* End PBXBuildFile section */ @@ -52,6 +75,13 @@ 6B3E79F119D48B7F006071F7 /* Starscream iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Starscream iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6B3E7A0019D48C2F006071F7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 742419BB1DC6BDBA003ACE43 /* StarscreamTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StarscreamTests.swift; path = StarscreamTests/StarscreamTests.swift; sourceTree = ""; }; + D85927D61ED761A0003460CB /* module.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + D85927D71ED76F25003460CB /* include.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = include.h; sourceTree = ""; }; + D88EAF7E1ED4DFB5004FE2C3 /* Compression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Compression.swift; path = Source/Compression.swift; sourceTree = SOURCE_ROOT; }; + D88EAF811ED4DFD3004FE2C3 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; + D88EAF831ED4E7D8004FE2C3 /* CompressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompressionTests.swift; sourceTree = ""; }; + D88EAF8D1ED4E92E004FE2C3 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; + D88EAF901ED4E949004FE2C3 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; D9C3E35F19E48FF1009FC285 /* Starscream.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Starscream.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D9C3E36919E48FF1009FC285 /* Starscream OSXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Starscream OSXTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -61,6 +91,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF911ED4E949004FE2C3 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +107,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF821ED4DFD3004FE2C3 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -83,6 +115,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF931ED4E975004FE2C3 /* libz.tbd in Frameworks */, + D88EAF921ED4E95D004FE2C3 /* Starscream.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -90,6 +124,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF8E1ED4E92E004FE2C3 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -107,9 +142,11 @@ 6B3E79DC19D48B7F006071F7 = { isa = PBXGroup; children = ( + D85927D51ED761A0003460CB /* zlib */, 6B3E79E819D48B7F006071F7 /* Starscream */, 6B3E79FF19D48C2F006071F7 /* Tests */, 6B3E79E719D48B7F006071F7 /* Products */, + D88EAF801ED4DFD3004FE2C3 /* Frameworks */, ); sourceTree = ""; }; @@ -132,6 +169,7 @@ 5C1360001C473BEF00AA3A01 /* Starscream.h */, 5C135FFF1C473BEF00AA3A01 /* SSLSecurity.swift */, 5C1360011C473BEF00AA3A01 /* WebSocket.swift */, + D88EAF7E1ED4DFB5004FE2C3 /* Compression.swift */, 6B3E79E919D48B7F006071F7 /* Supporting Files */, ); path = Starscream; @@ -151,10 +189,30 @@ children = ( 6B3E7A0019D48C2F006071F7 /* Info.plist */, 742419BB1DC6BDBA003ACE43 /* StarscreamTests.swift */, + D88EAF831ED4E7D8004FE2C3 /* CompressionTests.swift */, ); path = Tests; sourceTree = ""; }; + D85927D51ED761A0003460CB /* zlib */ = { + isa = PBXGroup; + children = ( + D85927D61ED761A0003460CB /* module.modulemap */, + D85927D71ED76F25003460CB /* include.h */, + ); + path = zlib; + sourceTree = ""; + }; + D88EAF801ED4DFD3004FE2C3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D88EAF901ED4E949004FE2C3 /* libz.tbd */, + D88EAF8D1ED4E92E004FE2C3 /* libz.tbd */, + D88EAF811ED4DFD3004FE2C3 /* libz.tbd */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -163,6 +221,7 @@ buildActionMask = 2147483647; files = ( 5C1360071C473BEF00AA3A01 /* Starscream.h in Headers */, + D85927DA1ED76F25003460CB /* include.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -171,6 +230,7 @@ buildActionMask = 2147483647; files = ( 5C1360051C473BEF00AA3A01 /* Starscream.h in Headers */, + D85927D81ED76F25003460CB /* include.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -179,6 +239,7 @@ buildActionMask = 2147483647; files = ( 5C1360061C473BEF00AA3A01 /* Starscream.h in Headers */, + D85927D91ED76F25003460CB /* include.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -399,6 +460,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF8A1ED4E88D004FE2C3 /* Compression.swift in Sources */, 5C13600A1C473BEF00AA3A01 /* WebSocket.swift in Sources */, 5C1360041C473BEF00AA3A01 /* SSLSecurity.swift in Sources */, ); @@ -408,6 +470,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF8B1ED4E88D004FE2C3 /* Compression.swift in Sources */, + D85927DE1ED773AD003460CB /* SSLSecurity.swift in Sources */, + D85927DB1ED7737C003460CB /* WebSocket.swift in Sources */, + D88EAF861ED4E7D8004FE2C3 /* CompressionTests.swift in Sources */, 742419BE1DC6BDBA003ACE43 /* StarscreamTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -416,6 +482,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF7F1ED4DFB5004FE2C3 /* Compression.swift in Sources */, 5C1360081C473BEF00AA3A01 /* WebSocket.swift in Sources */, 5C1360021C473BEF00AA3A01 /* SSLSecurity.swift in Sources */, ); @@ -425,6 +492,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF871ED4E88C004FE2C3 /* Compression.swift in Sources */, + D85927D11ED65963003460CB /* SSLSecurity.swift in Sources */, + D85927D01ED65933003460CB /* WebSocket.swift in Sources */, + D88EAF841ED4E7D8004FE2C3 /* CompressionTests.swift in Sources */, 742419BC1DC6BDBA003ACE43 /* StarscreamTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -433,6 +504,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF881ED4E88C004FE2C3 /* Compression.swift in Sources */, 5C1360091C473BEF00AA3A01 /* WebSocket.swift in Sources */, 5C1360031C473BEF00AA3A01 /* SSLSecurity.swift in Sources */, ); @@ -442,6 +514,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF891ED4E88D004FE2C3 /* Compression.swift in Sources */, + D85927DD1ED773AC003460CB /* SSLSecurity.swift in Sources */, + D85927DC1ED7737C003460CB /* WebSocket.swift in Sources */, + D88EAF851ED4E7D8004FE2C3 /* CompressionTests.swift in Sources */, 742419BD1DC6BDBA003ACE43 /* StarscreamTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -595,6 +671,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + SWIFT_INCLUDE_PATHS = $SRCROOT/zlib; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -638,6 +715,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_INCLUDE_PATHS = $SRCROOT/zlib; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; diff --git a/Tests/CompressionTests.swift b/Tests/CompressionTests.swift new file mode 100644 index 00000000..04a9a320 --- /dev/null +++ b/Tests/CompressionTests.swift @@ -0,0 +1,65 @@ +////////////////////////////////////////////////////////////////////////////////////////////////// +// +// CompressionTests.swift +// +// Created by Joseph Ross on 7/16/14. +// Copyright © 2017 Joseph Ross. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +////////////////////////////////////////////////////////////////////////////////////////////////// + +import XCTest + +class CompressionTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testBasic() { + let compressor = Compressor(windowBits: 15)! + let decompressor = Decompressor(windowBits: 15)! + + let rawData = "Hello, World! Hello, World! Hello, World! Hello, World! Hello, World!".data(using: .utf8)! + + let compressed = try! compressor.compress(rawData) + let uncompressed = try! decompressor.decompress(compressed, finish: true) + + XCTAssert(rawData == uncompressed) + } + + func testHugeData() { + let compressor = Compressor(windowBits: 15)! + let decompressor = Decompressor(windowBits: 15)! + + // 2 Gigs! +// var rawData = Data(repeating: 0, count: 0x80000000) + var rawData = Data(repeating: 0, count: 0x80000) + rawData.withUnsafeMutableBytes { (ptr: UnsafeMutablePointer) -> Void in + arc4random_buf(ptr, rawData.count) + } + + let compressed = try! compressor.compress(rawData) + let uncompressed = try! decompressor.decompress(compressed, finish: true) + + XCTAssert(rawData == uncompressed) + } + +} diff --git a/examples/AutobahnTest/Autobahn/ViewController.swift b/examples/AutobahnTest/Autobahn/ViewController.swift index 139c0229..24a6d79e 100644 --- a/examples/AutobahnTest/Autobahn/ViewController.swift +++ b/examples/AutobahnTest/Autobahn/ViewController.swift @@ -20,7 +20,7 @@ class ViewController: UIViewController { //getTestInfo(1) } - func removeSocket(_ s: WebSocket) { + func removeSocket(_ s: WebSocket?) { socketArray = socketArray.filter{$0 != s} } @@ -28,15 +28,15 @@ class ViewController: UIViewController { let s = WebSocket(url: URL(string: "ws://\(host)/getCaseCount")!, protocols: []) socketArray.append(s) - s.onText = {[unowned self] (text: String) in + s.onText = { [weak self] (text: String) in if let c = Int(text) { print("number of cases is: \(c)") - self.caseCount = c + self?.caseCount = c } } - s.onDisconnect = {[unowned self] (error: NSError?) in - self.getTestInfo(1) - self.removeSocket(s) + s.onDisconnect = { [weak self, weak s] (error: NSError?) in + self?.getTestInfo(1) + self?.removeSocket(s) } s.connect() } @@ -44,7 +44,7 @@ class ViewController: UIViewController { func getTestInfo(_ caseNum: Int) { let s = createSocket("getCaseInfo",caseNum) socketArray.append(s) - s.onText = {(text: String) in + s.onText = { (text: String) in // let data = text.dataUsingEncoding(NSUTF8StringEncoding) // do { // let resp: AnyObject? = try NSJSONSerialization.JSONObjectWithData(data!, @@ -62,12 +62,12 @@ class ViewController: UIViewController { } var once = false - s.onDisconnect = {[unowned self] (error: NSError?) in + s.onDisconnect = { [weak self, weak s] (error: NSError?) in if !once { once = true - self.runTest(caseNum) + self?.runTest(caseNum) } - self.removeSocket(s) + self?.removeSocket(s) } s.connect() } @@ -75,19 +75,19 @@ class ViewController: UIViewController { func runTest(_ caseNum: Int) { let s = createSocket("runCase",caseNum) self.socketArray.append(s) - s.onText = {(text: String) in - s.write(string: text) + s.onText = { [weak s] (text: String) in + s?.write(string: text) } - s.onData = {(data: Data) in - s.write(data: data) + s.onData = { [weak s] (data: Data) in + s?.write(data: data) } var once = false - s.onDisconnect = {[unowned self] (error: NSError?) in + s.onDisconnect = {[weak self, weak s] (error: NSError?) in if !once { once = true print("case:\(caseNum) finished") - self.verifyTest(caseNum) - self.removeSocket(s) + self?.verifyTest(caseNum) + self?.removeSocket(s) } } s.connect() @@ -96,7 +96,7 @@ class ViewController: UIViewController { func verifyTest(_ caseNum: Int) { let s = createSocket("getCaseStatus",caseNum) self.socketArray.append(s) - s.onText = {(text: String) in + s.onText = { (text: String) in let data = text.data(using: String.Encoding.utf8) do { let resp: Any? = try JSONSerialization.jsonObject(with: data!, @@ -115,17 +115,17 @@ class ViewController: UIViewController { } } var once = false - s.onDisconnect = {[unowned self] (error: NSError?) in + s.onDisconnect = { [weak self, weak s] (error: NSError?) in if !once { once = true let nextCase = caseNum+1 - if nextCase <= self.caseCount { - self.getTestInfo(nextCase) + if nextCase <= (self?.caseCount)! { + self?.getTestInfo(nextCase) } else { - self.finishReports() + self?.finishReports() } } - self.removeSocket(s) + self?.removeSocket(s) } s.connect() } @@ -133,9 +133,9 @@ class ViewController: UIViewController { func finishReports() { let s = createSocket("updateReports",0) self.socketArray.append(s) - s.onDisconnect = {[unowned self] (error: NSError?) in + s.onDisconnect = { [weak self, weak s] (error: NSError?) in print("finished all the tests!") - self.removeSocket(s) + self?.removeSocket(s) } s.connect() } diff --git a/examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcuserdata/dalton.xcuserdatad/UserInterfaceState.xcuserstate b/examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcuserdata/dalton.xcuserdatad/UserInterfaceState.xcuserstate index 94951322..6405dd2d 100644 Binary files a/examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcuserdata/dalton.xcuserdatad/UserInterfaceState.xcuserstate and b/examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcuserdata/dalton.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/zlib/include.h b/zlib/include.h new file mode 100644 index 00000000..cb3747f2 --- /dev/null +++ b/zlib/include.h @@ -0,0 +1,2 @@ +#include +#include diff --git a/zlib/module.modulemap b/zlib/module.modulemap new file mode 100644 index 00000000..1b2abee1 --- /dev/null +++ b/zlib/module.modulemap @@ -0,0 +1,9 @@ +module CZLib [system] { + header "include.h" + link "z" + export * +} +module CommonCrypto [system] { + header "include.h" + export * +}