├── .gitignore ├── DNWebSocket.podspec ├── DNWebSocket.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── DNWebSocket-iOS.xcscheme │ ├── DNWebSocket-macOS.xcscheme │ ├── DNWebSocket-tvOS.xcscheme │ └── DNWebSocket-watchOS.xcscheme ├── Example ├── main.swift └── test.plist ├── LICENSE ├── README.md ├── Sources ├── CZLib │ ├── module.modulemap │ └── shim.h ├── DNWebSocket.h ├── Extensions │ ├── Array+Chopped.swift │ ├── Data+Buffer.swift │ ├── Data+Compression.swift │ ├── Data+Mask.swift │ ├── Data+SHA1.swift │ ├── Date+Format.swift │ ├── OperationQueue+Setup.swift │ ├── Optional+Extensions.swift │ ├── RunLoop+Timeout.swift │ ├── Stream+SSLSettings.swift │ ├── Stream+SSLTrust.swift │ ├── String+Handshaking.swift │ ├── URL+Extensions.swift │ ├── URLRequest+Handshake.swift │ ├── URLRequest+WebSocket.swift │ └── UnsafePointer+Extensions.swift ├── Info-iOS.plist ├── Info-macOS.plist ├── Info-tvOS.plist ├── Info-watchOS.plist ├── Models │ ├── Registry.swift │ ├── Result.swift │ └── WebSocketModels.swift ├── Protocols │ ├── SSLContextRetrievable.swift │ └── SizeRetrievable.swift ├── Security │ ├── SSLCertificate.swift │ ├── SSLSettings.swift │ └── SSLValidator.swift ├── WebSocket │ ├── Frame.swift │ ├── Handshake.swift │ ├── IOStream.swift │ ├── StreamBuffer.swift │ └── WebSocket.swift └── Сompression │ ├── CompressionObject.swift │ ├── CompressionSettings.swift │ ├── CompressionStatus.swift │ ├── Deflater.swift │ └── Inflater.swift └── Tests-macOS ├── AutobahnTests ├── AutobahnTestAction.swift ├── AutobahnTests.swift └── Configuration.swift ├── Info.plist └── TestCommon.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /DNWebSocket.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'DNWebSocket' 3 | s.version = '1.1.0' 4 | s.summary = 'Pure Swift WebSocket Library' 5 | s.description = <<-DESC 6 | Object-Oriented, Swift-style WebSocket Library (RFC 6455) for Swift-compatible Platforms. 7 | Conforms to all necessary Autobahn fuzzing tests. 8 | DESC 9 | 10 | s.homepage = 'https://github.com/GlebRadchenko/DNWebSocket' 11 | s.license = { :type => 'MIT', :file => 'LICENSE' } 12 | s.author = { 'Gleb Radchenko' => 'gleb.radchenko3@gmail.com' } 13 | s.source = { :git => 'https://github.com/GlebRadchenko/DNWebSocket.git', :tag => s.version.to_s } 14 | 15 | s.swift_version = '4.2' 16 | 17 | s.ios.deployment_target = '8.0' 18 | s.osx.deployment_target = '10.10' 19 | s.tvos.deployment_target = '9.0' 20 | s.watchos.deployment_target = '2.0' 21 | 22 | s.source_files = 'Sources/**/*.swift' 23 | s.pod_target_xcconfig = {'SWIFT_INCLUDE_PATHS' => '$(PODS_TARGET_SRCROOT)/Sources/CZLib/**'} 24 | s.preserve_path = 'Sources/CZLib/**' 25 | 26 | end 27 | -------------------------------------------------------------------------------- /DNWebSocket.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DNWebSocket.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DNWebSocket.xcodeproj/xcshareddata/xcschemes/DNWebSocket-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /DNWebSocket.xcodeproj/xcshareddata/xcschemes/DNWebSocket-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /DNWebSocket.xcodeproj/xcshareddata/xcschemes/DNWebSocket-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /DNWebSocket.xcodeproj/xcshareddata/xcschemes/DNWebSocket-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Example/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // Example 4 | // 5 | // Created by Gleb Radchenko on 2/2/18. 6 | // 7 | 8 | import Foundation 9 | 10 | let str = "wss://echo.websocket.org:443" 11 | let url = URL(string: str)! 12 | 13 | let ws = WebSocket(url: url) 14 | ws.settings.debugMode = true 15 | 16 | ws.onDebugInfo = { info in print(info) } 17 | ws.onConnect = { 18 | ws.send(string: "Hello, world!", chopSize: 1) 19 | } 20 | 21 | ws.onEvent = { (event) in 22 | print(event) 23 | } 24 | 25 | ws.connect() 26 | 27 | RunLoop.main.run() 28 | -------------------------------------------------------------------------------- /Example/test.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleIdentifier 11 | $(PRODUCT_BUNDLE_IDENTIFIER) 12 | CFBundleShortVersionString 13 | 1 14 | CFBundleVersion 15 | 1 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gleb Radchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DNWebSocket 2 | 3 |

4 | 5 | Swift 4.2 6 | 7 | 8 | MIT 9 | 10 |

