├── .gitignore
├── .jazzy.yaml
├── .swift-version
├── .travis.yml
├── AutobahnTests.md
├── LICENSE.txt
├── Package.swift
├── Package@swift-4.swift
├── README.md
├── Sources
└── KituraWebSocket
│ ├── WSConnectionUpgradeFactory.swift
│ ├── WSFrame.swift
│ ├── WSFrameParser.swift
│ ├── WSServerRequest.swift
│ ├── WSSocketProcessor.swift
│ ├── WebSocket.swift
│ ├── WebSocketCloseReasonCode.swift
│ ├── WebSocketConnection.swift
│ ├── WebSocketError.swift
│ └── WebSocketService.swift
├── Tests
├── KituraWebSocketTests
│ ├── BasicTests.swift
│ ├── ComplexTests.swift
│ ├── ConnectionCleanupTests.swift
│ ├── KituraTest+Frames.swift
│ ├── KituraTest.swift
│ ├── PrintLogger.swift
│ ├── ProtocolErrorTests.swift
│ ├── TestLinuxSafeguard.swift
│ ├── TestWebSocketService.swift
│ └── UpgradeErrors.swift
└── LinuxMain.swift
└── docs
├── Classes.html
├── Classes
├── WSConnectionUpgradeFactory.html
├── WebSocket.html
└── WebSocketConnection.html
├── Enums.html
├── Enums
├── WebSocketCloseReasonCode.html
└── WebSocketError.html
├── Protocols.html
├── Protocols
└── WebSocketService.html
├── badge.svg
├── css
├── highlight.css
└── jazzy.css
├── docsets
├── KituraWebSocket.docset
│ └── Contents
│ │ ├── Info.plist
│ │ └── Resources
│ │ ├── Documents
│ │ ├── Classes.html
│ │ ├── Classes
│ │ │ ├── WSConnectionUpgradeFactory.html
│ │ │ ├── WebSocket.html
│ │ │ └── WebSocketConnection.html
│ │ ├── Enums.html
│ │ ├── Enums
│ │ │ ├── WebSocketCloseReasonCode.html
│ │ │ └── WebSocketError.html
│ │ ├── Protocols.html
│ │ ├── Protocols
│ │ │ └── WebSocketService.html
│ │ ├── css
│ │ │ ├── highlight.css
│ │ │ └── jazzy.css
│ │ ├── img
│ │ │ ├── carat.png
│ │ │ ├── dash.png
│ │ │ ├── gh.png
│ │ │ └── spinner.gif
│ │ ├── index.html
│ │ ├── js
│ │ │ ├── jazzy.js
│ │ │ ├── jazzy.search.js
│ │ │ ├── jquery.min.js
│ │ │ ├── lunr.min.js
│ │ │ └── typeahead.jquery.js
│ │ └── search.json
│ │ └── docSet.dsidx
└── KituraWebSocket.tgz
├── img
├── carat.png
├── dash.png
├── gh.png
└── spinner.gif
├── index.html
├── js
├── jazzy.js
├── jazzy.search.js
├── jquery.min.js
├── lunr.min.js
└── typeahead.jquery.js
├── search.json
└── undocumented.json
/.gitignore:
--------------------------------------------------------------------------------
1 | Package.resolved
2 | .build
3 | build
4 | .vagrant
5 | Packages*
6 | *.pkg
7 | Kitura-WebSocket.xcodeproj
8 | *.DS_Store
9 |
--------------------------------------------------------------------------------
/.jazzy.yaml:
--------------------------------------------------------------------------------
1 | module: KituraWebSocket
2 | author: IBM & Kitura project authors
3 | github_url: https://github.com/Kitura/Kitura-WebSocket/
4 |
5 | theme: fullwidth
6 | clean: true
7 | exclude: [Packages]
8 |
9 | readme: README.md
10 |
11 | skip_undocumented: false
12 | hide_documentation_coverage: false
13 |
14 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.1
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Travis CI build file.
2 |
3 | # whitelist (branches that should be built)
4 | branches:
5 | only:
6 | - master
7 | - /^issue.*$/
8 |
9 | # the matrix of builds should cover each combination of Swift version
10 | # and platform that is supported. The version of Swift used is specified
11 | # by .swift-version, unless SWIFT_SNAPSHOT is specified.
12 | matrix:
13 | include:
14 | - os: linux
15 | dist: xenial
16 | sudo: required
17 | services: docker
18 | env: DOCKER_IMAGE=swift:4.0.3 SWIFT_SNAPSHOT=4.0.3
19 | - os: linux
20 | dist: xenial
21 | sudo: required
22 | services: docker
23 | env: DOCKER_IMAGE=swift:4.1.3 SWIFT_SNAPSHOT=4.1.3
24 | - os: linux
25 | dist: xenial
26 | sudo: required
27 | services: docker
28 | env: DOCKER_IMAGE=swift:4.2.4 SWIFT_SNAPSHOT=4.2.4
29 | - os: linux
30 | dist: xenial
31 | sudo: required
32 | services: docker
33 | env: DOCKER_IMAGE=swift:5.0.3-xenial SWIFT_SNAPSHOT=5.0.3
34 | - os: linux
35 | dist: xenial
36 | sudo: required
37 | services: docker
38 | env: DOCKER_IMAGE=swift:5.1
39 | - os: linux
40 | dist: xenial
41 | sudo: required
42 | services: docker
43 | env: DOCKER_IMAGE=swift:5.1 SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT
44 | - os: osx
45 | osx_image: xcode9.2
46 | sudo: required
47 | env: SWIFT_SNAPSHOT=4.0.3
48 | - os: osx
49 | osx_image: xcode9.4
50 | sudo: required
51 | env: SWIFT_SNAPSHOT=4.1.2
52 | - os: osx
53 | osx_image: xcode10.1
54 | sudo: required
55 | env: SWIFT_SNAPSHOT=4.2.1
56 | - os: osx
57 | osx_image: xcode10.2
58 | sudo: required
59 | env: SWIFT_SNAPSHOT=5.0.1 JAZZY_ELIGIBLE=true
60 | - os: osx
61 | osx_image: xcode11
62 | sudo: required
63 | - os: osx
64 | osx_image: xcode11
65 | sudo: required
66 | env: SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT
67 |
68 | before_install:
69 | - git clone https://github.com/Kitura/Package-Builder.git
70 |
71 | script:
72 | - ./Package-Builder/build-package.sh -projectDir $TRAVIS_BUILD_DIR
73 |
--------------------------------------------------------------------------------
/AutobahnTests.md:
--------------------------------------------------------------------------------
1 | # WebSockets Autobahn-Testsuite
2 |
3 | This document will take you through the steps to test Kitura-Websockets using [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite).
4 |
5 | ### Creating a EchoServer
6 | These tests are run against a WebSocket EchoServer so we must first set one up.
7 |
8 | 1. Create a swift project:
9 | ```
10 | mkdir ~/WebSocketEchoServer
11 | cd ~/WebSocketEchoServer
12 | swift package init --type executable
13 | open Package.swift
14 | ```
15 |
16 | 2. Inside the `Package.swift` replace the text with:
17 | ```swift
18 | // swift-tools-version:4.0
19 | import PackageDescription
20 |
21 | let package = Package(
22 | name: "WebSocketEchoServer",
23 | dependencies: [
24 | .package(url: "https://github.com/Kitura/Kitura.git", from: "2.3.0"),
25 | .package(url: "https://github.com/Kitura/HeliumLogger.git", .upToNextMinor(from: "1.7.0")),
26 | .package(url: "https://github.com/Kitura/Kitura-WebSocket.git", from: "2.0.0")
27 | ],
28 | targets: [
29 | .target(
30 | name: "WebSocketEchoServer",
31 | dependencies: ["Kitura", "HeliumLogger", "Kitura-WebSocket"]),
32 | ]
33 | )
34 | ```
35 | 3. Open the projects main.swift file:
36 | ```
37 | open ~/WebSocketEchoServer/Sources/WebSocketEchoServer/main.swift
38 | ```
39 | 4. Inside the `main.swift` file replace `print("Hello, world!")` with:
40 | ```swift
41 | import Foundation
42 | import Kitura
43 | import KituraWebSocket
44 | import HeliumLogger
45 |
46 | // Using an implementation for a Logger
47 | HeliumLogger.use(.info)
48 |
49 | let router = Router()
50 |
51 | WebSocket.register(service: EchoService(), onPath: "/")
52 |
53 | let port = 9001
54 |
55 | Kitura.addHTTPServer(onPort: port, with: router)
56 | Kitura.run()
57 | ```
58 | 5. Create the `EchoService` file
59 | ```
60 | cd ~/WebSocketEchoServer/Sources/WebSocketEchoServer/
61 | touch EchoService.swift
62 | open EchoService.swift
63 | ```
64 | 6. Inside `EchoService` add the following code:
65 | ```swift
66 | import Foundation
67 |
68 | import KituraWebSocket
69 | import LoggerAPI
70 |
71 | class EchoService: WebSocketService {
72 |
73 | public func connected(connection: WebSocketConnection) {}
74 |
75 | public func disconnected(connection: WebSocketConnection, reason: WebSocketCloseReasonCode) {}
76 |
77 | public func received(message: Data, from: WebSocketConnection) {
78 | from.send(message: message)
79 | }
80 |
81 | public func received(message: String, from: WebSocketConnection) {
82 | Log.info("Got message \(message)... sending it back")
83 | from.send(message: message)
84 | }
85 | }
86 | ```
87 | 7. Start the EchoServer:
88 | ```
89 | cd ~/WebSocketEchoServer
90 | swift build
91 | .build/x86_64-apple-macosx10.10/debug/WebSocketEchoServer
92 | ```
93 |
94 | ### Install and run the test suite
95 | Open a new terminal window and enter:
96 | ```
97 | pip install autobahntestsuite
98 | cd ~
99 | mkdir testAutobahn
100 | cd testAutobahn
101 | wstest -m fuzzingclient
102 | ```
103 |
104 | This will run the autobahn tests against a server running on port 9001.
105 | To change the tests inside `testAutobahn`:
106 | `open fuzzingclient.json`
107 |
108 | In here you can select specific tests to include or exclude.
109 |
110 | To view the results `open ~/testAutobahn/reports/servers/index.html`
111 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | /**
5 | * Copyright IBM Corporation 2016, 2017
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | **/
19 |
20 | import PackageDescription
21 |
22 | let package = Package(
23 | name: "Kitura-WebSocket",
24 | products: [
25 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
26 | .library(
27 | name: "Kitura-WebSocket",
28 | targets: ["KituraWebSocket"]),
29 | ],
30 | dependencies: [
31 | // Dependencies declare other packages that this package depends on.
32 | // .package(url: /* package url */, from: "1.0.0"),
33 | .package(url: "https://github.com/Kitura/Kitura-net.git", from: "2.4.200"),
34 | .package(url: "https://github.com/Kitura/BlueCryptor.git", from: "1.0.200"),
35 |
36 | ],
37 | targets: [
38 | // Targets are the basic building blocks of a package. A target defines a module or a test suite.
39 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
40 | .target(
41 | name: "KituraWebSocket",
42 | dependencies: ["KituraNet", "Cryptor"]),
43 | .testTarget(
44 | name: "KituraWebSocketTests",
45 | dependencies: ["KituraWebSocket"]),
46 | ]
47 | )
48 |
--------------------------------------------------------------------------------
/Package@swift-4.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | /**
5 | * Copyright IBM Corporation and the Kitura project authors 2016-2020
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | **/
19 |
20 | import PackageDescription
21 |
22 | let package = Package(
23 | name: "Kitura-WebSocket",
24 | products: [
25 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
26 | .library(
27 | name: "Kitura-WebSocket",
28 | targets: ["KituraWebSocket"]),
29 | ],
30 | dependencies: [
31 | // Dependencies declare other packages that this package depends on.
32 | // .package(url: /* package url */, from: "1.0.0"),
33 | .package(url: "https://github.com/Kitura/Kitura-net.git", from: "2.4.200"),
34 | .package(url: "https://github.com/Kitura/BlueCryptor.git", from: "1.0.200"),
35 |
36 | ],
37 | targets: [
38 | // Targets are the basic building blocks of a package. A target defines a module or a test suite.
39 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
40 | .target(
41 | name: "KituraWebSocket",
42 | dependencies: ["KituraNet", "Cryptor"]),
43 | .testTarget(
44 | name: "KituraWebSocketTests",
45 | dependencies: ["KituraWebSocket"]),
46 | ]
47 | )
48 |
--------------------------------------------------------------------------------
/Sources/KituraWebSocket/WSConnectionUpgradeFactory.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright IBM Corporation 2016-2017
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import Foundation
18 |
19 | import Cryptor
20 | import KituraNet
21 |
22 | /// The implementation of the ConnectionUpgradeFactory protocol for the WebSocket protocol.
23 | /// Participates in the HTTP protocol upgrade process when upgarding to the WebSocket protocol.
24 | public class WSConnectionUpgradeFactory: ConnectionUpgradeFactory {
25 | private var registry = Dictionary()
26 |
27 | private let wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
28 |
29 | /// The name of the protocol supported by this `ConnectionUpgradeFactory`.
30 | public let name = "websocket"
31 |
32 | init() {
33 | ConnectionUpgrader.register(factory: self)
34 | }
35 |
36 | /// "Upgrade" a connection to the WebSocket protocol. Invoked by the KituraNet.ConnectionUpgrader when
37 | /// an upgrade request is being handled.
38 | ///
39 | /// - Parameter handler: The `IncomingSocketHandler` that is handling the connection being upgraded.
40 | /// - Parameter request: The `ServerRequest` object of the incoming "upgrade" request.
41 | /// - Parameter response: The `ServerResponse` object that will be used to send the response of the "upgrade" request.
42 | ///
43 | /// - Returns: A tuple of the created `WSSocketProcessor` and a message to send as the body of the response to
44 | /// the upgrade request. The `WSSocketProcessor` will be nil if the upgrade request wasn't successful.
45 | /// If the message is nil, the response will not contain a body.
46 | public func upgrade(handler: IncomingSocketHandler, request: ServerRequest, response: ServerResponse) -> (IncomingSocketProcessor?, String?) {
47 |
48 | guard let protocolVersion = request.headers["Sec-WebSocket-Version"] else {
49 | return (nil, "Sec-WebSocket-Version header missing in the upgrade request")
50 | }
51 |
52 | guard protocolVersion[0] == "13" else {
53 | response.headers["Sec-WebSocket-Version"] = ["13"]
54 | return (nil, "Only WebSocket protocol version 13 is supported")
55 | }
56 |
57 | guard let securityKey = request.headers["Sec-WebSocket-Key"] else {
58 | return (nil, "Sec-WebSocket-Key header missing in the upgrade request")
59 | }
60 |
61 | guard let service = registry[request.urlURL.path] else {
62 | return (nil, "No service has been registered for the path \(request.urlURL.path)")
63 | }
64 |
65 | let sha1 = Digest(using: .sha1)
66 | let sha1Bytes = sha1.update(string: securityKey[0] + wsGUID)!.final()
67 | let sha1Data = Data(bytes: sha1Bytes, count: sha1Bytes.count)
68 | response.headers["Sec-WebSocket-Accept"] =
69 | [sha1Data.base64EncodedString(options: .lineLength64Characters)]
70 | response.headers["Sec-WebSocket-Protocol"] = request.headers["Sec-WebSocket-Protocol"]
71 |
72 | let connection = WebSocketConnection(request: request, service: service)
73 | let processor = WSSocketProcessor(connection: connection)
74 | connection.processor = processor
75 |
76 | return (processor, nil)
77 | }
78 |
79 | func register(service: WebSocketService, onPath: String) {
80 | let path: String
81 | if onPath.hasPrefix("/") {
82 | path = onPath
83 | }
84 | else {
85 | path = "/" + onPath
86 | }
87 | registry[path] = service
88 | }
89 |
90 | func unregister(path thePath: String) {
91 | let path: String
92 | if thePath.hasPrefix("/") {
93 | path = thePath
94 | }
95 | else {
96 | path = "/" + thePath
97 | }
98 | registry.removeValue(forKey: path)
99 | }
100 |
101 | /// Clear the `WebSocketService` registry. Used in testing.
102 | func clear() {
103 | registry.removeAll()
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/KituraWebSocket/WSFrame.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright IBM Corporation 2015
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import Foundation
18 |
19 | #if os(Linux)
20 | import Glibc
21 | #else
22 | import Darwin
23 | #endif
24 |
25 | struct WSFrame {
26 | var finalFrame = false
27 |
28 | enum FrameOpcode: Int {
29 | case continuation = 0, text = 1, binary = 2, close = 8, ping = 9, pong = 10, unknown = -1
30 | }
31 | var opCode = FrameOpcode.unknown
32 |
33 | let payload = NSMutableData()
34 |
35 | static func createFrameHeader(finalFrame: Bool, opCode: FrameOpcode, payloadLength: Int, buffer: NSMutableData) {
36 |
37 | var bytes: [UInt8] = [(finalFrame ? 0x80 : 0x00) | UInt8(opCode.rawValue), 0, 0,0,0,0,0,0,0,0]
38 | var length = 1
39 |
40 | if payloadLength < 126 {
41 | bytes[1] = UInt8(payloadLength)
42 | length += 1
43 | } else if payloadLength <= Int(UInt16.max) {
44 | bytes[1] = 126
45 | let tempPayloadLengh = UInt16(payloadLength)
46 | var payloadLengthUInt16: UInt16
47 | #if os(Linux)
48 | payloadLengthUInt16 = Glibc.htons(tempPayloadLengh)
49 | #else
50 | payloadLengthUInt16 = CFSwapInt16HostToBig(tempPayloadLengh)
51 | #endif
52 | let asBytes = UnsafeMutablePointer(&payloadLengthUInt16)
53 | #if swift(>=4.1)
54 | (UnsafeMutableRawPointer(mutating: bytes)+length+1).copyMemory(from: asBytes, byteCount: 2)
55 | #else
56 | (UnsafeMutableRawPointer(mutating: bytes)+length+1).copyBytes(from: asBytes, count: 2)
57 | #endif
58 | length += 3
59 | } else {
60 | bytes[1] = 127
61 | let tempPayloadLengh = UInt32(payloadLength)
62 | var payloadLengthUInt32: UInt32
63 | #if os(Linux)
64 | payloadLengthUInt32 = Glibc.htonl(tempPayloadLengh)
65 | #else
66 | payloadLengthUInt32 = CFSwapInt32HostToBig(tempPayloadLengh)
67 | #endif
68 | let asBytes = UnsafeMutablePointer(&payloadLengthUInt32)
69 | #if swift(>=4.1)
70 | (UnsafeMutableRawPointer(mutating: bytes)+length+5).copyMemory(from: asBytes, byteCount: 4)
71 | #else
72 | (UnsafeMutableRawPointer(mutating: bytes)+length+5).copyBytes(from: asBytes, count: 4)
73 | #endif
74 | length += 9
75 | }
76 | buffer.append(bytes, length: length)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Sources/KituraWebSocket/WSFrameParser.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright IBM Corporation 2015
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import Foundation
18 |
19 | #if os(Linux)
20 | import Glibc
21 | #else
22 | import Darwin
23 | #endif
24 |
25 | struct WSFrameParser {
26 |
27 | private enum FrameParsingState {
28 | case initial, opCodeParsed, lengthParsed
29 | }
30 | private var state = FrameParsingState.initial
31 |
32 | var frame = WSFrame()
33 |
34 | var masked = false
35 | private var payloadLength = -1
36 | private var mask: [UInt8] = [0x00, 0x00, 0x00, 0x00]
37 | private let maskSize = 4
38 |
39 | mutating func reset() {
40 | state = .initial
41 | payloadLength = -1
42 | frame.payload.length = 0
43 | }
44 |
45 | mutating func parse(_ buffer: NSData, from: Int) -> (Bool, WebSocketError?, Int) {
46 | var bytesParsed = 0
47 | var byteIndex = from
48 | let bytes = UnsafeRawBufferPointer(start: buffer.bytes, count: buffer.length)
49 |
50 | let length = buffer.length
51 |
52 | while byteIndex < length && payloadLength != frame.payload.length {
53 | switch(state) {
54 | case .initial:
55 | let (error, bytesConsumed) = parseOpCode(bytes: bytes, from: byteIndex)
56 | guard error == nil else { return (false, error, 0) }
57 |
58 | bytesParsed += bytesConsumed
59 | byteIndex += bytesConsumed
60 | state = .opCodeParsed
61 |
62 | case .opCodeParsed:
63 | let (error, bytesConsumed) = parseMaskAndLength(bytes: bytes, from: byteIndex, length: length)
64 | guard error == nil else { return (false, error, 0) }
65 | guard bytesConsumed > 0 else { return (false, error, bytesParsed) }
66 |
67 | bytesParsed += bytesConsumed
68 | byteIndex += bytesConsumed
69 | state = .lengthParsed
70 |
71 | case .lengthParsed:
72 | let bytesConsumed = parsePayload(bytes: bytes, from: byteIndex, length: length)
73 |
74 | bytesParsed += bytesConsumed
75 | byteIndex += bytesConsumed
76 | }
77 | }
78 |
79 | return (payloadLength == frame.payload.length, nil, bytesParsed)
80 | }
81 |
82 | private mutating func parseOpCode(bytes: UnsafeRawBufferPointer, from: Int) -> (WebSocketError?, Int) {
83 | let byte = bytes[from]
84 | // 0x.. is the hexadecimal representation of the bits you would like to check
85 | // e.g.0x0f is 15 which is 00001111 so rawOpCode is the last 4 digits
86 |
87 | // Check RSV (three reserved bits) is equal to 0
88 | let rsv = (byte & 0x70) >> 4
89 | guard rsv == 0 else {
90 | return (.invalidOpCode(byte & 0x7F), 0)
91 | }
92 | frame.finalFrame = byte & 0x80 != 0
93 | let rawOpCode = byte & 0x0f
94 | if let opCode = WSFrame.FrameOpcode(rawValue: Int(rawOpCode)) {
95 | self.frame.opCode = opCode
96 | return (nil, 1)
97 | }
98 | else {
99 | return (.invalidOpCode(rawOpCode), 0)
100 | }
101 | }
102 |
103 | private mutating func parseMaskAndLength(bytes: UnsafeRawBufferPointer, from: Int, length: Int) -> (WebSocketError?, Int) {
104 | let byte = bytes[from]
105 | masked = byte & 0x80 != 0
106 |
107 | guard masked else { return (.unmaskedFrame, 0) }
108 |
109 | var bytesConsumed = 0
110 |
111 | let lengthByte = byte & 0x7f
112 | switch lengthByte {
113 | case 126:
114 | if length - from >= 3 {
115 | // We cannot perform an unaligned load of a 16-bit value from the buffer.
116 | // Instead, create correctly aligned storage for a 16-bit value and copy
117 | // the bytes from the buffer into its storage.
118 | var networkOrderedUInt16 = UInt16(0)
119 | withUnsafeMutableBytes(of: &networkOrderedUInt16) { ptr in
120 | let unalignedUInt16 = UnsafeRawBufferPointer(rebasing: bytes[from+1 ..< from+3])
121 | #if swift(>=4.1)
122 | ptr.copyMemory(from: unalignedUInt16)
123 | #else
124 | ptr.copyBytes(from: unalignedUInt16)
125 | #endif
126 | }
127 | #if os(Linux)
128 | payloadLength = Int(Glibc.ntohs(networkOrderedUInt16))
129 | #else
130 | payloadLength = Int(CFSwapInt16BigToHost(networkOrderedUInt16))
131 | #endif
132 | bytesConsumed += 3
133 | }
134 | case 127:
135 | if length - from >= 9 {
136 | // We cannot perform an unaligned load of a 32-bit value from the buffer.
137 | // Instead, create correctly aligned storage for a 32-bit value and copy
138 | // the bytes from the buffer into its storage.
139 | var networkOrderedUInt32 = UInt32(0)
140 | withUnsafeMutableBytes(of: &networkOrderedUInt32) { ptr in
141 | let unalignedUInt32 = UnsafeRawBufferPointer(rebasing: bytes[from+5 ..< from+9])
142 | #if swift(>=4.1)
143 | ptr.copyMemory(from: unalignedUInt32)
144 | #else
145 | ptr.copyBytes(from: unalignedUInt32)
146 | #endif
147 | }
148 | #if os(Linux)
149 | payloadLength = Int(Glibc.ntohl(networkOrderedUInt32))
150 | #else
151 | payloadLength = Int(CFSwapInt32BigToHost(networkOrderedUInt32))
152 | #endif
153 | bytesConsumed += 9
154 | }
155 | /* Error if length > Int.max */
156 | default:
157 | payloadLength = Int(lengthByte)
158 | bytesConsumed += 1
159 | }
160 |
161 | if bytesConsumed > 0 {
162 | if length - from - bytesConsumed >= maskSize {
163 | for i in 0.. Int {
179 | //let payloadPiece = bytes + from
180 | var unmaskedBytes = [UInt8](repeating: 0, count: 125)
181 | var bytesConsumed: Int = 0
182 |
183 | let bytesToUnmask = min(payloadLength - frame.payload.length, length - from)
184 | var bytesUnmasked = frame.payload.length
185 |
186 | // Loop to unmask the bytes we have in this frame piece
187 | while bytesConsumed < bytesToUnmask {
188 |
189 | let bytesToUnMaskInLoop = min(unmaskedBytes.count, bytesToUnmask-bytesConsumed)
190 |
191 | for index in 0 ..< bytesToUnMaskInLoop {
192 | unmaskedBytes[index] = bytes[from + bytesConsumed] ^ mask[bytesUnmasked % maskSize]
193 | bytesUnmasked += 1
194 | bytesConsumed += 1
195 | }
196 | frame.payload.append(unmaskedBytes, length: bytesToUnMaskInLoop)
197 | }
198 | return bytesConsumed
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/Sources/KituraWebSocket/WSServerRequest.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright IBM Corporation 2017
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import Foundation
18 |
19 | import KituraNet
20 |
21 | /// An internal class used to retain information from the original request that
22 | /// was used to create the WebSocket connection. The ServerRequest from KituraNet
23 | /// may get freed.
24 | class WSServerRequest: ServerRequest {
25 |
26 | /// The set of headers received with the incoming request
27 | let headers = HeadersContainer()
28 |
29 | /// The URL from the request in string form
30 | /// This contains just the path and query parameters starting with '/'
31 | /// Use 'urlURL' for the full URL
32 | @available(*, deprecated, message:
33 | "This contains just the path and query parameters starting with '/'. use 'urlURL' instead")
34 | var urlString: String { return String(data: url, encoding: .utf8) ?? "" }
35 |
36 | /// The URL from the request in UTF-8 form
37 | /// This contains just the path and query parameters starting with '/'
38 | /// Use 'urlURL' for the full URL
39 | let url: Data
40 |
41 | /// The URL from the request
42 | let urlURL: URL
43 |
44 | /// The URL from the request as URLComponents
45 | public let urlComponents: URLComponents
46 |
47 | /// The IP address of the client
48 | let remoteAddress: String
49 |
50 | /// Major version of HTTP of the request
51 | var httpVersionMajor: UInt16? = 1
52 |
53 | /// Minor version of HTTP of the request
54 | var httpVersionMinor: UInt16? = 1
55 |
56 | /// The HTTP Method specified in the request
57 | var method: String = "GET"
58 |
59 | init(request: ServerRequest) {
60 | for (key, values) in request.headers {
61 | headers.append(key, value: values)
62 | }
63 |
64 | url = request.url
65 | urlURL = request.urlURL
66 | urlComponents = URLComponents(url: request.urlURL, resolvingAgainstBaseURL: false) ?? URLComponents()
67 |
68 | remoteAddress = request.remoteAddress
69 | }
70 |
71 | /// Read data from the body of the request
72 | ///
73 | /// - Parameter data: A Data struct to hold the data read in.
74 | ///
75 | /// - Throws: Socket.error if an error occurred while reading from the socket
76 | /// - Returns: The number of bytes read
77 | func read(into data: inout Data) throws -> Int {
78 | return 0
79 | }
80 |
81 | /// Read a string from the body of the request.
82 | ///
83 | /// - Throws: Socket.error if an error occurred while reading from the socket
84 | /// - Returns: An Optional string
85 | func readString() throws -> String? {
86 | return nil
87 | }
88 |
89 |
90 | /// Read all of the data in the body of the request
91 | ///
92 | /// - Parameter data: A Data struct to hold the data read in.
93 | ///
94 | /// - Throws: Socket.error if an error occurred while reading from the socket
95 | /// - Returns: The number of bytes read
96 | func readAllData(into data: inout Data) throws -> Int {
97 | return 0
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/KituraWebSocket/WSSocketProcessor.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright IBM Corporation 2016-2017
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import Foundation
18 |
19 | import KituraNet
20 | import LoggerAPI
21 |
22 | /// The implementation of the `IncomingSocketProcessor` protocol for the WebSocket protocol.
23 | /// Receives data from the `IncomingSocketHandler` for a specific socket and provides APIs
24 | /// upwards for sending data to the client over the socket.
25 | class WSSocketProcessor: IncomingSocketProcessor {
26 | /// A back reference to the `IncomingSocketHandler` processing the socket that
27 | /// this `IncomingDataProcessor` is processing.
28 | public weak var handler: IncomingSocketHandler? {
29 | didSet {
30 | guard handler != nil else { return }
31 | connection.fireConnected()
32 | }
33 | }
34 |
35 | /// The socket if idle will be kept alive until...
36 | public var keepAliveUntil: TimeInterval = 500.0
37 |
38 | /// A flag to indicate that the socket has a request in progress
39 | public var inProgress = true
40 |
41 | private var parser = WSFrameParser()
42 |
43 | private var byteIndex = 0
44 |
45 | private let connection: WebSocketConnection
46 |
47 | init(connection: WebSocketConnection) {
48 | self.connection = connection
49 | }
50 |
51 | /// Process data read from the socket.
52 | ///
53 | /// - Parameter buffer: An NSData object containing the data that was read in
54 | /// and needs to be processed.
55 | ///
56 | /// - Returns: true if the data was processed, false if it needs to be processed later.
57 | public func process(_ buffer: NSData) -> Bool {
58 | let length = buffer.length
59 |
60 | while byteIndex < length {
61 | let (completed, error, bytesConsumed) = parser.parse(buffer, from: byteIndex)
62 |
63 | guard error == nil else {
64 | // What should be done if there is an error?
65 | Log.error("Error parsing frame. \(error!)")
66 | connection.close(reason: .protocolError, description: error?.description)
67 | return true
68 | }
69 |
70 | if bytesConsumed == 0 {
71 | break
72 | }
73 |
74 | byteIndex += bytesConsumed
75 |
76 | if completed {
77 | connection.received(frame: parser.frame)
78 | parser.reset()
79 | }
80 | }
81 |
82 | let finishedBuffer: Bool
83 | if byteIndex >= length {
84 | finishedBuffer = true
85 | byteIndex = 0
86 | }
87 | else {
88 | finishedBuffer = false
89 | }
90 | return finishedBuffer
91 | }
92 |
93 | /// Write data to the socket
94 | ///
95 | /// - Parameter from: An NSData object containing the bytes to be written to the socket.
96 | public func write(from data: NSData) {
97 | handler?.write(from: data)
98 | }
99 |
100 | /// Write a sequence of bytes in an array to the socket
101 | ///
102 | /// - Parameter from: An UnsafeRawPointer to the sequence of bytes to be written to the socket.
103 | /// - Parameter length: The number of bytes to write to the socket.
104 | public func write(from bytes: UnsafeRawPointer, length: Int) {
105 | handler?.write(from: bytes, length: length)
106 | }
107 |
108 | /// Close the socket and mark this handler as no longer in progress.
109 | public func close() {
110 | if connection.active {
111 | connection.connectionClosed(reason: .closedAbnormally)
112 | }
113 | handler?.prepareToClose()
114 | }
115 |
116 | /// Called by the `IncomingSocketHandler` to tell us that the socket has been closed.
117 | public func socketClosed() {
118 | connection.connectionClosed(reason: .noReasonCodeSent)
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Sources/KituraWebSocket/WebSocket.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright IBM Corporation 2016, 2017
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import KituraNet
18 |
19 | /// Main class for the Kitura-WebSocket API. Used to register `WebSocketService` classes
20 | /// that will handle WebSocket connections for specific paths.
21 | public class WebSocket {
22 | static let factory = WSConnectionUpgradeFactory()
23 |
24 | /// Register a `WebSocketService` for a specific path
25 | ///
26 | /// - Parameter service: The `WebSocketService` being registered.
27 | /// - Parameter onPath: The path that will be in the HTTP "Upgrade" request. Used
28 | /// to connect the upgrade request with a specific `WebSocketService`
29 | /// Caps-insensitive.
30 | public static func register(service: WebSocketService, onPath path: String) {
31 | factory.register(service: service, onPath: path.lowercased())
32 | }
33 |
34 | /// Unregister a `WebSocketService` for a specific path
35 | ///
36 | /// - Parameter path: The path on which the `WebSocketService` being unregistered,
37 | /// was registered on.
38 | public static func unregister(path: String) {
39 | factory.unregister(path: path.lowercased())
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/KituraWebSocket/WebSocketCloseReasonCode.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright IBM Corporation 2015
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /// The `WebSocketCloseReasonCode` enum defines the set of reason codes that a
18 | /// WebSocket application can send/receive when a connection is closed.
19 | public enum WebSocketCloseReasonCode {
20 | /// Closed abnormally (1006)
21 | case closedAbnormally
22 |
23 | /// An extension was missing that was required (1010)
24 | case extensionMissing
25 |
26 | /// Server is going away (1001)
27 | case goingAway
28 |
29 | /// Data within a message was invalid (1007)
30 | case invalidDataContents
31 |
32 | /// Message was of the incorrect type (binary/text) (1003)
33 | case invalidDataType
34 |
35 | /// Message was too large (1009)
36 | case messageTooLarge
37 |
38 | /// Closed normally (1000)
39 | case normal
40 |
41 | /// No reason code sent with the close request (1005)
42 | case noReasonCodeSent
43 |
44 | /// A policy violation occurred (1008)
45 | case policyViolation
46 |
47 | /// A protocol error occurred (1002)
48 | case protocolError
49 |
50 | /// The server had an error with the request (1011)
51 | case serverError
52 |
53 | /// This reason code is used to send application defined reason codes.
54 | case userDefined(UInt16)
55 |
56 | /// Get the sixteen bit integer code for a WebSocketCloseReasonCode instance
57 | public func code() -> UInt16 {
58 | switch self {
59 | case .closedAbnormally: return 1006
60 | case .extensionMissing: return 1010
61 | case .goingAway: return 1001
62 | case .invalidDataContents: return 1007
63 | case .invalidDataType: return 1003
64 | case .messageTooLarge: return 1009
65 | case .normal: return 1000
66 | case .noReasonCodeSent: return 1005
67 | case .policyViolation: return 1008
68 | case .protocolError: return 1002
69 | case .serverError: return 1011
70 | case .userDefined(let userCode): return userCode
71 | }
72 | }
73 |
74 | /// Convert a sixteen bit WebSocket close frame reason code to a WebSocketCloseReasonCode instance
75 | public static func from(code reasonCode: UInt16) -> WebSocketCloseReasonCode {
76 | switch reasonCode {
77 | case 1000: return .normal
78 | case 1001: return .goingAway
79 | case 1002: return .protocolError
80 | case 1003: return .invalidDataType
81 | case 1007: return .invalidDataContents
82 | case 1008: return .policyViolation
83 | case 1009: return .messageTooLarge
84 | case 1010: return .extensionMissing
85 | case 1011: return .serverError
86 | default:
87 | if reasonCode < 3000 {
88 | return .protocolError
89 | } else {
90 | return .userDefined(reasonCode)
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Sources/KituraWebSocket/WebSocketError.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright IBM Corporation 2016
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import Foundation
18 |
19 | /// An error enum used when throwing errors within KituraWebSocket.
20 | public enum WebSocketError: Error {
21 |
22 | /// An invalid opcode was received in a WebSocket frame
23 | case invalidOpCode(UInt8)
24 |
25 | /// A frame was received that had an unmasked payload
26 | case unmaskedFrame
27 | }
28 |
29 |
30 | extension WebSocketError: CustomStringConvertible {
31 | /// Generate a printable version of this enum.
32 | public var description: String {
33 | switch self {
34 | case .invalidOpCode(let opCode):
35 | return "Parsed a frame with an invalid operation code of \(opCode)"
36 | case .unmaskedFrame:
37 | return "Received a frame from a client that wasn't masked"
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/KituraWebSocket/WebSocketService.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright IBM Corporation 2016-2017
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import Foundation
18 |
19 | /// The `WebSocketService` protocol is implemented by classes that wish to be WebSocket server side
20 | /// end points. An instance of the `WebSocketService` protocol is the server side of a WebSocket connection.
21 | /// There can be many WebSocket connections connected to a single `WebSocketService` protocol instance.
22 | /// The protocol is a set of callbacks that are invoked when various events occur.
23 | public protocol WebSocketService: class {
24 |
25 | /// Called when a WebSocket client connects to the server and is connected to a specific
26 | /// `WebSocketService`.
27 | ///
28 | /// - Parameter connection: The `WebSocketConnection` object that represents the client's
29 | /// connection to this `WebSocketService`
30 | func connected(connection: WebSocketConnection)
31 |
32 | /// Called when a WebSocket client disconnects from the server.
33 | ///
34 | /// - Parameter connection: The `WebSocketConnection` object that represents the connection that
35 | /// was disconnected from this `WebSocketService`.
36 | /// - Parameter reason: The `WebSocketCloseReasonCode` that describes why the client disconnected.
37 | func disconnected(connection: WebSocketConnection, reason: WebSocketCloseReasonCode)
38 |
39 | /// Called when a WebSocket client sent a binary message to the server to this `WebSocketService`.
40 | ///
41 | /// - Parameter message: A Data struct containing the bytes of the binary message sent by the client.
42 | /// - Parameter client: The `WebSocketConnection` object that represents the connection over which
43 | /// the client sent the message to this `WebSocketService`
44 | func received(message: Data, from: WebSocketConnection)
45 |
46 | /// Called when a WebSocket client sent a text message to the server to this `WebSocketService`.
47 | ///
48 | /// - Parameter message: A String containing the text message sent by the client.
49 | /// - Parameter client: The `WebSocketConnection` object that represents the connection over which
50 | /// the client sent the message to this `WebSocketService`
51 | func received(message: String, from: WebSocketConnection)
52 |
53 | /// The time in seconds that a connection must be unresponsive to be automatically closed by the server. If the WebSocket server has not received any messages in the first half of the timeout time it will ping the connection. If a pong is not received in the remaining half of the timeout, the connection will be closed with a 1006 (connection closed abnormally) status code. The `connectionTimeout` defaults to `nil`, meaning no connection cleanup will take place.
54 | var connectionTimeout: Int? { get }
55 | }
56 |
57 | public extension WebSocketService {
58 | /// Default computed value for `connectionTimeout` that returns `nil`.
59 | var connectionTimeout: Int? {
60 | return nil
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Tests/KituraWebSocketTests/ComplexTests.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright IBM Corporation 2016
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | import XCTest
18 | import Foundation
19 |
20 | import LoggerAPI
21 | @testable import KituraWebSocket
22 |
23 | class ComplexTests: KituraTest {
24 |
25 | static var allTests: [(String, (ComplexTests) -> () throws -> Void)] {
26 | return [
27 | ("testBinaryShortAndMediumFrames", testBinaryShortAndMediumFrames),
28 | ("testBinaryTwoShortFrames", testBinaryTwoShortFrames),
29 | ("testPingBetweenBinaryFrames", testPingBetweenBinaryFrames),
30 | ("testPingBetweenTextFrames", testPingBetweenTextFrames),
31 | ("testTextShortAndMediumFrames", testTextShortAndMediumFrames),
32 | ("testTextTwoShortFrames", testTextTwoShortFrames)
33 | ]
34 | }
35 |
36 | func testBinaryShortAndMediumFrames() {
37 | register(closeReason: .noReasonCodeSent)
38 |
39 | performServerTest() { expectation in
40 |
41 | var bytes = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e]
42 |
43 | let shortBinaryPayload = NSMutableData(bytes: &bytes, length: bytes.count)
44 |
45 | let mediumBinaryPayload = NSMutableData(bytes: &bytes, length: bytes.count)
46 | repeat {
47 | mediumBinaryPayload.append(mediumBinaryPayload.bytes, length: mediumBinaryPayload.length)
48 | } while mediumBinaryPayload.length < 1000
49 |
50 | let expectedBinaryPayload = NSMutableData()
51 | expectedBinaryPayload.append(shortBinaryPayload.bytes, length: shortBinaryPayload.length)
52 | expectedBinaryPayload.append(mediumBinaryPayload.bytes, length: mediumBinaryPayload.length)
53 |
54 | self.performTest(framesToSend: [(false, self.opcodeBinary, shortBinaryPayload), (true, self.opcodeContinuation, mediumBinaryPayload)],
55 | expectedFrames: [(true, self.opcodeBinary, expectedBinaryPayload)],
56 | expectation: expectation)
57 | }
58 | }
59 |
60 | func testBinaryTwoShortFrames() {
61 | register(closeReason: .noReasonCodeSent)
62 |
63 | performServerTest() { expectation in
64 |
65 | var bytes = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e]
66 |
67 | let binaryPayload = NSMutableData(bytes: &bytes, length: bytes.count)
68 |
69 | let expectedBinaryPayload = NSMutableData(bytes: &bytes, length: bytes.count)
70 | expectedBinaryPayload.append(&bytes, length: bytes.count)
71 |
72 | self.performTest(framesToSend: [(false, self.opcodeBinary, binaryPayload), (true, self.opcodeContinuation, binaryPayload)],
73 | expectedFrames: [(true, self.opcodeBinary, expectedBinaryPayload)],
74 | expectation: expectation)
75 | }
76 | }
77 |
78 | func testPingBetweenBinaryFrames() {
79 | register(closeReason: .noReasonCodeSent)
80 |
81 | performServerTest() { expectation in
82 |
83 | var bytes = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e]
84 |
85 | let binaryPayload = NSMutableData(bytes: &bytes, length: bytes.count)
86 |
87 | let expectedBinaryPayload = NSMutableData(bytes: &bytes, length: bytes.count)
88 | expectedBinaryPayload.append(&bytes, length: bytes.count)
89 |
90 | let pingPayload = self.payload(text: "Testing, testing 1,2,3")
91 |
92 | self.performTest(framesToSend: [(false, self.opcodeBinary, binaryPayload),
93 | (true, self.opcodePing, pingPayload),
94 | (true, self.opcodeContinuation, binaryPayload)],
95 | expectedFrames: [(true, self.opcodePong, pingPayload), (true, self.opcodeBinary, expectedBinaryPayload)],
96 | expectation: expectation)
97 | }
98 | }
99 |
100 | func testPingBetweenTextFrames() {
101 | register(closeReason: .noReasonCodeSent)
102 |
103 | performServerTest() { expectation in
104 |
105 | let text = "Testing, testing 1, 2, 3. "
106 |
107 | let textPayload = self.payload(text: text)
108 |
109 | let textExpectedPayload = self.payload(text: text + text)
110 |
111 | let pingPayload = self.payload(text: "Testing, testing 1,2,3")
112 |
113 | self.performTest(framesToSend: [(false, self.opcodeText, textPayload),
114 | (true, self.opcodePing, pingPayload),
115 | (true, self.opcodeContinuation, textPayload)],
116 | expectedFrames: [(true, self.opcodePong, pingPayload), (true, self.opcodeText, textExpectedPayload)],
117 | expectation: expectation)
118 | }
119 | }
120 |
121 | func testTextShortAndMediumFrames() {
122 | register(closeReason: .noReasonCodeSent)
123 |
124 | performServerTest() { expectation in
125 |
126 | let shortText = "Testing, testing 1, 2, 3. "
127 | let shortTextPayload = self.payload(text: shortText)
128 |
129 | var mediumText = ""
130 | repeat {
131 | mediumText += "Testing, testing 1,2,3. "
132 | } while mediumText.count < 1000
133 | let mediumTextPayload = self.payload(text: mediumText)
134 |
135 | let textExpectedPayload = self.payload(text: shortText + mediumText)
136 |
137 | self.performTest(framesToSend: [(false, self.opcodeText, shortTextPayload), (true, self.opcodeContinuation, mediumTextPayload)],
138 | expectedFrames: [(true, self.opcodeText, textExpectedPayload)],
139 | expectation: expectation)
140 | }
141 | }
142 |
143 | func testTextTwoShortFrames() {
144 | register(closeReason: .noReasonCodeSent)
145 |
146 | performServerTest() { expectation in
147 |
148 | let text = "Testing, testing 1, 2, 3. "
149 |
150 | let textPayload = self.payload(text: text)
151 |
152 | let textExpectedPayload = self.payload(text: text + text)
153 |
154 | self.performTest(framesToSend: [(false, self.opcodeText, textPayload), (true, self.opcodeContinuation, textPayload)],
155 | expectedFrames: [(true, self.opcodeText, textExpectedPayload)],
156 | expectation: expectation)
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/Tests/KituraWebSocketTests/ConnectionCleanupTests.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright IBM Corporation 2016
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | import XCTest
18 | import Foundation
19 |
20 | import LoggerAPI
21 | @testable import KituraWebSocket
22 | import Socket
23 |
24 | class ConnectionCleanupTests: KituraTest {
25 |
26 | static var allTests: [(String, (ConnectionCleanupTests) -> () throws -> Void)] {
27 | return [
28 | ("testNilConnectionTimeOut", testNilConnectionTimeOut),
29 | ("testSingleConnectionTimeOut", testSingleConnectionTimeOut),
30 | ("testPingKeepsConnectionAlive", testPingKeepsConnectionAlive),
31 | ("testMultiConnectionTimeOut", testMultiConnectionTimeOut),
32 | ("testProccessorClose", testProccessorClose),
33 | ]
34 | }
35 |
36 | func testNilConnectionTimeOut() {
37 | let service = register(closeReason: .noReasonCodeSent, connectionTimeout: nil)
38 |
39 | performServerTest() { expectation in
40 | XCTAssertEqual(service.connections.count, 0, "Connections left on service at start of test")
41 | guard let socket = self.sendUpgradeRequest(toPath: self.servicePath, usingKey: self.secWebKey) else { return }
42 | let _ = self.checkUpgradeResponse(from: socket, forKey: self.secWebKey)
43 | usleep(5000)
44 | XCTAssertEqual(service.connections.count, 1, "Failed to create connection to service")
45 | usleep(1500000)
46 | XCTAssertEqual(service.connections.count, 1, "Stale connection was unexpectedly cleaned up")
47 | socket.close()
48 | usleep(1000)
49 | expectation.fulfill()
50 | }
51 | }
52 |
53 | func testSingleConnectionTimeOut() {
54 | let service = register(closeReason: .closedAbnormally, connectionTimeout: 1)
55 |
56 | performServerTest() { expectation in
57 | XCTAssertEqual(service.connections.count, 0, "Connections left on service at start of test")
58 | guard let socket = self.sendUpgradeRequest(toPath: self.servicePath, usingKey: self.secWebKey) else { return }
59 | let _ = self.checkUpgradeResponse(from: socket, forKey: self.secWebKey)
60 | usleep(5000)
61 | XCTAssertEqual(service.connections.count, 1, "Failed to create connection to service")
62 | usleep(1500000)
63 | XCTAssertEqual(service.connections.count, 0, "Stale connection was not cleaned up")
64 | socket.close()
65 | usleep(1000)
66 | expectation.fulfill()
67 | }
68 | }
69 |
70 | func testPingKeepsConnectionAlive() {
71 | let service = register(closeReason: .noReasonCodeSent, connectionTimeout: 1)
72 |
73 | performServerTest() { expectation in
74 | XCTAssertEqual(service.connections.count, 0, "Connections left on service at start of test")
75 | guard let socket = self.sendUpgradeRequest(toPath: self.servicePath, usingKey: self.secWebKey) else { return }
76 | let _ = self.checkUpgradeResponse(from: socket, forKey: self.secWebKey)
77 | usleep(5000)
78 | XCTAssertEqual(service.connections.count, 1, "Failed to create connection to service")
79 | usleep(500000)
80 | self.sendFrame(final: true, withOpcode: self.opcodePing, withPayload: NSData(), on: socket)
81 | usleep(500000)
82 | self.sendFrame(final: true, withOpcode: self.opcodePing, withPayload: NSData(), on: socket)
83 | usleep(500000)
84 | XCTAssertEqual(service.connections.count, 1, "Stale connection was unexpectedly cleaned up")
85 | self.sendFrame(final: true, withOpcode: self.opcodeClose, withPayload: NSData(), on: socket)
86 | usleep(500000)
87 | XCTAssertEqual(service.connections.count, 0, "Connection was not removed even after getting a close opcode")
88 | socket.close()
89 | usleep(1000)
90 | expectation.fulfill()
91 | }
92 | }
93 |
94 | func testMultiConnectionTimeOut() {
95 | let service = register(closeReason: .closedAbnormally, connectionTimeout: 1)
96 |
97 | performServerTest() { expectation in
98 | XCTAssertEqual(service.connections.count, 0, "Connections left on service at start of test")
99 | guard let socket = self.sendUpgradeRequest(toPath: self.servicePath, usingKey: self.secWebKey) else { return }
100 | let _ = self.checkUpgradeResponse(from: socket, forKey: self.secWebKey)
101 | usleep(5000)
102 | XCTAssertEqual(service.connections.count, 1, "Failed to create connection to service")
103 | guard let socket2 = self.sendUpgradeRequest(toPath: self.servicePath, usingKey: self.secWebKey) else { return }
104 | let _ = self.checkUpgradeResponse(from: socket2, forKey: self.secWebKey)
105 | usleep(5000)
106 | XCTAssertEqual(service.connections.count, 2, "Failed to create second connection to service")
107 | usleep(500000)
108 | self.sendFrame(final: true, withOpcode: self.opcodePing, withPayload: NSData(), on: socket)
109 | usleep(500000)
110 | self.sendFrame(final: true, withOpcode: self.opcodePing, withPayload: NSData(), on: socket)
111 | usleep(500000)
112 | self.sendFrame(final: true, withOpcode: self.opcodePing, withPayload: NSData(), on: socket)
113 | XCTAssertEqual(service.connections.count, 1, "Stale connection was not cleaned up")
114 | socket.close()
115 | socket2.close()
116 | usleep(1000)
117 | expectation.fulfill()
118 | }
119 | }
120 |
121 | func testProccessorClose() {
122 | let service = register(closeReason: .closedAbnormally, connectionTimeout: nil)
123 |
124 | performServerTest() { expectation in
125 | XCTAssertEqual(service.connections.count, 0, "Connections left on service at start of test")
126 | guard let socket = self.sendUpgradeRequest(toPath: self.servicePath, usingKey: self.secWebKey) else { return }
127 | let _ = self.checkUpgradeResponse(from: socket, forKey: self.secWebKey)
128 | usleep(5000)
129 | XCTAssertEqual(service.connections.count, 1, "Failed to create connection to service")
130 | let connections = Array(service.connections.values)
131 | connections[0].processor?.close()
132 | usleep(5000)
133 | XCTAssertEqual(service.connections.count, 0, "Service was not notified of connection disconnect")
134 | socket.close()
135 | usleep(1000)
136 | expectation.fulfill()
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Tests/KituraWebSocketTests/PrintLogger.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright IBM Corporation 2016
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | import Foundation
18 |
19 | import LoggerAPI
20 |
21 | /// The set of colors used when logging with colorized lines
22 | public enum TerminalColor: String {
23 | /// Log text in white.
24 | case white = "\u{001B}[0;37m" // white
25 | /// Log text in red, used for error messages.
26 | case red = "\u{001B}[0;31m" // red
27 | /// Log text in yellow, used for warning messages.
28 | case yellow = "\u{001B}[0;33m" // yellow
29 | /// Log text in the terminal's default foreground color.
30 | case foreground = "\u{001B}[0;39m" // default foreground color
31 | /// Log text in the terminal's default background color.
32 | case background = "\u{001B}[0;49m" // default background color
33 | }
34 |
35 | public class PrintLogger: Logger {
36 | let colored: Bool
37 |
38 | init(colored: Bool) {
39 | self.colored = colored
40 | }
41 |
42 | public func log(_ type: LoggerMessageType, msg: String,
43 | functionName: String, lineNum: Int, fileName: String ) {
44 | let message = "[\(type)] [\(getFile(fileName)):\(lineNum) \(functionName)] \(msg)"
45 |
46 | guard colored else {
47 | print(message)
48 | return
49 | }
50 |
51 | let color : TerminalColor
52 | switch type {
53 | case .warning:
54 | color = .yellow
55 | case .error:
56 | color = .red
57 | default:
58 | color = .foreground
59 | }
60 |
61 | print(color.rawValue + message + TerminalColor.foreground.rawValue)
62 | }
63 |
64 | public func isLogging(_ level: LoggerAPI.LoggerMessageType) -> Bool {
65 | return true
66 | }
67 |
68 | public static func use(colored: Bool) {
69 | Log.logger = PrintLogger(colored: colored)
70 | setbuf(stdout, nil)
71 | }
72 |
73 | private func getFile(_ path: String) -> String {
74 | guard let range = path.range(of: "/", options: .backwards) else {
75 | return path
76 | }
77 | #if swift(>=3.2)
78 | return String(path[range.upperBound...])
79 | #else
80 | return path.substring(from: range.upperBound)
81 | #endif
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Tests/KituraWebSocketTests/TestLinuxSafeguard.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright IBM Corporation 2017
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | #if os(OSX) && !swift(>=3.2)
18 | import XCTest
19 |
20 | class TestLinuxSafeguard: XCTestCase {
21 | func testVerifyLinuxTestCount() {
22 | var linuxCount: Int
23 | var darwinCount: Int
24 |
25 | // BasicTests
26 | linuxCount = BasicTests.allTests.count
27 | darwinCount = Int(BasicTests.defaultTestSuite().testCaseCount)
28 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from BasicTests.allTests")
29 |
30 | // ComplexTests
31 | linuxCount = ComplexTests.allTests.count
32 | darwinCount = Int(ComplexTests.defaultTestSuite().testCaseCount)
33 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from ComplexTests.allTests")
34 |
35 | // ProtocolErrorTests
36 | linuxCount = ProtocolErrorTests.allTests.count
37 | darwinCount = Int(ProtocolErrorTests.defaultTestSuite().testCaseCount)
38 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from ProtocolErrorTests.allTests")
39 |
40 | // UpgradeErrors
41 | linuxCount = UpgradeErrors.allTests.count
42 | darwinCount = Int(UpgradeErrors.defaultTestSuite().testCaseCount)
43 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from UpgradeErrors.allTests")
44 | }
45 | }
46 | #endif
47 |
--------------------------------------------------------------------------------
/Tests/KituraWebSocketTests/TestWebSocketService.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright IBM Corporation 2016-2017
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | import XCTest
18 | import Foundation
19 |
20 | @testable import KituraWebSocket
21 | import KituraNet
22 |
23 | class TestWebSocketService: WebSocketService {
24 | let closeReason: WebSocketCloseReasonCode
25 | let pingMessage: String?
26 | let testServerRequest: Bool
27 | var connectionTimeout: Int?
28 | var connections: [String: WebSocketConnection]
29 |
30 | public init(closeReason: WebSocketCloseReasonCode, testServerRequest: Bool, pingMessage: String?, connectionTimeout: Int? = nil) {
31 | self.closeReason = closeReason
32 | self.testServerRequest = testServerRequest
33 | self.pingMessage = pingMessage
34 | self.connectionTimeout = connectionTimeout
35 | self.connections = [:]
36 | }
37 |
38 | public func connected(connection: WebSocketConnection) {
39 | connections[connection.id] = connection
40 |
41 | if let pingMessage = pingMessage {
42 | if pingMessage.count > 0 {
43 | connection.ping(withMessage: pingMessage)
44 | }
45 | else {
46 | connection.ping()
47 | }
48 | }
49 |
50 | if testServerRequest {
51 | performServerRequestTests(request: connection.request)
52 |
53 | sleep(2)
54 |
55 | performServerRequestTests(request: connection.request)
56 | }
57 | }
58 |
59 | private func performServerRequestTests(request: ServerRequest) {
60 | XCTAssertEqual(request.method, "GET", "The method of the request should be GET, it was \(request.method))")
61 | XCTAssertEqual(request.httpVersionMajor, 1, "HTTP version major should be 1, it was \(String(describing: request.httpVersionMajor))")
62 | XCTAssertEqual(request.httpVersionMinor, 1, "HTTP version major should be 1, it was \(String(describing: request.httpVersionMinor))")
63 | XCTAssertEqual(request.urlURL.pathComponents[1], "wstester", "Path of the request should be /wstester, it was /\(request.urlURL.pathComponents[1])")
64 | XCTAssertEqual(request.url, "/wstester".data(using: .utf8)!, "Path of the request should be /wstester, it was \(String(data: request.url, encoding: .utf8) ?? "Not UTF-8")")
65 | let protocolVersion = request.headers["Sec-WebSocket-Version"]
66 | XCTAssertNotNil(protocolVersion, "The Sec-WebSocket-Version header wasn't in the headers")
67 | XCTAssertEqual(protocolVersion!.count, 1, "The Sec-WebSocket-Version header should have one value")
68 | XCTAssertEqual(protocolVersion![0], "13", "The Sec-WebSocket-Version header value should be 13, it was \(protocolVersion![0])")
69 |
70 | do {
71 | let bodyString = try request.readString()
72 | XCTAssertNil(bodyString, "Read of body should have returned nil, it returned \"\(String(describing: bodyString))\"")
73 | var body = Data()
74 | var count = try request.read(into: &body)
75 | XCTAssertEqual(count, 0, "Read of body into a Data should have returned 0, it returned \(count)")
76 | count = try request.readAllData(into: &body)
77 | XCTAssertEqual(count, 0, "Read of entire body into a Data should have returned 0, it returned \(count)")
78 | }
79 | catch {
80 | XCTFail("Failed to read from the body. Error=\(error)")
81 | }
82 | }
83 |
84 | public func disconnected(connection: WebSocketConnection, reason: WebSocketCloseReasonCode) {
85 | XCTAssertNotNil(connections[connection.id], "Client ID from connect wasn't client ID from disconnect")
86 | XCTAssert((closeReason.code() == reason.code()), "Excpected close reason code of \(closeReason) received \(reason)")
87 | connections[connection.id] = nil
88 | }
89 |
90 | public func received(message: Data, from: WebSocketConnection) {
91 | print("Received a binary message of length \(message.count)")
92 | from.send(message: message)
93 | }
94 |
95 | public func received(message: String, from: WebSocketConnection) {
96 | print("Received a String message of length \(message.count)")
97 | from.send(message: message)
98 |
99 | if message == "close" {
100 | from.close(reason: .goingAway, description: "Going away...")
101 | }
102 | else if message == "drop" {
103 | from.drop(reason: .policyViolation, description: "Droping...")
104 | }
105 | else if message == "ping" {
106 | from.ping(withMessage: "Hello")
107 | }
108 | }
109 | }
110 |
111 | extension TestWebSocketService: CustomStringConvertible {
112 | /// Generate a printable version of this enum.
113 | public var description: String {
114 | return "TestWebSocketService(closeReason: \(closeReason))"
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Tests/KituraWebSocketTests/UpgradeErrors.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright IBM Corporation 2016
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | import XCTest
18 | import Foundation
19 |
20 | @testable import KituraWebSocket
21 | @testable import KituraNet
22 | import Socket
23 |
24 | class UpgradeErrors: KituraTest {
25 |
26 | static var allTests: [(String, (UpgradeErrors) -> () throws -> Void)] {
27 | return [
28 | ("testNoSecWebSocketKey", testNoSecWebSocketKey),
29 | ("testNoSecWebSocketVersion", testNoSecWebSocketVersion),
30 | ("testNoService", testNoService)
31 | ]
32 | }
33 |
34 | func testNoSecWebSocketKey() {
35 | WebSocket.factory.clear()
36 |
37 | performServerTest() { expectation in
38 | guard let socket = self.sendUpgradeRequest(forProtocolVersion: "13", toPath: "/testing123", usingKey: nil) else { return }
39 |
40 | self.checkUpgradeFailureResponse(from: socket, expectedMessage: "Sec-WebSocket-Key header missing in the upgrade request", expectation: expectation)
41 | }
42 | }
43 |
44 | func testNoSecWebSocketVersion() {
45 | WebSocket.factory.clear()
46 |
47 | performServerTest(asyncTasks: { expectation in
48 | guard let socket = self.sendUpgradeRequest(forProtocolVersion: nil, toPath: "/testing123", usingKey: nil) else { return }
49 |
50 | self.checkUpgradeFailureResponse(from: socket, expectedMessage: "Sec-WebSocket-Version header missing in the upgrade request", expectation: expectation)
51 | },
52 | { expectation in
53 | guard let socket = self.sendUpgradeRequest(forProtocolVersion: "12", toPath: "/testing123", usingKey: nil) else { return }
54 |
55 | self.checkUpgradeFailureResponse(from: socket, expectedMessage: "Only WebSocket protocol version 13 is supported", expectation: expectation)
56 | })
57 | }
58 |
59 | func testNoService() {
60 | WebSocket.factory.clear()
61 |
62 | performServerTest() { expectation in
63 | guard let socket = self.sendUpgradeRequest(forProtocolVersion: "13", toPath: "/testing123", usingKey: "test") else { return }
64 |
65 | self.checkUpgradeFailureResponse(from: socket, expectedMessage: "No service has been registered for the path /testing123", expectation: expectation)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright IBM Corporation 2016
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | import XCTest
18 | import Glibc
19 | @testable import KituraWebSocketTests
20 |
21 | // http://stackoverflow.com/questions/24026510/how-do-i-shuffle-an-array-in-swift
22 | #if swift(>=3.2)
23 | extension MutableCollection {
24 | mutating func shuffle() {
25 | let c = count
26 | guard c > 1 else { return }
27 |
28 | srand(UInt32(time(nil)))
29 | for (firstUnshuffled , unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) {
30 | #if swift(>=4.1)
31 | let d: Int = numericCast(random() % numericCast(unshuffledCount))
32 | #else
33 | let d: IndexDistance = numericCast(random() % numericCast(unshuffledCount))
34 | #endif
35 | guard d != 0 else { continue }
36 | let i = index(firstUnshuffled, offsetBy: d)
37 | swapAt(firstUnshuffled, i)
38 | }
39 | }
40 | }
41 | #else
42 | extension MutableCollection where Indices.Iterator.Element == Index {
43 | mutating func shuffle() {
44 | let c = count
45 | guard c > 1 else { return }
46 |
47 | srand(UInt32(time(nil)))
48 | for (firstUnshuffled , unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) {
49 | let d: IndexDistance = numericCast(random() % numericCast(unshuffledCount))
50 | guard d != 0 else { continue }
51 | let i = index(firstUnshuffled, offsetBy: d)
52 | swap(&self[firstUnshuffled], &self[i])
53 | }
54 | }
55 | }
56 | #endif
57 |
58 | extension Sequence {
59 | func shuffled() -> [Iterator.Element] {
60 | var result = Array(self)
61 | result.shuffle()
62 | return result
63 | }
64 | }
65 |
66 | XCTMain([
67 | testCase(BasicTests.allTests.shuffled()),
68 | testCase(ComplexTests.allTests.shuffled()),
69 | testCase(ConnectionCleanupTests.allTests.shuffled()),
70 | testCase(ProtocolErrorTests.allTests.shuffled()),
71 | testCase(UpgradeErrors.allTests.shuffled())
72 | ].shuffled())
73 |
--------------------------------------------------------------------------------
/docs/Classes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Classes Reference
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
The implementation of the ConnectionUpgradeFactory protocol for the WebSocket protocol.
116 | Participates in the HTTP protocol upgrade process when upgarding to the WebSocket protocol.
Represents a specific WebSocket connection. Provides a unique identifier for
174 | the connection and APIs to send messages and control commands to the client
175 | at the other end of the connection.
The WebSocketService protocol is implemented by classes that wish to be WebSocket server side
116 | end points. An instance of the WebSocketService protocol is the server side of a WebSocket connection.
117 | There can be many WebSocket connections connected to a single WebSocketService protocol instance.
118 | The protocol is a set of callbacks that are invoked when various events occur.
The implementation of the ConnectionUpgradeFactory protocol for the WebSocket protocol.
116 | Participates in the HTTP protocol upgrade process when upgarding to the WebSocket protocol.
Represents a specific WebSocket connection. Provides a unique identifier for
174 | the connection and APIs to send messages and control commands to the client
175 | at the other end of the connection.
The WebSocketService protocol is implemented by classes that wish to be WebSocket server side
116 | end points. An instance of the WebSocketService protocol is the server side of a WebSocket connection.
117 | There can be many WebSocket connections connected to a single WebSocketService protocol instance.
118 | The protocol is a set of callbacks that are invoked when various events occur.
Called when a WebSocket client connects to the server and is connected to a specific","parent_name":"WebSocketService"},"Protocols/WebSocketService.html#/s:15KituraWebSocket0bC7ServiceP12disconnected10connection6reasonyAA0bC10ConnectionC_AA0bC15CloseReasonCodeOtF":{"name":"disconnected(connection:reason:)","abstract":"
Called when a WebSocket client disconnects from the server.
The time in seconds that a connection must be unresponsive to be automatically closed by the server. If the WebSocket server has not received any messages in the first half of the timeout time it will ping the connection. If a pong is not received in the remaining half of the timeout, the connection will be closed with a 1006 (connection closed abnormally) status code. The connectionTimeout defaults to nil, meaning no connection cleanup will take place.
The WebSocketService protocol is implemented by classes that wish to be WebSocket server side"},"Enums/WebSocketError.html#/s:15KituraWebSocket0bC5ErrorO13invalidOpCodeyACs5UInt8VcACmF":{"name":"invalidOpCode(_:)","abstract":"
An invalid opcode was received in a WebSocket frame
Close a WebSocket connection by sending a close control command to the client optionally","parent_name":"WebSocketConnection"},"Classes/WebSocketConnection.html#/s:15KituraWebSocket0bC10ConnectionC4drop6reason11descriptionyAA0bC15CloseReasonCodeOSg_SSSgtF":{"name":"drop(reason:description:)","abstract":"
Forcefully close a WebSocket connection by sending a close control command to the client optionally","parent_name":"WebSocketConnection"},"Classes/WebSocketConnection.html#/s:15KituraWebSocket0bC10ConnectionC4ping11withMessageySSSg_tF":{"name":"ping(withMessage:)","abstract":"
“Upgrade” a connection to the WebSocket protocol. Invoked by the KituraNet.ConnectionUpgrader when","parent_name":"WSConnectionUpgradeFactory"},"Classes/WSConnectionUpgradeFactory.html":{"name":"WSConnectionUpgradeFactory","abstract":"
The implementation of the ConnectionUpgradeFactory protocol for the WebSocket protocol."},"Classes/WebSocket.html":{"name":"WebSocket","abstract":"
Main class for the Kitura-WebSocket API. Used to register WebSocketService classes"},"Classes/WebSocketConnection.html":{"name":"WebSocketConnection","abstract":"
Represents a specific WebSocket connection. Provides a unique identifier for"},"Classes.html":{"name":"Classes","abstract":"
Called when a WebSocket client connects to the server and is connected to a specific","parent_name":"WebSocketService"},"Protocols/WebSocketService.html#/s:15KituraWebSocket0bC7ServiceP12disconnected10connection6reasonyAA0bC10ConnectionC_AA0bC15CloseReasonCodeOtF":{"name":"disconnected(connection:reason:)","abstract":"
Called when a WebSocket client disconnects from the server.
The time in seconds that a connection must be unresponsive to be automatically closed by the server. If the WebSocket server has not received any messages in the first half of the timeout time it will ping the connection. If a pong is not received in the remaining half of the timeout, the connection will be closed with a 1006 (connection closed abnormally) status code. The connectionTimeout defaults to nil, meaning no connection cleanup will take place.
The WebSocketService protocol is implemented by classes that wish to be WebSocket server side"},"Enums/WebSocketError.html#/s:15KituraWebSocket0bC5ErrorO13invalidOpCodeyACs5UInt8VcACmF":{"name":"invalidOpCode(_:)","abstract":"
An invalid opcode was received in a WebSocket frame
Close a WebSocket connection by sending a close control command to the client optionally","parent_name":"WebSocketConnection"},"Classes/WebSocketConnection.html#/s:15KituraWebSocket0bC10ConnectionC4drop6reason11descriptionyAA0bC15CloseReasonCodeOSg_SSSgtF":{"name":"drop(reason:description:)","abstract":"
Forcefully close a WebSocket connection by sending a close control command to the client optionally","parent_name":"WebSocketConnection"},"Classes/WebSocketConnection.html#/s:15KituraWebSocket0bC10ConnectionC4ping11withMessageySSSg_tF":{"name":"ping(withMessage:)","abstract":"
“Upgrade” a connection to the WebSocket protocol. Invoked by the KituraNet.ConnectionUpgrader when","parent_name":"WSConnectionUpgradeFactory"},"Classes/WSConnectionUpgradeFactory.html":{"name":"WSConnectionUpgradeFactory","abstract":"
The implementation of the ConnectionUpgradeFactory protocol for the WebSocket protocol."},"Classes/WebSocket.html":{"name":"WebSocket","abstract":"
Main class for the Kitura-WebSocket API. Used to register WebSocketService classes"},"Classes/WebSocketConnection.html":{"name":"WebSocketConnection","abstract":"
Represents a specific WebSocket connection. Provides a unique identifier for"},"Classes.html":{"name":"Classes","abstract":"