├── .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 |
6 |
7 |
8 |
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 |
--------------------------------------------------------------------------------