11 | 12 | Object-Oriented, Swift-style WebSocket Library ([RFC 6455](https://tools.ietf.org/html/rfc6455>)) for Swift-compatible Platforms. 13 | 14 | - [Tests](#tests) 15 | - [Installation](#installation) 16 | - [Requirements](#requirements) 17 | - [Usage](#usage) 18 | 19 | 20 | ## Tests 21 | 22 | Conforms to all necessary Autobahn fuzzing tests. [Autobahn](https://github.com/crossbario/autobahn-testsuite) 23 | 24 | Test results for DNWebSocket you can see [here](https://glebradchenko.github.io/dnwebsocket.github.io/). 25 | 26 | In comparison with [SocketRocket](http://facebook.github.io/SocketRocket/results/), this library shows 2-10 times better performance in many Limits/Performance tests. 27 | 28 | Cases 6.4.1, 6.4.2, 6.4.3, 6.4.4 received result Non-Strict due to perfomance improvements(it's complicated to validate each fragmented text message) 29 | 30 | ## Installation 31 | 32 | ### Cocoapods 33 | 34 | To install DNWebSocket via [CocoaPods](http://cocoapods.org), get it: 35 | 36 | ```bash 37 | $ gem install cocoapods 38 | ``` 39 | 40 | Then, create a `Podfile` in your project root directory: 41 | 42 | ```ruby 43 | source 'https://github.com/CocoaPods/Specs.git' 44 | platform :ios, '8.0' 45 | use_frameworks! 46 | 47 | target '' do 48 | pod 'DNWebSocket', '~> 1.1.0' 49 | end 50 | ``` 51 | 52 | And run: 53 | 54 | ```bash 55 | $ pod install 56 | ``` 57 | For swift version < 4.2 use 1.0.2 version of pod. 58 | 59 | ### Swift Package Manager 60 | 61 | Currently, I'm looking for a generic approach which will allow to use C libraries with all Package Managers. 62 | So right now, please, use [DNWebSocket-SPM](https://github.com/GlebRadchenko/DNWebSocket-SPM) repo. 63 | 64 | ## Requirements 65 | 66 | - iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ 67 | - Swift 4.0 + (but I didn't try earlier versions by the way :D) 68 | 69 | ## Usage 70 | 71 | Import library as follows: 72 | 73 | ``` swift 74 | import DNWebSocket 75 | ``` 76 | 77 | Now create websocket, configure it and connect: 78 | 79 | ``` swift 80 | let websocket = WebSocket(url: URL(string: "wss://echo.websocket.org:80")!, 81 | timeout: 10, 82 | protocols: ["chat", "superchat"]) 83 | 84 | websocket.onConnect = { 85 | print("connected") 86 | websocket.sendPing(data: Data()) 87 | } 88 | 89 | websocket.onData = { (data) in 90 | websocket.send(data: data) 91 | } 92 | 93 | websocket.onText = { (text) in 94 | websocket.send(string: text) 95 | } 96 | 97 | websocket.onPing = { (data) in 98 | websocket.sendPong(data: data) 99 | } 100 | 101 | websocket.onPong = { (data) in 102 | print("Received pong from server") 103 | } 104 | 105 | websocket.onDebugInfo = { (debugInfo) in 106 | print(debugInfo) 107 | } 108 | 109 | websocket.onDisconnect = { (error, closeCode) in 110 | print("disconnected: \(closeCode)") 111 | } 112 | 113 | websocket.connect() 114 | ``` 115 | 116 | You can create custom connection by accessing .settings and .securitySettings properties: 117 | 118 | ``` swift 119 | 120 | websocket.settings.timeout = 5 // sec 121 | websocket.settings.debugMode = true // will trigger .onDebugInfo callback and send .debug(String) event 122 | websocket.settings.useCompression = true // false by default 123 | websocket.settings.maskOutputData = true // true by default 124 | websocket.settings.respondPingRequestsAutomatically = true // true by default 125 | websocket.settings.callbackQueue = .main 126 | 127 | websocket.securitySettings.useSSL = false // true by default 128 | websocket.securitySettings.overrideTrustHostname = true // false by default 129 | websocket.securitySettings.trustHostname = /*your hostname*/ 130 | websocket.securitySettings.certificateValidationEnabled = true 131 | websocket.securitySettings.cipherSuites = [] 132 | 133 | ``` 134 | -------------------------------------------------------------------------------- /Sources/CZLib/module.modulemap: -------------------------------------------------------------------------------- 1 | module CZLib [system] { 2 | header "shim.h" 3 | link "z" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Sources/CZLib/shim.h: -------------------------------------------------------------------------------- 1 | #ifndef zlib_shim_h 2 | #define zlib_shim_h 3 | 4 | #import 5 | #import 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /Sources/DNWebSocket.h: -------------------------------------------------------------------------------- 1 | // 2 | // DNWebSocket.h 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/2/18. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for DNWebSocket. 11 | FOUNDATION_EXPORT double DNWebSocket_VersionNumber; 12 | 13 | //! Project version string for DNWebSocket. 14 | FOUNDATION_EXPORT const unsigned char DNWebSocket_VersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /Sources/Extensions/Array+Chopped.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Chopped.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 3/27/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | func chopped(by chopSize: Int) -> [Data] { 12 | guard chopSize < count else { 13 | return [self] 14 | } 15 | 16 | var chunks: [Data] = [] 17 | var start = 0 18 | 19 | while start < count { 20 | let end = Swift.min(start.advanced(by: chopSize), count) 21 | let chunk = Data(self[start.. [[Element]] { 32 | return stride(from: 0, to: count, by: chopSize).map { (start) -> [Element] in 33 | let end = Swift.min(start.advanced(by: chopSize), count) 34 | return Array(self[start.. Data { 16 | return Data(count: bufferSize) 17 | } 18 | 19 | func unsafeBuffer() -> UnsafeBufferPointer { 20 | return withUnsafeBytes { (pointer) in 21 | UnsafeBufferPointer(start: pointer, count: count) 22 | } 23 | } 24 | 25 | mutating func unsafeMutableBuffer() -> UnsafeMutableBufferPointer { 26 | let count = self.count 27 | return withUnsafeMutableBytes { (pointer) in 28 | UnsafeMutableBufferPointer(start: pointer, count: count) 29 | } 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Sources/Extensions/Data+Compression.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Compression.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/5/18. 6 | // 7 | 8 | import Foundation 9 | import CZLib 10 | 11 | extension Data { 12 | private static var tail: [UInt8] { 13 | return [0x00, 0x00, 0xFF, 0xFF] 14 | } 15 | 16 | mutating public func addTail() { 17 | append(contentsOf: Data.tail) 18 | } 19 | 20 | mutating public func removeTail() { 21 | guard count > 3 else { return } 22 | removeLast(4) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Extensions/Data+Mask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Mask.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/7/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | mutating func mask(with mask: Data) { 12 | guard !isEmpty else { return } 13 | let buffer = unsafeMutableBuffer() 14 | buffer.enumerated().forEach { (index, byte) in 15 | buffer[index] = byte ^ mask[index % 4] 16 | } 17 | } 18 | 19 | mutating func unmask(with mask: Data) { 20 | self.mask(with: mask) 21 | } 22 | 23 | static func randomMask() -> Data { 24 | let size = Int(UInt32.memoryLayoutSize) 25 | var data = Data(count: size) 26 | 27 | _ = data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in 28 | SecRandomCopyBytes(kSecRandomDefault, size, bytes) 29 | } 30 | 31 | return data 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Sources/Extensions/Data+SHA1.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Sha1.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/5/18. 6 | // 7 | 8 | import Foundation 9 | import CommonCrypto 10 | 11 | extension Data { 12 | func sha1() -> Data { 13 | var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) 14 | withUnsafeBytes { _ = CC_SHA1($0, CC_LONG(count), &digest) } 15 | return Data(bytes: digest) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Extensions/Date+Format.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Format.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/9/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | struct Format { 12 | static let iso8601ms: DateFormatter = { 13 | let formatter = DateFormatter() 14 | formatter.calendar = Foundation.Calendar(identifier: .iso8601) 15 | formatter.locale = Locale(identifier: "en_US_POSIX") 16 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 17 | return formatter 18 | }() 19 | } 20 | 21 | var iso8601ms: String { 22 | return Format.iso8601ms.string(from: self) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Extensions/OperationQueue+Setup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationQueue+Setup.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/7/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension OperationQueue { 11 | convenience init(qos: QualityOfService, maxOperationCount: Int = 1) { 12 | self.init() 13 | qualityOfService = qos 14 | maxConcurrentOperationCount = maxOperationCount 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Extensions/Optional+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional+Extensions.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/9/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Optional { 11 | var isNil: Bool { 12 | switch self { 13 | case .none: 14 | return true 15 | default: 16 | return false 17 | } 18 | } 19 | 20 | var isNotNil: Bool { 21 | return !isNil 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Extensions/RunLoop+Timeout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunLoop+Timeout.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/9/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension RunLoop { 11 | public static func runUntil(timeout: TimeInterval, predicate: () -> Bool) -> Bool { 12 | let timeoutData = Date(timeIntervalSinceNow: timeout) 13 | 14 | let timeoutInterval = timeoutData.timeIntervalSinceReferenceDate 15 | var currentInterval = Date.timeIntervalSinceReferenceDate 16 | 17 | while !predicate() && currentInterval < timeoutInterval { 18 | RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow: 0.1)) 19 | currentInterval = Date.timeIntervalSinceReferenceDate 20 | } 21 | 22 | return currentInterval <= timeoutInterval 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Extensions/Stream+SSLSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stream+SSLSettings.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/6/18. 6 | // 7 | 8 | #if os(watchOS) || os(Linux) 9 | #else 10 | 11 | import Foundation 12 | 13 | extension Stream { 14 | func apply(_ settings: SSLSettings) throws { 15 | guard settings.useSSL else { return } 16 | 17 | let sslSettings = settings.cfSettings() 18 | setProperty(StreamSocketSecurityLevel.negotiatedSSL as AnyObject, forKey: .socketSecurityLevelKey) 19 | setProperty(sslSettings, forKey: Stream.PropertyKey(kCFStreamPropertySSLSettings as String)) 20 | 21 | guard !settings.cipherSuites.isEmpty else { return } 22 | guard let sslContext = (self as? SSLContextRetrievable)?.sslContext else { return } 23 | var suites = settings.cipherSuites 24 | 25 | let status = SSLSetEnabledCiphers(sslContext, &suites, suites.count) 26 | guard status == errSecSuccess else { throw IOStream.StreamError.osError(status: status) } 27 | } 28 | } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/Extensions/Stream+SSLTrust.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OutputStream+SSLTrust.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/5/18. 6 | // 7 | 8 | #if os(watchOS) || os(Linux) 9 | #else 10 | 11 | import Foundation 12 | 13 | extension InputStream: SSLContextRetrievable { 14 | var sslContext: SSLContext? { 15 | return CFReadStreamCopyProperty(self, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext? 16 | } 17 | } 18 | 19 | extension OutputStream: SSLContextRetrievable { 20 | var sslContext: SSLContext? { 21 | return CFWriteStreamCopyProperty(self, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext? 22 | } 23 | 24 | var secTrust: SecTrust? { 25 | return property(forKey: Stream.PropertyKey(kCFStreamPropertySSLPeerTrust as String)) as! SecTrust? 26 | } 27 | 28 | var domain: String? { 29 | if let domain = property(forKey: Stream.PropertyKey(kCFStreamSSLPeerName as String)) as? String { 30 | return domain 31 | } else if let context = sslContext { 32 | var peerNameLength: Int = 0 33 | SSLGetPeerDomainNameLength(context, &peerNameLength) 34 | var peerName = Data(count: peerNameLength) 35 | 36 | peerName.withUnsafeMutableBytes { (peerNamePtr: UnsafeMutablePointer) in 37 | SSLGetPeerDomainName(context, peerNamePtr, &peerNameLength) 38 | return 39 | } 40 | 41 | return String(bytes: peerName, encoding: .utf8) 42 | } else { 43 | return nil 44 | } 45 | } 46 | } 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /Sources/Extensions/String+Handshaking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Base64+SHA1.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/5/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | static func generateSecKey() -> String { 12 | let seed = 16 13 | 14 | let characters: [Character] = (0.. String { 23 | guard let data = self.data(using: .utf8) else { return "" } 24 | return data.sha1().base64EncodedString() 25 | } 26 | 27 | func base64() -> String { 28 | guard let data = self.data(using: .utf8) else { return "" } 29 | return data.base64EncodedString() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Extensions/URL+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Extensions.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URL { 11 | var sslSupported: Bool { 12 | guard let scheme = scheme else { return false } 13 | return SSLSettings.supportedSSLSchemes.contains(scheme) 14 | } 15 | 16 | var webSocketPort: Int { 17 | return port ?? (sslSupported ? 433 : 80) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Extensions/URLRequest+Handshake.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest+Handshake.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLRequest { 11 | func webSocketHandshake() -> String { 12 | guard let url = url else { return "" } 13 | 14 | var path = url.path.isEmpty ? "/" : url.path 15 | if let query = url.query { 16 | path += "?" + query 17 | } 18 | 19 | var handshake = "\(httpMethod ?? "GET") \(path) HTTP/1.1\r\n" 20 | allHTTPHeaderFields?.forEach { (key, value) in 21 | let pair = key + ": " + value + "\r\n" 22 | handshake += pair 23 | } 24 | 25 | handshake += "\r\n" 26 | 27 | return handshake 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Extensions/URLRequest+WebSocket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest+WebSocket.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | //GET /chat HTTP/1.1 11 | //Host: server.example.com 12 | //Upgrade: websocket 13 | //Connection: Upgrade 14 | //Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 15 | //Origin: http://example.com 16 | //Sec-WebSocket-Protocol: chat, superchat 17 | //Sec-WebSocket-Version: 13 18 | 19 | extension URLRequest { 20 | mutating func prepare(secKey: String, url: URL, addPortToHost: Bool, useCompression: Bool, protocols: [String]) { 21 | var host = allHTTPHeaderFields?[WebSocket.Header.host] ?? "\(url.host ?? "")" 22 | if addPortToHost { 23 | host += ":\(url.webSocketPort)" 24 | } 25 | 26 | var origin = url.absoluteString 27 | 28 | if let hostURL = URL(string: "/", relativeTo: url) { 29 | origin = hostURL.absoluteString 30 | origin.removeLast() 31 | } 32 | 33 | setValue(host, forHTTPHeaderField: WebSocket.Header.host) 34 | setValue(WebSocket.HeaderValue.upgrade, forHTTPHeaderField: WebSocket.Header.upgrade) 35 | setValue(WebSocket.HeaderValue.connection, forHTTPHeaderField: WebSocket.Header.connection) 36 | 37 | setValue(secKey, forHTTPHeaderField: WebSocket.Header.secKey) 38 | setValue(origin, forHTTPHeaderField: WebSocket.Header.origin) 39 | 40 | if !protocols.isEmpty { 41 | setValue(protocols.joined(separator: ","), forHTTPHeaderField: WebSocket.Header.secProtocol) 42 | } 43 | 44 | if useCompression { 45 | setValue(WebSocket.HeaderValue.extension, forHTTPHeaderField: WebSocket.Header.secExtension) 46 | } 47 | 48 | setValue(WebSocket.HeaderValue.version, forHTTPHeaderField: WebSocket.Header.secVersion) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Extensions/UnsafePointer+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnsafePointer+Extensions.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/2/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension UnsafePointer { 11 | func mutable() -> UnsafeMutablePointer { 12 | return UnsafeMutablePointer(mutating: self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Info-iOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Info-macOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Info-tvOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Info-watchOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Models/Registry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Registry.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/5/18. 6 | // 7 | 8 | import Foundation 9 | 10 | //https://tools.ietf.org/html/rfc6455#page-65 11 | extension WebSocket { 12 | public enum Opcode: UInt8 { 13 | case continuationFrame = 0x0 14 | case textFrame = 0x1 15 | case binaryFrame = 0x2 16 | //* %x3-7 are reserved for further non-control frames 17 | case connectionCloseFrame = 0x8 18 | case pingFrame = 0x9 19 | case pongFrame = 0xA 20 | //* %xB-F are reserved for further control frames 21 | case unknown = 0xF 22 | } 23 | 24 | public enum CloseCode: UInt16 { 25 | case normalClosure = 1000 26 | case goingAway = 1001 27 | case protocolError = 1002 28 | case unsupportedData = 1003 29 | /*1004 reserved*/ 30 | case noStatusReceived = 1005 31 | case abnormalClosure = 1006 32 | case invalidFramePayloadData = 1007 33 | case policyViolation = 1008 34 | case messageTooBig = 1009 35 | case mandatoryExt = 1010 36 | case internalServerError = 1011 37 | case TLSHandshake = 1015 38 | 39 | static func code(with rawCode: UInt16) -> CloseCode? { 40 | if let closeCode = CloseCode(rawValue: rawCode) { 41 | return closeCode 42 | } 43 | 44 | if rawCode >= 3000 /*&& rawCode < 5000*/ { 45 | return .normalClosure 46 | } 47 | 48 | return nil 49 | } 50 | } 51 | 52 | // 0 1 2 3 53 | // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 54 | // +-+-+-+-+-------+-+-------------+-------------------------------+ 55 | // |F|R|R|R| opcode|M| Payload len | Extended payload length | 56 | // |I|S|S|S| (4) |A| (7) | (16/64) | 57 | // |N|V|V|V| |S| | (if payload len==126/127) | 58 | // | |1|2|3| |K| | | 59 | // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 60 | // | Extended payload length continued, if payload len == 127 | 61 | // + - - - - - - - - - - - - - - - +-------------------------------+ 62 | // | |Masking-key, if MASK set to 1 | 63 | // +-------------------------------+-------------------------------+ 64 | // | Masking-key (continued) | Payload Data | 65 | // +-------------------------------- - - - - - - - - - - - - - - - + 66 | // : Payload Data continued ... : 67 | // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 68 | // | Payload Data continued ... | 69 | // +---------------------------------------------------------------+ 70 | public struct Mask { 71 | static let fin: UInt8 = 0b10000000 72 | static let rsv: UInt8 = 0b01110000 73 | static let rsv1: UInt8 = 0b01000000 74 | static let rsv2: UInt8 = 0b00100000 75 | static let rsv3: UInt8 = 0b00010000 76 | static let opCode: UInt8 = 0b00001111 77 | static let mask: UInt8 = 0b10000000 78 | static let payloadLen: UInt8 = 0b01111111 79 | } 80 | 81 | public struct Header { 82 | static let origin = "Origin" 83 | static let upgrade = "Upgrade" 84 | static let host = "Host" 85 | static let connection = "Connection" 86 | 87 | static let secProtocol = "Sec-WebSocket-Protocol" 88 | static let secVersion = "Sec-WebSocket-Version" 89 | static let secExtension = "Sec-WebSocket-Extensions" 90 | static let secKey = "Sec-WebSocket-Key" 91 | static let accept = "Sec-WebSocket-Accept" 92 | } 93 | 94 | public struct HeaderValue { 95 | static let connection = "Upgrade" 96 | static let upgrade = "websocket" 97 | static let `extension` = "permessage-deflate; client_max_window_bits; server_max_window_bits=15" 98 | static var version = "13" 99 | } 100 | 101 | public enum HTTPCode: Int { 102 | case `continue` = 100 103 | case switching = 101 104 | case processing = 102 105 | 106 | case ok = 200 107 | case created = 201 108 | case accepted = 202 109 | 110 | case badRequest = 400 111 | case unauthorized = 401 112 | case forbidden = 403 113 | case notFound = 404 114 | 115 | case internalServerError = 500 116 | case notImplemented = 501 117 | case badGateway = 502 118 | case serviceUnavailable = 503 119 | case gatewayTimeout = 504 120 | 121 | case sslHandshakeFailed = 525 122 | case invalidSSLCertificate = 526 123 | 124 | case unknown = -999 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /Sources/Models/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/5/18. 6 | // 7 | 8 | import Foundation 9 | 10 | typealias Completion = (Result) -> Void 11 | 12 | enum Result { 13 | case value(T) 14 | case error(Error) 15 | } 16 | 17 | extension Result { 18 | public var empty: Result { 19 | switch self { 20 | case let .error(error): 21 | return error.result() 22 | case .value: 23 | return .success 24 | } 25 | } 26 | 27 | public func onPositive(_ handler: (_ value: T) -> Void) { 28 | switch self { 29 | case .value(let value): 30 | handler(value) 31 | default: 32 | break 33 | } 34 | } 35 | 36 | public func onNegative(_ handler: (_ error: Error) -> Void) { 37 | switch self { 38 | case .error(let error): 39 | handler(error) 40 | default: 41 | break 42 | } 43 | } 44 | 45 | public func map(_ transform: (T) throws -> R) -> Result { 46 | do { 47 | switch self { 48 | case .value(let value): 49 | return .value(try transform(value)) 50 | case .error(let error): 51 | return error.result() 52 | } 53 | } catch { 54 | return error.result() 55 | } 56 | } 57 | } 58 | 59 | extension Result where T == Void { 60 | static var success: Result { 61 | return .value(()) 62 | } 63 | } 64 | 65 | extension Error { 66 | func result() -> Result { 67 | return .error(self) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Models/WebSocketModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocketModels.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension WebSocket { 11 | 12 | public struct Settings { 13 | public var debugMode = false 14 | public var callbackQueue: DispatchQueue? 15 | public var timeout: TimeInterval = 5 16 | public var useCompression = false 17 | public var maskOutputData: Bool = true 18 | public var respondPingRequestsAutomatically = true 19 | public var addPortToHostInHeader = true 20 | } 21 | 22 | public enum Status { 23 | case disconnected 24 | case connecting 25 | case connected 26 | case disconnecting 27 | } 28 | 29 | public enum ClosingStatus { 30 | case closingByClient 31 | case closingByServer 32 | case none 33 | } 34 | 35 | public enum Event { 36 | case connected 37 | case textReceived(String) 38 | case dataReceived(Data) 39 | case pongReceived(Data) 40 | case pingReceived(Data) 41 | case disconnected(Error?, WebSocket.CloseCode) 42 | case debug(String) 43 | } 44 | 45 | public enum WebSocketError: LocalizedError { 46 | case sslValidationFailed 47 | case handshakeFailed(response: String) 48 | case missingHeader(header: String) 49 | case wrongOpCode 50 | case wrongChopSize 51 | case timeout 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Sources/Protocols/SSLContextRetrievable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSLContextRetrievable.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol SSLContextRetrievable { 11 | var sslContext: SSLContext? { get } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Protocols/SizeRetrievable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SizeRetrievable.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/2/18. 6 | // 7 | 8 | import Foundation 9 | import CZLib 10 | 11 | protocol SizeRetrievable { } 12 | extension SizeRetrievable { 13 | static var memoryLayoutSize: Int32 { 14 | return Int32(MemoryLayout.size) 15 | } 16 | } 17 | 18 | extension z_stream: SizeRetrievable { } 19 | extension UInt8: SizeRetrievable { } 20 | extension UInt16: SizeRetrievable { } 21 | extension UInt32: SizeRetrievable { } 22 | extension UInt64: SizeRetrievable { } 23 | -------------------------------------------------------------------------------- /Sources/Security/SSLCertificate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSLCertificate.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/5/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public class SSLSertificate { 11 | public var data: Data? 12 | public var publicKey: SecKey? 13 | 14 | var secCertificate: SecCertificate? { 15 | guard let data = data else { return nil } 16 | return SecCertificateCreateWithData(nil, data as CFData) 17 | } 18 | 19 | public init(data: Data? = nil, publicKey: SecKey? = nil) { 20 | self.data = data 21 | self.publicKey = publicKey 22 | } 23 | 24 | func extractPublicKey() { 25 | guard publicKey.isNil else { return } 26 | guard let data = data else { return } 27 | guard let secCertificate = SecCertificateCreateWithData(nil, data as CFData) else { return } 28 | publicKey = SSLSertificate.extractPublicKey(for: secCertificate, policy: SecPolicyCreateBasicX509()) 29 | } 30 | 31 | static func extractPublicKey(for sertificate: SecCertificate, policy: SecPolicy) -> SecKey? { 32 | var possibleTrust: SecTrust? 33 | SecTrustCreateWithCertificates(sertificate, policy, &possibleTrust) 34 | 35 | guard let trust = possibleTrust else { return nil } 36 | var result: SecTrustResultType = .unspecified 37 | SecTrustEvaluate(trust, &result) 38 | return SecTrustCopyPublicKey(trust) 39 | } 40 | 41 | static func secCertificates(for trust: SecTrust) -> [SecCertificate] { 42 | return (0.. SecCertificate? in 43 | return SecTrustGetCertificateAtIndex(trust, certificateIndex) 44 | } 45 | } 46 | 47 | static func certificatesData(for trust: SecTrust) -> [Data] { 48 | return secCertificates(for: trust).map { (sertificate) -> Data in 49 | return SecCertificateCopyData(sertificate) as Data 50 | } 51 | } 52 | 53 | static func publicKeys(for trust: SecTrust, policy: SecPolicy = SecPolicyCreateBasicX509()) -> [SecKey] { 54 | return secCertificates(for: trust).compactMap { (certificate) -> SecKey? in 55 | return extractPublicKey(for: certificate, policy: policy) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Security/SSLSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSLSettings.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/5/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public class SSLSettings { 11 | static var supportedSSLSchemes = ["wss", "https"] 12 | 13 | public var useSSL: Bool 14 | public var certificateValidationEnabled: Bool 15 | public var overrideTrustHostname: Bool 16 | public var trustHostname: String? 17 | public var cipherSuites: [SSLCipherSuite] = [] 18 | 19 | public static var `default`: SSLSettings { 20 | return SSLSettings(useSSL: true) 21 | } 22 | 23 | public init(useSSL: Bool) { 24 | self.useSSL = useSSL 25 | certificateValidationEnabled = true 26 | overrideTrustHostname = true 27 | } 28 | 29 | func cfSettings() -> [CFString: NSObject] { 30 | var settings: [CFString: NSObject] = [:] 31 | #if os(watchOS) || os(Linux) 32 | #else 33 | settings[kCFStreamSSLValidatesCertificateChain] = NSNumber(value: certificateValidationEnabled) 34 | if overrideTrustHostname { 35 | settings[kCFStreamSSLPeerName] = trustHostname as NSString? ?? kCFNull 36 | } 37 | #endif 38 | return settings 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Sources/Security/SSLValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSLValidator.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/5/18. 6 | // 7 | 8 | import Foundation 9 | import Security 10 | 11 | public class SSLValidator { 12 | public var shouldValidateDomainName: Bool = true 13 | public var usePublicKeys: Bool 14 | public var certificates: [SSLSertificate] 15 | 16 | public init(certificates: [SSLSertificate], usePublicKeys: Bool) { 17 | self.usePublicKeys = usePublicKeys 18 | self.certificates = certificates 19 | 20 | if usePublicKeys { 21 | preparePublicKeys() 22 | } 23 | } 24 | 25 | public convenience init(usePublicKeys: Bool = false) { 26 | let urls = Bundle.main.urls(forResourcesWithExtension: "cer", subdirectory: nil) ?? [] 27 | let certificates = urls.compactMap { (url) -> SSLSertificate? in 28 | guard let data = try? Data(contentsOf: url) else { return nil } 29 | return SSLSertificate(data: data) 30 | } 31 | 32 | self.init(certificates: certificates, usePublicKeys: usePublicKeys) 33 | } 34 | 35 | fileprivate func preparePublicKeys() { 36 | certificates.forEach { $0.extractPublicKey() } 37 | } 38 | 39 | func isValid(trust: SecTrust, domain: String?, validateAll: Bool = true) -> Bool { 40 | let policy: SecPolicy = shouldValidateDomainName 41 | ? SecPolicyCreateSSL(true, domain as CFString?) 42 | : SecPolicyCreateBasicX509() 43 | 44 | SecTrustSetPolicies(trust, policy) 45 | 46 | if usePublicKeys { 47 | return isValidPublicKeys(trust: trust) 48 | } else { 49 | return isValidCertificates(trust: trust, validateAll: validateAll) 50 | } 51 | } 52 | 53 | fileprivate func isValidPublicKeys(trust: SecTrust) -> Bool { 54 | let clientPublicKeys = Set(certificates.compactMap { $0.publicKey }) 55 | let serverPublicKeys = Set(SSLSertificate.publicKeys(for: trust)) 56 | 57 | return !clientPublicKeys.intersection(serverPublicKeys).isEmpty 58 | } 59 | 60 | fileprivate func isValidCertificates(trust: SecTrust, validateAll: Bool) -> Bool { 61 | let secCertificates = certificates.compactMap { $0.secCertificate } 62 | SecTrustSetAnchorCertificates(trust, secCertificates as CFArray) 63 | 64 | var result = SecTrustResultType.unspecified 65 | SecTrustEvaluate(trust, &result) 66 | 67 | switch result { 68 | case .proceed, .unspecified: 69 | if validateAll { 70 | let clientCertificates = Set(secCertificates) 71 | let serverCertificates = Set(SSLSertificate.secCertificates(for: trust)) 72 | 73 | return serverCertificates.intersection(clientCertificates).count == serverCertificates.count 74 | } else { 75 | return true 76 | } 77 | default: 78 | return false 79 | } 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /Sources/WebSocket/Frame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Frame.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/7/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public class Frame { 11 | typealias Mask = WebSocket.Mask 12 | 13 | var fin: Bool = false 14 | var rsv1: Bool = false 15 | var rsv2: Bool = false 16 | var rsv3: Bool = false 17 | var opCode: WebSocket.Opcode = .unknown 18 | var isMasked: Bool = false 19 | var payloadLength: UInt64 = 0 20 | var mask: Data = Data() 21 | var payload: Data = Data() 22 | 23 | var isFullfilled = false 24 | var frameSize: UInt64 = 0 25 | 26 | var isControlFrame: Bool { 27 | return opCode == .connectionCloseFrame || opCode == .pingFrame || opCode == .pongFrame 28 | } 29 | 30 | var isDataFrame: Bool { 31 | return opCode == .binaryFrame || opCode == .textFrame || opCode == .continuationFrame 32 | } 33 | 34 | var rsv: Bool { 35 | return (rsv1 || rsv2 || rsv3) 36 | } 37 | 38 | init() { } 39 | 40 | init(fin: Bool, rsv1: Bool = false, rsv2: Bool = false, rsv3: Bool = false, opCode: WebSocket.Opcode) { 41 | self.fin = fin 42 | self.rsv1 = rsv1 43 | self.rsv2 = rsv2 44 | self.rsv3 = rsv3 45 | self.opCode = opCode 46 | } 47 | 48 | func closeCode() -> WebSocket.CloseCode? { 49 | if payloadLength == 0 { return .normalClosure } 50 | guard let rawCode = rawCloseCode() else { return nil } 51 | return WebSocket.CloseCode.code(with: UInt16(rawCode)) 52 | } 53 | 54 | func rawCloseCode() -> UInt16? { 55 | guard opCode == .connectionCloseFrame else { return nil } 56 | guard payloadLength <= 125 else { return 1002 } 57 | guard payloadLength >= 2 else { return nil } 58 | 59 | let rawCode = Frame.extractValue(from: payload.unsafeBuffer(), offset: 0, count: 2) 60 | return UInt16(rawCode) 61 | } 62 | 63 | func closeInfo() -> String? { 64 | guard opCode == .connectionCloseFrame else { return nil } 65 | guard payloadLength > 2 else { return nil } 66 | 67 | let messageData = payload[2.. Data { 95 | var bytes: [UInt8] = [0, 0] 96 | 97 | if frame.fin { 98 | bytes[0] |= Mask.fin 99 | } 100 | 101 | if frame.rsv1 { 102 | bytes[0] |= Mask.rsv1 103 | } 104 | 105 | if frame.rsv2 { 106 | bytes[0] |= Mask.rsv2 107 | } 108 | 109 | if frame.rsv3 { 110 | bytes[0] |= Mask.rsv3 111 | } 112 | 113 | bytes[0] |= frame.opCode.rawValue 114 | 115 | if frame.isMasked { 116 | bytes[1] |= Mask.mask 117 | } 118 | 119 | let payloadLength = frame.payloadLength 120 | var lengthData: Data? 121 | 122 | if payloadLength <= 125 { 123 | bytes[1] |= UInt8(payloadLength) 124 | } else if payloadLength <= UInt64(UInt16.max) { 125 | bytes[1] |= 126 126 | var length = UInt16(frame.payloadLength).bigEndian 127 | lengthData = Data(bytes: &length, count: Int(UInt16.memoryLayoutSize)) 128 | } else if payloadLength <= UInt64.max { 129 | bytes[1] |= 127 130 | var length = UInt64(frame.payloadLength).bigEndian 131 | lengthData = Data(bytes: &length, count: Int(UInt64.memoryLayoutSize)) 132 | } 133 | 134 | var data = Data(bytes) 135 | 136 | if let lengthData = lengthData { 137 | data.append(lengthData) 138 | } 139 | 140 | if frame.isMasked { 141 | data.append(frame.mask) 142 | } 143 | 144 | data.append(frame.payload) 145 | 146 | return data 147 | } 148 | 149 | static func decode(from unsafeBuffer: UnsafeBufferPointer, fromOffset: Int) -> (Frame, Int)? { 150 | guard unsafeBuffer.count >= fromOffset + 2 else { return nil } 151 | var offset = fromOffset 152 | 153 | let frame = Frame() 154 | let firstByte = offset 155 | let secondByte = offset + 1 156 | 157 | frame.fin = unsafeBuffer[firstByte] & Mask.fin != 0 158 | frame.rsv1 = unsafeBuffer[firstByte] & Mask.rsv1 != 0 159 | frame.rsv2 = unsafeBuffer[firstByte] & Mask.rsv2 != 0 160 | frame.rsv3 = unsafeBuffer[firstByte] & Mask.rsv3 != 0 161 | frame.opCode = WebSocket.Opcode(rawValue: unsafeBuffer[firstByte] & Mask.opCode) ?? .unknown 162 | frame.isMasked = unsafeBuffer[secondByte] & Mask.mask != 0 163 | frame.payloadLength = UInt64(unsafeBuffer[secondByte] & Mask.payloadLen) 164 | 165 | offset = fullFill(frame: frame, buffer: unsafeBuffer, globalOffset: offset) 166 | 167 | return (frame, offset) 168 | } 169 | 170 | static func fullFill(frame: Frame, buffer: UnsafeBufferPointer, globalOffset: Int) -> Int { 171 | var estimatedFrameSize: UInt64 = 2 //first two bytes 172 | estimatedFrameSize += frame.isMasked ? 4 : 0 173 | 174 | guard buffer.count >= estimatedFrameSize + UInt64(globalOffset) else { return globalOffset } 175 | 176 | var payloadLengthSize = 0 177 | if frame.payloadLength == 126 { 178 | //Next 2 bytes indicate length 179 | payloadLengthSize = 2 180 | } else if frame.payloadLength == 127 { 181 | //Next 8 bytes indicate length 182 | payloadLengthSize = 8 183 | } 184 | 185 | estimatedFrameSize += UInt64(payloadLengthSize) 186 | guard buffer.count >= estimatedFrameSize + UInt64(globalOffset) else { return globalOffset } 187 | 188 | var offset = globalOffset + 2 189 | if payloadLengthSize > 0 { 190 | frame.payloadLength = extractValue(from: buffer, offset: offset, count: payloadLengthSize) 191 | } 192 | 193 | estimatedFrameSize += frame.payloadLength 194 | guard buffer.count >= estimatedFrameSize + UInt64(globalOffset) else { return globalOffset } 195 | offset += payloadLengthSize 196 | 197 | if frame.isMasked { 198 | // next 4 bytes - mask 199 | frame.mask = Data(buffer[offset.., offset: Int, count: Int) -> UInt64 { 215 | var value: UInt64 = 0 216 | (0.. 1 else { return nil } 30 | let rawStatusCode = Int(statusComponents[1]) ?? -1 31 | 32 | code = WebSocket.HTTPCode(rawValue: rawStatusCode) ?? .unknown 33 | statusLine = statusComponent 34 | rawBodyString = bodyString 35 | extractHeaders(from: components) 36 | } 37 | 38 | func extractHeaders(from components: [String]) { 39 | components.forEach { (component) in 40 | let keyValue = component.components(separatedBy: ":") 41 | guard keyValue.count > 1 else { return } 42 | 43 | let key = keyValue[0].lowercased().trimmingCharacters(in: .whitespaces) 44 | let value = keyValue[1].trimmingCharacters(in: .whitespaces) 45 | 46 | httpHeaders[key] = value 47 | } 48 | } 49 | 50 | static func httpDataRange(for data: Data) -> Range? { 51 | guard let endData = "\r\n\r\n".data(using: .utf8) else { return nil } 52 | guard let endRange = data.range(of: endData) else { return nil } 53 | 54 | return Range(uncheckedBounds: (data.startIndex, endRange.upperBound)) 55 | } 56 | 57 | static func remainingData(for data: Data, usefulRange: Range) -> Data? { 58 | guard data.endIndex > usefulRange.upperBound else { return nil } 59 | 60 | return data[usefulRange.upperBound.. Void)? 19 | 20 | deinit { disconnect() } 21 | 22 | override init() { 23 | queue = DispatchQueue(label: "dialognet-websocket-io-stream-queue", qos: .userInitiated) 24 | super.init() 25 | } 26 | 27 | init(queue: DispatchQueue) { 28 | self.queue = queue 29 | } 30 | 31 | func connect(url: URL, port: uint, timeout: TimeInterval, networkSystemType: URLRequest.NetworkServiceType = .default, settings: SSLSettings, completion: @escaping Completion) { 32 | do { 33 | try createIOPair(url: url, port: port) 34 | try setupNetworkServiceType(networkSystemType) 35 | try configureProxySetting() 36 | #if os(watchOS) || os(Linux) 37 | #else 38 | try configureSSLSettings(settings) 39 | #endif 40 | try setupIOPair() 41 | 42 | openConnection(timeout: timeout, completion: completion) 43 | } catch { 44 | completion(error.result()) 45 | } 46 | } 47 | 48 | func disconnect() { 49 | if let stream = inputStream { 50 | stream.delegate = nil 51 | CFReadStreamSetDispatchQueue(stream, nil) 52 | stream.close() 53 | } 54 | 55 | if let stream = outputStream { 56 | stream.delegate = nil 57 | CFWriteStreamSetDispatchQueue(stream, nil) 58 | stream.close() 59 | } 60 | 61 | inputStream = nil 62 | outputStream = nil 63 | } 64 | 65 | func read() throws -> Data { 66 | guard let input = inputStream else { throw StreamError.wrongIOPair } 67 | var buffer = Data.buffer() 68 | 69 | let readLength = buffer.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in 70 | return input.read(bytes, maxLength: Data.bufferSize) 71 | } 72 | 73 | if readLength < 0 { 74 | throw input.streamError ?? StreamError.unknown 75 | } 76 | 77 | buffer.count = readLength 78 | return buffer 79 | } 80 | 81 | @discardableResult 82 | func write(_ data: Data) throws -> Int { 83 | return try data.withUnsafeBytes { (bytes: UnsafePointer) in 84 | return try write(bytes, count: data.count) 85 | } 86 | } 87 | 88 | @discardableResult 89 | func write(_ bytes: UnsafePointer, count: Int) throws -> Int { 90 | guard let output = outputStream else { throw StreamError.wrongIOPair } 91 | 92 | let writeLength = output.write(bytes, maxLength: count) 93 | if writeLength <= 0 { 94 | throw output.streamError ?? StreamError.unknown 95 | } 96 | 97 | return writeLength 98 | } 99 | 100 | fileprivate func createIOPair(url: URL, port: uint) throws { 101 | guard let host = url.host as CFString? else { throw StreamError.wrongHost } 102 | 103 | var readStream: Unmanaged? 104 | var writeStream: Unmanaged? 105 | CFStreamCreatePairWithSocketToHost(nil, host, port, &readStream, &writeStream) 106 | inputStream = readStream?.takeRetainedValue() 107 | outputStream = writeStream?.takeRetainedValue() 108 | } 109 | 110 | fileprivate func setupNetworkServiceType(_ type: URLRequest.NetworkServiceType) throws { 111 | guard let input = inputStream, let output = outputStream else { 112 | throw StreamError.wrongIOPair 113 | } 114 | 115 | let streamNetworkServiceType: StreamNetworkServiceTypeValue 116 | 117 | switch type { 118 | case .default: 119 | return 120 | case .voip: 121 | if #available(iOS 8, *) { 122 | let message = "This service type is deprecated. Please use PushKit for VoIP control" 123 | debugPrint("\(type) - \(message)") 124 | return 125 | } else { 126 | streamNetworkServiceType = .voIP 127 | } 128 | case .voice: 129 | streamNetworkServiceType = .voice 130 | case .video: 131 | streamNetworkServiceType = .video 132 | case .callSignaling: 133 | if #available(iOS 10.0, OSX 10.12, watchOS 3.0, tvOS 10.0, *) { 134 | streamNetworkServiceType = .callSignaling 135 | } else { 136 | return 137 | } 138 | case .background: 139 | streamNetworkServiceType = .background 140 | case .responsiveData: 141 | streamNetworkServiceType = .background 142 | } 143 | 144 | input.setValue(streamNetworkServiceType.rawValue, forKey: Stream.PropertyKey.networkServiceType.rawValue) 145 | output.setValue(streamNetworkServiceType.rawValue, forKey: Stream.PropertyKey.networkServiceType.rawValue) 146 | } 147 | 148 | fileprivate func configureProxySetting() throws { 149 | #if os(watchOS) || os(Linux) 150 | return 151 | #else 152 | guard enableProxy else { return } 153 | guard let input = inputStream, let output = outputStream else { 154 | throw StreamError.wrongIOPair 155 | } 156 | 157 | guard let proxySettings = CFNetworkCopySystemProxySettings() else { return } 158 | let settings = CFDictionaryCreateMutableCopy(nil, 0, proxySettings.takeRetainedValue()) 159 | let key = CFStreamPropertyKey(rawValue: kCFStreamPropertySOCKSProxy) 160 | 161 | CFReadStreamSetProperty(input, key, settings) 162 | CFWriteStreamSetProperty(output, key, settings) 163 | #endif 164 | } 165 | 166 | #if os(watchOS) || os(Linux) 167 | #else 168 | fileprivate func configureSSLSettings(_ settings: SSLSettings) throws { 169 | guard let input = inputStream, let output = outputStream else { 170 | throw StreamError.wrongIOPair 171 | } 172 | 173 | try input.apply(settings) 174 | try output.apply(settings) 175 | } 176 | #endif 177 | 178 | fileprivate func setupIOPair() throws { 179 | guard let input = inputStream, let output = outputStream else { 180 | throw StreamError.wrongIOPair 181 | } 182 | 183 | input.delegate = self 184 | output.delegate = self 185 | 186 | CFReadStreamSetDispatchQueue(input, queue) 187 | CFWriteStreamSetDispatchQueue(output, queue) 188 | } 189 | 190 | fileprivate func openConnection(timeout: TimeInterval, completion: @escaping Completion) { 191 | inputStream?.open() 192 | outputStream?.open() 193 | waitForConnection(timeout: timeout, completion: completion) 194 | } 195 | 196 | fileprivate func waitForConnection(timeout: TimeInterval, delay: Int = 100, completion: @escaping Completion) { 197 | queue.asyncAfter(deadline: .now() + .milliseconds(delay)) { [weak self] in 198 | do { 199 | guard let wSelf = self else { throw StreamError.deinited } 200 | guard let output = wSelf.outputStream else { throw StreamError.wrongIOPair } 201 | guard timeout >= Double(delay) else { throw StreamError.connectionTimeout } 202 | 203 | if let streamError = output.streamError { 204 | throw streamError 205 | } 206 | 207 | if output.hasSpaceAvailable { 208 | completion(.success) 209 | } else { 210 | wSelf.waitForConnection(timeout: timeout - TimeInterval(delay), completion: completion) 211 | } 212 | } catch { 213 | completion(error.result()) 214 | } 215 | } 216 | } 217 | } 218 | 219 | extension IOStream: StreamDelegate { 220 | public func stream(_ aStream: Stream, handle eventCode: Stream.Event) { 221 | let event = Event(eventCode: eventCode) 222 | let streamType: StreamType = aStream == inputStream 223 | ? .input 224 | : .output 225 | 226 | onReceiveEvent?(event, streamType) 227 | } 228 | } 229 | 230 | extension IOStream { 231 | public enum StreamError: Error { 232 | case noURL 233 | case wrongHost 234 | case wrongIOPair 235 | case connectionTimeout 236 | case deinited 237 | case osError(status: OSStatus) 238 | 239 | case unknown 240 | } 241 | 242 | enum StreamType { 243 | case input 244 | case output 245 | } 246 | 247 | enum Event { 248 | case openCompleted 249 | case hasBytesAvailable 250 | case hasSpaceAvailable 251 | case errorOccurred 252 | case endEncountered 253 | case unknown 254 | 255 | init(eventCode: Stream.Event) { 256 | switch eventCode { 257 | case Stream.Event.openCompleted: 258 | self = .openCompleted 259 | case Stream.Event.hasBytesAvailable: 260 | self = .hasBytesAvailable 261 | case Stream.Event.hasSpaceAvailable: 262 | self = .hasSpaceAvailable 263 | case Stream.Event.errorOccurred: 264 | self = .errorOccurred 265 | case Stream.Event.endEncountered: 266 | self = .endEncountered 267 | default: 268 | self = .unknown 269 | } 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /Sources/WebSocket/StreamBuffer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamBuffer.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | class StreamBuffer { 11 | var buffer: Data 12 | 13 | init() { 14 | buffer = Data() 15 | } 16 | 17 | func enqueue(_ chunk: Data) { 18 | buffer.append(chunk) 19 | } 20 | 21 | func clearBuffer() { 22 | buffer.removeAll() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/WebSocket/WebSocket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocket.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/2/18. 6 | // 7 | 8 | import Foundation 9 | 10 | open class WebSocket { 11 | public static let GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 12 | 13 | public fileprivate(set) var stream: IOStream 14 | public fileprivate(set) var url: URL 15 | public fileprivate(set) var request: URLRequest 16 | public fileprivate(set) var protocols: [String] 17 | public fileprivate(set) var certificatesValidated = false 18 | 19 | fileprivate var queue = DispatchQueue(label: "dialognet-websocket-queue", qos: .default, attributes: .concurrent) 20 | fileprivate var compressionSettings: CompressionSettings = .default 21 | fileprivate var inputStreamBuffer = StreamBuffer() 22 | fileprivate let operationQueue: OperationQueue 23 | fileprivate var currentInputFrame: Frame? 24 | fileprivate var secKey = "" 25 | 26 | fileprivate var closingStatus: ClosingStatus = .none 27 | fileprivate var _status: Status = .disconnected 28 | fileprivate var statusLock = NSLock() 29 | public fileprivate(set) var status: Status { 30 | get { 31 | statusLock.lock(); defer { statusLock.unlock() } 32 | let status = _status 33 | return status 34 | } 35 | 36 | set { 37 | statusLock.lock() 38 | _status = newValue 39 | statusLock.unlock() 40 | } 41 | } 42 | 43 | //MARK: - Settings 44 | public var settings: Settings = Settings() 45 | public var securitySettings: SSLSettings 46 | public var securityValidator: SSLValidator 47 | public var httpHeaders: [String: String] { 48 | get { 49 | return request.allHTTPHeaderFields ?? [:] 50 | } 51 | 52 | set { 53 | httpHeaders.forEach { (key, value) in 54 | request.setValue(value, forHTTPHeaderField: key) 55 | } 56 | } 57 | } 58 | 59 | //MARK: - Callbacks 60 | public var onEvent: ((Event) -> Void)? 61 | public var onConnect: (() -> Void)? 62 | public var onText: ((String) -> Void)? 63 | public var onData: ((Data) -> Void)? 64 | public var onPong: ((Data) -> Void)? 65 | public var onPing: ((Data) -> Void)? 66 | public var onDisconnect: ((Error?, CloseCode) -> Void)? 67 | public var onDebugInfo: ((String) -> Void)? 68 | 69 | //MARK: - Public methods 70 | deinit { tearDown(reasonError: nil, code: .normalClosure) } 71 | 72 | public convenience init(url: URL, 73 | timeout: TimeInterval = 5, 74 | protocols: [String] = [], 75 | callbackQueue: DispatchQueue? = nil, 76 | processingQoS: QualityOfService = .userInteractive) { 77 | 78 | let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: timeout) 79 | self.init(request: request, timeout: timeout, protocols: protocols, callbackQueue: callbackQueue, processingQoS: processingQoS) 80 | } 81 | 82 | public init(request: URLRequest, 83 | timeout: TimeInterval = 5, 84 | protocols: [String] = [], 85 | callbackQueue: DispatchQueue? = nil, 86 | processingQoS: QualityOfService = .default) { 87 | 88 | self.stream = IOStream() 89 | 90 | self.url = request.url! 91 | self.request = request 92 | self.protocols = protocols 93 | 94 | settings.callbackQueue = callbackQueue 95 | settings.timeout = timeout 96 | 97 | operationQueue = OperationQueue(qos: processingQoS) 98 | securitySettings = SSLSettings(useSSL: url.sslSupported) 99 | securityValidator = SSLValidator() 100 | } 101 | 102 | open func connect() { 103 | guard status == .disconnected || status == .disconnecting else { return } 104 | status = .connecting 105 | 106 | secKey = String.generateSecKey() 107 | request.prepare(secKey: secKey, 108 | url: url, 109 | addPortToHost: settings.addPortToHostInHeader, 110 | useCompression: settings.useCompression, 111 | protocols: protocols) 112 | 113 | let port = uint(url.webSocketPort) 114 | openConnecttion(port: port, msTimeout: settings.timeout * 1000) { [weak self] (result) in 115 | guard let wSelf = self else { return } 116 | result.onNegative { wSelf.tearDown(reasonError: $0, code: .noStatusReceived) } 117 | result.onPositive { wSelf.handleSuccessConnection() } 118 | } 119 | } 120 | 121 | open func disconnect() { 122 | disconnect(settings.timeout) 123 | } 124 | 125 | open func disconnect(_ timeout: TimeInterval) { 126 | guard closingStatus == .none else { return } 127 | 128 | closingStatus = .closingByClient 129 | closeConnection(timeout: timeout, code: .normalClosure) 130 | } 131 | 132 | open func send(data: Data, chopSize: Int? = nil, completion: (() -> Void)? = nil) { 133 | performSend(data: data, code: .binaryFrame, chopSize: chopSize, completion: completion) 134 | } 135 | 136 | open func send(string: String, chopSize: Int? = nil, completion: (() -> Void)? = nil) { 137 | guard let data = string.data(using: .utf8) else { return } 138 | performSend(data: data, code: .textFrame, chopSize: chopSize, completion: completion) 139 | } 140 | 141 | open func sendPing(data: Data, completion: (() -> Void)? = nil) { 142 | performSend(data: data, code: .pingFrame, completion: completion) 143 | } 144 | 145 | open func sendPong(data: Data, completion: (() -> Void)? = nil) { 146 | performSend(data: data, code: .pongFrame, completion: completion) 147 | } 148 | 149 | open func send(payload: Data, opCode: Opcode, chopSize: Int? = nil, completion: (() -> Void)? = nil) { 150 | performSend(data: payload, code: opCode, chopSize: chopSize, completion: completion) 151 | } 152 | } 153 | 154 | //MARK: - Lifecycle 155 | extension WebSocket { 156 | fileprivate func openConnecttion(port: uint, msTimeout: TimeInterval, completion: @escaping Completion) { 157 | stream.onReceiveEvent = streamEventHandler() 158 | stream.connect(url: url, 159 | port: port, 160 | timeout: msTimeout, 161 | networkSystemType: request.networkServiceType, 162 | settings: securitySettings, 163 | completion: completion) 164 | } 165 | 166 | fileprivate func handleSuccessConnection() { 167 | log("Connection opened") 168 | let operation = BlockOperation() 169 | operation.addExecutionBlock { [weak self, weak operation] in 170 | guard let wSelf = self else { return } 171 | guard let wOperation = operation, !wOperation.isCancelled else { return } 172 | 173 | do { 174 | try wSelf.validateCertificates() 175 | try wSelf.performHandshake(operation: wOperation) 176 | } catch { 177 | wSelf.tearDown(reasonError: error, code: .TLSHandshake) 178 | } 179 | } 180 | 181 | operationQueue.addOperation(operation) 182 | } 183 | 184 | fileprivate func validateCertificates() throws { 185 | #if os(watchOS) || os(Linux) 186 | #else 187 | if securitySettings.useSSL, !certificatesValidated && !securityValidator.certificates.isEmpty { 188 | let domain = stream.outputStream?.domain 189 | 190 | if let secTrust = stream.outputStream?.secTrust, securityValidator.isValid(trust: secTrust, domain: domain) { 191 | certificatesValidated = true 192 | } else { 193 | certificatesValidated = false 194 | throw WebSocketError.sslValidationFailed 195 | } 196 | } 197 | #endif 198 | } 199 | 200 | fileprivate func performHandshake(operation: Operation) throws { 201 | let rawHandshake = request.webSocketHandshake() 202 | log("Sending Handshake", message: rawHandshake) 203 | guard let data = rawHandshake.data(using: .utf8) else { 204 | throw WebSocketError.handshakeFailed(response: rawHandshake) 205 | } 206 | 207 | let buffer = data.unsafeBuffer() 208 | try write(buffer: buffer, totalSize: data.count, operation: operation) 209 | } 210 | 211 | fileprivate func closeConnection(code: CloseCode) { 212 | closeConnection(timeout: settings.timeout, code: code) 213 | } 214 | 215 | fileprivate func closeConnection(timeout: TimeInterval, code: CloseCode) { 216 | guard status != .disconnected else { return } 217 | log("Closing connection", message: "CloseCode: \(code)") 218 | 219 | var value = code.rawValue.bigEndian 220 | let data = Data(bytes: &value, count: Int(UInt16.memoryLayoutSize)) 221 | 222 | performSend(data: data, code: .connectionCloseFrame, completion: nil) 223 | 224 | status = .disconnecting 225 | checkStatus(.disconnected, msTimeout: timeout * 1000) 226 | } 227 | 228 | fileprivate func checkStatus(_ status: Status, msTimeout: TimeInterval, delay: Int = 100) { 229 | queue.asyncAfter(deadline: .now() + .milliseconds(delay)) { [weak self] in 230 | guard let wSelf = self else { return } 231 | 232 | if msTimeout < Double(delay) { 233 | wSelf.tearDown(reasonError: WebSocketError.timeout, code: .noStatusReceived) 234 | } else if wSelf.status != status { 235 | wSelf.checkStatus(status, msTimeout: msTimeout - TimeInterval(delay)) 236 | } 237 | } 238 | } 239 | 240 | fileprivate func tearDown(reasonError: Error?, code: CloseCode) { 241 | guard status != .disconnected else { return } 242 | 243 | status = .disconnecting 244 | 245 | reasonError.isNil 246 | ? operationQueue.waitUntilAllOperationsAreFinished() 247 | : operationQueue.cancelAllOperations() 248 | 249 | stream.disconnect() 250 | inputStreamBuffer.clearBuffer() 251 | 252 | status = .disconnected 253 | handleEvent(.disconnected(reasonError, code)) 254 | 255 | let errorMessage = reasonError.isNil ? "No error." : "Error: \(reasonError!.localizedDescription)" 256 | log("Connection closed", message: "CloseCode: \(code). " + errorMessage) 257 | } 258 | } 259 | 260 | //MARK: - Event Handling 261 | extension WebSocket { 262 | fileprivate func log(_ event: String, message: String = "") { 263 | if settings.debugMode { 264 | let header = "\n**** \(event.uppercased()) ****\n" 265 | let date = Date().iso8601ms + "\n" 266 | handleEvent(.debug(header + date + message)) 267 | } 268 | } 269 | 270 | fileprivate func handleEvent(_ event: Event) { 271 | let notifyBlock = { [weak self] in 272 | guard let wSelf = self else { return } 273 | 274 | wSelf.onEvent?(event) 275 | switch event { 276 | case .connected: 277 | wSelf.onConnect?() 278 | case let .dataReceived(data): 279 | wSelf.onData?(data) 280 | case let .textReceived(text): 281 | wSelf.onText?(text) 282 | case let .pingReceived(data): 283 | wSelf.onPing?(data) 284 | case let .pongReceived(data): 285 | wSelf.onPong?(data) 286 | case let .disconnected(error, code): 287 | wSelf.onDisconnect?(error, code) 288 | case let .debug(info): 289 | wSelf.onDebugInfo?(info) 290 | } 291 | } 292 | 293 | if let callbackQueue = settings.callbackQueue { 294 | callbackQueue.async(execute: notifyBlock) 295 | } else { 296 | notifyBlock() 297 | } 298 | } 299 | } 300 | 301 | //MARK: - I/O Processing 302 | extension WebSocket { 303 | fileprivate func streamEventHandler() -> (IOStream.Event, IOStream.StreamType) -> Void { 304 | return { [weak self] (event, type) in 305 | guard let wSelf = self else { return } 306 | 307 | type == .input ? wSelf.handleInputEvent(event) : wSelf.handleOutputEvent(event) 308 | } 309 | } 310 | 311 | //MARK: - Input Flow 312 | fileprivate func handleInputEvent(_ event: IOStream.Event) { 313 | switch event { 314 | case .openCompleted, .hasSpaceAvailable, .unknown: 315 | break 316 | case .hasBytesAvailable: 317 | handleInputBytesAvailable() 318 | case .endEncountered: 319 | let closeCode: CloseCode = (status == .disconnecting || status == .disconnected) 320 | ? .normalClosure 321 | : .abnormalClosure 322 | tearDown(reasonError: stream.inputStream?.streamError, code: closeCode) 323 | case .errorOccurred: 324 | handleInputError() 325 | } 326 | } 327 | 328 | fileprivate func handleInputError() { 329 | let error = stream.inputStream?.streamError ?? IOStream.StreamError.unknown 330 | tearDown(reasonError: error, code: .abnormalClosure) 331 | } 332 | 333 | fileprivate func handleInputBytesAvailable() { 334 | log("New bytes available") 335 | do { 336 | let data = try stream.read() 337 | inputStreamBuffer.enqueue(data) 338 | processInputStreamData() 339 | } catch { 340 | tearDown(reasonError: error, code: .abnormalClosure) 341 | } 342 | } 343 | 344 | fileprivate func processInputStreamData() { 345 | while inputStreamBuffer.buffer.count >= 2 && processInputBufferData() { } 346 | } 347 | 348 | fileprivate func processInputBufferData() -> Bool { 349 | if status == .connecting { 350 | guard let handshake = Handshake(data: inputStreamBuffer.buffer) else { return false } 351 | inputStreamBuffer.clearBuffer() 352 | 353 | log("Handshake received", message: handshake.rawBodyString) 354 | do { try processHandshake(handshake) } 355 | catch { tearDown(reasonError: error, code: .TLSHandshake) } 356 | } 357 | 358 | let data = inputStreamBuffer.buffer 359 | guard data.count >= 2 else { return false } 360 | inputStreamBuffer.clearBuffer() 361 | return processData(data) 362 | } 363 | 364 | fileprivate func processHandshake(_ handshake: Handshake) throws { 365 | if let remainingData = handshake.remainingData { 366 | inputStreamBuffer.buffer = remainingData 367 | handshake.remainingData = nil 368 | } 369 | 370 | guard handshake.code == .switching else { 371 | throw WebSocketError.handshakeFailed(response: handshake.rawBodyString) 372 | } 373 | 374 | guard let acceptKey = handshake.httpHeaders[Header.accept.lowercased()] else { 375 | throw WebSocketError.missingHeader(header: Header.accept) 376 | } 377 | 378 | let clientKey = (secKey + WebSocket.GUID).sha1base64() 379 | guard clientKey == acceptKey else { 380 | throw WebSocketError.handshakeFailed(response: handshake.rawBodyString) 381 | } 382 | 383 | if let extensions = handshake.httpHeaders[Header.secExtension.lowercased()] { 384 | compressionSettings.update(with: extensions) 385 | } 386 | 387 | log("Handshake successed") 388 | status = .connected 389 | handleEvent(.connected) 390 | } 391 | 392 | fileprivate func processData(_ data: Data) -> Bool { 393 | var data = data 394 | let unsafeBuffer = data.unsafeBuffer() 395 | 396 | var offset = 0 397 | var successed = true 398 | 399 | while offset + 2 <= unsafeBuffer.count { 400 | if let (frame, newOffset) = Frame.decode(from: unsafeBuffer, fromOffset: offset) { 401 | if frame.isFullfilled, processFrame(frame) { 402 | offset = newOffset 403 | continue 404 | } else { 405 | successed = false 406 | break 407 | } 408 | } 409 | } 410 | 411 | if offset < unsafeBuffer.count { 412 | data.removeFirst(offset) 413 | inputStreamBuffer.buffer = data 414 | } 415 | 416 | return successed 417 | } 418 | 419 | fileprivate func processFrame(_ frame: Frame) -> Bool { 420 | log("Frame received", message: frame.description) 421 | if frame.opCode == .unknown { 422 | tearDown(reasonError: nil, code: .protocolError) 423 | return false 424 | } 425 | 426 | if frame.rsv, !compressionSettings.useCompression { 427 | closeConnection(code: .protocolError) 428 | return false 429 | } 430 | 431 | if frame.isMasked && frame.fin { 432 | frame.payload.unmask(with: frame.mask) 433 | } 434 | 435 | if frame.isControlFrame { 436 | return processControlFrame(frame) 437 | } 438 | 439 | if frame.isDataFrame { 440 | return processDataFrame(frame) 441 | } 442 | 443 | tearDown(reasonError: WebSocketError.wrongOpCode, code: .protocolError) 444 | return false 445 | } 446 | 447 | fileprivate func processControlFrame(_ frame: Frame) -> Bool { 448 | guard frame.fin, frame.payloadLength <= 125 else { 449 | closeConnection(code: .protocolError) 450 | return false 451 | } 452 | 453 | switch frame.opCode { 454 | case .pingFrame: 455 | handleEvent(.pingReceived(frame.payload)) 456 | if settings.respondPingRequestsAutomatically { 457 | sendPong(data: frame.payload) 458 | } 459 | case .pongFrame: 460 | do { 461 | try decompressFrameIfNeeded(frame) 462 | } catch { 463 | closeConnection(code: .invalidFramePayloadData) 464 | return false 465 | } 466 | 467 | handleEvent(.pongReceived(frame.payload)) 468 | case .connectionCloseFrame: 469 | switch closingStatus { 470 | case .none: 471 | closingStatus = .closingByServer 472 | if checkCloseFramePayload(frame) { 473 | if let closeCode = frame.closeCode() { 474 | processCloseFrameCode(closeCode) 475 | } else { 476 | closeConnection(code: .protocolError) 477 | } 478 | } else { 479 | closeConnection(code: .invalidFramePayloadData) 480 | } 481 | break 482 | case .closingByClient: 483 | tearDown(reasonError: nil, code: frame.closeCode() ?? .protocolError) 484 | case .closingByServer: 485 | //Just ignore 486 | return true 487 | } 488 | default: 489 | return false 490 | } 491 | 492 | return true 493 | } 494 | 495 | fileprivate func checkCloseFramePayload(_ frame: Frame) -> Bool { 496 | guard frame.opCode == .connectionCloseFrame else { return false } 497 | guard frame.payloadLength > 2 else { return true } // Only code 498 | 499 | return frame.closeInfo().isNotNil 500 | } 501 | 502 | fileprivate func processCloseFrameCode(_ code: CloseCode) { 503 | switch code { 504 | case .noStatusReceived, .abnormalClosure, .TLSHandshake: 505 | closeConnection(code: .protocolError) 506 | default: 507 | closeConnection(code: .normalClosure) 508 | } 509 | } 510 | 511 | fileprivate func processDataFrame(_ frame: Frame) -> Bool { 512 | if frame.opCode == .continuationFrame { 513 | return processContinuationFrame(frame) 514 | } 515 | 516 | if currentInputFrame.isNotNil { 517 | //Received new Data frame when not fullfill current fragmented yet 518 | closeConnection(code: .protocolError) 519 | return false 520 | } 521 | 522 | if frame.rsv && !compressionSettings.useCompression { 523 | closeConnection(code: .protocolError) 524 | return false 525 | } 526 | 527 | if frame.fin { 528 | do { try decompressFrameIfNeeded(frame) } 529 | catch { closeConnection(code: .invalidFramePayloadData); return false } 530 | 531 | if frame.opCode == .binaryFrame { 532 | handleEvent(.dataReceived(frame.payload)) 533 | } else if frame.opCode == .textFrame, let text = String(data: frame.payload, encoding: .utf8) { 534 | handleEvent(.textReceived(text)) 535 | } else { 536 | closeConnection(code: .invalidFramePayloadData) 537 | return false 538 | } 539 | } else { 540 | currentInputFrame = frame 541 | } 542 | 543 | return true 544 | } 545 | 546 | fileprivate func processContinuationFrame(_ frame: Frame) -> Bool { 547 | guard let inputFrame = currentInputFrame else { 548 | closeConnection(code: .protocolError) 549 | return false 550 | } 551 | 552 | inputFrame.merge(frame) 553 | 554 | if inputFrame.fin { 555 | currentInputFrame = nil 556 | return processFrame(inputFrame) 557 | } 558 | 559 | return true 560 | } 561 | 562 | fileprivate func decompressFrameIfNeeded(_ frame: Frame) throws { 563 | guard let inflater = compressionSettings.inflater else { return } 564 | guard compressionSettings.useCompression && frame.rsv1 else { return } 565 | 566 | frame.payload.addTail() 567 | let decompressedPayload = try inflater.decompress(windowBits: compressionSettings.serverMaxWindowBits, data: frame.payload) 568 | frame.payload = decompressedPayload 569 | 570 | if compressionSettings.serverNoContextTakeover { 571 | inflater.reset() 572 | } 573 | } 574 | 575 | //MARK: - Output Flow 576 | fileprivate func handleOutputEvent(_ event: IOStream.Event) { 577 | switch event { 578 | case .openCompleted, .hasSpaceAvailable, .hasBytesAvailable, .unknown: 579 | break 580 | case .endEncountered: 581 | tearDown(reasonError: stream.outputStream?.streamError, code: .abnormalClosure) 582 | case .errorOccurred: 583 | handleOutputError() 584 | } 585 | } 586 | 587 | fileprivate func handleOutputError() { 588 | let error = stream.outputStream?.streamError ?? IOStream.StreamError.unknown 589 | tearDown(reasonError: error, code: .abnormalClosure) 590 | } 591 | 592 | fileprivate func performSend(data: Data, code: Opcode, chopSize: Int? = nil, completion: (() -> Void)?) { 593 | guard status == .connected else { return } 594 | 595 | let operation = BlockOperation() 596 | operation.addExecutionBlock { [weak self, weak operation] in 597 | guard let wSelf = self else { return } 598 | guard let wOperation = operation, !wOperation.isCancelled else { return } 599 | 600 | var frames: [Frame] = [] 601 | 602 | if let chopSize = chopSize, chopSize < data.count { 603 | do { 604 | frames = try wSelf.chopFrames(from: data, opCode: code, chopSize: chopSize) 605 | } catch { 606 | wSelf.log("Error happened while chopping frames. \nError: \(error.localizedDescription) \n Ignoring.") 607 | return 608 | } 609 | } else { 610 | let frame = wSelf.prepareFrame(payload: data, opCode: code) 611 | frames.append(frame) 612 | } 613 | 614 | wSelf.performSend(frames: frames, operation: wOperation, completion: completion) 615 | } 616 | 617 | operationQueue.addOperation(operation) 618 | } 619 | 620 | fileprivate func performSend(frames: [Frame], operation: Operation, completion: (() -> Void)?) { 621 | frames.enumerated().forEach { (index, frame) in 622 | let frameData = Frame.encode(frame) 623 | let frameSize = frameData.count 624 | frame.frameSize = UInt64(frameSize) 625 | 626 | let buffer = frameData.unsafeBuffer() 627 | var streamError: Error? 628 | 629 | do { 630 | try write(buffer: buffer, totalSize: frameSize, operation: operation) 631 | } catch { 632 | streamError = error 633 | } 634 | 635 | if let error = streamError, status == .connected { 636 | tearDown(reasonError: error, code: .unsupportedData) 637 | return 638 | } 639 | 640 | log("Frame #\(index) has sent") 641 | } 642 | 643 | guard let completion = completion else { return } 644 | 645 | if let callbackQueue = settings.callbackQueue { 646 | callbackQueue.async(execute: completion) 647 | } else { 648 | completion() 649 | } 650 | } 651 | 652 | fileprivate func write(buffer: UnsafeBufferPointer, fromByteIndex: Int = 0, totalSize: Int, operation: Operation) throws { 653 | var bytesWritten = fromByteIndex 654 | 655 | while bytesWritten < totalSize && !operation.isCancelled { 656 | let pointer = buffer.baseAddress!.advanced(by: bytesWritten) 657 | bytesWritten += try stream.write(pointer, count: buffer.count - bytesWritten) 658 | } 659 | } 660 | 661 | fileprivate func chopFrames(from payload: Data, opCode: Opcode, chopSize: Int) throws -> [Frame] { 662 | guard opCode == .binaryFrame || opCode == .textFrame else { throw WebSocketError.wrongOpCode } 663 | guard chopSize > 0 else { throw WebSocketError.wrongChopSize } 664 | 665 | if payload.count <= chopSize { 666 | return [prepareFrame(payload: payload, opCode: opCode)] 667 | } 668 | 669 | let framePayloads = payload.chopped(by: chopSize) 670 | return framePayloads.enumerated().map { (index, framePayload) -> Frame in 671 | switch index { 672 | case 0: 673 | return prepareFrame(payload: framePayload, opCode: opCode, fin: false) 674 | case framePayloads.count - 1: 675 | return prepareFrame(payload: framePayload, opCode: .continuationFrame, fin: true) 676 | default: 677 | return prepareFrame(payload: framePayload, opCode: .continuationFrame, fin: false) 678 | } 679 | } 680 | } 681 | 682 | fileprivate func prepareFrame(payload: Data, opCode: Opcode, fin: Bool = true) -> Frame { 683 | let frame = Frame(fin: fin, opCode: opCode) 684 | frame.payload = payload 685 | 686 | if settings.maskOutputData { 687 | frame.isMasked = true 688 | frame.mask = Data.randomMask() 689 | } 690 | 691 | if compressionSettings.useCompression, let deflater = compressionSettings.deflater { 692 | do { 693 | frame.rsv1 = true 694 | let compressedPayload = try deflater.compress(windowBits: compressionSettings.clientMaxWindowBits, 695 | data: frame.payload) 696 | frame.payload = compressedPayload 697 | frame.payload.removeTail() 698 | 699 | if compressionSettings.clientNoContextTakeover { 700 | deflater.reset() 701 | } 702 | } catch { 703 | //Temporary solution 704 | debugPrint(error.localizedDescription) 705 | frame.rsv1 = false 706 | } 707 | } 708 | 709 | if frame.isMasked { 710 | frame.payload.mask(with: frame.mask) 711 | } 712 | 713 | frame.payloadLength = UInt64(frame.payload.count) 714 | 715 | return frame 716 | } 717 | } 718 | -------------------------------------------------------------------------------- /Sources/Сompression/CompressionObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompressionObject.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/12/18. 6 | // 7 | 8 | import Foundation 9 | import CZLib 10 | 11 | class CompressionObject { 12 | static var chunkSize: Int { 13 | return 0x2000 // 8192 bytes 14 | } 15 | 16 | var stream: z_stream = z_stream() 17 | 18 | func prepareZStream(for data: Data) { 19 | var data = data 20 | data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in 21 | stream.next_in = bytes 22 | } 23 | 24 | stream.avail_in = uint(data.count) 25 | } 26 | 27 | func process(_ code: CInt) throws { 28 | let status = CompressionStatus(status: code) 29 | 30 | switch status { 31 | case .ok: 32 | return 33 | default: 34 | throw DataProcessingError.error(status: status) 35 | } 36 | } 37 | 38 | func reset() { } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Сompression/CompressionSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompressionSettings.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CompressionSettings { 11 | var useCompression = false 12 | 13 | var clientMaxWindowBits: CInt = 15 14 | var serverMaxWindowBits: CInt = 15 15 | 16 | var clientNoContextTakeover = false 17 | var serverNoContextTakeover = false 18 | 19 | var inflater: Inflater? 20 | var deflater: Deflater? 21 | 22 | static var `default`: CompressionSettings { 23 | return CompressionSettings() 24 | } 25 | 26 | mutating func update(with rawExtensions: String) { 27 | rawExtensions.components(separatedBy: ";").forEach { (rawExtension) in 28 | let ext = rawExtension.trimmingCharacters(in: .whitespaces) 29 | 30 | switch ext { 31 | case "permessage-deflate": 32 | useCompression = true 33 | case "client_no_context_takeover": 34 | clientNoContextTakeover = true 35 | case "server_no_context_takeover": 36 | serverNoContextTakeover = true 37 | default: 38 | guard let value = extractIntValue(from: ext) else { return } 39 | 40 | if ext.hasPrefix("client_max_window_bits") { 41 | clientMaxWindowBits = value 42 | } else if ext.hasPrefix("server_max_window_bits") { 43 | serverMaxWindowBits = value 44 | } 45 | } 46 | } 47 | 48 | inflater = Inflater(windowBits: serverMaxWindowBits) 49 | deflater = Deflater(windowBits: clientMaxWindowBits) 50 | } 51 | 52 | fileprivate func extractIntValue(from ext: String) -> CInt? { 53 | let components = ext.components(separatedBy: "=") 54 | guard components.count > 1 else { return nil } 55 | return CInt(components[1].trimmingCharacters(in: .whitespaces)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Сompression/CompressionStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompressionStatus.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/2/18. 6 | // 7 | 8 | import Foundation 9 | import CZLib 10 | 11 | public enum CompressionStatus { 12 | case ok 13 | case streamEnd 14 | case needDict 15 | case errno 16 | case streamError 17 | case dataError 18 | case memError 19 | case bufError 20 | case versionError 21 | case unknown 22 | 23 | init(status: CInt) { 24 | switch status { 25 | case Z_OK: 26 | self = .ok 27 | case Z_STREAM_END: 28 | self = .streamEnd 29 | case Z_NEED_DICT: 30 | self = .needDict 31 | case Z_ERRNO: 32 | self = .errno 33 | case Z_STREAM_ERROR: 34 | self = .streamError 35 | case Z_DATA_ERROR: 36 | self = .dataError 37 | case Z_MEM_ERROR: 38 | self = .memError 39 | case Z_BUF_ERROR: 40 | self = .bufError 41 | case Z_VERSION_ERROR: 42 | self = .versionError 43 | default: 44 | self = .unknown 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Sources/Сompression/Deflater.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deflater.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/12/18. 6 | // 7 | 8 | import Foundation 9 | import CZLib 10 | 11 | class Deflater: CompressionObject { 12 | var windowBits: CInt 13 | 14 | deinit { deflateEnd(&stream) } 15 | init?(windowBits: CInt) { 16 | self.windowBits = windowBits 17 | super.init() 18 | 19 | do { 20 | try prepareDeflate() 21 | } catch { 22 | debugPrint(error) 23 | return nil 24 | } 25 | } 26 | 27 | override func reset() { 28 | deflateEnd(&stream) 29 | 30 | do { 31 | try prepareDeflate() 32 | } catch { 33 | debugPrint(error) 34 | } 35 | } 36 | 37 | func prepareDeflate() throws { 38 | let code = deflateInit2_(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 39 | -windowBits, 8, Z_DEFAULT_STRATEGY, 40 | ZLIB_VERSION, z_stream.memoryLayoutSize) 41 | try process(code) 42 | } 43 | 44 | public func compress(windowBits: CInt, data: Data) throws -> Data { 45 | var compressedData = Data() 46 | var buffer = Data(count: CompressionObject.chunkSize) 47 | 48 | guard data.count > 0 else { return compressedData } 49 | 50 | prepareZStream(for: data) 51 | 52 | var result: CompressionStatus = .ok 53 | while stream.avail_out == 0 && result == .ok { 54 | if Int(stream.total_out) >= buffer.count { 55 | buffer.count += CompressionObject.chunkSize 56 | } 57 | 58 | let count = buffer.count 59 | buffer.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in 60 | stream.next_out = bytes 61 | stream.avail_out = uInt(count) 62 | 63 | let code = deflate(&stream, Z_SYNC_FLUSH) 64 | result = CompressionStatus(status: code) 65 | 66 | let writtenCount = count - Int(stream.avail_out) 67 | compressedData.append(bytes, count: writtenCount) 68 | } 69 | } 70 | 71 | return compressedData 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Сompression/Inflater.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Inflater.swift 3 | // DNWebSocket 4 | // 5 | // Created by Gleb Radchenko on 2/12/18. 6 | // 7 | 8 | import Foundation 9 | import CZLib 10 | 11 | public enum DataProcessingError: LocalizedError { 12 | case error(status: CompressionStatus) 13 | } 14 | 15 | class Inflater: CompressionObject { 16 | var windowBits: CInt 17 | 18 | deinit { inflateEnd(&stream) } 19 | init?(windowBits: CInt) { 20 | self.windowBits = windowBits 21 | super.init() 22 | 23 | do { 24 | try prepareInflate() 25 | } catch { 26 | debugPrint(error.localizedDescription) 27 | return nil 28 | } 29 | } 30 | 31 | override func reset() { 32 | inflateEnd(&stream) 33 | 34 | do { 35 | try prepareInflate() 36 | } catch { 37 | debugPrint(error.localizedDescription) 38 | } 39 | } 40 | 41 | func prepareInflate() throws { 42 | let code = inflateInit2_(&stream, -windowBits, ZLIB_VERSION, z_stream.memoryLayoutSize) 43 | try process(code) 44 | } 45 | 46 | public func decompress(windowBits: CInt, data: Data) throws -> Data { 47 | var decompressedData = Data() 48 | var buffer = Data(count: CompressionObject.chunkSize) 49 | 50 | guard data.count > 0 else { return decompressedData } 51 | prepareZStream(for: data) 52 | 53 | var result: CompressionStatus = .ok 54 | repeat { 55 | if Int(stream.total_out) >= buffer.count { 56 | buffer.count += CompressionObject.chunkSize 57 | } 58 | 59 | let count = buffer.count 60 | buffer.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in 61 | stream.next_out = bytes 62 | stream.avail_out = uInt(count) 63 | 64 | let code = inflate(&stream, Z_NO_FLUSH) 65 | result = CompressionStatus(status: code) 66 | 67 | let writtenCount = count - Int(stream.avail_out) 68 | decompressedData.append(bytes, count: writtenCount) 69 | } 70 | 71 | } while result == .ok 72 | 73 | return decompressedData 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests-macOS/AutobahnTests/AutobahnTestAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutobahnTestAction.swift 3 | // Tests-macOS 4 | // 5 | // Created by Gleb Radchenko on 2/10/18. 6 | // 7 | 8 | import Foundation 9 | import DNWebSocket 10 | 11 | class WebSocketAction { 12 | var url: URL 13 | var websocket: WebSocket 14 | 15 | var isFinished: Bool = false 16 | 17 | init(url: URL) { 18 | self.url = url 19 | websocket = WebSocket(url: url, processingQoS: .userInteractive) 20 | configureWebsocket() 21 | } 22 | 23 | func configureWebsocket() { 24 | websocket.securitySettings.useSSL = false 25 | websocket.settings.useCompression = false 26 | websocket.onDisconnect = { (error, closeCode) in 27 | self.handleWebSocketDisconnect(error: error, closeCode: closeCode) 28 | } 29 | } 30 | 31 | func perform() { 32 | websocket.connect() 33 | } 34 | 35 | func handleWebSocketDisconnect(error: Error?, closeCode: WebSocket.CloseCode) { 36 | isFinished = true 37 | } 38 | 39 | @discardableResult 40 | func waitUntilFinished(timeout: TimeInterval) -> Bool { 41 | if isFinished { 42 | return true 43 | } 44 | 45 | return RunLoop.runUntil(timeout: timeout) { 46 | return self.isFinished 47 | } 48 | } 49 | } 50 | 51 | class AutobahnTestAction: WebSocketAction { 52 | var onText: ((WebSocket, String) -> Void)? 53 | var onData: ((WebSocket, Data) -> Void)? 54 | 55 | var error: Error? 56 | 57 | init(url: URL, path: String, caseNumber: Int?, agent: String?, onText: ((WebSocket, String) -> Void)?, onData: ((WebSocket, Data) -> Void)?) { 58 | self.onText = onText 59 | self.onData = onData 60 | 61 | var url = url 62 | url.appendPathComponent(path) 63 | var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! 64 | 65 | var items: [URLQueryItem] = [] 66 | if let caseNumber = caseNumber { 67 | items.append(URLQueryItem(name: "case", value: "\(caseNumber)")) 68 | } 69 | 70 | if let agent = agent { 71 | items.append(URLQueryItem(name: "agent", value: agent)) 72 | } 73 | 74 | components.queryItems = items 75 | super.init(url: components.url!) 76 | 77 | websocket.onText = { (text) in 78 | onText?(self.websocket, text) 79 | } 80 | 81 | websocket.onData = { (data) in 82 | onData?(self.websocket, data) 83 | } 84 | } 85 | 86 | override func handleWebSocketDisconnect(error: Error?, closeCode: WebSocket.CloseCode) { 87 | self.error = error 88 | super.handleWebSocketDisconnect(error: error, closeCode: closeCode) 89 | } 90 | } 91 | 92 | extension AutobahnTestAction { 93 | static func test(url: URL, caseNumber: Int?, agent: String?) -> AutobahnTestAction { 94 | return AutobahnTestAction(url: url, 95 | path: "/runCase", 96 | caseNumber: caseNumber, 97 | agent: agent, 98 | onText: { $0.send(string: $1) }, 99 | onData: { $0.send(data: $1) }) 100 | } 101 | 102 | static func testResult(url: URL, caseNumber: Int?, agent: String?, completion: (([String: String]) -> Void)?) -> AutobahnTestAction { 103 | return AutobahnTestAction(url: url, 104 | path: "/getCaseStatus", 105 | caseNumber: caseNumber, 106 | agent: agent, 107 | onText: { (websocket, text) in 108 | guard let data = text.data(using: .utf8) else { 109 | completion?([:]) 110 | return 111 | } 112 | 113 | guard let json = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves) else { 114 | completion?([:]) 115 | return 116 | } 117 | 118 | guard let dict = json as? [String: String] else { 119 | completion?([:]) 120 | return 121 | } 122 | 123 | completion?(dict) 124 | }, 125 | onData: nil) 126 | } 127 | 128 | static func testInfo(url: URL, caseNumber: Int?, completion: (([String: String]) -> Void)?) -> AutobahnTestAction { 129 | return AutobahnTestAction(url: url, 130 | path: "/getCaseInfo", 131 | caseNumber: caseNumber, 132 | agent: nil, 133 | onText: { (websocket, text) in 134 | guard let data = text.data(using: .utf8) else { 135 | completion?([:]) 136 | return 137 | } 138 | 139 | guard let json = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves) else { 140 | completion?([:]) 141 | return 142 | } 143 | 144 | guard let dict = json as? [String: String] else { 145 | completion?([:]) 146 | return 147 | } 148 | 149 | completion?(dict) 150 | }, 151 | onData: nil) 152 | } 153 | 154 | static func testsCount(url: URL, agent: String?, completion: ((Int) -> Void)?) -> AutobahnTestAction { 155 | return AutobahnTestAction(url: url, 156 | path: "/getCaseCount", 157 | caseNumber: nil, 158 | agent: agent, 159 | onText: { (websocket, text) in completion?(Int(text) ?? 0) }, 160 | onData: nil) 161 | } 162 | 163 | static func updateReport(url: URL, agent: String?) -> AutobahnTestAction { 164 | return AutobahnTestAction(url: url, 165 | path: "/updateReports", 166 | caseNumber: nil, 167 | agent: agent, 168 | onText: nil, 169 | onData: nil) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Tests-macOS/AutobahnTests/AutobahnTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutobahnTests.swift 3 | // Tests-macOS 4 | // 5 | // Created by Gleb Radchenko on 2/8/18. 6 | // 7 | 8 | import XCTest 9 | @testable import DNWebSocket 10 | 11 | class AutobahnTests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | } 16 | 17 | override func tearDown() { 18 | updateReports() 19 | super.tearDown() 20 | } 21 | 22 | func updateReports() { 23 | let action = AutobahnTestAction.updateReport(url: TestConfiguration.serverURL, 24 | agent: TestConfiguration.agent) 25 | action.perform() 26 | XCTAssert(action.waitUntilFinished(timeout: 60), "Update reports timeout") 27 | } 28 | 29 | func testAutoBahnCases() { 30 | let unimplementedCount = 210 31 | let count = 40//TestConfiguration.testsCount() - unimplementedCount 32 | (1...count).forEach { (number) in 33 | let info = TestConfiguration.testInfo(number: number) 34 | let id = info ["id"] ?? "Unknown id" 35 | let description = info["description"] ?? "" 36 | print("\nPerforming test \(number), id: \(id), description: \n" + description) 37 | 38 | makeTest(number: number, id: id) 39 | 40 | if number % 10 == 0 { 41 | updateReports() 42 | } 43 | } 44 | } 45 | 46 | func makeTest(number: Int, id: String) { 47 | let url = TestConfiguration.serverURL 48 | let agent = TestConfiguration.agent 49 | 50 | let testAction = AutobahnTestAction.test(url: url, caseNumber: number, agent: agent) 51 | testAction.perform() 52 | XCTAssert(testAction.waitUntilFinished(timeout: 60), "Test case \(id) timeout") 53 | 54 | let info = TestConfiguration.testResult(number: number) 55 | let result = info["behavior"] ?? "NO RESULT" 56 | let isAcceptable = TestConfiguration.isAcceptable(result: result) 57 | XCTAssert(isAcceptable, "Test \(number), id: \(id) failed with result: \(result)") 58 | 59 | if isAcceptable { 60 | print("+") 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests-macOS/AutobahnTests/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // Tests-macOS 4 | // 5 | // Created by Gleb Radchenko on 2/8/18. 6 | // 7 | 8 | import Foundation 9 | import DNWebSocket 10 | 11 | class TestConfiguration { 12 | static var agent: String { 13 | return Bundle(for: self).bundleIdentifier ?? "DNWebSocket" 14 | } 15 | 16 | static var serverURL: URL { 17 | return URL(string: "ws://localhost:9001")! 18 | } 19 | 20 | static func isAcceptable(result: String) -> Bool { 21 | let suitableResults = ["OK", "NON-STRICT", "INFORMATIONAL", "UNIMPLEMENTED"] 22 | return suitableResults.contains(result) 23 | } 24 | 25 | static func testsCount() -> Int { 26 | var count = 0 27 | 28 | let countAction = AutobahnTestAction.testsCount(url: serverURL, agent: agent) { (countReceived) in 29 | count = countReceived 30 | } 31 | 32 | countAction.perform() 33 | countAction.waitUntilFinished(timeout: 60) 34 | 35 | return count 36 | } 37 | 38 | static func testInfo(number: Int?) -> [String: String] { 39 | var info: [String: String] = [:] 40 | 41 | let infoAction = AutobahnTestAction.testInfo(url: serverURL, caseNumber: number) { (received) in 42 | info = received 43 | } 44 | 45 | infoAction.perform() 46 | infoAction.waitUntilFinished(timeout: 60) 47 | 48 | return info 49 | } 50 | 51 | static func testResult(number: Int?) -> [String: String] { 52 | var info: [String: String] = [:] 53 | 54 | let resultAction = AutobahnTestAction.testResult(url: serverURL, caseNumber: number, agent: agent) { (received) in 55 | info = received 56 | } 57 | 58 | resultAction.perform() 59 | resultAction.waitUntilFinished(timeout: 60) 60 | 61 | return info 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests-macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests-macOS/TestCommon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestCommon.swift 3 | // Tests-macOS 4 | // 5 | // Created by Gleb Radchenko on 2/8/18. 6 | // 7 | 8 | import XCTest 9 | @testable import DNWebSocket 10 | 11 | class TestCommon: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | super.tearDown() 20 | } 21 | 22 | func testMasking() { 23 | let inputString = "String to test mask/unmask String to test mask/unmask String to test mask/unmask String to test mask/unmask" 24 | var data = inputString.data(using: .utf8) 25 | XCTAssertNotNil(data, "String data is empty") 26 | var mask = Data.randomMask() 27 | 28 | data!.mask(with: mask) 29 | data!.unmask(with: mask) 30 | 31 | let outputString = String(data: data!, encoding: .utf8) ?? "" 32 | 33 | XCTAssertEqual(inputString, outputString) 34 | } 35 | 36 | func testHandshakeCodingEncoding() { 37 | let url = URL(string: "wss://www.testwebsocket.com/chat/superchat")! 38 | var request = URLRequest(url: url) 39 | let secKey = String.generateSecKey() 40 | request.prepare(secKey: secKey, url: url, useCompression: true, protocols: ["chat", "superchat"]) 41 | 42 | let decodedHandshake = request.webSocketHandshake() 43 | let data = decodedHandshake.data(using: .utf8)! 44 | let encodedHandshake = Handshake(data: data) 45 | XCTAssertNotNil(encodedHandshake) 46 | XCTAssertEqual(decodedHandshake, encodedHandshake!.rawBodyString) 47 | } 48 | 49 | func testFrameIOAllOccasions() { 50 | let useCompression = [true, false] 51 | let maskData = [true, false] 52 | let opCode: [WebSocket.Opcode] = [.binaryFrame, .textFrame, .continuationFrame, 53 | .connectionCloseFrame, .pingFrame, .pongFrame] 54 | let addPayload = [true, false] 55 | 56 | opCode.forEach { (oc) in 57 | addPayload.forEach { (ap) in 58 | useCompression.forEach { (uc) in 59 | maskData.forEach { (md) in 60 | testFrameIO(addPayload: ap, useCompression: uc, maskData: md, opCode: oc) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | func testFrameIO(addPayload: Bool, useCompression: Bool, maskData: Bool, opCode: WebSocket.Opcode) { 68 | print("payload: \(addPayload), compression: \(useCompression), mask: \(maskData), op: \(opCode)") 69 | let possiblePayload = """ 70 | PAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOAD 71 | PAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOAD 72 | PAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOAD 73 | PAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOAD 74 | PAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOAD 75 | PAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOAD 76 | PAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOADPAYLOAD" 77 | """ 78 | 79 | let payloadString = addPayload ? possiblePayload : "" 80 | let payload = addPayload ? payloadString.data(using: .utf8)! : Data() 81 | let inputFrame = prepareFrame(payload: payload, opCode: opCode, useC: useCompression, mask: maskData) 82 | let inputFrameData = Frame.encode(inputFrame) 83 | 84 | let result = Frame.decode(from: inputFrameData.unsafeBuffer(), fromOffset: 0) 85 | XCTAssertNotNil(result) 86 | let outputFrame = result!.0 87 | 88 | if outputFrame.isMasked && outputFrame.fin { 89 | outputFrame.payload.unmask(with: outputFrame.mask) 90 | } 91 | 92 | let outputString = String(data: outputFrame.payload, encoding: .utf8) 93 | XCTAssertNotNil(outputString) 94 | XCTAssertEqual(payloadString, outputString) 95 | } 96 | 97 | fileprivate func prepareFrame(payload: Data, opCode: WebSocket.Opcode, useC: Bool, mask: Bool) -> Frame { 98 | var payload = payload 99 | 100 | let frame = Frame(fin: true, opCode: opCode) 101 | frame.rsv1 = useC 102 | frame.isMasked = mask 103 | frame.mask = Data.randomMask() 104 | 105 | frame.payload = payload 106 | 107 | if frame.isMasked { 108 | frame.payload.mask(with: frame.mask) 109 | } 110 | 111 | frame.payloadLength = UInt64(frame.payload.count) 112 | 113 | return frame 114 | } 115 | } 116 | --------------------------------------------------------------------------------