From ccc9c4ea432f8eb2d5b7826322f1ff660785500b Mon Sep 17 00:00:00 2001 From: Joseph Ross Date: Wed, 24 May 2017 07:37:51 -0700 Subject: [PATCH 1/7] Add compression classes and unit tests. --- Source/Compression.swift | 199 +++++++++++++++++++++++++++ Starscream.xcodeproj/project.pbxproj | 46 +++++++ Tests/CompressionTests.swift | 52 +++++++ 3 files changed, 297 insertions(+) create mode 100644 Source/Compression.swift create mode 100644 Tests/CompressionTests.swift diff --git a/Source/Compression.swift b/Source/Compression.swift new file mode 100644 index 00000000..03b686a0 --- /dev/null +++ b/Source/Compression.swift @@ -0,0 +1,199 @@ +// +// Compression.swift +// Starscream +// +// Created by Joseph Ross on 5/23/17. +// Copyright © 2017 Vluxe. All rights reserved. +// + +import Foundation + +private let ZLIB_VERSION = Array("1.2.8".utf8CString) + +private let Z_OK:CInt = 0 +private let Z_BUF_ERROR:CInt = -5 + +private let Z_SYNC_FLUSH:CInt = 2 + +class Decompressor { + var strm = z_stream() + var buffer = [UInt8](repeating: 0, count: 0x2000) + var inflateInitialized = false + let windowBits:Int + + init?(windowBits:Int) { + self.windowBits = windowBits + guard initInflate() else { return nil } + } + + private func initInflate() -> Bool { + if Z_OK == inflateInit2(strm: &strm, windowBits: -CInt(windowBits), + version: ZLIB_VERSION, streamSize: CInt(MemoryLayout.size)) + { + inflateInitialized = true + return true + } + return false + } + + func reset() throws { + teardownInflate() + guard initInflate() else { throw NSError() } + } + + func decompress(_ data: Data) throws -> Data { + let data = data + let tail = Data([0x00, 0x00, 0xFF, 0xFF]) + + var decompressed = Data() + + try decompress(in: data, out: &decompressed) + try decompress(in: tail, out: &decompressed) + + return decompressed + + } + + private func decompress(in data: Data, out:inout Data) throws { + var res:CInt = 0 + data.withUnsafeBytes { (ptr:UnsafePointer) -> Void in + strm.next_in = ptr + strm.avail_in = CUnsignedInt(data.count) + + repeat { + strm.next_out = UnsafeMutablePointer(&buffer) + strm.avail_out = CUnsignedInt(buffer.count) + + res = inflate(strm: &strm, flush: 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()//"Error during inflate: \(res)") + } + } + + private func teardownInflate() { + if inflateInitialized, Z_OK == inflateEnd(strm: &strm) { + inflateInitialized = false + } + } + + deinit { + teardownInflate() + } + + @_silgen_name("inflateInit2_") func inflateInit2(strm: UnsafeMutableRawPointer, windowBits: CInt, + version: UnsafePointer, streamSize: CInt) -> CInt + @_silgen_name("inflate") func inflate(strm: UnsafeMutableRawPointer, flush: CInt) -> CInt + @discardableResult + @_silgen_name("inflateEnd") func inflateEnd(strm: UnsafeMutableRawPointer) -> CInt +} + +class Compressor { + var strm = z_stream() + var buffer = [UInt8](repeating: 0, count: 0x2000) + var deflateInitialized = false + let windowBits:Int + + init?(windowBits: Int) { + self.windowBits = windowBits + guard initDeflate() else { return nil } + } + + private func initDeflate() -> Bool { + if Z_OK == deflateInit2(strm: &strm, level: Z_DEFAULT_COMPRESSION, method: Z_DEFLATED, + windowBits: -CInt(windowBits), memLevel: 8, strategy: Z_DEFAULT_STRATEGY, + version: ZLIB_VERSION, streamSize: 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 = ptr + strm.avail_in = CUnsignedInt(data.count) + + repeat { + strm.next_out = UnsafeMutablePointer(&buffer) + strm.avail_out = CUnsignedInt(buffer.count) + + res = deflate(strm: &strm, flush: 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 { + NSLog("Error during deflate: \(res)") + throw NSError() + } + + compressed.removeLast(4) + return compressed + } + + private func teardownDeflate() { + if deflateInitialized, Z_OK == deflateEnd(strm: &strm) { + deflateInitialized = false + } + } + + deinit { + teardownDeflate() + } + + @_silgen_name("deflateInit2_") func deflateInit2(strm: UnsafeMutableRawPointer, level: CInt, method: CInt, + windowBits: CInt, memLevel: CInt, strategy: CInt, + version: UnsafePointer, streamSize: CInt) -> CInt + @_silgen_name("deflate") func deflate(strm: UnsafeMutableRawPointer, flush: CInt) -> CInt + @discardableResult + @_silgen_name("deflateEnd") func deflateEnd(strm: UnsafeMutableRawPointer) -> CInt + + private let Z_DEFAULT_COMPRESSION:CInt = -1 + private let Z_DEFLATED:CInt = 8 + private let Z_DEFAULT_STRATEGY:CInt = 0 +} + +struct z_stream { + var next_in: UnsafePointer? = nil /* next input byte */ + var avail_in: CUnsignedInt = 0 /* number of bytes available at next_in */ + var total_in: CUnsignedLong = 0 /* total number of input bytes read so far */ + + var next_out: UnsafeMutablePointer? = nil /* next output byte should be put there */ + var avail_out: CUnsignedInt = 0 /* remaining free space at next_out */ + var total_out: CUnsignedLong = 0 /* total number of bytes output so far */ + + var msg: UnsafePointer? = nil /* last error message, NULL if no error */ + private var state: OpaquePointer? = nil /* not visible by applications */ + + private var zalloc: OpaquePointer? = nil /* used to allocate the internal state */ + private var zfree: OpaquePointer? = nil /* used to free the internal state */ + private var opaque: OpaquePointer? = nil /* private data object passed to zalloc and zfree */ + + var data_type: CInt = 0 /* best guess about the data type: binary or text */ + var adler: CUnsignedLong = 0 /* adler32 value of the uncompressed data */ + private var reserved: CUnsignedLong = 0 /* reserved for future use */ +} + diff --git a/Starscream.xcodeproj/project.pbxproj b/Starscream.xcodeproj/project.pbxproj index 3734f5c3..ee76895f 100644 --- a/Starscream.xcodeproj/project.pbxproj +++ b/Starscream.xcodeproj/project.pbxproj @@ -20,6 +20,20 @@ 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 */; }; + 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 +66,11 @@ 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 = ""; }; + 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 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF911ED4E949004FE2C3 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +96,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF821ED4DFD3004FE2C3 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -83,6 +104,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF931ED4E975004FE2C3 /* libz.tbd in Frameworks */, + D88EAF921ED4E95D004FE2C3 /* Starscream.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -90,6 +113,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF8E1ED4E92E004FE2C3 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -110,6 +134,7 @@ 6B3E79E819D48B7F006071F7 /* Starscream */, 6B3E79FF19D48C2F006071F7 /* Tests */, 6B3E79E719D48B7F006071F7 /* Products */, + D88EAF801ED4DFD3004FE2C3 /* Frameworks */, ); sourceTree = ""; }; @@ -132,6 +157,7 @@ 5C1360001C473BEF00AA3A01 /* Starscream.h */, 5C135FFF1C473BEF00AA3A01 /* SSLSecurity.swift */, 5C1360011C473BEF00AA3A01 /* WebSocket.swift */, + D88EAF7E1ED4DFB5004FE2C3 /* Compression.swift */, 6B3E79E919D48B7F006071F7 /* Supporting Files */, ); path = Starscream; @@ -151,10 +177,21 @@ children = ( 6B3E7A0019D48C2F006071F7 /* Info.plist */, 742419BB1DC6BDBA003ACE43 /* StarscreamTests.swift */, + D88EAF831ED4E7D8004FE2C3 /* CompressionTests.swift */, ); path = Tests; sourceTree = ""; }; + D88EAF801ED4DFD3004FE2C3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D88EAF901ED4E949004FE2C3 /* libz.tbd */, + D88EAF8D1ED4E92E004FE2C3 /* libz.tbd */, + D88EAF811ED4DFD3004FE2C3 /* libz.tbd */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -399,6 +436,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF8A1ED4E88D004FE2C3 /* Compression.swift in Sources */, 5C13600A1C473BEF00AA3A01 /* WebSocket.swift in Sources */, 5C1360041C473BEF00AA3A01 /* SSLSecurity.swift in Sources */, ); @@ -408,6 +446,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF8B1ED4E88D004FE2C3 /* Compression.swift in Sources */, + D88EAF861ED4E7D8004FE2C3 /* CompressionTests.swift in Sources */, 742419BE1DC6BDBA003ACE43 /* StarscreamTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -416,6 +456,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF7F1ED4DFB5004FE2C3 /* Compression.swift in Sources */, 5C1360081C473BEF00AA3A01 /* WebSocket.swift in Sources */, 5C1360021C473BEF00AA3A01 /* SSLSecurity.swift in Sources */, ); @@ -425,6 +466,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF871ED4E88C004FE2C3 /* Compression.swift in Sources */, + D88EAF841ED4E7D8004FE2C3 /* CompressionTests.swift in Sources */, 742419BC1DC6BDBA003ACE43 /* StarscreamTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -433,6 +476,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF881ED4E88C004FE2C3 /* Compression.swift in Sources */, 5C1360091C473BEF00AA3A01 /* WebSocket.swift in Sources */, 5C1360031C473BEF00AA3A01 /* SSLSecurity.swift in Sources */, ); @@ -442,6 +486,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D88EAF891ED4E88D004FE2C3 /* Compression.swift in Sources */, + D88EAF851ED4E7D8004FE2C3 /* CompressionTests.swift in Sources */, 742419BD1DC6BDBA003ACE43 /* StarscreamTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/CompressionTests.swift b/Tests/CompressionTests.swift new file mode 100644 index 00000000..4bfdc833 --- /dev/null +++ b/Tests/CompressionTests.swift @@ -0,0 +1,52 @@ +// +// CompressionTests.swift +// Starscream +// +// Created by Joseph Ross on 5/23/17. +// Copyright © 2017 Vluxe. All rights reserved. +// + +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) + + 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) + + XCTAssert(rawData == uncompressed) + } + +} From ca85642c21332c20ddc6c0a0dd5c87b1e7460a0e Mon Sep 17 00:00:00 2001 From: Joseph Ross Date: Mon, 22 May 2017 17:18:19 -0700 Subject: [PATCH 2/7] Integrate compression classes. --- Source/Compression.swift | 7 ++--- Source/WebSocket.swift | 63 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/Source/Compression.swift b/Source/Compression.swift index 03b686a0..7c865117 100644 --- a/Source/Compression.swift +++ b/Source/Compression.swift @@ -41,14 +41,14 @@ class Decompressor { guard initInflate() else { throw NSError() } } - func decompress(_ data: Data) throws -> Data { + func decompress(_ data: Data, finish: Bool) throws -> Data { let data = data let tail = Data([0x00, 0x00, 0xFF, 0xFF]) var decompressed = Data() try decompress(in: data, out: &decompressed) - try decompress(in: tail, out: &decompressed) + if finish { try decompress(in: tail, out: &decompressed) } return decompressed @@ -68,8 +68,7 @@ class Decompressor { let byteCount = buffer.count - Int(strm.avail_out) out.append(buffer, count: byteCount) - } - while res == Z_OK && strm.avail_out == 0 + } while res == Z_OK && strm.avail_out == 0 } guard (res == Z_OK && strm.avail_out > 0) diff --git a/Source/WebSocket.swift b/Source/WebSocket.swift index 469da6be..0f7030cc 100644 --- a/Source/WebSocket.swift +++ b/Source/WebSocket.swift @@ -69,6 +69,7 @@ open class WebSocket : NSObject, StreamDelegate { enum InternalErrorCode: UInt16 { // 0-999 WebSocket status codes not used case outputStreamWriteError = 1 + case compressionError = 2 } // Where the callback is executed. It defaults to the main UI thread queue. @@ -86,6 +87,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,6 +95,7 @@ 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 @@ -128,6 +131,7 @@ open class WebSocket : NSObject, StreamDelegate { 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? @@ -145,6 +149,10 @@ open class WebSocket : NSObject, StreamDelegate { private var outputStream: OutputStream? private var connected = false private var isConnecting = false + private var supportsCompression = false + private var serverMaxWindowBits = 15 + private var decompressor:Decompressor? = nil + private var compressor:Compressor? = nil private var writeQueue = OperationQueue() private var readStack = [WSResponse]() private var inputQueue = [Data]() @@ -279,6 +287,10 @@ open class WebSocket : NSObject, StreamDelegate { 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) @@ -577,6 +589,23 @@ open class WebSocket : NSObject, StreamDelegate { } if let cfHeaders = CFHTTPMessageCopyAllHeaderFields(response) { let headers = cfHeaders.takeRetainedValue() as NSDictionary + if let extensionHeader = headers[headerWSExtensionName as NSString] as? NSString { + let parts = extensionHeader.components(separatedBy: ";") + for p in parts { + let part = p.trimmingCharacters(in: .whitespaces) + if part == "permessage-deflate" { + supportsCompression = true + } else if part.hasPrefix("server_max_window_bits="){ + let valString = part.components(separatedBy: "=")[1] + if let val = Int(valString.trimmingCharacters(in: .whitespaces)) { + decompressor = Decompressor(windowBits: serverMaxWindowBits) + compressor = Compressor(windowBits: serverMaxWindowBits) + serverMaxWindowBits = val + } + } + } + } + if let acceptKey = headers[headerWSAcceptName as NSString] as? NSString { if acceptKey.length > 0 { return 0 @@ -621,6 +650,8 @@ open class WebSocket : NSObject, StreamDelegate { } } + + var messageNeedsDecompression = false /** Process one message at the start of `buffer`. Return another buffer (sharing storage) that contains the leftover contents of `buffer` that I didn't process. */ @@ -650,7 +681,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 supportsCompression && receivedOpcode != .continueFrame { + messageNeedsDecompression = (RSV1Mask & baseAddress[0]) > 0 + } + if (isMasked > 0 || (RSVMask & baseAddress[0]) > 0) && receivedOpcode != .pong && !messageNeedsDecompression { let errCode = CloseCode.protocolError.rawValue doDisconnect(errorWithDetail("masked and rsv data is not currently supported", code: errCode)) writeError(errCode) @@ -710,7 +744,20 @@ open class WebSocket : NSObject, StreamDelegate { offset += size len -= UInt64(size) } - let data = Data(bytes: baseAddress+offset, count: Int(len)) + let data: Data + if messageNeedsDecompression, let decompressor = decompressor { + do { + data = try decompressor.decompress(Data(bytes: baseAddress+offset, count: Int(len)), finish: isFin > 0) + } 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" @@ -864,10 +911,20 @@ 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.compressor { + do { + data = try compressor.compress(data) + 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) { From 4ba077ee4baa972a146b96fcd305c052f42e7df3 Mon Sep 17 00:00:00 2001 From: Joseph Ross Date: Wed, 24 May 2017 13:06:37 -0700 Subject: [PATCH 3/7] Fixes to handle different windowsBits and noContextTakeover. --- Source/Compression.swift | 30 +++++++++++------------ Source/WebSocket.swift | 53 ++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/Source/Compression.swift b/Source/Compression.swift index 7c865117..fc615222 100644 --- a/Source/Compression.swift +++ b/Source/Compression.swift @@ -16,10 +16,10 @@ private let Z_BUF_ERROR:CInt = -5 private let Z_SYNC_FLUSH:CInt = 2 class Decompressor { - var strm = z_stream() - var buffer = [UInt8](repeating: 0, count: 0x2000) - var inflateInitialized = false - let windowBits:Int + 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 @@ -88,18 +88,18 @@ class Decompressor { teardownInflate() } - @_silgen_name("inflateInit2_") func inflateInit2(strm: UnsafeMutableRawPointer, windowBits: CInt, + @_silgen_name("inflateInit2_") private func inflateInit2(strm: UnsafeMutableRawPointer, windowBits: CInt, version: UnsafePointer, streamSize: CInt) -> CInt - @_silgen_name("inflate") func inflate(strm: UnsafeMutableRawPointer, flush: CInt) -> CInt + @_silgen_name("inflate") private func inflate(strm: UnsafeMutableRawPointer, flush: CInt) -> CInt @discardableResult - @_silgen_name("inflateEnd") func inflateEnd(strm: UnsafeMutableRawPointer) -> CInt + @_silgen_name("inflateEnd") private func inflateEnd(strm: UnsafeMutableRawPointer) -> CInt } class Compressor { - var strm = z_stream() - var buffer = [UInt8](repeating: 0, count: 0x2000) - var deflateInitialized = false - let windowBits:Int + 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 @@ -163,19 +163,19 @@ class Compressor { teardownDeflate() } - @_silgen_name("deflateInit2_") func deflateInit2(strm: UnsafeMutableRawPointer, level: CInt, method: CInt, + @_silgen_name("deflateInit2_") private func deflateInit2(strm: UnsafeMutableRawPointer, level: CInt, method: CInt, windowBits: CInt, memLevel: CInt, strategy: CInt, version: UnsafePointer, streamSize: CInt) -> CInt - @_silgen_name("deflate") func deflate(strm: UnsafeMutableRawPointer, flush: CInt) -> CInt + @_silgen_name("deflate") private func deflate(strm: UnsafeMutableRawPointer, flush: CInt) -> CInt @discardableResult - @_silgen_name("deflateEnd") func deflateEnd(strm: UnsafeMutableRawPointer) -> CInt + @_silgen_name("deflateEnd") private func deflateEnd(strm: UnsafeMutableRawPointer) -> CInt private let Z_DEFAULT_COMPRESSION:CInt = -1 private let Z_DEFLATED:CInt = 8 private let Z_DEFAULT_STRATEGY:CInt = 0 } -struct z_stream { +private struct z_stream { var next_in: UnsafePointer? = nil /* next input byte */ var avail_in: CUnsignedInt = 0 /* number of bytes available at next_in */ var total_in: CUnsignedLong = 0 /* total number of input bytes read so far */ diff --git a/Source/WebSocket.swift b/Source/WebSocket.swift index 0f7030cc..8c49059a 100644 --- a/Source/WebSocket.swift +++ b/Source/WebSocket.swift @@ -143,16 +143,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 supportsCompression = false - private var serverMaxWindowBits = 15 - private var decompressor:Decompressor? = nil - private var compressor:Compressor? = nil + private var compressionState = CompressionState() private var writeQueue = OperationQueue() private var readStack = [WSResponse]() private var inputQueue = [Data]() @@ -594,16 +602,27 @@ open class WebSocket : NSObject, StreamDelegate { for p in parts { let part = p.trimmingCharacters(in: .whitespaces) if part == "permessage-deflate" { - supportsCompression = true + 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)) { - decompressor = Decompressor(windowBits: serverMaxWindowBits) - compressor = Compressor(windowBits: serverMaxWindowBits) - serverMaxWindowBits = val + 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) + } } if let acceptKey = headers[headerWSAcceptName as NSString] as? NSString { @@ -650,8 +669,6 @@ open class WebSocket : NSObject, StreamDelegate { } } - - var messageNeedsDecompression = false /** Process one message at the start of `buffer`. Return another buffer (sharing storage) that contains the leftover contents of `buffer` that I didn't process. */ @@ -681,10 +698,10 @@ open class WebSocket : NSObject, StreamDelegate { let isMasked = (MaskMask & baseAddress[1]) let payloadLen = (PayloadLenMask & baseAddress[1]) var offset = 2 - if supportsCompression && receivedOpcode != .continueFrame { - messageNeedsDecompression = (RSV1Mask & baseAddress[0]) > 0 + if compressionState.supportsCompression && receivedOpcode != .continueFrame { + compressionState.messageNeedsDecompression = (RSV1Mask & baseAddress[0]) > 0 } - if (isMasked > 0 || (RSVMask & baseAddress[0]) > 0) && receivedOpcode != .pong && !messageNeedsDecompression { + 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) @@ -745,9 +762,12 @@ open class WebSocket : NSObject, StreamDelegate { len -= UInt64(size) } let data: Data - if messageNeedsDecompression, let decompressor = decompressor { + if compressionState.messageNeedsDecompression, let decompressor = compressionState.decompressor { do { data = try decompressor.decompress(Data(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 @@ -913,9 +933,12 @@ open class WebSocket : NSObject, StreamDelegate { var offset = 2 var firstByte:UInt8 = s.FinMask | code.rawValue var data = data - if [.textFrame, .binaryFrame].contains(code), let compressor = s.compressor { + 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. From 19793796bbaf8d42a1972d2890b1e7d731452ecd Mon Sep 17 00:00:00 2001 From: Joseph Ross Date: Wed, 24 May 2017 13:39:06 -0700 Subject: [PATCH 4/7] Fix capture memory leaks in Autobahn tester. --- .../Autobahn/ViewController.swift | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) 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() } From 45546818fbe4ec5580dc3c0955c01bf280c11506 Mon Sep 17 00:00:00 2001 From: Joseph Ross Date: Wed, 24 May 2017 17:17:12 -0700 Subject: [PATCH 5/7] Avoid unnecessary copying. Confirm Autobahn fuzzing test results are comparable with the autobahn library itself. --- Source/Compression.swift | 47 +++++++++++++++------------- Source/WebSocket.swift | 2 +- Starscream.xcodeproj/project.pbxproj | 4 +++ Tests/CompressionTests.swift | 4 +-- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/Source/Compression.swift b/Source/Compression.swift index fc615222..a19052a4 100644 --- a/Source/Compression.swift +++ b/Source/Compression.swift @@ -42,39 +42,43 @@ class Decompressor { } func decompress(_ data: Data, finish: Bool) throws -> Data { - let data = data - let tail = Data([0x00, 0x00, 0xFF, 0xFF]) - + 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) - try decompress(in: data, out: &decompressed) - if finish { try decompress(in: tail, 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(in data: Data, out:inout Data) throws { + private func decompress(bytes: UnsafePointer, count: Int, out:inout Data) throws { var res:CInt = 0 - data.withUnsafeBytes { (ptr:UnsafePointer) -> Void in - strm.next_in = ptr - strm.avail_in = CUnsignedInt(data.count) + strm.next_in = bytes + strm.avail_in = CUnsignedInt(count) + + repeat { + strm.next_out = UnsafeMutablePointer(&buffer) + strm.avail_out = CUnsignedInt(buffer.count) - repeat { - strm.next_out = UnsafeMutablePointer(&buffer) - strm.avail_out = CUnsignedInt(buffer.count) - - res = inflate(strm: &strm, flush: 0) - - let byteCount = buffer.count - Int(strm.avail_out) - out.append(buffer, count: byteCount) - } while res == Z_OK && strm.avail_out == 0 + res = inflate(strm: &strm, flush: 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()//"Error during inflate: \(res)") + throw NSError(domain: WebSocket.ErrorDomain, code: Int(WebSocket.InternalErrorCode.compressionError.rawValue), userInfo: nil) } } @@ -145,8 +149,7 @@ class Compressor { guard res == Z_OK && strm.avail_out > 0 || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) else { - NSLog("Error during deflate: \(res)") - throw NSError() + throw NSError(domain: WebSocket.ErrorDomain, code: Int(WebSocket.InternalErrorCode.compressionError.rawValue), userInfo: nil) } compressed.removeLast(4) diff --git a/Source/WebSocket.swift b/Source/WebSocket.swift index 8c49059a..1df185bd 100644 --- a/Source/WebSocket.swift +++ b/Source/WebSocket.swift @@ -764,7 +764,7 @@ open class WebSocket : NSObject, StreamDelegate { let data: Data if compressionState.messageNeedsDecompression, let decompressor = compressionState.decompressor { do { - data = try decompressor.decompress(Data(bytes: baseAddress+offset, count: Int(len)), finish: isFin > 0) + data = try decompressor.decompress(bytes: baseAddress+offset, count: Int(len), finish: isFin > 0) if isFin > 0 && compressionState.serverNoContextTakeover{ try decompressor.reset() } diff --git a/Starscream.xcodeproj/project.pbxproj b/Starscream.xcodeproj/project.pbxproj index ee76895f..ac7e1a8f 100644 --- a/Starscream.xcodeproj/project.pbxproj +++ b/Starscream.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ 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 */; }; 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 */; }; @@ -467,6 +469,8 @@ 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 */, ); diff --git a/Tests/CompressionTests.swift b/Tests/CompressionTests.swift index 4bfdc833..68712009 100644 --- a/Tests/CompressionTests.swift +++ b/Tests/CompressionTests.swift @@ -27,7 +27,7 @@ class CompressionTests: XCTestCase { 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) + let uncompressed = try! decompressor.decompress(compressed, finish: true) XCTAssert(rawData == uncompressed) } @@ -44,7 +44,7 @@ class CompressionTests: XCTestCase { } let compressed = try! compressor.compress(rawData) - let uncompressed = try! decompressor.decompress(compressed) + let uncompressed = try! decompressor.decompress(compressed, finish: true) XCTAssert(rawData == uncompressed) } From 35fc161193333e7d52a4220f5b4428b669b7d54f Mon Sep 17 00:00:00 2001 From: Joseph Ross Date: Thu, 25 May 2017 10:24:51 -0700 Subject: [PATCH 6/7] Add information about compression support to README. Other changes based on PR feedback. --- README.md | 12 ++++++++ Source/Compression.swift | 26 ++++++++++++++-- Source/WebSocket.swift | 59 ++++++++++++++++++++---------------- Tests/CompressionTests.swift | 19 ++++++++++-- 4 files changed, 84 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 7120f775..59b2a769 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ It's Objective-C counter part can be found here: [Jetfire](https://github.com/ac - 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 @@ -197,6 +198,17 @@ 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. +### 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. diff --git a/Source/Compression.swift b/Source/Compression.swift index a19052a4..420ef610 100644 --- a/Source/Compression.swift +++ b/Source/Compression.swift @@ -1,10 +1,30 @@ +////////////////////////////////////////////////////////////////////////////////////////////////// // // Compression.swift -// Starscream // -// Created by Joseph Ross on 5/23/17. -// Copyright © 2017 Vluxe. All rights reserved. +// 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 diff --git a/Source/WebSocket.swift b/Source/WebSocket.swift index 1df185bd..6b054917 100644 --- a/Source/WebSocket.swift +++ b/Source/WebSocket.swift @@ -597,32 +597,8 @@ open class WebSocket : NSObject, StreamDelegate { } if let cfHeaders = CFHTTPMessageCopyAllHeaderFields(response) { let headers = cfHeaders.takeRetainedValue() as NSDictionary - if let extensionHeader = headers[headerWSExtensionName as NSString] as? NSString { - 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) - } + if let extensionHeader = headers[headerWSExtensionName as NSString] as? String { + processExtensionHeader(extensionHeader) } if let acceptKey = headers[headerWSAcceptName as NSString] as? NSString { @@ -634,6 +610,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 */ diff --git a/Tests/CompressionTests.swift b/Tests/CompressionTests.swift index 68712009..04a9a320 100644 --- a/Tests/CompressionTests.swift +++ b/Tests/CompressionTests.swift @@ -1,10 +1,23 @@ +////////////////////////////////////////////////////////////////////////////////////////////////// // // CompressionTests.swift -// Starscream // -// Created by Joseph Ross on 5/23/17. -// Copyright © 2017 Vluxe. All rights reserved. +// 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 From 44fdfa3791130a91b8b6df36b0b7103f00bdfba5 Mon Sep 17 00:00:00 2001 From: Joseph Ross Date: Thu, 25 May 2017 13:14:22 -0700 Subject: [PATCH 7/7] Include zlib as a module, rather than using `@_silgen_name` and redefinition. This is less verbose and less error-prone. --- Source/Compression.swift | 68 +++++----------------------- Starscream.xcodeproj/project.pbxproj | 28 ++++++++++++ zlib/include.h | 2 + zlib/module.modulemap | 4 ++ 4 files changed, 46 insertions(+), 56 deletions(-) create mode 100644 zlib/include.h create mode 100644 zlib/module.modulemap diff --git a/Source/Compression.swift b/Source/Compression.swift index 420ef610..24f123cd 100644 --- a/Source/Compression.swift +++ b/Source/Compression.swift @@ -27,13 +27,7 @@ ////////////////////////////////////////////////////////////////////////////////////////////////// import Foundation - -private let ZLIB_VERSION = Array("1.2.8".utf8CString) - -private let Z_OK:CInt = 0 -private let Z_BUF_ERROR:CInt = -5 - -private let Z_SYNC_FLUSH:CInt = 2 +import zlib class Decompressor { private var strm = z_stream() @@ -47,8 +41,8 @@ class Decompressor { } private func initInflate() -> Bool { - if Z_OK == inflateInit2(strm: &strm, windowBits: -CInt(windowBits), - version: ZLIB_VERSION, streamSize: CInt(MemoryLayout.size)) + if Z_OK == inflateInit2_(&strm, -CInt(windowBits), + ZLIB_VERSION, CInt(MemoryLayout.size)) { inflateInitialized = true return true @@ -82,14 +76,14 @@ class Decompressor { private func decompress(bytes: UnsafePointer, count: Int, out:inout Data) throws { var res:CInt = 0 - strm.next_in = bytes + 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: &strm, flush: 0) + res = inflate(&strm, 0) let byteCount = buffer.count - Int(strm.avail_out) out.append(buffer, count: byteCount) @@ -103,7 +97,7 @@ class Decompressor { } private func teardownInflate() { - if inflateInitialized, Z_OK == inflateEnd(strm: &strm) { + if inflateInitialized, Z_OK == inflateEnd(&strm) { inflateInitialized = false } } @@ -111,12 +105,6 @@ class Decompressor { deinit { teardownInflate() } - - @_silgen_name("inflateInit2_") private func inflateInit2(strm: UnsafeMutableRawPointer, windowBits: CInt, - version: UnsafePointer, streamSize: CInt) -> CInt - @_silgen_name("inflate") private func inflate(strm: UnsafeMutableRawPointer, flush: CInt) -> CInt - @discardableResult - @_silgen_name("inflateEnd") private func inflateEnd(strm: UnsafeMutableRawPointer) -> CInt } class Compressor { @@ -131,9 +119,9 @@ class Compressor { } private func initDeflate() -> Bool { - if Z_OK == deflateInit2(strm: &strm, level: Z_DEFAULT_COMPRESSION, method: Z_DEFLATED, - windowBits: -CInt(windowBits), memLevel: 8, strategy: Z_DEFAULT_STRATEGY, - version: ZLIB_VERSION, streamSize: CInt(MemoryLayout.size)) + 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 @@ -150,14 +138,14 @@ class Compressor { var compressed = Data() var res:CInt = 0 data.withUnsafeBytes { (ptr:UnsafePointer) -> Void in - strm.next_in = ptr + 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: &strm, flush: Z_SYNC_FLUSH) + res = deflate(&strm, Z_SYNC_FLUSH) let byteCount = buffer.count - Int(strm.avail_out) compressed.append(buffer, count: byteCount) @@ -177,7 +165,7 @@ class Compressor { } private func teardownDeflate() { - if deflateInitialized, Z_OK == deflateEnd(strm: &strm) { + if deflateInitialized, Z_OK == deflateEnd(&strm) { deflateInitialized = false } } @@ -185,37 +173,5 @@ class Compressor { deinit { teardownDeflate() } - - @_silgen_name("deflateInit2_") private func deflateInit2(strm: UnsafeMutableRawPointer, level: CInt, method: CInt, - windowBits: CInt, memLevel: CInt, strategy: CInt, - version: UnsafePointer, streamSize: CInt) -> CInt - @_silgen_name("deflate") private func deflate(strm: UnsafeMutableRawPointer, flush: CInt) -> CInt - @discardableResult - @_silgen_name("deflateEnd") private func deflateEnd(strm: UnsafeMutableRawPointer) -> CInt - - private let Z_DEFAULT_COMPRESSION:CInt = -1 - private let Z_DEFLATED:CInt = 8 - private let Z_DEFAULT_STRATEGY:CInt = 0 -} - -private struct z_stream { - var next_in: UnsafePointer? = nil /* next input byte */ - var avail_in: CUnsignedInt = 0 /* number of bytes available at next_in */ - var total_in: CUnsignedLong = 0 /* total number of input bytes read so far */ - - var next_out: UnsafeMutablePointer? = nil /* next output byte should be put there */ - var avail_out: CUnsignedInt = 0 /* remaining free space at next_out */ - var total_out: CUnsignedLong = 0 /* total number of bytes output so far */ - - var msg: UnsafePointer? = nil /* last error message, NULL if no error */ - private var state: OpaquePointer? = nil /* not visible by applications */ - - private var zalloc: OpaquePointer? = nil /* used to allocate the internal state */ - private var zfree: OpaquePointer? = nil /* used to free the internal state */ - private var opaque: OpaquePointer? = nil /* private data object passed to zalloc and zfree */ - - var data_type: CInt = 0 /* best guess about the data type: binary or text */ - var adler: CUnsignedLong = 0 /* adler32 value of the uncompressed data */ - private var reserved: CUnsignedLong = 0 /* reserved for future use */ } diff --git a/Starscream.xcodeproj/project.pbxproj b/Starscream.xcodeproj/project.pbxproj index ac7e1a8f..7c6144a4 100644 --- a/Starscream.xcodeproj/project.pbxproj +++ b/Starscream.xcodeproj/project.pbxproj @@ -22,6 +22,13 @@ 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 */; }; @@ -68,6 +75,8 @@ 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 = ""; }; @@ -133,6 +142,7 @@ 6B3E79DC19D48B7F006071F7 = { isa = PBXGroup; children = ( + D85927D51ED761A0003460CB /* zlib */, 6B3E79E819D48B7F006071F7 /* Starscream */, 6B3E79FF19D48C2F006071F7 /* Tests */, 6B3E79E719D48B7F006071F7 /* Products */, @@ -184,6 +194,15 @@ path = Tests; sourceTree = ""; }; + D85927D51ED761A0003460CB /* zlib */ = { + isa = PBXGroup; + children = ( + D85927D61ED761A0003460CB /* module.modulemap */, + D85927D71ED76F25003460CB /* include.h */, + ); + path = zlib; + sourceTree = ""; + }; D88EAF801ED4DFD3004FE2C3 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -202,6 +221,7 @@ buildActionMask = 2147483647; files = ( 5C1360071C473BEF00AA3A01 /* Starscream.h in Headers */, + D85927DA1ED76F25003460CB /* include.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -210,6 +230,7 @@ buildActionMask = 2147483647; files = ( 5C1360051C473BEF00AA3A01 /* Starscream.h in Headers */, + D85927D81ED76F25003460CB /* include.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -218,6 +239,7 @@ buildActionMask = 2147483647; files = ( 5C1360061C473BEF00AA3A01 /* Starscream.h in Headers */, + D85927D91ED76F25003460CB /* include.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -449,6 +471,8 @@ 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 */, ); @@ -491,6 +515,8 @@ 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 */, ); @@ -645,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"; @@ -688,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/zlib/include.h b/zlib/include.h new file mode 100644 index 00000000..bb92a98f --- /dev/null +++ b/zlib/include.h @@ -0,0 +1,2 @@ + +#include diff --git a/zlib/module.modulemap b/zlib/module.modulemap new file mode 100644 index 00000000..0fdb2181 --- /dev/null +++ b/zlib/module.modulemap @@ -0,0 +1,4 @@ +module zlib [system] { + header "include.h" + link "z" +}