├── .codeclimate.yml
├── .gitignore
├── .hound.yaml
├── .pre-commit-config.yaml
├── .swiftlint.yaml
├── .travis.yml
├── Embassy.podspec
├── Embassy.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcshareddata
│ └── xcschemes
│ │ ├── Embassy-iOS.xcscheme
│ │ ├── Embassy-macOS.xcscheme
│ │ └── Embassy-tvOS.xcscheme
└── xcuserdata
│ └── victorlin.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── Embassy.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
├── Atomic.swift
├── DefaultHTTPServer.swift
├── DefaultLogFormatter.swift
├── DefaultLogger.swift
├── Embassy.h
├── Errors.swift
├── EventLoop.swift
├── FileLogHandler.swift
├── HTTPConnection.swift
├── HTTPHeaderParser.swift
├── HTTPRequest.swift
├── HTTPServer.swift
├── HeapSort.swift
├── IOUtils.swift
├── Info.plist
├── KqueueSelector.swift
├── LogFormatter.swift
├── LogHandler.swift
├── Logger.swift
├── MultiDictionary.swift
├── PrintLogHandler.swift
├── PropagateLogHandler.swift
├── SWSGI.swift
├── SWSGIUtils.swift
├── SelectSelector.swift
├── Selector.swift
├── SelectorEventLoop.swift
├── SystemLibrary.swift
├── TCPSocket.swift
├── TransformLogHandler.swift
└── Transport.swift
├── Tests
├── EmbassyTests
│ ├── HTTPHeaderParserTests.swift
│ ├── HTTPServerTests.swift
│ ├── HeapSortTetsts.swift
│ ├── Info.plist
│ ├── KqueueSelectorTests.swift
│ ├── MultiDictionaryTests.swift
│ ├── SelectSelectorTests.swift
│ ├── SelectorEventLoopTests.swift
│ ├── TCPSocketTests.swift
│ ├── TestingHelpers.swift
│ └── TransportTests.swift
└── LinuxMain.swift
└── run-docker.sh
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ---
2 | engines:
3 | tailor:
4 | enabled: true
5 | checks:
6 | forced-type-cast:
7 | enabled: false
8 | function-whitespace:
9 | enabled: false
10 | ratings:
11 | paths:
12 | - "**.swift"
13 | exclude_paths:
14 | - Carthage
15 | - Pods
16 | - fastlane
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata
2 | .build
3 |
--------------------------------------------------------------------------------
/.hound.yaml:
--------------------------------------------------------------------------------
1 | swift:
2 | config_file: .swiftlint.yml
3 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | - repo: git@github.com:pre-commit/pre-commit-hooks
2 | sha: 17478a0a50faf20fd8e5b3fefe7435cea410df0b
3 | hooks:
4 | - id: trailing-whitespace
5 | files: \.(c|cpp|html|erb|slim|haml|ejs|jade|js|coffee|json|rb|md|py|css|scss|less|sh|tmpl|txt|yaml|yml|pp|swift)$
6 | - id: end-of-file-fixer
7 | files: \.(c|cpp|html|erb|slim|haml|ejs|jade|js|coffee|json|rb|md|py|css|scss|less|sh|tmpl|txt|yaml|yml|pp|swift)$
8 | - id: check-json
9 | - id: check-yaml
10 | - id: check-merge-conflict
11 | - id: detect-private-key
12 |
--------------------------------------------------------------------------------
/.swiftlint.yaml:
--------------------------------------------------------------------------------
1 | disabled_rules: # rule identifiers to exclude from running
2 | - force_cast
3 | - force_try
4 | - todo
5 | excluded:
6 | - Carthage
7 | - Pods
8 | - fastlane
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 | osx_image: xcode10.2
3 | env:
4 | global:
5 | - LC_CTYPE=en_US.UTF-8
6 | - LANG=en_US.UTF-8
7 | - WORKSPACE=Embassy.xcworkspace
8 | - IOS_FRAMEWORK_SCHEME="Embassy-iOS"
9 | - IOS_SDK=iphonesimulator12.2
10 | - MACOS_FRAMEWORK_SCHEME="Embassy-macOS"
11 | - MACOS_SDK=macosx10.14
12 | matrix:
13 | - DESTINATION="OS=12.2,name=iPhone 6S" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK"
14 | - DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" SDK="$MACOS_SDK"
15 |
16 | script:
17 | - >
18 | set -o pipefail &&
19 | xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -sdk "$SDK"
20 | -destination "$DESTINATION" -configuration Debug
21 | ONLY_ACTIVE_ARCH=NO test | xcpretty -c;
22 |
--------------------------------------------------------------------------------
/Embassy.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = 'Embassy'
3 | spec.version = '4.1.4'
4 | spec.summary = 'Lightweight async HTTP server in pure Swift for iOS/macOS UI Automatic testing data mocking'
5 | spec.homepage = 'https://github.com/envoy/Embassy'
6 | spec.license = 'MIT'
7 | spec.license = { type: 'MIT', file: 'LICENSE' }
8 | spec.author = { 'Fang-Pen Lin' => 'fang@envoy.com' }
9 | spec.social_media_url = 'https://twitter.com/fangpenlin'
10 | spec.ios.deployment_target = '8.0'
11 | spec.tvos.deployment_target = '11.0'
12 | spec.osx.deployment_target = '10.10'
13 | spec.source = {
14 | git: 'https://github.com/envoy/Embassy.git',
15 | tag: "v#{spec.version}"
16 | }
17 | spec.source_files = 'Sources/**.swift', 'Sources/**/*.swift'
18 | spec.swift_versions = ['4.0']
19 | end
20 |
--------------------------------------------------------------------------------
/Embassy.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Embassy.xcodeproj/xcshareddata/xcschemes/Embassy-iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
66 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/Embassy.xcodeproj/xcshareddata/xcschemes/Embassy-macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
66 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/Embassy.xcodeproj/xcshareddata/xcschemes/Embassy-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 |
--------------------------------------------------------------------------------
/Embassy.xcodeproj/xcuserdata/victorlin.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Embassy-iOS.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | AA9242CD1DE91EA2009FE50B
16 |
17 | primary
18 |
19 |
20 | AA9242D51DE91EA2009FE50B
21 |
22 | primary
23 |
24 |
25 | D85613E81CEE6B160060CCB7
26 |
27 | primary
28 |
29 |
30 | D85613F21CEE6B160060CCB7
31 |
32 | primary
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Embassy.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Embassy.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Outrageous Labs, Inc
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 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.2
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Embassy",
6 | products: [.library(name: "Embassy", targets: ["Embassy"])],
7 | targets: [.target(name: "Embassy", path: "./Sources")]
8 | )
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Embassy
2 |
3 | [](https://travis-ci.org/envoy/Embassy)
4 | [](https://github.com/Carthage/Carthage)
5 | [](https://github.com/apple/swift-package-manager)
6 | []()
7 | 
8 | 
9 | [](https://github.com/envoy/Embassy/blob/master/LICENSE)
10 |
11 | Super lightweight async HTTP server in pure Swift.
12 |
13 | **Please read**: [Embedded web server for iOS UI testing](https://envoy.engineering/embedded-web-server-for-ios-ui-testing-8ff3cef513df#.c2i5tx380).
14 |
15 | **See also**: Our lightweight web framework [Ambassador](https://github.com/envoy/Ambassador) based on Embassy
16 |
17 | ## Features
18 |
19 | - Swift 4 & 5
20 | - iOS / tvOS / MacOS / Linux
21 | - Super lightweight, only 1.5 K of lines
22 | - Zero third-party dependency
23 | - Async event loop based HTTP server, makes long-polling, delay and bandwidth throttling all possible
24 | - HTTP Application based on [SWSGI](#whats-swsgi-swift-web-server-gateway-interface), super flexible
25 | - IPV6 ready, also supports IPV4 (dual stack)
26 | - Automatic testing covered
27 |
28 | ## Example
29 |
30 | Here's a simple example shows how Embassy works.
31 |
32 | ```Swift
33 | let loop = try! SelectorEventLoop(selector: try! KqueueSelector())
34 | let server = DefaultHTTPServer(eventLoop: loop, port: 8080) {
35 | (
36 | environ: [String: Any],
37 | startResponse: ((String, [(String, String)]) -> Void),
38 | sendBody: ((Data) -> Void)
39 | ) in
40 | // Start HTTP response
41 | startResponse("200 OK", [])
42 | let pathInfo = environ["PATH_INFO"]! as! String
43 | sendBody(Data("the path you're visiting is \(pathInfo.debugDescription)".utf8))
44 | // send EOF
45 | sendBody(Data())
46 | }
47 |
48 | // Start HTTP server to listen on the port
49 | try! server.start()
50 |
51 | // Run event loop
52 | loop.runForever()
53 | ```
54 |
55 | Then you can visit `http://[::1]:8080/foo-bar` in the browser and see
56 |
57 | ```
58 | the path you're visiting is "/foo-bar"
59 | ```
60 |
61 | By default, the server will be bound only to the localhost interface (::1) for security reasons. If you want to access your server over an external network, you'll need to change the bind interface to all addresses:
62 |
63 | ```Swift
64 | let server = DefaultHTTPServer(eventLoop: loop, interface: "::", port: 8080)
65 | ```
66 |
67 | ## Async Event Loop
68 |
69 | To use the async event loop, you can get it via key `embassy.event_loop` in `environ` dictionary and cast it to `EventLoop`. For example, you can create an SWSGI app which delays `sendBody` call like this
70 |
71 | ```Swift
72 | let app = { (
73 | environ: [String: Any],
74 | startResponse: ((String, [(String, String)]) -> Void),
75 | sendBody: @escaping ((Data) -> Void)
76 | ) in
77 | startResponse("200 OK", [])
78 |
79 | let loop = environ["embassy.event_loop"] as! EventLoop
80 |
81 | loop.call(withDelay: 1) {
82 | sendBody(Data("hello ".utf8))
83 | }
84 | loop.call(withDelay: 2) {
85 | sendBody(Data("baby ".utf8))
86 | }
87 | loop.call(withDelay: 3) {
88 | sendBody(Data("fin".utf8))
89 | sendBody(Data())
90 | }
91 | }
92 | ```
93 |
94 | Please notice that functions passed into SWSGI should be only called within the same thread for running the `EventLoop`, they are all not threadsafe, therefore, **you should not use [GCD](https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/) for delaying any call**. Instead, there are some methods from `EventLoop` you can use, and they are all threadsafe
95 |
96 | ### call(callback: (Void) -> Void)
97 |
98 | Call given callback as soon as possible in the event loop
99 |
100 | ### call(withDelay: TimeInterval, callback: (Void) -> Void)
101 |
102 | Schedule given callback to `withDelay` seconds then call it in the event loop.
103 |
104 | ### call(atTime: Date, callback: (Void) -> Void)
105 |
106 | Schedule given callback to be called at `atTime` in the event loop. If the given time is in the past or zero, this methods works exactly like `call` with only callback parameter.
107 |
108 | ## What's SWSGI (Swift Web Server Gateway Interface)?
109 |
110 | SWSGI is a hat tip to Python's [WSGI (Web Server Gateway Interface)](https://www.python.org/dev/peps/pep-3333/). It's a gateway interface enables web applications to talk to HTTP clients without knowing HTTP server implementation details.
111 |
112 | It's defined as
113 |
114 | ```Swift
115 | public typealias SWSGI = (
116 | [String: Any],
117 | @escaping ((String, [(String, String)]) -> Void),
118 | @escaping ((Data) -> Void)
119 | ) -> Void
120 | ```
121 |
122 | ### `environ`
123 |
124 | It's a dictionary contains all necessary information about the request. It basically follows WSGI standard, except `wsgi.*` keys will be `swsgi.*` instead. For example:
125 |
126 | ```Swift
127 | [
128 | "SERVER_NAME": "[::1]",
129 | "SERVER_PROTOCOL" : "HTTP/1.1",
130 | "SERVER_PORT" : "53479",
131 | "REQUEST_METHOD": "GET",
132 | "SCRIPT_NAME" : "",
133 | "PATH_INFO" : "/",
134 | "HTTP_HOST": "[::1]:8889",
135 | "HTTP_USER_AGENT" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
136 | "HTTP_ACCEPT_LANGUAGE" : "en-US,en;q=0.8,zh-TW;q=0.6,zh;q=0.4,zh-CN;q=0.2",
137 | "HTTP_CONNECTION" : "keep-alive",
138 | "HTTP_ACCEPT" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
139 | "HTTP_ACCEPT_ENCODING" : "gzip, deflate, sdch",
140 | "swsgi.version" : "0.1",
141 | "swsgi.input" : (Function),
142 | "swsgi.error" : "",
143 | "swsgi.multiprocess" : false,
144 | "swsgi.multithread" : false,
145 | "swsgi.url_scheme" : "http",
146 | "swsgi.run_once" : false
147 | ]
148 | ```
149 |
150 | To read request from body, you can use `swsgi.input`, for example
151 |
152 | ```Swift
153 | let input = environ["swsgi.input"] as! SWSGIInput
154 | input { data in
155 | // handle the body data here
156 | }
157 | ```
158 |
159 | An empty Data will be passed into the input data handler when EOF
160 | reached. Also please notice that, request body won't be read if `swsgi.input`
161 | is not set or set to nil. You can use `swsgi.input` as bandwidth control, set
162 | it to nil when you don't want to receive any data from client.
163 |
164 | Some extra Embassy server specific keys are
165 |
166 | - `embassy.connection` - `HTTPConnection` object for the request
167 | - `embassy.event_loop` - `EventLoop` object
168 | - `embassy.version` - Version of embassy as a String, e.g. `3.0.0`
169 |
170 | ### `startResponse`
171 |
172 | Function for starting to send HTTP response header to client, the first argument is status code with message, e.g. "200 OK". The second argument is headers, as a list of key value tuple.
173 |
174 | To response HTTP header, you can do
175 |
176 | ```Swift
177 | startResponse("200 OK", [("Set-Cookie", "foo=bar")])
178 | ```
179 |
180 | `startResponse` can only be called once per request, extra call will be simply ignored.
181 |
182 | ### `sendBody`
183 |
184 | Function for sending body data to client. You need to call `startResponse` first in order to call `sendBody`. If you don't call `startResponse` first, all calls to `sendBody` will be ignored. To send data, here you can do
185 |
186 | ```Swift
187 | sendBody(Data("hello".utf8))
188 | ```
189 |
190 | To end the response data stream simply call `sendBody` with an empty Data.
191 |
192 | ```Swift
193 | sendBody(Data())
194 | ```
195 |
196 | ## Install
197 |
198 | ### CocoaPods
199 |
200 | To install with CocoaPod, add Embassy to your Podfile:
201 |
202 | ```
203 | pod 'Embassy', '~> 4.1'
204 | ```
205 |
206 | ### Carthage
207 |
208 | To install with Carthage, add Embassy to your Cartfile:
209 |
210 | ```
211 | github "envoy/Embassy" ~> 4.1
212 | ```
213 |
214 | ### Package Manager
215 |
216 | Add it this Embassy repo in `Package.swift`, like this
217 |
218 | ```swift
219 | import PackageDescription
220 |
221 | let package = Package(
222 | name: "EmbassyExample",
223 | dependencies: [
224 | .package(url: "https://github.com/envoy/Embassy.git",
225 | from: "4.1.4"),
226 | ]
227 | )
228 | ```
229 |
230 | You can read this [example project](https://github.com/envoy/example-embassy) here.
231 |
--------------------------------------------------------------------------------
/Sources/Atomic.swift:
--------------------------------------------------------------------------------
1 | // Borrowed from https://github.com/ReactiveCocoa/ReactiveCocoa
2 | //
3 | // Atomic.swift
4 | // ReactiveCocoa
5 | //
6 | // Copyright (c) 2012 - 2016, GitHub, Inc. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11 | //
12 | /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13 | //
14 | // Created by Justin Spahr-Summers on 2014-06-10.
15 | // Copyright (c) 2014 GitHub. All rights reserved.
16 | //
17 |
18 | import Foundation
19 |
20 |
21 | /// An atomic variable.
22 | final class Atomic {
23 | private var mutex = pthread_mutex_t()
24 | private var _value: Value
25 |
26 | /// Atomically gets or sets the value of the variable.
27 | var value: Value {
28 | get {
29 | return withValue { $0 }
30 | }
31 |
32 | set(newValue) {
33 | modify { _ in newValue }
34 | }
35 | }
36 |
37 | /// Initializes the variable with the given initial value.
38 | init(_ value: Value) {
39 | _value = value
40 | let result = pthread_mutex_init(&mutex, nil)
41 | assert(result == 0, "Failed to initialize mutex with error \(result).")
42 | }
43 |
44 | deinit {
45 | let result = pthread_mutex_destroy(&mutex)
46 | assert(result == 0, "Failed to destroy mutex with error \(result).")
47 | }
48 |
49 | private func lock() {
50 | let result = pthread_mutex_lock(&mutex)
51 | assert(result == 0, "Failed to lock \(self) with error \(result).")
52 | }
53 |
54 | private func unlock() {
55 | let result = pthread_mutex_unlock(&mutex)
56 | assert(result == 0, "Failed to unlock \(self) with error \(result).")
57 | }
58 |
59 | /// Atomically replaces the contents of the variable.
60 | ///
61 | /// Returns the old value.
62 | func swap(newValue: Value) -> Value {
63 | return modify { _ in newValue }
64 | }
65 |
66 | /// Atomically modifies the variable.
67 | ///
68 | /// Returns the old value.
69 | @discardableResult
70 | func modify(action: (Value) throws -> Value) rethrows -> Value {
71 | return try withValue { value in
72 | let oldValue = value
73 | _value = try action(value)
74 | return oldValue
75 | }
76 | }
77 |
78 | /// Atomically performs an arbitrary action using the current value of the
79 | /// variable.
80 | ///
81 | /// Returns the result of the action.
82 | func withValue(action: (Value) throws -> Result) rethrows -> Result {
83 | lock()
84 | defer { unlock() }
85 |
86 | return try action(_value)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/DefaultHTTPServer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultHTTPServer.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/19/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Dispatch
11 |
12 | public final class DefaultHTTPServer: HTTPServer {
13 | public let logger = DefaultLogger()
14 | public var app: SWSGI
15 |
16 | /// Interface of TCP/IP to bind
17 | public let interface: String
18 | /// Port of TCP/IP to bind
19 | public let port: Int
20 |
21 | // the socket for accepting incoming connections
22 | private var acceptSocket: TCPSocket!
23 | private let eventLoop: EventLoop
24 | private var connections = Set()
25 |
26 | public init(
27 | eventLoop: EventLoop,
28 | interface: String = "::1",
29 | port: Int = 0,
30 | app: @escaping SWSGI
31 | ) {
32 | self.eventLoop = eventLoop
33 | self.app = app
34 | self.interface = interface
35 | self.port = port
36 | }
37 |
38 | deinit {
39 | stop()
40 | }
41 |
42 | public var listenAddress: (host: String, port: Int) {
43 | return try! acceptSocket.getSockName()
44 | }
45 |
46 | public func start() throws {
47 | guard acceptSocket == nil else {
48 | logger.error("Server already started")
49 | return
50 | }
51 | logger.info("Starting HTTP server on [\(interface)]:\(port) ...")
52 | acceptSocket = try TCPSocket()
53 | try acceptSocket.bind(port: port, interface: interface)
54 | try acceptSocket.listen()
55 | eventLoop.setReader(acceptSocket.fileDescriptor) { [unowned self] in
56 | self.handleNewConnection()
57 | }
58 | logger.info("HTTP server running")
59 | }
60 |
61 | public func stop() {
62 | guard acceptSocket != nil else {
63 | logger.error("Server not started")
64 | return
65 | }
66 | eventLoop.removeReader(acceptSocket.fileDescriptor)
67 | acceptSocket.close()
68 | for connection in connections {
69 | connection.close()
70 | }
71 | connections = []
72 | logger.info("HTTP server stopped")
73 | }
74 |
75 | public func stopAndWait() {
76 | let semaphore = DispatchSemaphore(value: 0)
77 | eventLoop.call {
78 | self.stop()
79 | semaphore.signal()
80 | }
81 | _ = semaphore.wait(timeout: DispatchTime.distantFuture)
82 | }
83 |
84 | // called to handle new connections
85 | private func handleNewConnection() {
86 | let clientSocket = try! acceptSocket.accept()
87 | let (address, port) = try! clientSocket.getPeerName()
88 | let transport = Transport(socket: clientSocket, eventLoop: eventLoop)
89 | let connection = HTTPConnection(
90 | app: appForConnection,
91 | serverName: "[\(interface)]",
92 | serverPort: self.port,
93 | transport: transport,
94 | eventLoop: eventLoop,
95 | logger: logger
96 | )
97 | connections.insert(connection)
98 | connection.closedCallback = { [unowned self, unowned connection] in
99 | self.connections.remove(connection)
100 | }
101 | logger.info("New connection \(connection.uuid) from [\(address)]:\(port)")
102 | }
103 |
104 | private func appForConnection(
105 | _ environ: [String: Any],
106 | startResponse: @escaping ((String, [(String, String)]) -> Void),
107 | sendBody: @escaping ((Data) -> Void)
108 | ) {
109 | app(environ, startResponse, sendBody)
110 | }
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/Sources/DefaultLogFormatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultLogFormatter.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 6/2/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct DefaultLogFormatter: LogFormatter {
12 | public func format(record: LogRecord) -> String {
13 | return "\(record.time) [\(record.level)] - \(record.loggerName): \(record.message)"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/DefaultLogger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultLogger.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/21/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public final class DefaultLogger: Logger {
12 | let name: String
13 | let level: LogLevel
14 | private(set) var handlers: [LogHandler] = []
15 |
16 | public init(name: String, level: LogLevel = .info) {
17 | self.name = name
18 | self.level = level
19 | }
20 |
21 | public init(fileName: String = #file, level: LogLevel = .info) {
22 | self.name = DefaultLogger.moduleNameForFileName(fileName)
23 | self.level = level
24 | }
25 |
26 | /// Add handler to self logger
27 | /// - Parameter handler: the handler to add
28 | public func add(handler: LogHandler) {
29 | handlers.append(handler)
30 | }
31 |
32 | public func debug(
33 | _ message: @autoclosure () -> String,
34 | caller: String = #function,
35 | file: String = #file,
36 | line: Int = #line
37 | ) {
38 | log(level: .debug, message: message(), caller: caller, file: file, line: line)
39 | }
40 |
41 | public func info(
42 | _ message: @autoclosure () -> String,
43 | caller: String = #function,
44 | file: String = #file,
45 | line: Int = #line
46 | ) {
47 | log(level: .info, message: message(), caller: caller, file: file, line: line)
48 | }
49 |
50 | public func warning(
51 | _ message: @autoclosure () -> String,
52 | caller: String = #function,
53 | file: String = #file,
54 | line: Int = #line
55 | ) {
56 | log(level: .warning, message: message(), caller: caller, file: file, line: line)
57 | }
58 |
59 | public func error(
60 | _ message: @autoclosure () -> String,
61 | caller: String = #function,
62 | file: String = #file,
63 | line: Int = #line
64 | ) {
65 | log(level: .error, message: message(), caller: caller, file: file, line: line)
66 | }
67 |
68 | public func critical(
69 | _ message: @autoclosure () -> String,
70 | caller: String = #function,
71 | file: String = #file,
72 | line: Int = #line
73 | ) {
74 | log(level: .critical, message: message(), caller: caller, file: file, line: line)
75 | }
76 |
77 | func log(
78 | level: LogLevel,
79 | message: @autoclosure () -> String,
80 | caller: String = #function,
81 | file: String = #file,
82 | line: Int = #line
83 | ) {
84 | let record = LogRecord(
85 | loggerName: name,
86 | level: level,
87 | message: message(),
88 | file: file,
89 | function: caller,
90 | line: line,
91 | time: Date()
92 | )
93 | log(record: record)
94 | }
95 |
96 | public func log(record: LogRecord) {
97 | guard record.level.rawValue >= level.rawValue else {
98 | return
99 | }
100 | for handler in handlers {
101 | handler.emit(record: record)
102 | }
103 | }
104 |
105 | /// Strip file name and return only the name part, e.g. /path/to/MySwiftModule.swift will be
106 | /// MySwiftModule
107 | /// - Parameter fileName: file name to be stripped
108 | /// - Returns: stripped file name
109 | static func moduleNameForFileName(_ fileName: String) -> String {
110 | return URL(fileURLWithPath: fileName).deletingPathExtension().lastPathComponent
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/Sources/Embassy.h:
--------------------------------------------------------------------------------
1 | //
2 | // Embassy-iOS.h
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/19/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for Embassy.
12 | FOUNDATION_EXPORT double EmbassyVersionNumber;
13 |
14 | //! Project version string for Embassy.
15 | FOUNDATION_EXPORT const unsigned char EmbassyVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Sources/Errors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Errors.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/21/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Error from operation system
12 | public enum OSError: Error {
13 | case ioError(number: Int32, message: String)
14 | /// Return a socket error with the last error number and description
15 | static func lastIOError() -> OSError {
16 | return .ioError(number: errno, message: lastErrorDescription())
17 | }
18 | }
19 |
20 | /// Return description for last error
21 | func lastErrorDescription() -> String {
22 | return String(cString: strerror(errno))
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/EventLoop.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EventLoop.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/23/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// EventLoop uses given selector to monitor IO events, trigger callbacks when needed to
12 | /// Follow Python EventLoop design https://docs.python.org/3/library/asyncio-eventloop.html
13 | public protocol EventLoop {
14 | /// Indicate whether is this event loop running
15 | var running: Bool { get }
16 |
17 | /// Set a read-ready callback for given fileDescriptor
18 | /// - Parameter fileDescriptor: target file descriptor
19 | /// - Parameter callback: callback function to be triggered when file is ready to be read
20 | func setReader(_ fileDescriptor: Int32, callback: @escaping () -> Void)
21 |
22 | /// Remove reader callback for given fileDescriptor
23 | /// - Parameter fileDescriptor: target file descriptor
24 | func removeReader(_ fileDescriptor: Int32)
25 |
26 | /// Set a write-ready callback for given fileDescriptor
27 | /// - Parameter fileDescriptor: target file descriptor
28 | /// - Parameter callback: callback function to be triggered when file is ready to be written
29 | func setWriter(_ fileDescriptor: Int32, callback: @escaping () -> Void)
30 |
31 | /// Remove writer callback for given fileDescriptor
32 | /// - Parameter fileDescriptor: target file descriptor
33 | func removeWriter(_ fileDescriptor: Int32)
34 |
35 | /// Call given callback as soon as possible (the next event loop iteration)
36 | /// - Parameter callback: the callback function to be called
37 | func call(callback: @escaping () -> Void)
38 |
39 | /// Call given callback `withDelay` seconds later
40 | /// - Parameter withDelay: delaying in seconds
41 | /// - Parameter callback: the callback function to be called
42 | func call(withDelay: TimeInterval, callback: @escaping () -> Void)
43 |
44 | /// Call given callback at specific time
45 | /// - Parameter atTime: time the callback to be called
46 | /// - Parameter callback: the callback function to be called
47 | func call(atTime: Date, callback: @escaping () -> Void)
48 |
49 | /// Stop the event loop
50 | func stop()
51 |
52 | /// Run the event loop forever
53 | func runForever()
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/FileLogHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileLogHandler.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 6/2/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Dispatch
11 |
12 | /// A log handler which writes log records to given file handle
13 | public struct FileLogHandler: LogHandler {
14 | let fileHandle: FileHandle
15 | public var formatter: LogFormatter?
16 |
17 | private let queue = DispatchQueue(
18 | label: "com.envoy.Embassy.logging.FileLogHandler.queue",
19 | attributes: []
20 | )
21 |
22 | public init(fileHandle: FileHandle, formatter: LogFormatter? = nil) {
23 | self.fileHandle = fileHandle
24 | self.formatter = formatter ?? DefaultLogFormatter()
25 | }
26 |
27 | public func emit(record: LogRecord) {
28 | queue.async {
29 | if let formatter = self.formatter {
30 | let msg = formatter.format(record: record) + "\n"
31 | self.fileHandle.write(msg.data(using: String.Encoding.utf8)!)
32 | self.fileHandle.synchronizeFile()
33 | }
34 | }
35 | }
36 |
37 | public static func stderrHandler() -> FileLogHandler {
38 | return FileLogHandler(fileHandle: FileHandle.standardError)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/HTTPConnection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPConnection.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/21/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// HTTPConnection represents an active HTTP connection
12 | public final class HTTPConnection {
13 | enum RequestState {
14 | case parsingHeader
15 | case readingBody
16 | }
17 |
18 | enum ResponseState {
19 | case sendingHeader
20 | case sendingBody
21 | }
22 |
23 | public let logger = DefaultLogger()
24 | public let uuid: String = UUID().uuidString
25 | public let transport: Transport
26 | public let app: SWSGI
27 | public let serverName: String
28 | public let serverPort: Int
29 | /// Callback to be called when this connection closed
30 | var closedCallback: (() -> Void)?
31 |
32 | private(set) var requestState: RequestState = .parsingHeader
33 | private(set) var responseState: ResponseState = .sendingHeader
34 | private(set) public var eventLoop: EventLoop!
35 | private var headerParser: HTTPHeaderParser!
36 | private var headerElements: [HTTPHeaderParser.Element] = []
37 | private var request: HTTPRequest!
38 | private var initialBody: Data?
39 | private var inputHandler: ((Data) -> Void)?
40 | // total content length to read
41 | private var contentLength: Int?
42 | // total data bytes we've already read
43 | private var readDataLength: Int = 0
44 |
45 | public init(
46 | app: @escaping SWSGI,
47 | serverName: String,
48 | serverPort: Int,
49 | transport: Transport,
50 | eventLoop: EventLoop,
51 | logger: Logger,
52 | closedCallback: (() -> Void)? = nil
53 | ) {
54 | self.app = app
55 | self.serverName = serverName
56 | self.serverPort = serverPort
57 | self.transport = transport
58 | self.eventLoop = eventLoop
59 | self.closedCallback = closedCallback
60 |
61 | transport.readDataCallback = { [unowned self] data in
62 | self.handleDataReceived(data)
63 | }
64 | transport.closedCallback = { [unowned self] reason in
65 | self.handleConnectionClosed(reason)
66 | }
67 |
68 | let propagateHandler = PropagateLogHandler(logger: logger)
69 | let contextHandler = TransformLogHandler(
70 | handler: propagateHandler
71 | ) { [unowned self] record in
72 | return record.overwriteMessage { [unowned self] in "[\(self.uuid)] \($0.message)" }
73 | }
74 | self.logger.add(handler: contextHandler)
75 | }
76 |
77 | public func close() {
78 | transport.close()
79 | }
80 |
81 | // called to handle data received
82 | private func handleDataReceived(_ data: Data) {
83 | switch requestState {
84 | case .parsingHeader:
85 | handleHeaderData(data)
86 | case .readingBody:
87 | handleBodyData(data)
88 | }
89 | }
90 |
91 | // called to handle header data
92 | private func handleHeaderData(_ data: Data) {
93 | if headerParser == nil {
94 | headerParser = HTTPHeaderParser()
95 | }
96 | headerElements += headerParser.feed(data)
97 | // we only handle when there are elements in header parser
98 | guard let lastElement = headerElements.last else {
99 | return
100 | }
101 | // we only handle the it when we get the end of header
102 | guard case .end = lastElement else {
103 | return
104 | }
105 |
106 | var method: String!
107 | var path: String!
108 | var version: String!
109 | var headers: [(String, String)] = []
110 | for element in headerElements {
111 | switch element {
112 | case .head(let headMethod, let headPath, let headVersion):
113 | method = headMethod
114 | path = headPath
115 | version = headVersion
116 | case .header(let key, let value):
117 | headers.append((key, value))
118 | case .end(let bodyPart):
119 | initialBody = bodyPart
120 | }
121 | }
122 | logger.info(
123 | "Header parsed, method=\(method!), path=\(path.debugDescription), " +
124 | "version=\(version.debugDescription), headers=\(headers)"
125 | )
126 | request = HTTPRequest(
127 | method: HTTPRequest.Method.fromString(method),
128 | path: path,
129 | version: version,
130 | headers: headers
131 | )
132 | var environ = SWSGIUtils.environFor(request: request)
133 | environ["SERVER_NAME"] = serverName
134 | environ["SERVER_PORT"] = String(serverPort)
135 | environ["SERVER_PROTOCOL"] = "HTTP/1.1"
136 |
137 | // set SWSGI keys
138 | environ["swsgi.version"] = "0.1"
139 | environ["swsgi.url_scheme"] = "http"
140 | environ["swsgi.input"] = { [unowned self] (handler: ((Data) -> Void)?) in
141 | self.swsgiInput(handler)
142 | }
143 | // TODO: add output file for error
144 | environ["swsgi.error"] = ""
145 | environ["swsgi.multithread"] = false
146 | environ["swsgi.multiprocess"] = false
147 | environ["swsgi.run_once"] = false
148 |
149 | // set embassy specific keys
150 | environ["embassy.connection"] = self
151 | environ["embassy.event_loop"] = eventLoop
152 | environ["embassy.headers"] = headers
153 |
154 | if
155 | let bundle = Bundle(identifier: "com.envoy.Embassy"),
156 | let version = bundle.infoDictionary?["CFBundleShortVersionString"] as? String
157 | {
158 | environ["embassy.version"] = version
159 | } else {
160 | // TODO: not sure what's the method we can use to get current package version for Linux,
161 | // just put unknown here to make test pass for now
162 | environ["embassy.version"] = "unknown"
163 | }
164 |
165 | if let contentLength = request.headers["Content-Length"], let length = Int(contentLength) {
166 | self.contentLength = length
167 | }
168 |
169 | // change state for incoming request to
170 | requestState = .readingBody
171 | // pause the reading for now, let `swsgi.input` called and resume it later
172 | transport.resume(reading: false)
173 |
174 | app(environ, startResponse, sendBody)
175 | }
176 |
177 | private func swsgiInput(_ handler: ((Data) -> Void)?) {
178 | inputHandler = handler
179 | // reading handler provided
180 | if handler != nil {
181 | if let initialBody = initialBody {
182 | if !initialBody.isEmpty {
183 | handleBodyData(initialBody)
184 | }
185 | self.initialBody = nil
186 | }
187 | transport.resume(reading: true)
188 | logger.info("Resume reading")
189 | // if the input handler is set to nil, it means pause reading
190 | } else {
191 | logger.info("Pause reading")
192 | transport.resume(reading: false)
193 | }
194 | }
195 |
196 | private func handleBodyData(_ data: Data) {
197 | guard let handler = inputHandler else {
198 | fatalError("Not suppose to read body data when input handler is not provided")
199 | }
200 | handler(data)
201 | readDataLength += data.count
202 | // we finish reading all the content, send EOF to input handler
203 | if let length = contentLength, readDataLength >= length {
204 | handler(Data())
205 | inputHandler = nil
206 | }
207 | }
208 |
209 | private func startResponse(_ status: String, headers: [(String, String)]) {
210 | guard case .sendingHeader = responseState else {
211 | logger.error("Response is not ready for sending header")
212 | return
213 | }
214 | var headers = headers
215 | let headerList = MultiDictionary(items: headers)
216 | // we don't support keep-alive connection for now, just force it to be closed
217 | if headerList["Connection"] == nil {
218 | headers.append(("Connection", "close"))
219 | }
220 | if headerList["Server"] == nil {
221 | headers.append(("Server", "Embassy"))
222 | }
223 | logger.debug("Start response, status=\(status.debugDescription), headers=\(headers.debugDescription)")
224 | let headersPart = headers.map { (key, value) in
225 | return "\(key): \(value)"
226 | }.joined(separator: "\r\n")
227 | let parts = [
228 | "HTTP/1.1 \(status)",
229 | headersPart,
230 | "\r\n"
231 | ]
232 | transport.write(string: parts.joined(separator: "\r\n"))
233 | responseState = .sendingBody
234 | }
235 |
236 | private func sendBody(_ data: Data) {
237 | guard case .sendingBody = responseState else {
238 | logger.error("Response is not ready for sending body")
239 | return
240 | }
241 | guard data.count > 0 else {
242 | // TODO: support keep-alive connection here?
243 | logger.info("Finish response")
244 | transport.close()
245 | return
246 | }
247 | transport.write(data: data)
248 | }
249 |
250 | // called to handle connection closed
251 | private func handleConnectionClosed(_ reason: Transport.CloseReason) {
252 | logger.info("Connection closed, reason=\(reason)")
253 | close()
254 | if let handler = inputHandler {
255 | handler(Data())
256 | inputHandler = nil
257 | }
258 | if let callback = closedCallback {
259 | callback()
260 | }
261 | }
262 | }
263 |
264 | extension HTTPConnection: Equatable {
265 | }
266 |
267 | public func == (lhs: HTTPConnection, rhs: HTTPConnection) -> Bool {
268 | return lhs.uuid == rhs.uuid
269 | }
270 |
271 | extension HTTPConnection: Hashable {
272 | public func hash(into hasher: inout Hasher) {
273 | hasher.combine(uuid)
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/Sources/HTTPHeaderParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPHeaderParser.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/19/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension String {
12 | /// String without leading spaces
13 | var withoutLeadingSpaces: String {
14 | var firstNoneSpace: Int = count
15 | for (i, char) in enumerated() {
16 | if char != " " {
17 | firstNoneSpace = i
18 | break
19 | }
20 | }
21 | return String(suffix(from: index(startIndex, offsetBy: firstNoneSpace)))
22 | }
23 | }
24 |
25 | /// Parser for HTTP headers
26 | public struct HTTPHeaderParser {
27 | private static let CR = UInt8(13)
28 | private static let LF = UInt8(10)
29 | private static let NEWLINE = (CR, LF)
30 |
31 | public enum Element {
32 | case head(method: String, path: String, version: String)
33 | case header(key: String, value: String)
34 | case end(bodyPart: Data)
35 | }
36 |
37 | private enum State {
38 | case head
39 | case headers
40 | }
41 | private var state: State = .head
42 | private var buffer: Data = Data()
43 |
44 | /// Feed data to HTTP parser
45 | /// - Parameter data: the data to feed
46 | /// - Returns: parsed headers elements
47 | mutating func feed(_ data: Data) -> [Element] {
48 | buffer.append(data)
49 | var elements = [Element]()
50 | while buffer.count > 0 {
51 | // pair of (0th, 1st), (1st, 2nd), (2nd, 3rd) ... chars, so that we can find
52 | let charPairs: [(UInt8, UInt8)] = Array(zip(
53 | buffer[0.. in current buffer
57 | guard let index = (charPairs).firstIndex(where: { $0 == HTTPHeaderParser.NEWLINE }) else {
58 | // no found, just return the current elements
59 | return elements
60 | }
61 | let bytes = Array(buffer[0.. 0 else {
76 | elements.append(.end(bodyPart: buffer))
77 | return elements
78 | }
79 | let parts = string.components(separatedBy: ":")
80 | let key = parts[0]
81 | let value = parts[1.. Method {
50 | switch name.uppercased() {
51 | case "GET":
52 | return .get
53 | case "HEAD":
54 | return .head
55 | case "POST":
56 | return .post
57 | case "PUT":
58 | return .put
59 | case "DELETE":
60 | return .delete
61 | case "TRACE":
62 | return .trace
63 | case "OPTIONS":
64 | return .options
65 | case "CONNECT":
66 | return .connect
67 | case "PATCH":
68 | return .patch
69 | default:
70 | return .other(name: name)
71 | }
72 | }
73 | }
74 |
75 | /// Request method
76 | let method: Method
77 | /// Request path
78 | let path: String
79 | /// Request HTTP version (e.g. HTTP/1.0)
80 | let version: String
81 | /// Request headers
82 | let headers: MultiDictionary
83 |
84 | public init(method: Method, path: String, version: String, headers: [(String, String)]) {
85 | self.method = method
86 | self.path = path
87 | self.version = version
88 | self.headers = MultiDictionary(items: headers)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/HTTPServer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPServer.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/19/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// HTTPServerType is the protocol for basic SWSGI server
12 | public protocol HTTPServer {
13 | /// The SWSGI app to serve
14 | var app: SWSGI { get set }
15 | /// Start the HTTP server
16 | func start() throws
17 | /// Stop the HTTP server.
18 | /// This is not thread-safe, needs to be called inside event loop, call `stopAndWait` instead
19 | /// from other thread
20 | func stop()
21 | /// Stop the HTTP server in thread-safe manner, also wait until the server completely stopped.
22 | /// If there is pending connections, they will all be closed without waiting them to finish up.
23 | func stopAndWait()
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/HeapSort.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeapSort.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/25/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct HeapSort {
12 | /// Do a heap push to the heap queue (maintained the heap in correct order)
13 | /// - Parameter heap: the heap queue array, should already be in heap order
14 | /// - Parameter isOrderredBefore: the function to return is the first argument's order before the second argument
15 | /// - Parameter item: the new item to be appended into the heap queue
16 | static func heapPush(_ heap: inout Array, item: T, isOrderredBefore: (T, T) -> Bool) {
17 | heap.append(item)
18 | shiftDown(&heap, startPos: 0, pos: heap.count - 1, isOrderredBefore: isOrderredBefore)
19 | }
20 |
21 | /// Do a heap push to the heap queue (maintained the heap in correct order)
22 | /// - Parameter heap: the heap queue array, should already be in heap order
23 | /// - Parameter item: the new item to be appended into the heap queue
24 | static func heapPush(_ heap: inout Array, item: T) {
25 | heapPush(&heap, item: item, isOrderredBefore: <)
26 | }
27 |
28 | /// Do a smallest heap pop from the heap queue
29 | /// - Parameter heap: the heap queue array, should already be in heap order
30 | /// - Parameter isOrderredBefore: the function to return is the first argument's order before the second argument
31 | /// - Returns: the smallest item popped from the heap queue
32 | static func heapPop(_ heap: inout Array, isOrderredBefore: (T, T) -> Bool) -> T {
33 | let lastItem = heap.removeLast()
34 | guard !heap.isEmpty else {
35 | return lastItem
36 | }
37 | let firstItem = heap[0]
38 | heap[0] = lastItem
39 | shiftUp(&heap, pos: 0, isOrderredBefore: isOrderredBefore)
40 | return firstItem
41 | }
42 |
43 | /// Do a smallest heap pop from the heap queue
44 | /// - Parameter heap: the heap queue array, should already be in heap order
45 | /// - Returns: the smallest item popped from the heap queue
46 | static func heapPop(_ heap: inout Array) -> T {
47 | return heapPop(&heap, isOrderredBefore: <)
48 | }
49 |
50 | private static func shiftDown(_ heap: inout Array, startPos: Array.Index, pos: Array.Index, isOrderredBefore: (T, T) -> Bool) {
51 | var pos = pos
52 | let newItem = heap[pos]
53 | // Follow the path to the root, moving parents down until finding a place newitem fits.
54 | while pos > startPos {
55 | let parentPos = (pos - 1) / 2
56 | let parent = heap[parentPos]
57 | // new item already in the right position, break
58 | guard isOrderredBefore(newItem, parent) else {
59 | break
60 | }
61 | // move parent down
62 | heap[pos] = parent
63 | pos = parentPos
64 | }
65 | heap[pos] = newItem
66 | }
67 |
68 | private static func shiftUp(_ heap: inout Array, pos: Array.Index, isOrderredBefore: (T, T) -> Bool) {
69 | var pos = pos
70 | let endPos = heap.count
71 | let newItem = heap[pos]
72 | let startPos = pos
73 | // leftmost child position
74 | var childPos = 2 * pos + 1
75 | // Bubble up the smaller child until hitting a leaf.
76 | while childPos < endPos {
77 | // Set childpos to index of smaller child.
78 | let rightPos = childPos + 1
79 | if rightPos < endPos && !isOrderredBefore(heap[childPos], heap[rightPos]) {
80 | childPos = rightPos
81 | }
82 | // Move the smaller child up.
83 | heap[pos] = heap[childPos]
84 | pos = childPos
85 | childPos = 2 * pos + 1
86 | }
87 | // The leaf at pos is empty now. Put newitem there, and bubble it up to its final resting place (by sifting its parents down).
88 | heap[pos] = newItem
89 | shiftDown(&heap, startPos: startPos, pos: pos, isOrderredBefore: isOrderredBefore)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/IOUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IOUtils.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/21/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct IOUtils {
12 | /// Get blocking mode from a file descriptor
13 | /// - Parameter fileDescriptor: target file descriptor
14 | /// - Returns: is the file in blocking mode or not
15 | static func getBlocking(fileDescriptor: Int32) -> Bool {
16 | let flags = fcntl(fileDescriptor, F_GETFL, 0)
17 | return flags & O_NONBLOCK == 0
18 | }
19 |
20 | /// Set blocking mode for a file descriptor
21 | /// - Parameter fileDescriptor: target file descriptor
22 | /// - Parameter blocking: enable blocking mode or not
23 | static func setBlocking(fileDescriptor: Int32, blocking: Bool) {
24 | let flags = fcntl(fileDescriptor, F_GETFL, 0)
25 | let newFlags: Int32
26 | if blocking {
27 | newFlags = flags & ~O_NONBLOCK
28 | } else {
29 | newFlags = flags | O_NONBLOCK
30 | }
31 | let _ = fcntl(fileDescriptor, F_SETFL, newFlags)
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
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 | 3.0.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Sources/KqueueSelector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KqueueSelector.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/20/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | #if !os(Linux)
12 |
13 | public final class KqueueSelector: Selector {
14 | enum Error: Swift.Error {
15 | case keyError(fileDescriptor: Int32)
16 | }
17 |
18 | // the maximum event number to select from kqueue at once (one kevent call)
19 | private let selectMaximumEvent: Int
20 | private let kqueue: Int32
21 | private var fileDescriptorMap: [Int32: SelectorKey] = [:]
22 |
23 | public init(selectMaximumEvent: Int = 1024) throws {
24 | kqueue = Darwin.kqueue()
25 | guard kqueue >= 0 else {
26 | throw OSError.lastIOError()
27 | }
28 | self.selectMaximumEvent = selectMaximumEvent
29 | }
30 |
31 | deinit {
32 | close()
33 | }
34 |
35 | @discardableResult
36 | public func register(
37 | _ fileDescriptor: Int32,
38 | events: Set,
39 | data: Any?
40 | ) throws -> SelectorKey {
41 | // ensure the file descriptor doesn't exist already
42 | guard fileDescriptorMap[fileDescriptor] == nil else {
43 | throw Error.keyError(fileDescriptor: fileDescriptor)
44 | }
45 | let key = SelectorKey(fileDescriptor: fileDescriptor, events: events, data: data)
46 | fileDescriptorMap[fileDescriptor] = key
47 |
48 | var kevents: [Darwin.kevent] = []
49 | for event in events {
50 | let filter: Int16
51 | switch event {
52 | case .read:
53 | filter = Int16(EVFILT_READ)
54 | case .write:
55 | filter = Int16(EVFILT_WRITE)
56 | }
57 | let kevent = Darwin.kevent(
58 | ident: UInt(fileDescriptor),
59 | filter: filter,
60 | flags: UInt16(EV_ADD),
61 | fflags: UInt32(0),
62 | data: Int(0),
63 | udata: nil
64 | )
65 | kevents.append(kevent)
66 | }
67 |
68 | // register events to kqueue
69 | guard kevents.withUnsafeMutableBufferPointer({ pointer in
70 | kevent(kqueue, pointer.baseAddress, Int32(pointer.count), nil, Int32(0), nil) >= 0
71 | }) else {
72 | throw OSError.lastIOError()
73 | }
74 | return key
75 | }
76 |
77 | @discardableResult
78 | public func unregister(_ fileDescriptor: Int32) throws -> SelectorKey {
79 | // ensure the file descriptor exists
80 | guard let key = fileDescriptorMap[fileDescriptor] else {
81 | throw Error.keyError(fileDescriptor: fileDescriptor)
82 | }
83 | fileDescriptorMap.removeValue(forKey: fileDescriptor)
84 | var kevents: [Darwin.kevent] = []
85 | for event in key.events {
86 | let filter: Int16
87 | switch event {
88 | case .read:
89 | filter = Int16(EVFILT_READ)
90 | case .write:
91 | filter = Int16(EVFILT_WRITE)
92 | }
93 | let kevent = Darwin.kevent(
94 | ident: UInt(fileDescriptor),
95 | filter: filter,
96 | flags: UInt16(EV_DELETE),
97 | fflags: UInt32(0),
98 | data: Int(0),
99 | udata: nil
100 | )
101 | kevents.append(kevent)
102 | }
103 |
104 | // unregister events from kqueue
105 | guard kevents.withUnsafeMutableBufferPointer({ pointer in
106 | kevent(kqueue, pointer.baseAddress, Int32(pointer.count), nil, Int32(0), nil) >= 0
107 | }) else {
108 | throw OSError.lastIOError()
109 | }
110 | return key
111 | }
112 |
113 | public func close() {
114 | _ = Darwin.close(kqueue)
115 | }
116 |
117 | public func select(timeout: TimeInterval?) throws -> [(SelectorKey, Set)] {
118 | var timeSpec: timespec?
119 | if let timeout = timeout {
120 | if timeout > 0 {
121 | var integer = 0.0
122 | let nsec = Int(modf(timeout, &integer) * Double(NSEC_PER_SEC))
123 | timeSpec = timespec(tv_sec: Int(timeout), tv_nsec: nsec)
124 | } else {
125 | timeSpec = timespec()
126 | }
127 | }
128 |
129 | var kevents = Array(repeating: Darwin.kevent(), count: selectMaximumEvent)
130 | let eventCount:Int32 = kevents.withUnsafeMutableBufferPointer { pointer in
131 | return withUnsafeOptionalPointer(to: &timeSpec) { timeSpecPointer in
132 | return kevent(
133 | kqueue,
134 | nil,
135 | 0,
136 | pointer.baseAddress,
137 | Int32(selectMaximumEvent),
138 | timeSpecPointer
139 | )
140 | }
141 | }
142 | guard eventCount >= 0 else {
143 | throw OSError.lastIOError()
144 | }
145 |
146 | var fileDescriptorIOEvents = [Int32: Set]()
147 | for index in 0..()
151 | if event.filter == Int16(EVFILT_READ) {
152 | ioEvents.insert(.read)
153 | } else if event.filter == Int16(EVFILT_WRITE) {
154 | ioEvents.insert(.write)
155 | }
156 | fileDescriptorIOEvents[fileDescriptor] = ioEvents
157 | }
158 | let fdMap = fileDescriptorMap
159 | return fileDescriptorIOEvents.compactMap { event in
160 | fdMap[event.0].map { ($0, event.1) } ?? nil
161 | }
162 | }
163 |
164 | public subscript(fileDescriptor: Int32) -> SelectorKey? {
165 | get {
166 | return fileDescriptorMap[fileDescriptor]
167 | }
168 | }
169 |
170 | private func withUnsafeOptionalPointer(to: inout T?, body: (UnsafePointer?) throws -> Result) rethrows -> Result {
171 | if to != nil {
172 | return try withUnsafePointer(to: &to!) { try body($0) }
173 | } else {
174 | return try body(nil)
175 | }
176 | }
177 |
178 | }
179 |
180 | #endif
181 |
--------------------------------------------------------------------------------
/Sources/LogFormatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogFormatter.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 6/2/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Log formatter convert given log record into printable text
12 | public protocol LogFormatter {
13 | func format(record: LogRecord) -> String
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/LogHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogHandler.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 6/2/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol LogHandler {
12 | var formatter: LogFormatter? { get set }
13 |
14 | /// Handle given record from logger
15 | /// - Parameter record: log record
16 | func emit(record: LogRecord)
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Logger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logger.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 6/2/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum LogLevel: Int {
12 | case notset = 0
13 | case debug = 10
14 | case info = 20
15 | case warning = 30
16 | case error = 40
17 | case critical = 50
18 |
19 | var name: String {
20 | switch self {
21 | case .notset:
22 | return "NOTSET"
23 | case .debug:
24 | return "DEBUG"
25 | case .info:
26 | return "INFO"
27 | case .warning:
28 | return "WARNING"
29 | case .error:
30 | return "ERROR"
31 | case .critical:
32 | return "CRITICAL"
33 | }
34 | }
35 | }
36 |
37 | public struct LogRecord {
38 | let loggerName: String
39 | let level: LogLevel
40 | let message: String
41 | let file: String
42 | let function: String
43 | let line: Int
44 | let time: Date
45 | }
46 |
47 | extension LogRecord {
48 | /// Overwrite message and return a new record
49 | /// - Parameter overwrite: closure to accept self record and return overwritten string
50 | /// - Returns: the overwritten log record
51 | public func overwriteMessage(overwrite: ((LogRecord) -> String)) -> LogRecord {
52 | return LogRecord(
53 | loggerName: loggerName,
54 | level: level,
55 | message: overwrite(self),
56 | file: file,
57 | function: function,
58 | line: line,
59 | time: time
60 | )
61 | }
62 | }
63 |
64 | public protocol Logger {
65 | /// Add a handler to the logger
66 | func add(handler: LogHandler)
67 |
68 | /// Write log record to logger
69 | func log(record: LogRecord)
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/MultiDictionary.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultiDictionary.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 6/1/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Transformer for MultiDictionary keys, like lower case
12 | public protocol KeyTransformer {
13 | associatedtype Key: Hashable
14 | static func transform(key: Key) -> Key
15 | }
16 |
17 | /// A key transformer that does nothing to the key but simply return it
18 | public struct NoOpKeyTransform: KeyTransformer {
19 | public typealias Key = T
20 | public static func transform(key: T) -> Key {
21 | return key
22 | }
23 | }
24 |
25 | /// A key transformer that lowers case of the String key, so that the MultiDictionary will be
26 | /// case-insenstive
27 | public struct LowercaseKeyTransform: KeyTransformer {
28 | public typealias Key = String
29 | public static func transform(key: Key) -> Key {
30 | return key.lowercased()
31 | }
32 | }
33 |
34 | /// MultiDictionary is a Dictionary and Array like container, it allows one key to have multiple
35 | /// values
36 | public struct MultiDictionary<
37 | Key,
38 | Value,
39 | KeyTransform: KeyTransformer>
40 | where KeyTransform.Key == Key
41 | {
42 | public typealias ArrayType = Array<(Key, Value)>
43 | public typealias DictionaryType = Dictionary>
44 |
45 | // Items in this multi dictionary
46 | fileprivate let items: ArrayType
47 | // Dictionary mapping from key to tuple of original key (before transform) and all values in
48 | /// order
49 | fileprivate let keyValuesMap: DictionaryType
50 |
51 | public init(items: Array<(Key, Value)>) {
52 | self.items = items
53 | var keyValuesMap: DictionaryType = [:]
54 | for (key, value) in items {
55 | let transformedKey = KeyTransform.transform(key: key)
56 | var values = keyValuesMap[transformedKey] ?? []
57 | values.append(value)
58 | keyValuesMap[transformedKey] = values
59 | }
60 | self.keyValuesMap = keyValuesMap
61 | }
62 |
63 | /// Get all values for given key in occurrence order
64 | /// - Parameter key: the key
65 | /// - Returns: tuple of array of values for given key
66 | public func valuesFor(key: Key) -> Array? {
67 | return keyValuesMap[KeyTransform.transform(key: key)]
68 | }
69 | /// Get the first value for given key if available
70 | /// - Parameter key: the key
71 | /// - Returns: first value for the key if available, otherwise nil will be returned
72 | public subscript(key: Key) -> Value? {
73 | return valuesFor(key: key)?.first
74 | }
75 | }
76 |
77 | // MARK: CollectionType
78 | extension MultiDictionary: Collection {
79 |
80 | public typealias Index = ArrayType.Index
81 |
82 | public var startIndex: Index {
83 | return items.startIndex
84 | }
85 |
86 | public var endIndex: Index {
87 | return items.endIndex
88 | }
89 |
90 | public subscript(position: Index) -> Element {
91 | return items[position]
92 | }
93 |
94 | public func index(after i: Index) -> Index {
95 | guard i != endIndex else { fatalError("Cannot increment endIndex") }
96 | return i + 1
97 | }
98 | }
99 |
100 | // MARK: ArrayLiteralConvertible
101 | extension MultiDictionary: ExpressibleByArrayLiteral {
102 | public typealias Element = ArrayType.Element
103 | public init(arrayLiteral elements: Element...) {
104 | items = elements
105 | var keyValuesMap: DictionaryType = [:]
106 | for (key, value) in items {
107 | let transformedKey = KeyTransform.transform(key: key)
108 | var values = keyValuesMap[transformedKey] ?? []
109 | values.append(value)
110 | keyValuesMap[transformedKey] = values
111 | }
112 | self.keyValuesMap = keyValuesMap
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Sources/PrintLogHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrintLogHandler.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 6/2/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// A log handler which prints (stdout) log records
12 | public struct PrintLogHandler: LogHandler {
13 | public var formatter: LogFormatter?
14 |
15 | public init(formatter: LogFormatter? = nil) {
16 | self.formatter = formatter ?? DefaultLogFormatter()
17 | }
18 |
19 | public func emit(record: LogRecord) {
20 | if let formatter = formatter {
21 | print(formatter.format(record: record))
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/PropagateLogHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PropagateLogHandler.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 6/2/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// A log handler which propagates record to another logger
12 | public struct PropagateLogHandler: LogHandler {
13 | public let logger: Logger
14 | public var formatter: LogFormatter?
15 |
16 | public init(logger: Logger) {
17 | self.logger = logger
18 | }
19 |
20 | public func emit(record: LogRecord) {
21 | logger.log(record: record)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/SWSGI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SWSGI.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/19/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | /**
13 | Swift Web Server Gateway Interface
14 |
15 | This is a HTTP server gateway interface inspired by Python's WSGI
16 |
17 | - Parameter environ: environ variables for the incoming HTTP request (TODO: keys need to be defined)
18 | - Parameter startResponse: function to call to inform server to start sending HTTP response header to client,
19 | first argument is the status text, e.g. "200 OK". The second argument is a list of
20 | header key and value pair
21 | - Parameter sendBody: function to call to send the HTTP body to client, to end the stream, simply send an UInt8
22 | with zero length
23 |
24 | */
25 | public typealias SWSGI = (
26 | [String: Any],
27 | @escaping ((String, [(String, String)]) -> Void),
28 | @escaping ((Data) -> Void)
29 | ) -> Void
30 |
31 | /**
32 | SWSGI Input interface for receiving incoming data from request.
33 | To receive data, pass handler function, to pause reading data, just pass nil as the handler
34 | */
35 | public typealias SWSGIInput = (((Data) -> Void)?) -> Void
36 |
--------------------------------------------------------------------------------
/Sources/SWSGIUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SWSGIUtils.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/23/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // from http://stackoverflow.com/a/24052094/25077
12 | /// Update one dictionay by another
13 | private func += (left: inout [K: V], right: [K: V]) {
14 | for (k, v) in right {
15 | left.updateValue(v, forKey: k)
16 | }
17 | }
18 |
19 | public struct SWSGIUtils {
20 | /// Transform given request into environ dictionary
21 | static func environFor(request: HTTPRequest) -> [String: Any] {
22 | var environ: [String: Any] = [
23 | "REQUEST_METHOD": String(describing: request.method),
24 | "SCRIPT_NAME": ""
25 | ]
26 |
27 | let queryParts = request.path.components(separatedBy: "?")
28 | if queryParts.count > 1 {
29 | environ["PATH_INFO"] = queryParts[0]
30 | environ["QUERY_STRING"] = queryParts[1..
48 | ) -> [String: Any] {
49 | var environ: [String: Any] = [:]
50 | for (key, value) in headers {
51 | let key = "HTTP_" + key.uppercased().replacingOccurrences(
52 | of: "-",
53 | with: "_"
54 | )
55 | environ[key] = value
56 | }
57 | return environ
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/SelectSelector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectSelector.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 1/6/17.
6 | // Copyright © 2017 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public final class SelectSelector: Selector {
12 | enum Error: Swift.Error {
13 | case keyError(fileDescriptor: Int32)
14 | }
15 |
16 | private var fileDescriptorMap: [Int32: SelectorKey] = [:]
17 |
18 | public init() throws {
19 | }
20 |
21 | @discardableResult
22 | public func register(
23 | _ fileDescriptor: Int32,
24 | events: Set,
25 | data: Any?
26 | ) throws -> SelectorKey {
27 | // ensure the file descriptor doesn't exist already
28 | guard fileDescriptorMap[fileDescriptor] == nil else {
29 | throw Error.keyError(fileDescriptor: fileDescriptor)
30 | }
31 | let key = SelectorKey(fileDescriptor: fileDescriptor, events: events, data: data)
32 | fileDescriptorMap[fileDescriptor] = key
33 | return key
34 | }
35 |
36 | @discardableResult
37 | public func unregister(_ fileDescriptor: Int32) throws -> SelectorKey {
38 | // ensure the file descriptor exists
39 | guard let key = fileDescriptorMap[fileDescriptor] else {
40 | throw Error.keyError(fileDescriptor: fileDescriptor)
41 | }
42 | fileDescriptorMap.removeValue(forKey: fileDescriptor)
43 | return key
44 | }
45 |
46 | public func close() {
47 | }
48 |
49 | public func select(timeout: TimeInterval?) throws -> [(SelectorKey, Set)] {
50 | var readSet = fd_set()
51 | var writeSet = fd_set()
52 |
53 | var maxFd: Int32 = 0
54 | for (fd, key) in fileDescriptorMap {
55 | if fd > maxFd {
56 | maxFd = fd
57 | }
58 | if key.events.contains(.read) {
59 | SystemLibrary.fdSet(fd: fd, set: &readSet)
60 | }
61 | if key.events.contains(.write) {
62 | SystemLibrary.fdSet(fd: fd, set: &writeSet)
63 | }
64 | }
65 | let status: Int32
66 | let microsecondsPerSecond = 1000000
67 | if let timeout = timeout {
68 | var timeoutVal = timeval()
69 | #if os(Linux)
70 | timeoutVal.tv_sec = Int(timeout)
71 | timeoutVal.tv_usec = Int(
72 | Int(timeout * Double(microsecondsPerSecond)) -
73 | timeoutVal.tv_sec * microsecondsPerSecond
74 | )
75 | #else
76 | // TODO: not sure is the calculation correct here
77 | timeoutVal.tv_sec = Int(timeout)
78 | timeoutVal.tv_usec = __darwin_suseconds_t(
79 | Int(timeout * Double(microsecondsPerSecond)) -
80 | timeoutVal.tv_sec * microsecondsPerSecond
81 | )
82 | #endif
83 | status = SystemLibrary.select(maxFd + 1, &readSet, &writeSet, nil, &timeoutVal)
84 | } else {
85 | status = SystemLibrary.select(maxFd + 1, &readSet, &writeSet, nil, nil)
86 | }
87 | switch status {
88 | case 0:
89 | // TODO: timeout?
90 | return []
91 | // Error
92 | case -1:
93 | throw OSError.lastIOError()
94 | default:
95 | break
96 | }
97 |
98 | var result: [(SelectorKey, Set)] = []
99 | for (fd, key) in fileDescriptorMap {
100 | var events = Set()
101 | if SystemLibrary.fdIsSet(fd: fd, set: &readSet) {
102 | events.insert(.read)
103 | }
104 | if SystemLibrary.fdIsSet(fd: fd, set: &writeSet) {
105 | events.insert(.write)
106 | }
107 | if events.count > 0 {
108 | result.append((key, events))
109 | }
110 | }
111 | return result
112 | }
113 |
114 | public subscript(fileDescriptor: Int32) -> SelectorKey? {
115 | get {
116 | return fileDescriptorMap[fileDescriptor]
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Sources/Selector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Selector.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/20/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Event of IO
12 | public enum IOEvent {
13 | case read
14 | case write
15 | }
16 |
17 | /// Represent a subscription for a file descriptor in Selector
18 | public struct SelectorKey {
19 | /// File descriptor
20 | let fileDescriptor: Int32
21 | /// Events to monitor
22 | let events: Set
23 | /// User custom data to be returned when we see an IO event
24 | let data: Any?
25 | }
26 |
27 | /// Selector provides a way to poll lots of file descriptors for IO events in an efficient way.
28 | /// The basic interface design follows https://docs.python.org/3/library/selectors.html
29 | public protocol Selector {
30 | /// Register a file descriptor for given IO events to watch
31 | /// - Parameter fileDescriptor: the file descriptor to watch
32 | /// - Parameter events: IO events to watch
33 | /// - Parameter data: user custom data to be returned when we see an IO event
34 | /// - Returns: added SelectorKey
35 | @discardableResult
36 | func register(_ fileDescriptor: Int32, events: Set, data: Any?) throws -> SelectorKey
37 |
38 | /// Unregister a file descriptor from selector
39 | @discardableResult
40 | func unregister(_ fileDescriptor: Int32) throws -> SelectorKey
41 |
42 | /// Close the selector to release underlaying resource
43 | func close()
44 |
45 | /// Select to see if the registered file descriptors have IO events, wait until
46 | /// we see a file descriptor ready or timeout
47 | /// - Parameter timeout: how long time to wait until return empty list,
48 | /// if timeout <= 0, it won't block but returns current file descriptor status immediately,
49 | /// if timeout == nil, it will block until there is a file descriptor ready
50 | /// - Returns: an array of (key, events) for ready file descriptors
51 | func select(timeout: TimeInterval?) throws -> [(SelectorKey, Set)]
52 |
53 | /// Return the SelectorKey for given file descriptor
54 | subscript(fileDescriptor: Int32) -> SelectorKey? { get }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/SelectorEventLoop.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectorEventLoop.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/20/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | private class CallbackHandle {
13 | let reader: (() -> Void)?
14 | let writer: (() -> Void)?
15 | init(reader: (() -> Void)? = nil, writer: (() -> Void)? = nil) {
16 | self.reader = reader
17 | self.writer = writer
18 | }
19 | }
20 |
21 | /// EventLoop uses given selector to monitor IO events, trigger callbacks when needed to
22 | /// Follow Python EventLoop design https://docs.python.org/3/library/asyncio-eventloop.html
23 | public final class SelectorEventLoop: EventLoop {
24 | private(set) public var running: Bool = false
25 | private let selector: Selector
26 | // these are for self-pipe-trick ref: https://cr.yp.to/docs/selfpipe.html
27 | // to be able to interrupt the blocking selector, we create a pipe and add it to the
28 | // selector, whenever we want to interrupt the selector, we send a byte
29 | private let pipeSender: Int32
30 | private let pipeReceiver: Int32
31 | // callbacks ready to be called at the next iteration
32 | private var readyCallbacks = Atomic<[(() -> Void)]>([])
33 | // callbacks scheduled to be called later
34 | private var scheduledCallbacks = Atomic<[(Date, (() -> Void))]>([])
35 |
36 | public init(selector: Selector) throws {
37 | self.selector = selector
38 | var pipeFds = [Int32](repeating: 0, count: 2)
39 | let pipeResult = pipeFds.withUnsafeMutableBufferPointer {
40 | SystemLibrary.pipe($0.baseAddress)
41 | }
42 | guard pipeResult >= 0 else {
43 | throw OSError.lastIOError()
44 | }
45 | pipeReceiver = pipeFds[0]
46 | pipeSender = pipeFds[1]
47 | IOUtils.setBlocking(fileDescriptor: pipeSender, blocking: false)
48 | IOUtils.setBlocking(fileDescriptor: pipeReceiver, blocking: false)
49 | // subscribe to pipe receiver read-ready event, do nothing, just allow selector
50 | // to be interrupted
51 |
52 | // Notice: we use a local copy of pipeReceiver to avoid referencing self
53 | // here, thus we won't have reference cycle problem
54 | let localPipeReceiver = pipeReceiver
55 | setReader(pipeReceiver) {
56 | // consume the pipe receiver, so that it won't keep triggering read event
57 | let size = PIPE_BUF
58 | var bytes = Data(count: Int(size))
59 | var readSize = 1
60 | while readSize > 0 {
61 | readSize = bytes.withUnsafeMutableBytes { pointer in
62 | return SystemLibrary.read(localPipeReceiver, pointer, Int(size))
63 | }
64 | }
65 | }
66 | }
67 |
68 | deinit {
69 | stop()
70 | removeReader(pipeReceiver)
71 | let _ = SystemLibrary.close(pipeSender)
72 | let _ = SystemLibrary.close(pipeReceiver)
73 | }
74 |
75 | public func setReader(_ fileDescriptor: Int32, callback: @escaping () -> Void) {
76 | // we already have the file descriptor in selector, unregister it then register
77 | if let key = selector[fileDescriptor] {
78 | let oldHandle = key.data as! CallbackHandle
79 | let handle = CallbackHandle(reader: callback, writer: oldHandle.writer)
80 | try! selector.unregister(fileDescriptor)
81 | try! selector.register(
82 | fileDescriptor,
83 | events: key.events.union([.read]),
84 | data: handle
85 | )
86 | // register the new file descriptor
87 | } else {
88 | try! selector.register(
89 | fileDescriptor,
90 | events: [.read],
91 | data: CallbackHandle(reader: callback)
92 | )
93 | }
94 | }
95 |
96 | public func removeReader(_ fileDescriptor: Int32) {
97 | guard let key = selector[fileDescriptor] else {
98 | return
99 | }
100 | try! selector.unregister(fileDescriptor)
101 | let newEvents = key.events.subtracting([.read])
102 | guard !newEvents.isEmpty else {
103 | return
104 | }
105 | let oldHandle = key.data as! CallbackHandle
106 | let handle = CallbackHandle(reader: nil, writer: oldHandle.writer)
107 | try! selector.register(fileDescriptor, events: newEvents, data: handle)
108 | }
109 |
110 | public func setWriter(_ fileDescriptor: Int32, callback: @escaping () -> Void) {
111 | // we already have the file descriptor in selector, unregister it then register
112 | if let key = selector[fileDescriptor] {
113 | let oldHandle = key.data as! CallbackHandle
114 | let handle = CallbackHandle(reader: oldHandle.reader, writer: callback)
115 | try! selector.unregister(fileDescriptor)
116 | try! selector.register(
117 | fileDescriptor,
118 | events: key.events.union([.write]),
119 | data: handle
120 | )
121 | // register the new file descriptor
122 | } else {
123 | try! selector.register(
124 | fileDescriptor,
125 | events: [.write],
126 | data: CallbackHandle(writer: callback)
127 | )
128 | }
129 | }
130 |
131 | public func removeWriter(_ fileDescriptor: Int32) {
132 | guard let key = selector[fileDescriptor] else {
133 | return
134 | }
135 | try! selector.unregister(fileDescriptor)
136 | let newEvents = key.events.subtracting([.write])
137 | guard !newEvents.isEmpty else {
138 | return
139 | }
140 | let oldHandle = key.data as! CallbackHandle
141 | let handle = CallbackHandle(reader: oldHandle.reader, writer: nil)
142 | try! selector.register(fileDescriptor, events: newEvents, data: handle)
143 | }
144 |
145 | public func call(callback: @escaping () -> Void) {
146 | readyCallbacks.modify { callbacks in
147 | var callbacks = callbacks
148 | callbacks.append(callback)
149 | return callbacks
150 | }
151 | interruptSelector()
152 | }
153 |
154 | public func call(withDelay delay: TimeInterval, callback: @escaping () -> Void) {
155 | call(atTime: Date().addingTimeInterval(delay), callback: callback)
156 | }
157 |
158 | public func call(atTime time: Date, callback: @escaping () -> Void) {
159 | scheduledCallbacks.modify { callbacks in
160 | var callbacks = callbacks
161 | HeapSort.heapPush(&callbacks, item: (time, callback)) {
162 | $0.0.timeIntervalSince1970 < $1.0.timeIntervalSince1970
163 | }
164 | return callbacks
165 | }
166 | interruptSelector()
167 | }
168 |
169 | public func stop() {
170 | running = false
171 | interruptSelector()
172 | }
173 |
174 | public func runForever() {
175 | running = true
176 | while running {
177 | runOnce()
178 | }
179 | }
180 |
181 | // interrupt the selector
182 | private func interruptSelector() {
183 | let byte = [UInt8](repeating: 0, count: 1)
184 | let rc = write(pipeSender, byte, byte.count)
185 | assert(
186 | rc >= 0,
187 | "Failed to interrupt selector, errno=\(errno), message=\(lastErrorDescription())"
188 | )
189 | }
190 |
191 | // Run once iteration for the event loop
192 | private func runOnce() {
193 | var timeout: TimeInterval?
194 | scheduledCallbacks.withValue { callbacks in
195 | // as the scheduledCallbacks is a heap queue, the first one will be the smallest one
196 | // (the latest one)
197 | if let firstTuple = callbacks.first {
198 | // schedule timeout for the very next scheduled callback
199 | let (minTime, _) = firstTuple
200 | timeout = max(0, minTime.timeIntervalSince(Date()))
201 | } else {
202 | timeout = nil
203 | }
204 | }
205 |
206 | var events: [(SelectorKey, Set)] = []
207 | // Poll IO events
208 | do {
209 | events = try selector.select(timeout: timeout)
210 | } catch OSError.ioError(let number, let message) {
211 | assert(number == EINTR, "Failed to call selector, errno=\(number), message=\(message)")
212 | } catch {
213 | fatalError("Failed to call selector, errno=\(errno), message=\(lastErrorDescription())")
214 | }
215 | for (key, ioEvents) in events {
216 | guard let handle = key.data as? CallbackHandle else {
217 | continue
218 | }
219 | for ioEvent in ioEvents {
220 | switch ioEvent {
221 | case .read:
222 | if let callback = handle.reader {
223 | callback()
224 | }
225 | case .write:
226 | if let callback = handle.writer {
227 | callback()
228 | }
229 | }
230 | }
231 | }
232 |
233 | // Call scheduled callbacks
234 | let now = Date()
235 | var readyScheduledCallbacks: [(() -> Void)] = []
236 | scheduledCallbacks.modify { callbacks in
237 | var notExpiredCallbacks = callbacks
238 | // keep poping expired callbacks
239 | let timestamp = now.timeIntervalSince1970
240 | while (
241 | !notExpiredCallbacks.isEmpty &&
242 | timestamp >= notExpiredCallbacks.first!.0.timeIntervalSince1970
243 | ) {
244 | // pop the expired callbacks from heap queue and add them to ready callback list
245 | let (_, callback) = HeapSort.heapPop(¬ExpiredCallbacks) {
246 | $0.0.timeIntervalSince1970 < $1.0.timeIntervalSince1970
247 | }
248 | readyScheduledCallbacks.append(callback)
249 | }
250 | return notExpiredCallbacks
251 | }
252 |
253 | // Call ready callbacks
254 | let callbacks = readyCallbacks.swap(newValue: []) + readyScheduledCallbacks
255 | for callback in callbacks {
256 | callback()
257 | }
258 | }
259 |
260 | }
261 |
--------------------------------------------------------------------------------
/Sources/SystemLibrary.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemLibrary.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 1/14/17.
6 | // Copyright © 2017 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | #if os(Linux)
12 | import Glibc
13 | #else
14 | import Darwin
15 | #endif
16 |
17 | /// Collection of system library methods and constants
18 | struct SystemLibrary {
19 | #if os(Linux)
20 | // MARK: Linux constants
21 | static let fdSetSize = FD_SETSIZE
22 | static let nfdbits: Int32 = Int32(MemoryLayout.size) * 8
23 |
24 | // MARK: Linux methods
25 | static let pipe = Glibc.pipe
26 | static let socket = Glibc.socket
27 | static let select = Glibc.select
28 | static let htons = Glibc.htons
29 | static let ntohs = Glibc.ntohs
30 | static let connect = Glibc.connect
31 | static let bind = Glibc.bind
32 | static let listen = Glibc.listen
33 | static let accept = Glibc.accept
34 | static let send = Glibc.send
35 | static let recv = Glibc.recv
36 | static let read = Glibc.read
37 | static let shutdown = Glibc.shutdown
38 | static let close = Glibc.close
39 | static let getpeername = Glibc.getpeername
40 | static let getsockname = Glibc.getsockname
41 |
42 | static func fdSet(fd: Int32, set: inout fd_set) {
43 | let intOffset = Int(fd / SystemLibrary.nfdbits)
44 | let bitOffset = Int(fd % SystemLibrary.nfdbits)
45 | let mask = 1 << bitOffset
46 | switch intOffset {
47 | case 0: set.__fds_bits.0 = set.__fds_bits.0 | mask
48 | case 1: set.__fds_bits.1 = set.__fds_bits.1 | mask
49 | case 2: set.__fds_bits.2 = set.__fds_bits.2 | mask
50 | case 3: set.__fds_bits.3 = set.__fds_bits.3 | mask
51 | case 4: set.__fds_bits.4 = set.__fds_bits.4 | mask
52 | case 5: set.__fds_bits.5 = set.__fds_bits.5 | mask
53 | case 6: set.__fds_bits.6 = set.__fds_bits.6 | mask
54 | case 7: set.__fds_bits.7 = set.__fds_bits.7 | mask
55 | case 8: set.__fds_bits.8 = set.__fds_bits.8 | mask
56 | case 9: set.__fds_bits.9 = set.__fds_bits.9 | mask
57 | case 10: set.__fds_bits.10 = set.__fds_bits.10 | mask
58 | case 11: set.__fds_bits.11 = set.__fds_bits.11 | mask
59 | case 12: set.__fds_bits.12 = set.__fds_bits.12 | mask
60 | case 13: set.__fds_bits.13 = set.__fds_bits.13 | mask
61 | case 14: set.__fds_bits.14 = set.__fds_bits.14 | mask
62 | case 15: set.__fds_bits.15 = set.__fds_bits.15 | mask
63 | default: break
64 | }
65 | }
66 |
67 | static func fdIsSet(fd: Int32, set: inout fd_set) -> Bool {
68 | let intOffset = Int(fd / SystemLibrary.nfdbits)
69 | let bitOffset = Int(fd % SystemLibrary.nfdbits)
70 | let mask = Int(1 << bitOffset)
71 | switch intOffset {
72 | case 0: return set.__fds_bits.0 & mask != 0
73 | case 1: return set.__fds_bits.1 & mask != 0
74 | case 2: return set.__fds_bits.2 & mask != 0
75 | case 3: return set.__fds_bits.3 & mask != 0
76 | case 4: return set.__fds_bits.4 & mask != 0
77 | case 5: return set.__fds_bits.5 & mask != 0
78 | case 6: return set.__fds_bits.6 & mask != 0
79 | case 7: return set.__fds_bits.7 & mask != 0
80 | case 8: return set.__fds_bits.8 & mask != 0
81 | case 9: return set.__fds_bits.9 & mask != 0
82 | case 10: return set.__fds_bits.10 & mask != 0
83 | case 11: return set.__fds_bits.11 & mask != 0
84 | case 12: return set.__fds_bits.12 & mask != 0
85 | case 13: return set.__fds_bits.13 & mask != 0
86 | case 14: return set.__fds_bits.14 & mask != 0
87 | case 15: return set.__fds_bits.15 & mask != 0
88 | default: return false
89 | }
90 | }
91 | #else
92 | // MARK: Darwin constants
93 | static let fdSetSize = __DARWIN_FD_SETSIZE
94 | // MARK: Darwin methods
95 |
96 | static let pipe = Darwin.pipe
97 | static let socket = Darwin.socket
98 | static let select = Darwin.select
99 | static let isLittleEndian = Int(OSHostByteOrder()) == OSLittleEndian
100 | static let htons = isLittleEndian ? _OSSwapInt16 : { $0 }
101 | static let ntohs = isLittleEndian ? _OSSwapInt16 : { $0 }
102 | static let connect = Darwin.connect
103 | static let bind = Darwin.bind
104 | static let listen = Darwin.listen
105 | static let accept = Darwin.accept
106 | static let send = Darwin.send
107 | static let recv = Darwin.recv
108 | static let read = Darwin.read
109 | static let shutdown = Darwin.shutdown
110 | static let close = Darwin.close
111 | static let getpeername = Darwin.getpeername
112 | static let getsockname = Darwin.getsockname
113 |
114 | static func fdSet(fd: Int32, set: inout fd_set) {
115 | let intOffset = Int(fd / 32)
116 | let bitOffset = fd % 32
117 | let mask = Int32(1 << bitOffset)
118 | switch intOffset {
119 | case 0: set.fds_bits.0 = set.fds_bits.0 | mask
120 | case 1: set.fds_bits.1 = set.fds_bits.1 | mask
121 | case 2: set.fds_bits.2 = set.fds_bits.2 | mask
122 | case 3: set.fds_bits.3 = set.fds_bits.3 | mask
123 | case 4: set.fds_bits.4 = set.fds_bits.4 | mask
124 | case 5: set.fds_bits.5 = set.fds_bits.5 | mask
125 | case 6: set.fds_bits.6 = set.fds_bits.6 | mask
126 | case 7: set.fds_bits.7 = set.fds_bits.7 | mask
127 | case 8: set.fds_bits.8 = set.fds_bits.8 | mask
128 | case 9: set.fds_bits.9 = set.fds_bits.9 | mask
129 | case 10: set.fds_bits.10 = set.fds_bits.10 | mask
130 | case 11: set.fds_bits.11 = set.fds_bits.11 | mask
131 | case 12: set.fds_bits.12 = set.fds_bits.12 | mask
132 | case 13: set.fds_bits.13 = set.fds_bits.13 | mask
133 | case 14: set.fds_bits.14 = set.fds_bits.14 | mask
134 | case 15: set.fds_bits.15 = set.fds_bits.15 | mask
135 | case 16: set.fds_bits.16 = set.fds_bits.16 | mask
136 | case 17: set.fds_bits.17 = set.fds_bits.17 | mask
137 | case 18: set.fds_bits.18 = set.fds_bits.18 | mask
138 | case 19: set.fds_bits.19 = set.fds_bits.19 | mask
139 | case 20: set.fds_bits.20 = set.fds_bits.20 | mask
140 | case 21: set.fds_bits.21 = set.fds_bits.21 | mask
141 | case 22: set.fds_bits.22 = set.fds_bits.22 | mask
142 | case 23: set.fds_bits.23 = set.fds_bits.23 | mask
143 | case 24: set.fds_bits.24 = set.fds_bits.24 | mask
144 | case 25: set.fds_bits.25 = set.fds_bits.25 | mask
145 | case 26: set.fds_bits.26 = set.fds_bits.26 | mask
146 | case 27: set.fds_bits.27 = set.fds_bits.27 | mask
147 | case 28: set.fds_bits.28 = set.fds_bits.28 | mask
148 | case 29: set.fds_bits.29 = set.fds_bits.29 | mask
149 | case 30: set.fds_bits.30 = set.fds_bits.30 | mask
150 | case 31: set.fds_bits.31 = set.fds_bits.31 | mask
151 | default: break
152 | }
153 | }
154 |
155 | static func fdIsSet(fd: Int32, set: inout fd_set) -> Bool {
156 | let intOffset = Int(fd / 32)
157 | let bitOffset = fd % 32
158 | let mask = Int32(1 << bitOffset)
159 | switch intOffset {
160 | case 0: return set.fds_bits.0 & mask != 0
161 | case 1: return set.fds_bits.1 & mask != 0
162 | case 2: return set.fds_bits.2 & mask != 0
163 | case 3: return set.fds_bits.3 & mask != 0
164 | case 4: return set.fds_bits.4 & mask != 0
165 | case 5: return set.fds_bits.5 & mask != 0
166 | case 6: return set.fds_bits.6 & mask != 0
167 | case 7: return set.fds_bits.7 & mask != 0
168 | case 8: return set.fds_bits.8 & mask != 0
169 | case 9: return set.fds_bits.9 & mask != 0
170 | case 10: return set.fds_bits.10 & mask != 0
171 | case 11: return set.fds_bits.11 & mask != 0
172 | case 12: return set.fds_bits.12 & mask != 0
173 | case 13: return set.fds_bits.13 & mask != 0
174 | case 14: return set.fds_bits.14 & mask != 0
175 | case 15: return set.fds_bits.15 & mask != 0
176 | case 16: return set.fds_bits.16 & mask != 0
177 | case 17: return set.fds_bits.17 & mask != 0
178 | case 18: return set.fds_bits.18 & mask != 0
179 | case 19: return set.fds_bits.19 & mask != 0
180 | case 20: return set.fds_bits.20 & mask != 0
181 | case 21: return set.fds_bits.21 & mask != 0
182 | case 22: return set.fds_bits.22 & mask != 0
183 | case 23: return set.fds_bits.23 & mask != 0
184 | case 24: return set.fds_bits.24 & mask != 0
185 | case 25: return set.fds_bits.25 & mask != 0
186 | case 26: return set.fds_bits.26 & mask != 0
187 | case 27: return set.fds_bits.27 & mask != 0
188 | case 28: return set.fds_bits.28 & mask != 0
189 | case 29: return set.fds_bits.29 & mask != 0
190 | case 30: return set.fds_bits.30 & mask != 0
191 | case 31: return set.fds_bits.31 & mask != 0
192 | default: return false
193 | }
194 | }
195 | #endif
196 |
197 | }
198 |
--------------------------------------------------------------------------------
/Sources/TCPSocket.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TCPSocket.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/20/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Class wrapping around TCP/IPv6 socket
12 | public final class TCPSocket {
13 | /// The file descriptor number for socket
14 | var fileDescriptor: Int32
15 |
16 | /// Whether is this socket in block mode or not
17 | var blocking: Bool {
18 | get {
19 | return IOUtils.getBlocking(fileDescriptor: fileDescriptor)
20 | }
21 |
22 | set {
23 | IOUtils.setBlocking(fileDescriptor: fileDescriptor, blocking: newValue)
24 | }
25 | }
26 |
27 | /// Whether to ignore SIGPIPE signal or not
28 | var ignoreSigPipe: Bool {
29 | get {
30 | #if os(Linux)
31 | return false
32 | #else
33 | var value: Int32 = 0
34 | var size = socklen_t(MemoryLayout.size)
35 | assert(
36 | getsockopt(fileDescriptor, SOL_SOCKET, SO_NOSIGPIPE, &value, &size) >= 0,
37 | "Failed to get SO_NOSIGPIPE, errno=\(errno), message=\(lastErrorDescription())"
38 | )
39 | return value == 1
40 | #endif
41 | }
42 |
43 | set {
44 | #if os(Linux)
45 | // TODO: maybe we should call signal(SIGPIPE, SIG_IGN) here? but it affects
46 | // whole process
47 | return
48 | #else
49 | var value: Int32 = newValue ? 1 : 0
50 | assert(
51 | setsockopt(
52 | fileDescriptor,
53 | SOL_SOCKET,
54 | SO_NOSIGPIPE,
55 | &value,
56 | socklen_t(MemoryLayout.size)
57 | ) >= 0,
58 | "Failed to set SO_NOSIGPIPE, errno=\(errno), message=\(lastErrorDescription())"
59 | )
60 | #endif
61 | }
62 | }
63 |
64 | init(blocking: Bool = false) throws {
65 | #if os(Linux)
66 | let socketType = Int32(SOCK_STREAM.rawValue)
67 | #else
68 | let socketType = SOCK_STREAM
69 | #endif
70 | fileDescriptor = SystemLibrary.socket(AF_INET6, socketType, 0)
71 | guard fileDescriptor >= 0 else {
72 | throw OSError.lastIOError()
73 | }
74 | self.blocking = blocking
75 | }
76 |
77 | init(fileDescriptor: Int32, blocking: Bool = false) {
78 | self.fileDescriptor = fileDescriptor
79 | self.blocking = blocking
80 | }
81 |
82 | deinit {
83 | close()
84 | }
85 |
86 | /// Bind the socket at given port and interface
87 | /// - Parameter port: port number to bind to
88 | /// - Parameter interface: networking interface to bind to, in IPv6 format
89 | /// - Parameter addressReusable: should we make address reusable
90 | func bind(port: Int, interface: String = "::", addressReusable: Bool = true) throws {
91 | // make address reusable
92 | if addressReusable {
93 | var reuse = Int32(1)
94 | guard setsockopt(
95 | fileDescriptor,
96 | SOL_SOCKET,
97 | SO_REUSEADDR,
98 | &reuse,
99 | socklen_t(MemoryLayout.size)
100 | ) >= 0 else {
101 | throw OSError.lastIOError()
102 | }
103 | }
104 | // create IPv6 socket address
105 | var address = sockaddr_in6()
106 | #if !os(Linux)
107 | address.sin6_len = UInt8(MemoryLayout.stride)
108 | #endif
109 | address.sin6_family = sa_family_t(AF_INET6)
110 | address.sin6_port = UInt16(port).bigEndian
111 | address.sin6_flowinfo = 0
112 | address.sin6_addr = try ipAddressToStruct(address: interface)
113 | address.sin6_scope_id = 0
114 | let size = socklen_t(MemoryLayout.size)
115 | // bind the address and port on socket
116 | guard withUnsafePointer(to: &address, { pointer in
117 | return pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { pointer in
118 | return SystemLibrary.bind(fileDescriptor, pointer, size) >= 0
119 | }
120 | }) else {
121 | throw OSError.lastIOError()
122 | }
123 | }
124 |
125 | /// Listen incomming connections
126 | /// - Parameter backlog: maximum backlog of incoming connections
127 | func listen(backlog: Int = Int(SOMAXCONN)) throws {
128 | guard SystemLibrary.listen(fileDescriptor, Int32(backlog)) != -1 else {
129 | throw OSError.lastIOError()
130 | }
131 | }
132 |
133 | /// Accept a new connection
134 | func accept() throws -> TCPSocket {
135 | var address = sockaddr_in6()
136 | var size = socklen_t(MemoryLayout.size)
137 | let clientFileDescriptor = withUnsafeMutablePointer(to: &address) { pointer in
138 | return pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { pointer in
139 | return SystemLibrary.accept(fileDescriptor, pointer, &size)
140 | }
141 | }
142 | guard clientFileDescriptor >= 0 else {
143 | throw OSError.lastIOError()
144 | }
145 | return TCPSocket(fileDescriptor: clientFileDescriptor)
146 | }
147 |
148 | /// Connect to a peer
149 | /// - Parameter host: the target host to connect, in IPv4 or IPv6 format, like 127.0.0.1 or ::1
150 | /// - Parameter port: the target host port number to connect
151 | func connect(host: String, port: Int) throws {
152 | // create IPv6 socket address
153 | var address = sockaddr_in6()
154 | #if !os(Linux)
155 | address.sin6_len = UInt8(MemoryLayout.stride)
156 | #endif
157 | address.sin6_family = sa_family_t(AF_INET6)
158 | address.sin6_port = UInt16(port).bigEndian
159 | address.sin6_flowinfo = 0
160 | address.sin6_addr = try ipAddressToStruct(address: host)
161 | address.sin6_scope_id = 0
162 | let size = socklen_t(MemoryLayout.size)
163 | // connect to the host and port
164 | let connectResult = withUnsafePointer(to: &address) { pointer in
165 | return pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { pointer in
166 | return SystemLibrary.connect(fileDescriptor, pointer, size)
167 | }
168 | }
169 | guard connectResult >= 0 || errno == EINPROGRESS else {
170 | throw OSError.lastIOError()
171 | }
172 | }
173 |
174 | /// Send data to peer
175 | /// - Parameter data: data bytes to send
176 | /// - Returns: bytes sent to peer
177 | @discardableResult
178 | func send(data: Data) throws -> Int {
179 | let bytesSent = data.withUnsafeBytes { pointer in
180 | SystemLibrary.send(fileDescriptor, pointer, data.count, Int32(0))
181 | }
182 | guard bytesSent >= 0 else {
183 | throw OSError.lastIOError()
184 | }
185 | return bytesSent
186 | }
187 |
188 | /// Read data from peer
189 | /// - Parameter size: size of bytes to read
190 | /// - Returns: bytes read from peer
191 | func recv(size: Int) throws -> Data {
192 | var bytes = Data(count: size)
193 | let bytesRead = bytes.withUnsafeMutableBytes { pointer in
194 | return SystemLibrary.recv(fileDescriptor, pointer, size, Int32(0))
195 | }
196 | guard bytesRead >= 0 else {
197 | throw OSError.lastIOError()
198 | }
199 | return bytes.subdata(in: 0.. (String, Int) {
213 | return try getName(function: getpeername)
214 | }
215 |
216 | func getSockName() throws -> (String, Int) {
217 | return try getName(function: getsockname)
218 | }
219 |
220 | private func getName(
221 | function: (Int32, UnsafeMutablePointer, UnsafeMutablePointer) -> Int32
222 | ) throws -> (String, Int) {
223 | var address = sockaddr_storage()
224 | var size = socklen_t(MemoryLayout.size)
225 | return try withUnsafeMutablePointer(to: &address) { pointer in
226 | let result = pointer.withMemoryRebound(
227 | to: sockaddr.self,
228 | capacity: 1
229 | ) { addressptr in
230 | return function(fileDescriptor, addressptr, &size)
231 | }
232 | guard result >= 0 else {
233 | throw OSError.lastIOError()
234 | }
235 | switch Int32(pointer.pointee.ss_family) {
236 | case AF_INET:
237 | return try pointer.withMemoryRebound(
238 | to: sockaddr_in.self,
239 | capacity: 1
240 | ) { addressptr in
241 | return (
242 | try structToAddress(
243 | addrStruct: addressptr.pointee.sin_addr,
244 | family: AF_INET,
245 | addressLength: INET_ADDRSTRLEN
246 | ),
247 | Int(SystemLibrary.ntohs(addressptr.pointee.sin_port))
248 | )
249 | }
250 | case AF_INET6:
251 | return try pointer.withMemoryRebound(
252 | to: sockaddr_in6.self,
253 | capacity: 1
254 | ) { addressptr in
255 | return (
256 | try structToAddress(
257 | addrStruct: addressptr.pointee.sin6_addr,
258 | family: AF_INET6,
259 | addressLength: INET6_ADDRSTRLEN
260 | ),
261 | Int(SystemLibrary.ntohs(addressptr.pointee.sin6_port))
262 | )
263 | }
264 | default:
265 | fatalError("Unsupported address family")
266 | }
267 | }
268 | }
269 |
270 | // Convert IP address to binary struct
271 | private func ipAddressToStruct(address: String) throws -> in6_addr {
272 | // convert interface string into IPv6 address struct
273 | var binary: in6_addr = in6_addr()
274 | guard address.withCString({ inet_pton(AF_INET6, $0, &binary) >= 0 }) else {
275 | throw OSError.lastIOError()
276 | }
277 | return binary
278 | }
279 |
280 | private func structToAddress(
281 | addrStruct: StructType,
282 | family: Int32,
283 | addressLength: Int32
284 | ) throws -> String {
285 | var addrStruct = addrStruct
286 | // convert address struct into address string
287 | var address = Data(count: Int(addressLength))
288 | guard address.withUnsafeMutableBytes({ pointer in
289 | inet_ntop(
290 | family,
291 | &addrStruct,
292 | pointer,
293 | socklen_t(addressLength)
294 | ) != nil
295 | }) else {
296 | throw OSError.lastIOError()
297 | }
298 | if let index = address.firstIndex(of: 0) {
299 | address = address.subdata(in: 0 ..< index)
300 | }
301 | return String(data: address, encoding: .utf8)!
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/Sources/TransformLogHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransformLogHandler.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 6/2/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// A log handler transforms record and relays it to another handler
12 | public struct TransformLogHandler: LogHandler {
13 | public let handler: LogHandler
14 | public var formatter: LogFormatter? = nil
15 | public let transform: (LogRecord) -> LogRecord
16 |
17 | public init(handler: LogHandler, transform: @escaping (LogRecord) -> LogRecord) {
18 | self.handler = handler
19 | self.transform = transform
20 | }
21 |
22 | public func emit(record: LogRecord) {
23 | handler.emit(record: transform(record))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Transport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Transport.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/21/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public final class Transport {
12 | enum CloseReason {
13 | /// Connection closed by peer
14 | case byPeer
15 | /// Connection closed by ourselve
16 | case byLocal
17 |
18 | var isByPeer: Bool {
19 | if case .byPeer = self {
20 | return true
21 | }
22 | return false
23 | }
24 |
25 | var isByLocal: Bool {
26 | if case .byLocal = self {
27 | return true
28 | }
29 | return false
30 | }
31 | }
32 |
33 | /// Size for recv
34 | static let recvChunkSize = 1024
35 |
36 | /// Is this transport closed or not
37 | private(set) var closed: Bool = false
38 | /// Is this transport closing
39 | private(set) var closing: Bool = false
40 | var closedCallback: ((CloseReason) -> Void)?
41 | var readDataCallback: ((Data) -> Void)?
42 |
43 | private let socket: TCPSocket
44 | private let eventLoop: EventLoop
45 | // buffer for sending data out
46 | private var outgoingBuffer = Data()
47 | // is reading enabled or not
48 | private var reading: Bool = true
49 |
50 | init(
51 | socket: TCPSocket,
52 | eventLoop: EventLoop,
53 | closedCallback: ((CloseReason) -> Void)? = nil,
54 | readDataCallback: ((Data) -> Void)? = nil
55 | ) {
56 | socket.ignoreSigPipe = true
57 | self.socket = socket
58 | self.eventLoop = eventLoop
59 | self.closedCallback = closedCallback
60 | self.readDataCallback = readDataCallback
61 | eventLoop.setReader(socket.fileDescriptor, callback: handleRead)
62 | }
63 |
64 | deinit {
65 | eventLoop.removeReader(socket.fileDescriptor)
66 | eventLoop.removeWriter(socket.fileDescriptor)
67 | }
68 |
69 | /// Send data to peer (append in buffer and will be sent out later)
70 | /// - Parameter data: data to send
71 | func write(data: Data) {
72 | // ensure we are not closed nor closing
73 | guard !closed && !closing else {
74 | // TODO: or raise error?
75 | return
76 | }
77 | // TODO: more efficient way to handle the outgoing buffer?
78 | outgoingBuffer.append(data)
79 | handleWrite()
80 | }
81 |
82 | /// Send string with UTF8 encoding to peer
83 | /// - Parameter string: string to send as UTF8
84 | func write(string: String) {
85 | write(data: Data(string.utf8))
86 | }
87 |
88 | /// Flush outgoing data and close the transport
89 | func close() {
90 | // ensure we are not closed nor closing
91 | guard !closed && !closing else {
92 | // TODO: or raise error?
93 | return
94 | }
95 | closing = true
96 | handleWrite()
97 | }
98 |
99 | func resume(reading: Bool) {
100 | // switch from not-reading to reading
101 | if reading && !self.reading {
102 | // call handle read later to check is there data available for reading
103 | eventLoop.call {
104 | self.handleRead()
105 | }
106 | }
107 | self.reading = reading
108 | }
109 |
110 | private func closedByPeer() {
111 | closed = true
112 | eventLoop.removeReader(socket.fileDescriptor)
113 | eventLoop.removeWriter(socket.fileDescriptor)
114 | if let callback = closedCallback {
115 | callback(.byPeer)
116 | }
117 | socket.close()
118 | }
119 |
120 | private func handleRead() {
121 | // ensure we are not closed
122 | guard !closed else {
123 | return
124 | }
125 | guard reading else {
126 | return
127 | }
128 | var data: Data!
129 | do {
130 | data = try socket.recv(size: Transport.recvChunkSize)
131 | } catch OSError.ioError(let number, _) {
132 | guard number != EAGAIN else {
133 | // if it's EAGAIN, it means no data to be read for now, just return
134 | // (usually means that this function was called by resumeReading)
135 | return
136 | }
137 | fatalError("Failed to read, errno=\(errno), message=\(lastErrorDescription())")
138 | } catch {
139 | fatalError("Failed to read")
140 | }
141 | guard data.count > 0 else {
142 | closedByPeer()
143 | return
144 | }
145 | // ensure we are not closing
146 | guard !closing else {
147 | return
148 | }
149 | if let callback = readDataCallback {
150 | callback(data)
151 | }
152 | }
153 |
154 | private func handleWrite() {
155 | // ensure we are not closed
156 | guard !closed else {
157 | return
158 | }
159 | // ensure we have something to write
160 | guard outgoingBuffer.count > 0 else {
161 | if closing {
162 | closed = true
163 | eventLoop.removeWriter(socket.fileDescriptor)
164 | eventLoop.removeReader(socket.fileDescriptor)
165 | if let callback = closedCallback {
166 | callback(.byLocal)
167 | }
168 | socket.close()
169 | }
170 | return
171 | }
172 | do {
173 | let sentBytes = try socket.send(data: outgoingBuffer)
174 | outgoingBuffer.removeFirst(sentBytes)
175 | if outgoingBuffer.count > 0 {
176 | // Not all was written; register write handler.
177 | eventLoop.setWriter(socket.fileDescriptor, callback: handleWrite)
178 | } else {
179 | eventLoop.removeWriter(socket.fileDescriptor)
180 | if closing {
181 | closed = true
182 | eventLoop.removeReader(socket.fileDescriptor)
183 | if let callback = closedCallback {
184 | callback(.byLocal)
185 | }
186 | socket.close()
187 | }
188 | }
189 | } catch let OSError.ioError(number, message) {
190 | switch number {
191 | case EAGAIN:
192 | break
193 | // Apparently on macOS EPROTOTYPE can be returned when the socket is not
194 | // fully shutdown (as an EPIPE would indicate). Here we treat them
195 | // essentially the same since we just tear the transport down anyway.
196 | // http://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/
197 | case EPROTOTYPE:
198 | fallthrough
199 | case EPIPE:
200 | closedByPeer()
201 |
202 | default:
203 | fatalError("Failed to send, errno=\(number), message=\(message)")
204 | }
205 | } catch {
206 | fatalError("Failed to send")
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/HTTPHeaderParserTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPHeaderParserTests.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/19/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | @testable import Embassy
13 |
14 | #if os(Linux)
15 | extension HTTPHeaderParserTests {
16 | static var allTests = [
17 | ("testSimpleParsing", testSimpleParsing),
18 | ("testPartialParsing", testPartialParsing),
19 | ("testHeaders", testHeaders),
20 | ("testColonInHeader", testColonInHeader),
21 | ("testNoSpaceAfterColonForHeader", testNoSpaceAfterColonForHeader),
22 | ("testStripLeadingSpaces", testStripLeadingSpaces),
23 | ]
24 | }
25 | #endif
26 |
27 | extension HTTPHeaderParser.Element: Equatable {
28 | }
29 | public func == (lhs: HTTPHeaderParser.Element, rhs: HTTPHeaderParser.Element) -> Bool {
30 | switch lhs {
31 | case .head(let lhsMethod, let lhsPath, let lhsVersion):
32 | if case .head(let rhsMethod, let rhsPath, let rhsVersion) = rhs {
33 | return (lhsMethod == rhsMethod && lhsPath == rhsPath && lhsVersion == rhsVersion)
34 | }
35 | case .header(let lhsKey, let lhsValue):
36 | if case .header(let rhsKey, let rhsValue) = rhs {
37 | return (lhsKey == rhsKey && lhsValue == rhsValue)
38 | }
39 | case .end(let lhsBody):
40 | if case .end(let rhsBody) = rhs {
41 | return (lhsBody == rhsBody)
42 | }
43 | }
44 | return false
45 | }
46 |
47 | class HTTPHeaderParserTests: XCTestCase {
48 |
49 | func testSimpleParsing() {
50 | let header = "GET /index.html HTTP/1.1\r\nHost: www.example.com\r\n\r\nbody goes here"
51 | var parser = HTTPHeaderParser()
52 | let elements = parser.feed(Data(header.utf8))
53 | XCTAssertEqual(elements, [
54 | HTTPHeaderParser.Element.head(method: "GET", path: "/index.html", version: "HTTP/1.1"),
55 | HTTPHeaderParser.Element.header(key: "Host", value: "www.example.com"),
56 | HTTPHeaderParser.Element.end(bodyPart: Data("body goes here".utf8))
57 | ])
58 | }
59 |
60 | func testPartialParsing() {
61 | let line1Part1 = "GET /index.html"
62 | let line1Part2 = " HTTP/1.1\r\n"
63 |
64 | let line2Part1 = "Host: www.exam"
65 | let line2Part2 = "ple.com\r"
66 | let line2Part3 = "\n"
67 |
68 | let line3Part1 = "\r"
69 | let line3Part2 = "\nhere comes the body"
70 |
71 | var parser = HTTPHeaderParser()
72 |
73 | // try to feed empty array
74 | XCTAssertEqual(parser.feed(Data()), [])
75 |
76 | XCTAssertEqual(parser.feed(Data(line1Part1.utf8)), [])
77 | XCTAssertEqual(parser.feed(Data(line1Part2.utf8)), [
78 | HTTPHeaderParser.Element.head(method: "GET", path: "/index.html", version: "HTTP/1.1")
79 | ])
80 |
81 | XCTAssertEqual(parser.feed(Data(line2Part1.utf8)), [])
82 | XCTAssertEqual(parser.feed(Data(line2Part2.utf8)), [])
83 | XCTAssertEqual(parser.feed(Data(line2Part3.utf8)), [
84 | HTTPHeaderParser.Element.header(key: "Host", value: "www.example.com"),
85 | ])
86 |
87 | // try to feed empty array
88 | XCTAssertEqual(parser.feed(Data()), [])
89 |
90 | XCTAssertEqual(parser.feed(Data(line3Part1.utf8)), [])
91 | XCTAssertEqual(parser.feed(Data(line3Part2.utf8)), [
92 | HTTPHeaderParser.Element.end(bodyPart: Data("here comes the body".utf8))
93 | ])
94 | }
95 |
96 | func testHeaders() {
97 | let header = [
98 | "GET /index.html HTTP/1.1",
99 | "Host: foobar.com",
100 | "Date: Mon, 23 May 2005 22:38:34 GMT",
101 | "Content-Type: text/html; charset=UTF-8",
102 | "Content-Encoding: UTF-8",
103 | "Content-Length: 138",
104 | "Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT",
105 | "Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)",
106 | "ETag: \"3f80f-1b6-3e1cb03b\"",
107 | "Accept-Ranges: bytes",
108 | "Connection: close"
109 | ].joined(separator: "\r\n") + "\r\n\r\n"
110 | var parser = HTTPHeaderParser()
111 | let elements = parser.feed(Data(header.utf8))
112 | XCTAssertEqual(elements, [
113 | HTTPHeaderParser.Element.head(method: "GET", path: "/index.html", version: "HTTP/1.1"),
114 | HTTPHeaderParser.Element.header(key: "Host", value: "foobar.com"),
115 | HTTPHeaderParser.Element.header(key: "Date", value: "Mon, 23 May 2005 22:38:34 GMT"),
116 | HTTPHeaderParser.Element.header(key: "Content-Type", value: "text/html; charset=UTF-8"),
117 | HTTPHeaderParser.Element.header(key: "Content-Encoding", value: "UTF-8"),
118 | HTTPHeaderParser.Element.header(key: "Content-Length", value: "138"),
119 | HTTPHeaderParser.Element.header(key: "Last-Modified", value: "Wed, 08 Jan 2003 23:11:55 GMT"),
120 | HTTPHeaderParser.Element.header(key: "Server", value: "Apache/1.3.3.7 (Unix) (Red-Hat/Linux)"),
121 | HTTPHeaderParser.Element.header(key: "ETag", value: "\"3f80f-1b6-3e1cb03b\""),
122 | HTTPHeaderParser.Element.header(key: "Accept-Ranges", value: "bytes"),
123 | HTTPHeaderParser.Element.header(key: "Connection", value: "close"),
124 | HTTPHeaderParser.Element.end(bodyPart: Data())
125 | ])
126 | }
127 |
128 | func testColonInHeader() {
129 | let header = [
130 | "GET /index.html HTTP/1.1",
131 | "Host: foobar.com",
132 | "X-My-Header: MyFaboriteColor: Green",
133 | "Connection: close"
134 | ].joined(separator: "\r\n") + "\r\n\r\n"
135 | var parser = HTTPHeaderParser()
136 | let elements = parser.feed(Data(header.utf8))
137 | XCTAssertEqual(elements, [
138 | HTTPHeaderParser.Element.head(method: "GET", path: "/index.html", version: "HTTP/1.1"),
139 | HTTPHeaderParser.Element.header(key: "Host", value: "foobar.com"),
140 | HTTPHeaderParser.Element.header(key: "X-My-Header", value: "MyFaboriteColor: Green"),
141 | HTTPHeaderParser.Element.header(key: "Connection", value: "close"),
142 | HTTPHeaderParser.Element.end(bodyPart: Data())
143 | ])
144 | }
145 |
146 | func testNoSpaceAfterColonForHeader() {
147 | let header = [
148 | "GET /index.html HTTP/1.1",
149 | "Host: foobar.com",
150 | "X-My-Header:MyFaboriteColor: Green",
151 | "Connection:close"
152 | ].joined(separator: "\r\n") + "\r\n\r\n"
153 | var parser = HTTPHeaderParser()
154 | let elements = parser.feed(Data(header.utf8))
155 | XCTAssertEqual(elements, [
156 | HTTPHeaderParser.Element.head(method: "GET", path: "/index.html", version: "HTTP/1.1"),
157 | HTTPHeaderParser.Element.header(key: "Host", value: "foobar.com"),
158 | HTTPHeaderParser.Element.header(key: "X-My-Header", value: "MyFaboriteColor: Green"),
159 | HTTPHeaderParser.Element.header(key: "Connection", value: "close"),
160 | HTTPHeaderParser.Element.end(bodyPart: Data())
161 | ])
162 | }
163 |
164 | func testStripLeadingSpaces() {
165 | XCTAssertEqual("x".withoutLeadingSpaces, "x")
166 | XCTAssertEqual("eggsspam".withoutLeadingSpaces, "eggsspam")
167 | XCTAssertEqual("foo bar".withoutLeadingSpaces, "foo bar")
168 | XCTAssertEqual("foo bar ".withoutLeadingSpaces, "foo bar ")
169 | XCTAssertEqual(" foo bar ".withoutLeadingSpaces, "foo bar ")
170 | XCTAssertEqual(" foo bar ".withoutLeadingSpaces, "foo bar ")
171 | XCTAssertEqual(" ".withoutLeadingSpaces, "")
172 | XCTAssertEqual("".withoutLeadingSpaces, "")
173 | }
174 |
175 | }
176 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/HTTPServerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPServerTests.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/21/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Dispatch
11 | import XCTest
12 |
13 | @testable import Embassy
14 |
15 | #if os(Linux)
16 | extension HTTPServerTests {
17 | static var allTests = [
18 | ("testEnviron", testEnviron),
19 | ("testStartResponse", testStartResponse),
20 | ("testSendBody", testSendBody),
21 | ("testAsyncSendBody", testAsyncSendBody),
22 | ("testPostBody", testPostBody),
23 | ("testPostWithInitialBody", testPostWithInitialBody),
24 | ("testAddressReuse", testAddressReuse),
25 | ("testStopAndWait", testStopAndWait),
26 | ]
27 | }
28 | #endif
29 |
30 | class HTTPServerTests: XCTestCase {
31 | let queue = DispatchQueue(label: "com.envoy.embassy-tests.http-server", attributes: [])
32 | var loop: SelectorEventLoop!
33 | var session: URLSession!
34 |
35 | override func setUp() {
36 | super.setUp()
37 | loop = try! SelectorEventLoop(selector: try! TestingSelector())
38 |
39 | let sessionConfig = URLSessionConfiguration.default
40 | session = URLSession(configuration: sessionConfig)
41 | // set a 30 seconds timeout
42 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(30 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
43 | if self.loop.running {
44 | self.loop.stop()
45 | XCTFail("Time out")
46 | }
47 | }
48 | }
49 |
50 | override func tearDown() {
51 | super.tearDown()
52 | }
53 |
54 | func testEnviron() {
55 | let port = try! getUnusedTCPPort()
56 | var receivedEnviron: [String: Any]!
57 | let server = DefaultHTTPServer(eventLoop: loop, port: port) {
58 | (
59 | environ: [String: Any],
60 | startResponse: ((String, [(String, String)]) -> Void),
61 | sendBody: ((Data) -> Void)
62 | ) in
63 | receivedEnviron = environ
64 | self.loop.stop()
65 | }
66 |
67 | try! server.start()
68 |
69 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
70 | let task = self.session.dataTask(
71 | with: URL(string: "http://[::1]:\(port)/path?foo=bar")!
72 | )
73 | task.resume()
74 | }
75 |
76 | loop.runForever()
77 |
78 | XCTAssertEqual(receivedEnviron["REQUEST_METHOD"] as? String, "GET")
79 | XCTAssertEqual(receivedEnviron["HTTP_HOST"] as? String, "[::1]:\(port)")
80 | XCTAssertEqual(receivedEnviron["SERVER_PROTOCOL"] as? String, "HTTP/1.1")
81 | XCTAssertEqual(receivedEnviron["SERVER_PORT"] as? String, String(port))
82 | XCTAssertEqual(receivedEnviron["SCRIPT_NAME"] as? String, "")
83 | XCTAssertEqual(receivedEnviron["PATH_INFO"] as? String, "/path")
84 | XCTAssertEqual(receivedEnviron["QUERY_STRING"] as? String, "foo=bar")
85 | XCTAssertEqual(receivedEnviron["swsgi.version"] as? String, "0.1")
86 | XCTAssertEqual(receivedEnviron["swsgi.multithread"] as? Bool, false)
87 | XCTAssertEqual(receivedEnviron["swsgi.multiprocess"] as? Bool, false)
88 | XCTAssertEqual(receivedEnviron["swsgi.url_scheme"] as? String, "http")
89 | XCTAssertEqual(receivedEnviron["swsgi.run_once"] as? Bool, false)
90 | XCTAssertNotNil(receivedEnviron["embassy.connection"] as? HTTPConnection)
91 | XCTAssertNotNil(receivedEnviron["embassy.event_loop"] as? EventLoop)
92 | XCTAssertNotNil(receivedEnviron["embassy.version"] as? String)
93 | }
94 |
95 | func testStartResponse() {
96 | let port = try! getUnusedTCPPort()
97 | let server = DefaultHTTPServer(eventLoop: loop, port: port) {
98 | (
99 | environ: [String: Any],
100 | startResponse: ((String, [(String, String)]) -> Void),
101 | sendBody: ((Data) -> Void)
102 | ) in
103 | startResponse("451 Big brother doesn't like this", [
104 | ("Content-Type", "video/porn"),
105 | ("Server", "Embassy-by-envoy"),
106 | ("X-Foo", "Bar"),
107 | ])
108 | sendBody(Data())
109 | }
110 |
111 | try! server.start()
112 |
113 | var receivedData: Data?
114 | var receivedResponse: HTTPURLResponse?
115 | var receivedError: Error?
116 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
117 | let task = self.session.dataTask(with: URL(string: "http://[::1]:\(port)")!, completionHandler: { (data, response, error) in
118 | receivedData = data
119 | receivedResponse = response as? HTTPURLResponse
120 | receivedError = error
121 | self.loop.stop()
122 | })
123 | task.resume()
124 | }
125 |
126 | loop.runForever()
127 |
128 | XCTAssertEqual(receivedData?.count, 0)
129 | XCTAssertNil(receivedError)
130 | XCTAssertEqual(receivedResponse?.statusCode, 451)
131 | // XXX
132 | /*
133 | XCTAssertEqual(receivedResponse?.allHeaderFields["Content-Type"], "video/porn")
134 | XCTAssertEqual(receivedResponse?.allHeaderFields["Server"], "Embassy-by-envoy")
135 | XCTAssertEqual(receivedResponse?.allHeaderFields["X-Foo"], "Bar")*/
136 | }
137 |
138 | func testSendBody() {
139 | let port = try! getUnusedTCPPort()
140 | let bigDataChunk = Data(makeRandomString(574300).utf8)
141 | let server = DefaultHTTPServer(eventLoop: loop, port: port) {
142 | (
143 | environ: [String: Any],
144 | startResponse: ((String, [(String, String)]) -> Void),
145 | sendBody: ((Data) -> Void)
146 | ) in
147 | startResponse("200 OK", [])
148 | sendBody(bigDataChunk)
149 | sendBody(Data())
150 | }
151 |
152 | try! server.start()
153 |
154 | var receivedData: Data?
155 | var receivedResponse: HTTPURLResponse?
156 | var receivedError: Error?
157 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
158 | let task = self.session.dataTask(with: URL(string: "http://[::1]:\(port)")!, completionHandler: { (data, response, error) in
159 | receivedData = data
160 | receivedResponse = response as? HTTPURLResponse
161 | receivedError = error
162 | self.loop.stop()
163 | })
164 | task.resume()
165 | }
166 |
167 | loop.runForever()
168 |
169 | let data = receivedData ?? Data()
170 | XCTAssertEqual(receivedData?.count, bigDataChunk.count)
171 | XCTAssertEqual(data, bigDataChunk)
172 | XCTAssertNil(receivedError)
173 | XCTAssertEqual(receivedResponse?.statusCode, 200)
174 | }
175 |
176 | func testAsyncSendBody() {
177 | let port = try! getUnusedTCPPort()
178 | let server = DefaultHTTPServer(eventLoop: loop, port: port) {
179 | (
180 | environ: [String: Any],
181 | startResponse: @escaping ((String, [(String, String)]) -> Void),
182 | sendBody: @escaping ((Data) -> Void)
183 | ) in
184 | startResponse("200 OK", [])
185 |
186 | let loop = environ["embassy.event_loop"] as! EventLoop
187 |
188 | loop.call(withDelay: 1) {
189 | sendBody(Data("hello ".utf8))
190 | }
191 | loop.call(withDelay: 2) {
192 | sendBody(Data("baby ".utf8))
193 | }
194 | loop.call(withDelay: 3) {
195 | sendBody(Data("fin".utf8))
196 | sendBody(Data())
197 | }
198 | }
199 |
200 | try! server.start()
201 |
202 | var receivedData: Data?
203 | var receivedResponse: HTTPURLResponse?
204 | var receivedError: Error?
205 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
206 | let task = self.session.dataTask(with: URL(string: "http://[::1]:\(port)")!, completionHandler: { (data, response, error) in
207 | receivedData = data
208 | receivedResponse = response as? HTTPURLResponse
209 | receivedError = error
210 | self.loop.stop()
211 | })
212 | task.resume()
213 | }
214 |
215 | loop.runForever()
216 |
217 | XCTAssertEqual(NSString(data: receivedData!, encoding: String.Encoding.utf8.rawValue)!, "hello baby fin")
218 | XCTAssertNil(receivedError)
219 | XCTAssertEqual(receivedResponse?.statusCode, 200)
220 | }
221 |
222 | func testPostBody() {
223 | let port = try! getUnusedTCPPort()
224 |
225 | let postBodyString = makeRandomString(40960)
226 | var receivedInputData: [Data] = []
227 | let server = DefaultHTTPServer(eventLoop: loop, port: port) {
228 | (
229 | environ: [String: Any],
230 | startResponse: ((String, [(String, String)]) -> Void),
231 | sendBody: @escaping ((Data) -> Void)
232 | ) in
233 | if environ["HTTP_EXPECT"] as? String == "100-continue" {
234 | startResponse("100 Continue", [])
235 | } else {
236 | startResponse("200 OK", [])
237 | }
238 | let input = environ["swsgi.input"] as! SWSGIInput
239 | input { data in
240 | receivedInputData.append(data)
241 | sendBody(data)
242 | }
243 | }
244 |
245 | try! server.start()
246 |
247 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
248 | var request = URLRequest(url: URL(string: "http://[::1]:\(port)")!)
249 | request.httpMethod = "POST"
250 | request.httpBody = postBodyString.data(using: String.Encoding.utf8)
251 | let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
252 | self.loop.stop()
253 | })
254 | task.resume()
255 | }
256 |
257 | loop.runForever()
258 |
259 | // ensure EOF is passed
260 | XCTAssertEqual(receivedInputData.last?.count, 0)
261 |
262 | let receivedString = String(bytes: receivedInputData.joined(separator: []), encoding: String.Encoding.utf8)
263 | XCTAssertEqual(receivedString, postBodyString)
264 | }
265 |
266 | func testPostWithInitialBody() {
267 | let port = try! getUnusedTCPPort()
268 |
269 | // this chunk is small enough, ideally should be sent along with header (initial body)
270 | let postBodyString = "hello"
271 | var receivedInputData: [Data] = []
272 | let server = DefaultHTTPServer(eventLoop: loop, port: port) {
273 | (
274 | environ: [String: Any],
275 | startResponse: ((String, [(String, String)]) -> Void),
276 | sendBody: @escaping ((Data) -> Void)
277 | ) in
278 | if environ["HTTP_EXPECT"] as? String == "100-continue" {
279 | // Notice: under linux, it seems the underlying URLSession implementation (cURL in
280 | // this case I guess), will send Expect: 100-continue, we need to reply
281 | // 100 Continue so that it will continue sending body
282 | startResponse("100 Continue", [])
283 | } else {
284 | startResponse("200 OK", [])
285 | }
286 | let input = environ["swsgi.input"] as! SWSGIInput
287 | input { data in
288 | receivedInputData.append(data)
289 | sendBody(data)
290 | }
291 | }
292 |
293 | try! server.start()
294 |
295 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
296 | var request = URLRequest(url: URL(string: "http://[::1]:\(port)")!)
297 | request.httpMethod = "POST"
298 | request.httpBody = postBodyString.data(using: String.Encoding.utf8)
299 | let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
300 | self.loop.stop()
301 | })
302 | task.resume()
303 | }
304 |
305 | loop.runForever()
306 |
307 | // ensure EOF is passed
308 | XCTAssertEqual(receivedInputData.last?.count, 0)
309 |
310 | let receivedString = String(bytes: receivedInputData.joined(separator: []), encoding: String.Encoding.utf8)
311 | XCTAssertEqual(receivedString, postBodyString)
312 | }
313 |
314 | func testAddressReuse() {
315 | var called: Bool = false
316 | let port = try! getUnusedTCPPort()
317 | let app = { (environ: [String: Any], startResponse: ((String, [(String, String)]) -> Void), sendBody: ((Data) -> Void)) in
318 | startResponse("200 OK", [])
319 | sendBody(Data())
320 | self.loop.stop()
321 | called = true
322 | }
323 | let server1 = DefaultHTTPServer(eventLoop: loop, port: port, app: app)
324 | try! server1.start()
325 | server1.stop()
326 |
327 | let server2 = DefaultHTTPServer(eventLoop: loop, port: port, app: app)
328 | try! server2.start()
329 |
330 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
331 | let task = self.session.dataTask(
332 | with: URL(string: "http://[::1]:\(port)")!
333 | )
334 | task.resume()
335 | }
336 |
337 | loop.runForever()
338 | XCTAssert(called)
339 | }
340 |
341 | func testStopAndWait() {
342 | let port = try! getUnusedTCPPort()
343 | let server = DefaultHTTPServer(eventLoop: loop, port: port) {
344 | (
345 | environ: [String: Any],
346 | startResponse: ((String, [(String, String)]) -> Void),
347 | sendBody: ((Data) -> Void)
348 | ) in
349 | startResponse("200 OK", [])
350 | sendBody(Data())
351 | }
352 | try! server.start()
353 |
354 | queue.async {
355 | self.loop.runForever()
356 | }
357 | assertExecutingTime(0, accuracy: 0.5) {
358 | server.stopAndWait()
359 | }
360 | loop.stop()
361 | }
362 | }
363 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/HeapSortTetsts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeapSortTetsts.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/25/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import Embassy
12 |
13 | #if os(Linux)
14 | extension HeapSortTetsts {
15 | static var allTests = [
16 | ("testPush", testPush),
17 | ("testPop", testPop),
18 | ("testSortWithRandomNumbers", testSortWithRandomNumbers),
19 | ("testSortWithRandomNumbersWithCustomCompareFunction", testSortWithRandomNumbersWithCustomCompareFunction),
20 | ]
21 | }
22 | #endif
23 |
24 | class HeapSortTetsts: XCTestCase {
25 | func testPush() {
26 | var heap: [Int] = []
27 |
28 | HeapSort.heapPush(&heap, item: 100)
29 | XCTAssertEqual(heap, [100])
30 |
31 | HeapSort.heapPush(&heap, item: 50)
32 | XCTAssertEqual(heap, [50, 100])
33 |
34 | HeapSort.heapPush(&heap, item: 25)
35 | XCTAssertEqual(heap, [25, 100, 50])
36 |
37 | HeapSort.heapPush(&heap, item: 49)
38 | XCTAssertEqual(heap, [25, 49, 50, 100])
39 |
40 | HeapSort.heapPush(&heap, item: 51)
41 | XCTAssertEqual(heap, [25, 49, 50, 100, 51])
42 |
43 | HeapSort.heapPush(&heap, item: 52)
44 | XCTAssertEqual(heap, [25, 49, 50, 100, 51, 52])
45 |
46 | HeapSort.heapPush(&heap, item: 48)
47 | XCTAssertEqual(heap, [25, 49, 48, 100, 51, 52, 50])
48 | }
49 |
50 | func testPop() {
51 | var heap = [25, 49, 48, 100, 51, 52, 50]
52 |
53 | XCTAssertEqual(HeapSort.heapPop(&heap), 25)
54 | XCTAssertEqual(heap, [48, 49, 50, 100, 51, 52])
55 |
56 | XCTAssertEqual(HeapSort.heapPop(&heap), 48)
57 | XCTAssertEqual(heap, [49, 51, 50, 100, 52])
58 |
59 | XCTAssertEqual(HeapSort.heapPop(&heap), 49)
60 | XCTAssertEqual(heap, [50, 51, 52, 100])
61 |
62 | XCTAssertEqual(HeapSort.heapPop(&heap), 50)
63 | XCTAssertEqual(heap, [51, 100, 52])
64 |
65 | XCTAssertEqual(HeapSort.heapPop(&heap), 51)
66 | XCTAssertEqual(heap, [52, 100])
67 |
68 | XCTAssertEqual(HeapSort.heapPop(&heap), 52)
69 | XCTAssertEqual(heap, [100])
70 |
71 | XCTAssertEqual(HeapSort.heapPop(&heap), 100)
72 | XCTAssertEqual(heap, [])
73 | }
74 |
75 | func testSortWithRandomNumbers() {
76 | let array: [UInt32] = Array(0..<100).map { _ in random() }
77 | var heap: [UInt32] = []
78 | for num in array {
79 | HeapSort.heapPush(&heap, item: num)
80 | }
81 | var resultArray: [UInt32] = []
82 | while !heap.isEmpty {
83 | resultArray.append(HeapSort.heapPop(&heap))
84 | }
85 | XCTAssertEqual(resultArray, array.sorted())
86 | }
87 |
88 | func testSortWithRandomNumbersWithCustomCompareFunction() {
89 | let array: [UInt32] = Array(0..<100).map { _ in random() }
90 | var heap: [UInt32] = []
91 | for num in array {
92 | HeapSort.heapPush(&heap, item: num, isOrderredBefore: >)
93 | }
94 | var resultArray: [UInt32] = []
95 | while !heap.isEmpty {
96 | resultArray.append(HeapSort.heapPop(&heap, isOrderredBefore: >))
97 | }
98 | XCTAssertEqual(resultArray, array.sorted(by: >))
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
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 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/KqueueSelectorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KqueueSelectorTests.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/20/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | #if !os(Linux)
10 |
11 | import XCTest
12 |
13 | @testable import Embassy
14 |
15 | class KqueueSelectorTests: XCTestCase {
16 | let queue = DispatchQueue(label: "com.envoy.embassy-tests.kqueue", attributes: [])
17 |
18 | func testRegister() {
19 | let selector = try! KqueueSelector()
20 | let socket = try! TCPSocket()
21 |
22 | XCTAssertNil(selector[socket.fileDescriptor])
23 |
24 | let data = "my data"
25 | try! selector.register(socket.fileDescriptor, events: [.read], data: data)
26 |
27 | let key = selector[socket.fileDescriptor]
28 | XCTAssertEqual(key?.fileDescriptor, socket.fileDescriptor)
29 | XCTAssertEqual(key?.events, [.read])
30 | XCTAssertEqual(key?.data as? String, data)
31 | }
32 |
33 | func testUnregister() {
34 | let selector = try! KqueueSelector()
35 | let socket = try! TCPSocket()
36 |
37 | try! selector.register(socket.fileDescriptor, events: [.read], data: nil)
38 |
39 | let key = try! selector.unregister(socket.fileDescriptor)
40 | XCTAssertNil(selector[socket.fileDescriptor])
41 | XCTAssertNil(key.data as? String)
42 | XCTAssertEqual(key.fileDescriptor, socket.fileDescriptor)
43 | XCTAssertEqual(key.events, [.read])
44 | }
45 |
46 | func testRegisterKeyError() {
47 | let selector = try! KqueueSelector()
48 | let socket = try! TCPSocket()
49 | try! selector.register(socket.fileDescriptor, events: [.read], data: nil)
50 |
51 | XCTAssertThrowsError(try selector.register(
52 | socket.fileDescriptor,
53 | events: [.read],
54 | data: nil
55 | )) { error in
56 | guard let error = error as? KqueueSelector.Error else {
57 | XCTFail()
58 | return
59 | }
60 | guard case .keyError = error else {
61 | XCTFail()
62 | return
63 | }
64 | }
65 | }
66 |
67 | func testUnregisterKeyError() {
68 | let selector = try! KqueueSelector()
69 | let socket = try! TCPSocket()
70 |
71 | XCTAssertThrowsError(try selector.unregister(socket.fileDescriptor)) { error in
72 | guard let error = error as? KqueueSelector.Error else {
73 | XCTFail()
74 | return
75 | }
76 | guard case .keyError = error else {
77 | XCTFail()
78 | return
79 | }
80 | }
81 | }
82 |
83 | func testSelectOneSocket() {
84 | let selector = try! KqueueSelector()
85 |
86 | let port = try! getUnusedTCPPort()
87 | let listenSocket = try! TCPSocket()
88 | try! listenSocket.bind(port: port)
89 | try! listenSocket.listen()
90 |
91 | try! selector.register(listenSocket.fileDescriptor, events: [.read], data: nil)
92 |
93 | // ensure we have a correct timeout here
94 | assertExecutingTime(2, accuracy: 1) {
95 | XCTAssertEqual(try! selector.select(timeout: 2.0).count, 0)
96 | }
97 |
98 | let clientSocket = try! TCPSocket()
99 |
100 | // make a connect 1 seconds later
101 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
102 | try! clientSocket.connect(host: "::1", port: port)
103 | }
104 |
105 | let ioEvents = assertExecutingTime(1, accuracy: 1) {
106 | return try! selector.select(timeout: 10.0)
107 | }
108 | XCTAssertEqual(ioEvents.count, 1)
109 | XCTAssertEqual(ioEvents.first?.0.fileDescriptor, listenSocket.fileDescriptor)
110 | XCTAssertEqual(ioEvents.first?.0.events, [.read])
111 | XCTAssertNil(ioEvents.first?.0.data)
112 | }
113 |
114 | func testSelectEventFilter() {
115 | let selector = try! KqueueSelector()
116 |
117 | let port = try! getUnusedTCPPort()
118 | let listenSocket = try! TCPSocket()
119 | try! listenSocket.bind(port: port)
120 | try! listenSocket.listen()
121 |
122 | try! selector.register(listenSocket.fileDescriptor, events: [.write], data: nil)
123 |
124 | XCTAssertEqual(try! selector.select(timeout: 1.0).count, 0)
125 |
126 | let clientSocket = try! TCPSocket()
127 | // make a connect 1 seconds later
128 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
129 | try! clientSocket.connect(host: "::1", port: port)
130 | }
131 |
132 | // ensure we don't get any event triggered in two seconds
133 | XCTAssertEqual(try! selector.select(timeout: 2.0).count, 0)
134 | }
135 |
136 | func testSelectAfterUnregister() {
137 | let selector = try! KqueueSelector()
138 |
139 | let port = try! getUnusedTCPPort()
140 | let listenSocket = try! TCPSocket()
141 | try! listenSocket.bind(port: port)
142 | try! listenSocket.listen()
143 |
144 | try! selector.register(listenSocket.fileDescriptor, events: [.read], data: nil)
145 |
146 | let clientSocket = try! TCPSocket()
147 | // make a connect 1 seconds later
148 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
149 | try! clientSocket.connect(host: "::1", port: port)
150 | }
151 |
152 | assertExecutingTime(1, accuracy: 1) {
153 | let events = try! selector.select(timeout: 2.0)
154 | let result = toEventSet(events)
155 | XCTAssertEqual(result, Set([
156 | FileDescriptorEvent(fileDescriptor: listenSocket.fileDescriptor, ioEvent: .read),
157 | ]))
158 | }
159 |
160 | try! selector.unregister(listenSocket.fileDescriptor)
161 |
162 | let clientSocket2 = try! TCPSocket()
163 | // make a connect 1 seconds later
164 | queue.asyncAfter(
165 | deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)
166 | ) {
167 | try! clientSocket2.connect(host: "::1", port: port)
168 | }
169 |
170 | assertExecutingTime(2, accuracy: 1) {
171 | XCTAssertEqual(try! selector.select(timeout: 2.0).count, 0)
172 | }
173 | }
174 |
175 | func testSelectMultipleSocket() {
176 | let selector = try! KqueueSelector()
177 |
178 | let port = try! getUnusedTCPPort()
179 |
180 | let clientSocket = try! TCPSocket()
181 |
182 | let listenSocket = try! TCPSocket()
183 | try! listenSocket.bind(port: port)
184 | try! listenSocket.listen()
185 |
186 | try! selector.register(listenSocket.fileDescriptor, events: [.read, .write], data: nil)
187 | try! selector.register(clientSocket.fileDescriptor, events: [.read, .write], data: nil)
188 |
189 | try! clientSocket.connect(host: "::1", port: port)
190 |
191 | sleep(1)
192 |
193 | let ioEvents0 = assertExecutingTime(0, accuracy: 1) {
194 | return try! selector.select(timeout: 10.0)
195 | }
196 | let result0 = toEventSet(ioEvents0)
197 | XCTAssertEqual(result0, Set([
198 | FileDescriptorEvent(fileDescriptor: clientSocket.fileDescriptor, ioEvent: .write),
199 | FileDescriptorEvent(fileDescriptor: listenSocket.fileDescriptor, ioEvent: .read),
200 | ]))
201 |
202 | let acceptedSocket = try! listenSocket.accept()
203 | try! selector.register(acceptedSocket.fileDescriptor, events: [.read, .write], data: nil)
204 |
205 | let ioEvents1 = assertExecutingTime(0, accuracy: 1) {
206 | return try! selector.select(timeout: 10.0)
207 | }
208 | let result1 = toEventSet(ioEvents1)
209 | XCTAssertEqual(result1, Set([
210 | FileDescriptorEvent(fileDescriptor: clientSocket.fileDescriptor, ioEvent: .write),
211 | FileDescriptorEvent(fileDescriptor: acceptedSocket.fileDescriptor, ioEvent: .write),
212 | ]))
213 |
214 | // we should have no events now
215 | assertExecutingTime(1, accuracy: 1) {
216 | return try! selector.select(timeout: 1)
217 | }
218 |
219 | try! clientSocket.send(data: Data("hello".utf8))
220 |
221 | sleep(1)
222 |
223 | let ioEvents2 = assertExecutingTime(0, accuracy: 1) {
224 | return try! selector.select(timeout: 10.0)
225 | }
226 | let result2 = toEventSet(ioEvents2)
227 | XCTAssertEqual(result2, Set([
228 | FileDescriptorEvent(fileDescriptor: clientSocket.fileDescriptor, ioEvent: .write),
229 | FileDescriptorEvent(fileDescriptor: acceptedSocket.fileDescriptor, ioEvent: .read),
230 | FileDescriptorEvent(fileDescriptor: acceptedSocket.fileDescriptor, ioEvent: .write)
231 | ]))
232 |
233 | let receivedString = String(
234 | bytes: try! acceptedSocket.recv(size: 1024),
235 | encoding: String.Encoding.utf8
236 | )
237 | XCTAssertEqual(receivedString, "hello")
238 |
239 | let ioEvents3 = assertExecutingTime(0, accuracy: 1) {
240 | return try! selector.select(timeout: 10.0)
241 | }
242 | let result3 = toEventSet(ioEvents3)
243 | XCTAssertEqual(result3, Set([
244 | FileDescriptorEvent(fileDescriptor: clientSocket.fileDescriptor, ioEvent: .write),
245 | FileDescriptorEvent(fileDescriptor: acceptedSocket.fileDescriptor, ioEvent: .write)
246 | ]))
247 |
248 | // we should have no events now
249 | assertExecutingTime(1, accuracy: 1) {
250 | return try! selector.select(timeout: 1)
251 | }
252 | }
253 |
254 | fileprivate func toEventSet(_ events: [(SelectorKey, Set)]) -> Set {
255 | return Set(events.flatMap { (key, ioEvents) in
256 | return ioEvents.map { FileDescriptorEvent(fileDescriptor: key.fileDescriptor, ioEvent: $0) }
257 | })
258 | }
259 | }
260 |
261 | #endif
262 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/MultiDictionaryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultiDictionaryTests.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/23/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import Embassy
12 |
13 | #if os(Linux)
14 | extension MultiDictionaryTests {
15 | static var allTests = [
16 | ("testCaseInsenstiveMultiDictionary", testCaseInsenstiveMultiDictionary),
17 | ("testCaseSenstiveMultiDictionary", testCaseSenstiveMultiDictionary),
18 | ]
19 | }
20 | #endif
21 |
22 | class MultiDictionaryTests: XCTestCase {
23 | func testCaseInsenstiveMultiDictionary() {
24 | let dict = MultiDictionary(items: [
25 | ("Content-Type", "text/html"),
26 | ("Content-Length", "1234"),
27 | ("Set-cookie", "foo=bar"),
28 | ("Set-Cookie", "egg=spam")
29 | ])
30 |
31 | XCTAssertNil(dict["Not-Exists"])
32 | XCTAssertNil(dict.valuesFor(key: "Not-Exists"))
33 |
34 | XCTAssertEqual(dict["Content-Type"], "text/html")
35 | XCTAssertEqual(dict["content-type"], "text/html")
36 | XCTAssertEqual(dict.valuesFor(key: "Content-Type")!, ["text/html"])
37 | XCTAssertEqual(dict.valuesFor(key: "Content-type")!, ["text/html"])
38 |
39 | XCTAssertEqual(dict["Content-Length"], "1234")
40 | XCTAssertEqual(dict["CONTENT-LENGTH"], "1234")
41 | XCTAssertEqual(dict.valuesFor(key: "Content-Length")!, ["1234"])
42 | XCTAssertEqual(dict.valuesFor(key: "CONTENT-LENGTH")!, ["1234"])
43 |
44 | XCTAssertEqual(dict["Set-Cookie"], "foo=bar")
45 | XCTAssertEqual(dict["Set-cookie"], "foo=bar")
46 | XCTAssertEqual(dict.valuesFor(key: "Set-Cookie")!, ["foo=bar", "egg=spam"])
47 | XCTAssertEqual(dict.valuesFor(key: "Set-cookie")!, ["foo=bar", "egg=spam"])
48 | }
49 |
50 | func testCaseSenstiveMultiDictionary() {
51 | let dict = MultiDictionary>(items: [
52 | ("Foo", "Bar"),
53 | ("egg", "spam"),
54 | ("Egg", "Spam"),
55 | ("egg", "bacon")
56 | ])
57 |
58 | XCTAssertNil(dict["Not-Exists"])
59 | XCTAssertNil(dict.valuesFor(key: "Not-Exists"))
60 | XCTAssertNil(dict.valuesFor(key: "foo"))
61 | XCTAssertNil(dict.valuesFor(key: "FOO"))
62 | XCTAssertNil(dict.valuesFor(key: "EGG"))
63 |
64 | XCTAssertEqual(dict["Foo"], "Bar")
65 | XCTAssertEqual(dict.valuesFor(key: "Foo")!, ["Bar"])
66 |
67 | XCTAssertEqual(dict["egg"], "spam")
68 | XCTAssertEqual(dict.valuesFor(key: "egg")!, ["spam", "bacon"])
69 |
70 | XCTAssertEqual(dict["Egg"], "Spam")
71 | XCTAssertEqual(dict.valuesFor(key: "Egg")!, ["Spam"])
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/SelectSelectorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectSelectorTests.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 1/13/17.
6 | // Copyright © 2017 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Dispatch
12 |
13 | @testable import Embassy
14 |
15 | #if os(Linux)
16 | extension SelectSelectorTests {
17 | static var allTests = [
18 | ("testRegister", testRegister),
19 | ("testUnregister", testUnregister),
20 | ("testRegisterKeyError", testRegisterKeyError),
21 | ("testUnregisterKeyError", testUnregisterKeyError),
22 | ("testSelectOneSocket", testSelectOneSocket),
23 | ("testSelectEventFilter", testSelectEventFilter),
24 | ("testSelectAfterUnregister", testSelectAfterUnregister),
25 | ("testSelectMultipleSocket", testSelectMultipleSocket),
26 | ]
27 | }
28 | #endif
29 |
30 | class SelectSelectorTests: XCTestCase {
31 | let queue = DispatchQueue(label: "com.envoy.embassy-tests.select", attributes: [])
32 |
33 | func testRegister() {
34 | let selector = try! SelectSelector()
35 | let socket = try! TCPSocket()
36 |
37 | XCTAssertNil(selector[socket.fileDescriptor])
38 |
39 | let data = "my data"
40 | try! selector.register(socket.fileDescriptor, events: [.read], data: data)
41 |
42 | let key = selector[socket.fileDescriptor]
43 | XCTAssertEqual(key?.fileDescriptor, socket.fileDescriptor)
44 | XCTAssertEqual(key?.events, [.read])
45 | XCTAssertEqual(key?.data as? String, data)
46 | }
47 |
48 | func testUnregister() {
49 | let selector = try! SelectSelector()
50 | let socket = try! TCPSocket()
51 |
52 | try! selector.register(socket.fileDescriptor, events: [.read], data: nil)
53 |
54 | let key = try! selector.unregister(socket.fileDescriptor)
55 | XCTAssertNil(selector[socket.fileDescriptor])
56 | XCTAssertNil(key.data as? String)
57 | XCTAssertEqual(key.fileDescriptor, socket.fileDescriptor)
58 | XCTAssertEqual(key.events, [.read])
59 | }
60 |
61 | func testRegisterKeyError() {
62 | let selector = try! SelectSelector()
63 | let socket = try! TCPSocket()
64 | try! selector.register(socket.fileDescriptor, events: [.read], data: nil)
65 |
66 | XCTAssertThrowsError(try selector.register(
67 | socket.fileDescriptor,
68 | events: [.read],
69 | data: nil
70 | )) { error in
71 | guard let error = error as? SelectSelector.Error else {
72 | XCTFail()
73 | return
74 | }
75 | guard case .keyError = error else {
76 | XCTFail()
77 | return
78 | }
79 | }
80 | }
81 |
82 | func testUnregisterKeyError() {
83 | let selector = try! SelectSelector()
84 | let socket = try! TCPSocket()
85 |
86 | XCTAssertThrowsError(try selector.unregister(socket.fileDescriptor)) { error in
87 | guard let error = error as? SelectSelector.Error else {
88 | XCTFail()
89 | return
90 | }
91 | guard case .keyError = error else {
92 | XCTFail()
93 | return
94 | }
95 | }
96 | }
97 |
98 | func testSelectOneSocket() {
99 | let selector = try! SelectSelector()
100 |
101 | let port = try! getUnusedTCPPort()
102 | let listenSocket = try! TCPSocket()
103 | try! listenSocket.bind(port: port)
104 | try! listenSocket.listen()
105 |
106 | try! selector.register(listenSocket.fileDescriptor, events: [.read], data: nil)
107 |
108 | // ensure we have a correct timeout here
109 | assertExecutingTime(2, accuracy: 1) {
110 | XCTAssertEqual(try! selector.select(timeout: 2.0).count, 0)
111 | }
112 |
113 | let clientSocket = try! TCPSocket()
114 |
115 | let emptyIOEvents = assertExecutingTime(1, accuracy: 1) {
116 | return try! selector.select(timeout: 1.0)
117 | }
118 | XCTAssertEqual(emptyIOEvents.count, 0)
119 |
120 | // make a connect 1 seconds later
121 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
122 | try! clientSocket.connect(host: "::1", port: port)
123 | }
124 |
125 | let ioEvents = assertExecutingTime(1, accuracy: 1) {
126 | return try! selector.select(timeout: 10.0)
127 | }
128 | XCTAssertEqual(ioEvents.count, 1)
129 | XCTAssertEqual(ioEvents.first?.0.fileDescriptor, listenSocket.fileDescriptor)
130 | XCTAssertEqual(ioEvents.first?.0.events, [.read])
131 | XCTAssertNil(ioEvents.first?.0.data)
132 | }
133 |
134 | func testSelectEventFilter() {
135 | let selector = try! SelectSelector()
136 |
137 | let port = try! getUnusedTCPPort()
138 | let listenSocket = try! TCPSocket()
139 | try! listenSocket.bind(port: port)
140 | try! listenSocket.listen()
141 |
142 | try! selector.register(listenSocket.fileDescriptor, events: [.write], data: nil)
143 |
144 | XCTAssertEqual(try! selector.select(timeout: 1.0).count, 0)
145 |
146 | let clientSocket = try! TCPSocket()
147 | // make a connect 1 seconds later
148 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
149 | try! clientSocket.connect(host: "::1", port: port)
150 | }
151 |
152 | // ensure we don't get any event triggered in two seconds
153 | XCTAssertEqual(try! selector.select(timeout: 2.0).count, 0)
154 | }
155 |
156 | func testSelectAfterUnregister() {
157 | let selector = try! SelectSelector()
158 |
159 | let port = try! getUnusedTCPPort()
160 | let listenSocket = try! TCPSocket()
161 | try! listenSocket.bind(port: port)
162 | try! listenSocket.listen()
163 |
164 | try! selector.register(listenSocket.fileDescriptor, events: [.read], data: nil)
165 |
166 | let clientSocket = try! TCPSocket()
167 | // make a connect 1 seconds later
168 | queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
169 | try! clientSocket.connect(host: "::1", port: port)
170 | }
171 |
172 | assertExecutingTime(1, accuracy: 1) {
173 | let events = try! selector.select(timeout: 2.0)
174 | let result = toEventSet(events)
175 | XCTAssertEqual(result, Set([
176 | FileDescriptorEvent(fileDescriptor: listenSocket.fileDescriptor, ioEvent: .read),
177 | ]))
178 | }
179 |
180 | try! selector.unregister(listenSocket.fileDescriptor)
181 |
182 | let clientSocket2 = try! TCPSocket()
183 | // make a connect 1 seconds later
184 | queue.asyncAfter(
185 | deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)
186 | ) {
187 | try! clientSocket2.connect(host: "::1", port: port)
188 | }
189 |
190 | assertExecutingTime(2, accuracy: 1) {
191 | XCTAssertEqual(try! selector.select(timeout: 2.0).count, 0)
192 | }
193 | }
194 |
195 | func testSelectMultipleSocket() {
196 | let selector = try! SelectSelector()
197 |
198 | let port = try! getUnusedTCPPort()
199 |
200 | let clientSocket = try! TCPSocket()
201 |
202 | let listenSocket = try! TCPSocket()
203 | try! listenSocket.bind(port: port)
204 | try! listenSocket.listen()
205 | try! selector.register(listenSocket.fileDescriptor, events: [.read, .write], data: nil)
206 |
207 | try! clientSocket.connect(host: "::1", port: port)
208 | try! selector.register(clientSocket.fileDescriptor, events: [.read, .write], data: nil)
209 |
210 | sleep(1)
211 |
212 | let ioEvents0 = assertExecutingTime(0, accuracy: 1) {
213 | return try! selector.select(timeout: 10.0)
214 | }
215 | let result0 = toEventSet(ioEvents0)
216 | XCTAssertEqual(result0, Set([
217 | FileDescriptorEvent(fileDescriptor: clientSocket.fileDescriptor, ioEvent: .write),
218 | FileDescriptorEvent(fileDescriptor: listenSocket.fileDescriptor, ioEvent: .read),
219 | ]))
220 |
221 | let acceptedSocket = try! listenSocket.accept()
222 | try! selector.register(acceptedSocket.fileDescriptor, events: [.read, .write], data: nil)
223 |
224 | let ioEvents1 = assertExecutingTime(0, accuracy: 1) {
225 | return try! selector.select(timeout: 10.0)
226 | }
227 | let result1 = toEventSet(ioEvents1)
228 | XCTAssertEqual(result1, Set([
229 | FileDescriptorEvent(fileDescriptor: clientSocket.fileDescriptor, ioEvent: .write),
230 | FileDescriptorEvent(fileDescriptor: acceptedSocket.fileDescriptor, ioEvent: .write),
231 | ]))
232 |
233 | // we should have no events now
234 | assertExecutingTime(1, accuracy: 1) {
235 | return try! selector.select(timeout: 1)
236 | }
237 |
238 | try! clientSocket.send(data: Data("hello".utf8))
239 | // wait a little while so that read event will surely be triggered
240 | sleep(1)
241 |
242 | let ioEvents2 = assertExecutingTime(0, accuracy: 1) {
243 | return try! selector.select(timeout: 10.0)
244 | }
245 | let result2 = toEventSet(ioEvents2)
246 | XCTAssertEqual(result2, Set([
247 | FileDescriptorEvent(fileDescriptor: clientSocket.fileDescriptor, ioEvent: .write),
248 | FileDescriptorEvent(fileDescriptor: acceptedSocket.fileDescriptor, ioEvent: .read),
249 | FileDescriptorEvent(fileDescriptor: acceptedSocket.fileDescriptor, ioEvent: .write)
250 | ]))
251 |
252 | let receivedString = String(
253 | bytes: try! acceptedSocket.recv(size: 1024),
254 | encoding: String.Encoding.utf8
255 | )
256 | XCTAssertEqual(receivedString, "hello")
257 |
258 | let ioEvents3 = assertExecutingTime(0, accuracy: 1) {
259 | return try! selector.select(timeout: 10.0)
260 | }
261 | let result3 = toEventSet(ioEvents3)
262 | XCTAssertEqual(result3, Set([
263 | FileDescriptorEvent(fileDescriptor: clientSocket.fileDescriptor, ioEvent: .write),
264 | FileDescriptorEvent(fileDescriptor: acceptedSocket.fileDescriptor, ioEvent: .write)
265 | ]))
266 |
267 | // we should have no events now
268 | assertExecutingTime(1, accuracy: 1) {
269 | return try! selector.select(timeout: 1)
270 | }
271 | }
272 |
273 | fileprivate func toEventSet(_ events: [(SelectorKey, Set)]) -> Set {
274 | return Set(events.flatMap { (key, ioEvents) in
275 | return ioEvents.map { FileDescriptorEvent(fileDescriptor: key.fileDescriptor, ioEvent: $0) }
276 | })
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/SelectorEventLoopTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectorEventLoopTests.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/21/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Dispatch
12 |
13 | @testable import Embassy
14 |
15 | #if os(Linux)
16 | extension SelectorEventLoopTests {
17 | static var allTests = [
18 | ("testStop", testStop),
19 | ("testCallSoon", testCallSoon),
20 | ("testCallLater", testCallLater),
21 | ("testCallAtOrder", testCallAtOrder),
22 | ("testSetReader", testSetReader),
23 | ("testSetWriter", testSetWriter),
24 | ("testRemoveReader", testRemoveReader),
25 | ]
26 | }
27 | #endif
28 |
29 | class SelectorEventLoopTests: XCTestCase {
30 | let queue = DispatchQueue(label: "com.envoy.embassy-tests.event-loop", attributes: [])
31 | var loop: SelectorEventLoop!
32 |
33 | override func setUp() {
34 | super.setUp()
35 | loop = try! SelectorEventLoop(selector: try! TestingSelector())
36 |
37 | // set a 30 seconds timeout
38 | queue.asyncAfter(
39 | deadline: DispatchTime.now() + Double(Int64(30 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)
40 | ) {
41 | if self.loop.running {
42 | self.loop.stop()
43 | XCTFail("Time out")
44 | }
45 | }
46 | }
47 |
48 | override func tearDown() {
49 | super.tearDown()
50 | }
51 |
52 | func testStop() {
53 | queue.asyncAfter(
54 | deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)
55 | ) {
56 | XCTAssert(self.loop.running)
57 | self.loop.stop()
58 | XCTAssertFalse(self.loop.running)
59 | }
60 |
61 | XCTAssertFalse(loop.running)
62 | assertExecutingTime(1.0, accuracy: 0.5) {
63 | self.loop.runForever()
64 | }
65 | XCTAssertFalse(loop.running)
66 | }
67 |
68 | func testCallSoon() {
69 | var called = false
70 | loop.call {
71 | called = true
72 | self.loop.stop()
73 | }
74 | assertExecutingTime(0, accuracy: 0.5) {
75 | self.loop.runForever()
76 | }
77 | XCTAssert(called)
78 | }
79 |
80 | func testCallLater() {
81 | var events: [Int] = []
82 | loop.call(withDelay: 0) {
83 | events.append(0)
84 | }
85 | loop.call(withDelay: 1) {
86 | events.append(1)
87 | }
88 | loop.call(withDelay: 2) {
89 | self.loop.stop()
90 | }
91 | loop.call(withDelay: 3) {
92 | events.append(3)
93 | }
94 | assertExecutingTime(2, accuracy: 0.5) {
95 | self.loop.runForever()
96 | }
97 | XCTAssertEqual(events, [0, 1])
98 | }
99 |
100 | func testCallAtOrder() {
101 | var events: [Int] = []
102 | let now = Date()
103 | loop.call(atTime: now.addingTimeInterval(0)) {
104 | events.append(0)
105 | }
106 | loop.call(atTime: now.addingTimeInterval(0.000002)) {
107 | events.append(2)
108 | }
109 | loop.call(atTime: now.addingTimeInterval(0.000001)) {
110 | events.append(1)
111 | }
112 | loop.call(atTime: now.addingTimeInterval(0.000004)) {
113 | events.append(4)
114 | self.loop.stop()
115 | }
116 | loop.call(atTime: now.addingTimeInterval(0.000003)) {
117 | events.append(3)
118 | }
119 | assertExecutingTime(0, accuracy: 0.5) {
120 | self.loop.runForever()
121 | }
122 | XCTAssertEqual(events, [0, 1, 2, 3, 4])
123 | }
124 |
125 | func testSetReader() {
126 | let port = try! getUnusedTCPPort()
127 | let listenSocket = try! TCPSocket()
128 | try! listenSocket.bind(port: port)
129 | try! listenSocket.listen()
130 | var readerCalled = false
131 |
132 | loop.setReader(listenSocket.fileDescriptor) {
133 | readerCalled = true
134 | self.loop.stop()
135 | }
136 |
137 | let clientSocket = try! TCPSocket()
138 |
139 | // make a connection 1 seconds later
140 | loop.call(withDelay: 1) {
141 | try! clientSocket.connect(host: "::1", port: port)
142 | }
143 |
144 | assertExecutingTime(1.0, accuracy: 0.5) {
145 | self.loop.runForever()
146 | }
147 | XCTAssert(readerCalled)
148 | }
149 |
150 | func testSetWriter() {
151 | let port = try! getUnusedTCPPort()
152 | let listenSocket = try! TCPSocket()
153 | try! listenSocket.bind(port: port)
154 | try! listenSocket.listen()
155 | var writerCalled = false
156 |
157 | let clientSocket = try! TCPSocket()
158 |
159 | // make a connect 1 seconds later
160 | loop.call(withDelay: 1) { [unowned self] in
161 | try! clientSocket.connect(host: "::1", port: port)
162 |
163 | // Notice: It seems we should only select on the socket after it's either connecting
164 | // or listening, and that's why we put setWriter here instead of before or after
165 | // ref: http://stackoverflow.com/q/41656400/25077
166 | self.loop.setWriter(clientSocket.fileDescriptor) {
167 | writerCalled = true
168 | self.loop.stop()
169 | }
170 | }
171 |
172 | assertExecutingTime(1.0, accuracy: 0.5) {
173 | self.loop.runForever()
174 | }
175 | XCTAssert(writerCalled)
176 | }
177 |
178 | func testRemoveReader() {
179 | let port = try! getUnusedTCPPort()
180 | let listenSocket = try! TCPSocket()
181 | try! listenSocket.bind(port: port)
182 | try! listenSocket.listen()
183 |
184 | let clientSocket = try! TCPSocket()
185 | var acceptedSocket: TCPSocket!
186 |
187 | var readData = [String]()
188 | let readAcceptedSocket = {
189 | let data = try! acceptedSocket.recv(size: 1024)
190 | readData.append(String(bytes: data, encoding: String.Encoding.utf8)!)
191 | if readData.count >= 2 {
192 | self.loop.removeReader(acceptedSocket.fileDescriptor)
193 | }
194 | }
195 |
196 | loop.setReader(listenSocket.fileDescriptor) {
197 | acceptedSocket = try! listenSocket.accept()
198 | self.loop.setReader(acceptedSocket.fileDescriptor, callback: readAcceptedSocket)
199 | }
200 |
201 | try! clientSocket.connect(host: "::1", port: port)
202 |
203 | loop.call(withDelay: 1) {
204 | try! clientSocket.send(data: Data("hello".utf8))
205 | }
206 | loop.call(withDelay: 2) {
207 | try! clientSocket.send(data: Data("baby".utf8))
208 | }
209 | loop.call(withDelay: 3) {
210 | try! clientSocket.send(data: Data("fin".utf8))
211 | }
212 | loop.call(withDelay: 4) {
213 | self.loop.stop()
214 | }
215 |
216 | assertExecutingTime(4.0, accuracy: 0.5) {
217 | self.loop.runForever()
218 | }
219 | XCTAssertEqual(readData, ["hello", "baby"])
220 | }
221 |
222 | func testEventLoopReferenceCycle() {
223 | // Notice: we had a reference cycle from the setReader callback to the
224 | // selector loop object before, we ensure that when loop is not hold
225 | // by anybody, it should be released here
226 | weak var loop = try! SelectorEventLoop(selector: try! TestingSelector())
227 | XCTAssertNil(loop)
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/TCPSocketTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TCPSocketTests.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/20/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Dispatch
11 | import XCTest
12 |
13 | @testable import Embassy
14 |
15 | #if os(Linux)
16 | extension TCPSocketTests {
17 | static var allTests = [
18 | ("testAccept", testAccept),
19 | ("testReadAndWrite", testReadAndWrite),
20 | ("testGetPeerName", testGetPeerName),
21 | ("testGetSockName", testGetSockName),
22 | ]
23 | }
24 | #endif
25 |
26 | class TCPSocketTests: XCTestCase {
27 | let queue = DispatchQueue(label: "com.envoy.embassy-tests.tcp-sockets", attributes: [])
28 |
29 | func testAccept() {
30 | let port = try! getUnusedTCPPort()
31 | let listenSocket = try! TCPSocket(blocking: true)
32 | try! listenSocket.bind(port: port)
33 | try! listenSocket.listen()
34 |
35 | let exp0 = expectation(description: "socket accepcted")
36 | var acceptedSocket: TCPSocket!
37 | queue.async {
38 | acceptedSocket = try! listenSocket.accept()
39 | exp0.fulfill()
40 | }
41 |
42 | let clientSocket = try! TCPSocket()
43 | try! clientSocket.connect(host: "::1", port: port)
44 |
45 | waitForExpectations(timeout: 3) { error in
46 | XCTAssertNil(error)
47 | }
48 | XCTAssertNotNil(acceptedSocket)
49 | }
50 |
51 | func testReadAndWrite() {
52 | let port = try! getUnusedTCPPort()
53 | let listenSocket = try! TCPSocket(blocking: true)
54 | try! listenSocket.bind(port: port)
55 | try! listenSocket.listen()
56 |
57 | let exp0 = expectation(description: "socket accepcted")
58 | var acceptedSocket: TCPSocket!
59 | queue.async {
60 | acceptedSocket = try! listenSocket.accept()
61 | exp0.fulfill()
62 | }
63 |
64 | let clientSocket = try! TCPSocket(blocking: true)
65 | try! clientSocket.connect(host: "::1", port: port)
66 |
67 | waitForExpectations(timeout: 4) { error in
68 | XCTAssertNil(error)
69 | }
70 |
71 | let stringToSend = "hello baby"
72 | let bytesToSend = Data(stringToSend.utf8)
73 |
74 | var receivedData: Data?
75 | let exp1 = expectation(description: "socket received")
76 |
77 | let sentBytes = try! clientSocket.send(data: bytesToSend)
78 | XCTAssertEqual(sentBytes, bytesToSend.count)
79 |
80 | queue.async {
81 | receivedData = try! acceptedSocket.recv(size: 1024)
82 | exp1.fulfill()
83 | }
84 |
85 | waitForExpectations(timeout: 3) { error in
86 | XCTAssertNil(error)
87 | }
88 |
89 | XCTAssertEqual(String(bytes: receivedData!, encoding: String.Encoding.utf8), stringToSend)
90 | }
91 |
92 | func testGetPeerName() {
93 | let port = try! getUnusedTCPPort()
94 | let listenSocket = try! TCPSocket(blocking: true)
95 | try! listenSocket.bind(port: port)
96 | try! listenSocket.listen()
97 |
98 | let exp0 = expectation(description: "socket accepcted")
99 | var acceptedSocket: TCPSocket!
100 | queue.async {
101 | acceptedSocket = try! listenSocket.accept()
102 | exp0.fulfill()
103 | }
104 |
105 | let clientSocket = try! TCPSocket(blocking: true)
106 | try! clientSocket.connect(host: "::1", port: port)
107 |
108 | waitForExpectations(timeout: 4, handler: nil)
109 |
110 | XCTAssertEqual(try! acceptedSocket.getPeerName().0, "::1")
111 | XCTAssertEqual(try! clientSocket.getPeerName().0, "::1")
112 | }
113 |
114 | func testGetSockName() {
115 | let port = try! getUnusedTCPPort()
116 | let listenSocket = try! TCPSocket(blocking: true)
117 | try! listenSocket.bind(port: port)
118 | try! listenSocket.listen()
119 |
120 | let exp0 = expectation(description: "socket accepcted")
121 | var acceptedSocket: TCPSocket!
122 | queue.async {
123 | acceptedSocket = try! listenSocket.accept()
124 | exp0.fulfill()
125 | }
126 |
127 | let clientSocket = try! TCPSocket(blocking: true)
128 | try! clientSocket.connect(host: "::1", port: port)
129 |
130 | waitForExpectations(timeout: 4, handler: nil)
131 |
132 | XCTAssertEqual(try! acceptedSocket.getSockName().0, "::1")
133 | XCTAssertEqual(try! clientSocket.getSockName().0, "::1")
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/TestingHelpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestingHelpers.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/20/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | @testable import Embassy
13 |
14 | #if os(Linux)
15 | import Glibc
16 | let bind = Glibc.bind
17 | let NSEC_PER_SEC = 1000000000
18 | let random = { () -> UInt32 in
19 | return UInt32(Glibc.rand())
20 | }
21 | let randomUniform = { (upperBound: UInt32) -> UInt32 in
22 | let num: Float = Float(random()) / Float(UInt32.max)
23 | return UInt32(num * Float(upperBound))
24 | }
25 | typealias TestingSelector = SelectSelector
26 | #else
27 | import Darwin
28 | let isLittleEndian = Int(OSHostByteOrder()) == OSLittleEndian
29 | let htons = isLittleEndian ? _OSSwapInt16 : { $0 }
30 | let ntohs = isLittleEndian ? _OSSwapInt16 : { $0 }
31 | let bind = Darwin.bind
32 | let random = Darwin.arc4random
33 | let randomUniform = Darwin.arc4random_uniform
34 | typealias TestingSelector = KqueueSelector
35 | #endif
36 |
37 | /// Find an available localhost TCP port from 1024-65535 and return it.
38 | /// Ref: https://github.com/pytest-dev/pytest-asyncio/blob/412c63776b32229ed8320e6c7ea920d7498cd695/pytest_asyncio/plugin.py#L103-L107
39 | func getUnusedTCPPort() throws -> Int {
40 | var interfaceAddress: in_addr = in_addr()
41 | guard "127.0.0.1".withCString({ inet_pton(AF_INET, $0, &interfaceAddress) >= 0 }) else {
42 | throw OSError.lastIOError()
43 | }
44 |
45 | #if os(Linux)
46 | let socketType = Int32(SOCK_STREAM.rawValue)
47 | #else
48 | let socketType = SOCK_STREAM
49 | #endif
50 | let fileDescriptor = socket(AF_INET, socketType, 0)
51 | guard fileDescriptor >= 0 else {
52 | throw OSError.lastIOError()
53 | }
54 | defer {
55 | close(fileDescriptor)
56 | }
57 |
58 | var address = sockaddr_in()
59 | #if !os(Linux)
60 | address.sin_len = UInt8(MemoryLayout.stride)
61 | #endif
62 | address.sin_family = sa_family_t(AF_INET)
63 | address.sin_port = htons(UInt16(0))
64 | address.sin_addr = interfaceAddress
65 | address.sin_zero = (0, 0, 0, 0, 0, 0, 0, 0)
66 | let addressSize = socklen_t(MemoryLayout.size)
67 | // given port 0, and bind, it will find us an available port
68 | guard withUnsafePointer(to: &address, { pointer in
69 | return pointer.withMemoryRebound(
70 | to: sockaddr.self,
71 | capacity: 1
72 | ) { pointer in
73 | return bind(fileDescriptor, pointer, addressSize) >= 0
74 | }
75 | }) else {
76 | throw OSError.lastIOError()
77 | }
78 |
79 | var socketAddress = sockaddr_in()
80 | var socketAddressSize = socklen_t(MemoryLayout.size)
81 | guard withUnsafeMutablePointer(to: &socketAddress, { pointer in
82 | return pointer.withMemoryRebound(
83 | to: sockaddr.self,
84 | capacity: 1
85 | ) { pointer in
86 | return getsockname(fileDescriptor, pointer, &socketAddressSize) >= 0
87 | }
88 | }) else {
89 | throw OSError.lastIOError()
90 | }
91 | return Int(ntohs(socketAddress.sin_port))
92 | }
93 |
94 | func makeRandomString(_ length: Int) -> String {
95 | let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
96 | let endIndex = UInt32(letters.count - 1)
97 | let result: [Any?] = Array(repeating: nil, count: length)
98 | return String(result.map({ _ in
99 | letters[String.Index(encodedOffset: Int(arc4random_uniform(endIndex)))]
100 | }))
101 | }
102 |
103 | extension XCTestCase {
104 | @discardableResult
105 | func assertExecutingTime(
106 | _ time: TimeInterval,
107 | accuracy: TimeInterval,
108 | file: StaticString = #file,
109 | line: UInt = #line,
110 | closure: () -> T
111 | ) -> T {
112 | let begin = Date()
113 | let result = closure()
114 | let elapsed = Date().timeIntervalSince(begin)
115 | XCTAssertEqual(
116 | elapsed,
117 | time,
118 | accuracy: accuracy,
119 | "Wrong executing time",
120 | file: file,
121 | line: line
122 | )
123 | return result
124 | }
125 | }
126 |
127 | struct FileDescriptorEvent {
128 | let fileDescriptor: Int32
129 | let ioEvent: IOEvent
130 | }
131 |
132 | extension FileDescriptorEvent: Equatable {
133 | }
134 |
135 | func == (lhs: FileDescriptorEvent, rhs: FileDescriptorEvent) -> Bool {
136 | return lhs.fileDescriptor == rhs.fileDescriptor && lhs.ioEvent == rhs.ioEvent
137 | }
138 |
139 | extension FileDescriptorEvent: Hashable {
140 | func hash(into hasher: inout Hasher) {
141 | hasher.combine(fileDescriptor)
142 | hasher.combine(ioEvent)
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/Tests/EmbassyTests/TransportTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransportTests.swift
3 | // Embassy
4 | //
5 | // Created by Fang-Pen Lin on 5/21/16.
6 | // Copyright © 2016 Fang-Pen Lin. All rights reserved.
7 | //
8 |
9 | import Dispatch
10 | import XCTest
11 |
12 | @testable import Embassy
13 |
14 | #if os(Linux)
15 | extension TransportTests {
16 | static var allTests = [
17 | ("testBigChunkReadAndWrite", testBigChunkReadAndWrite),
18 | ("testReadAndWrite", testReadAndWrite),
19 | ("testCloseByPeer", testCloseByPeer),
20 | ("testReadingPause", testReadingPause),
21 | ]
22 | }
23 | #endif
24 |
25 |
26 | class TransportTests: XCTestCase {
27 | let queue = DispatchQueue(label: "com.envoy.embassy-tests.event-loop", attributes: [])
28 | func testBigChunkReadAndWrite() {
29 | let loop = try! SelectorEventLoop(selector: try! TestingSelector())
30 |
31 | let port = try! getUnusedTCPPort()
32 | let listenSocket = try! TCPSocket()
33 | try! listenSocket.bind(port: port)
34 | try! listenSocket.listen()
35 |
36 | var clientReceivedData: [String] = []
37 | var serverReceivedData: [String] = []
38 | var totalReceivedSize = 0
39 | let dataChunk1 = makeRandomString(128)
40 | let dataChunk2 = makeRandomString(5743)
41 | let dataChunk3 = makeRandomString(2731)
42 | let dataChunk4 = makeRandomString(538)
43 | let dataChunk5 = makeRandomString(2048)
44 | let dataChunk6 = makeRandomString(1)
45 | let totalDataSize = [
46 | dataChunk1,
47 | dataChunk2,
48 | dataChunk3,
49 | dataChunk4,
50 | dataChunk5,
51 | dataChunk6
52 | ].reduce(0) { $0 + $1.count }
53 |
54 | let clientSocket = try! TCPSocket()
55 | let clientTransport = Transport(socket: clientSocket, eventLoop: loop) { data in
56 | clientReceivedData.append(String(bytes: data, encoding: String.Encoding.utf8)!)
57 | totalReceivedSize += clientReceivedData.last!.count
58 | if totalReceivedSize >= totalDataSize {
59 | loop.stop()
60 | }
61 | }
62 | var acceptedSocket: TCPSocket!
63 | var serverTransport: Transport!
64 |
65 | loop.setReader(listenSocket.fileDescriptor) {
66 | acceptedSocket = try! listenSocket.accept()
67 | serverTransport = Transport(socket: acceptedSocket, eventLoop: loop) { data in
68 | serverReceivedData.append(String(bytes: data, encoding: String.Encoding.utf8)!)
69 | totalReceivedSize += serverReceivedData.last!.count
70 | if totalReceivedSize >= totalDataSize {
71 | loop.stop()
72 | }
73 | }
74 | }
75 |
76 | try! clientSocket.connect(host: "::1", port: port)
77 |
78 |
79 | loop.call(withDelay: 1) {
80 | clientTransport.write(string: dataChunk1)
81 | }
82 | loop.call(withDelay: 2) {
83 | serverTransport.write(string: dataChunk2)
84 | }
85 | loop.call(withDelay: 3) {
86 | clientTransport.write(string: dataChunk3)
87 | }
88 | loop.call(withDelay: 4) {
89 | serverTransport.write(string: dataChunk4)
90 | }
91 | loop.call(withDelay: 5) {
92 | clientTransport.write(string: dataChunk5)
93 | }
94 | loop.call(withDelay: 6) {
95 | serverTransport.write(string: dataChunk6)
96 | }
97 |
98 | loop.call(withDelay: 10) {
99 | loop.stop()
100 | }
101 |
102 | loop.runForever()
103 |
104 | XCTAssertEqual(serverReceivedData.joined(separator: ""), [
105 | dataChunk1,
106 | dataChunk3,
107 | dataChunk5
108 | ].joined(separator: ""))
109 | XCTAssertEqual(clientReceivedData.joined(separator: ""), [
110 | dataChunk2,
111 | dataChunk4,
112 | dataChunk6
113 | ].joined(separator: ""))
114 | }
115 |
116 | func testReadAndWrite() {
117 | let loop = try! SelectorEventLoop(selector: try! TestingSelector())
118 |
119 | let port = try! getUnusedTCPPort()
120 | let listenSocket = try! TCPSocket()
121 | try! listenSocket.bind(port: port)
122 | try! listenSocket.listen()
123 |
124 | var clientReceivedData: [String] = []
125 | var serverReceivedData: [String] = []
126 |
127 | let clientSocket = try! TCPSocket()
128 | let clientTransport = Transport(socket: clientSocket, eventLoop: loop) { data in
129 | clientReceivedData.append(String(bytes: data, encoding: String.Encoding.utf8)!)
130 | if clientReceivedData.count >= 3 && serverReceivedData.count >= 3 {
131 | loop.stop()
132 | }
133 | }
134 | var acceptedSocket: TCPSocket!
135 | var serverTransport: Transport!
136 |
137 | loop.setReader(listenSocket.fileDescriptor) {
138 | acceptedSocket = try! listenSocket.accept()
139 | serverTransport = Transport(socket: acceptedSocket, eventLoop: loop) { data in
140 | serverReceivedData.append(String(bytes: data, encoding: String.Encoding.utf8)!)
141 | if clientReceivedData.count >= 3 && serverReceivedData.count >= 3 {
142 | loop.stop()
143 | }
144 | }
145 | }
146 |
147 | try! clientSocket.connect(host: "::1", port: port)
148 |
149 | loop.call(withDelay: 1) {
150 | clientTransport.write(string: "a")
151 | }
152 | loop.call(withDelay: 2) {
153 | serverTransport.write(string: "1")
154 | }
155 | loop.call(withDelay: 3) {
156 | clientTransport.write(string: "b")
157 | }
158 | loop.call(withDelay: 4) {
159 | serverTransport.write(string: "2")
160 | }
161 | loop.call(withDelay: 5) {
162 | clientTransport.write(string: "c")
163 | }
164 | loop.call(withDelay: 6) {
165 | serverTransport.write(string: "3")
166 | }
167 |
168 | loop.call(withDelay: 10) {
169 | loop.stop()
170 | }
171 |
172 | loop.runForever()
173 |
174 | XCTAssertEqual(serverReceivedData, ["a", "b", "c"])
175 | XCTAssertEqual(clientReceivedData, ["1", "2", "3"])
176 | }
177 |
178 | func testCloseByPeer() {
179 | let loop = try! SelectorEventLoop(selector: try! TestingSelector())
180 |
181 | let port = try! getUnusedTCPPort()
182 | let listenSocket = try! TCPSocket()
183 | try! listenSocket.bind(port: port)
184 | try! listenSocket.listen()
185 |
186 | let clientSocket = try! TCPSocket()
187 | let clientTransport = Transport(socket: clientSocket, eventLoop: loop) { _ in
188 |
189 | }
190 | var acceptedSocket: TCPSocket!
191 | var serverTransport: Transport!
192 | var serverReceivedData: [String] = []
193 | var serverTransportClosed: Bool = false
194 |
195 | loop.setReader(listenSocket.fileDescriptor) {
196 | acceptedSocket = try! listenSocket.accept()
197 | serverTransport = Transport(
198 | socket: acceptedSocket,
199 | eventLoop: loop,
200 | closedCallback: { reason in
201 | XCTAssert(serverTransport.closed)
202 | XCTAssert(reason.isByPeer)
203 | serverTransportClosed = true
204 | loop.stop()
205 | },
206 | readDataCallback: { data in
207 | serverReceivedData.append(String(bytes: data, encoding: .utf8)!)
208 | }
209 | )
210 | }
211 |
212 | try! clientSocket.connect(host: "::1", port: port)
213 | let bigDataChunk = makeRandomString(574300)
214 |
215 | loop.call(withDelay: 1) {
216 | clientTransport.write(string: "hello")
217 | }
218 |
219 | loop.call(withDelay: 2) {
220 | XCTAssertFalse(clientTransport.closed)
221 | XCTAssertFalse(clientTransport.closing)
222 | clientTransport.write(string: bigDataChunk)
223 | clientTransport.close()
224 | XCTAssertTrue(clientTransport.closing)
225 | }
226 |
227 | loop.call(withDelay: 10) {
228 | loop.stop()
229 | }
230 |
231 | loop.runForever()
232 |
233 | XCTAssert(serverTransportClosed)
234 | XCTAssert(clientTransport.closed)
235 | XCTAssert(serverTransport.closed)
236 | XCTAssertEqual(
237 | serverReceivedData.joined(separator: "").count,
238 | "hello".count + bigDataChunk.count
239 | )
240 | }
241 |
242 | func testReadingPause() {
243 | let loop = try! SelectorEventLoop(selector: try! TestingSelector())
244 |
245 | let port = try! getUnusedTCPPort()
246 | let listenSocket = try! TCPSocket()
247 | try! listenSocket.bind(port: port)
248 | try! listenSocket.listen()
249 |
250 | var clientReceivedData: [String] = []
251 | var serverReceivedData: [String] = []
252 |
253 | let clientSocket = try! TCPSocket()
254 | let clientTransport = Transport(socket: clientSocket, eventLoop: loop) { data in
255 | clientReceivedData.append(String(bytes: data, encoding: String.Encoding.utf8)!)
256 | if clientReceivedData.count >= 3 && serverReceivedData.count >= 3 {
257 | loop.stop()
258 | }
259 | }
260 | var acceptedSocket: TCPSocket!
261 | var serverTransport: Transport!
262 |
263 | loop.setReader(listenSocket.fileDescriptor) {
264 | acceptedSocket = try! listenSocket.accept()
265 | serverTransport = Transport(socket: acceptedSocket, eventLoop: loop) { data in
266 | serverReceivedData.append(String(bytes: data, encoding: String.Encoding.utf8)!)
267 | if clientReceivedData.count >= 3 && serverReceivedData.count >= 3 {
268 | loop.stop()
269 | }
270 | }
271 | }
272 |
273 | try! clientSocket.connect(host: "::1", port: port)
274 |
275 | loop.call(withDelay: 1) {
276 | clientTransport.write(string: "a")
277 | }
278 | loop.call(withDelay: 2) {
279 | serverTransport.write(string: "1")
280 | }
281 | loop.call(withDelay: 3) {
282 | clientTransport.resume(reading: false)
283 | serverTransport.resume(reading: false)
284 | clientTransport.write(string: "b")
285 | }
286 | loop.call(withDelay: 4) {
287 | XCTAssertEqual(clientReceivedData.count, 1)
288 | XCTAssertEqual(serverReceivedData.count, 1)
289 | serverTransport.write(string: "2")
290 | }
291 | loop.call(withDelay: 5) {
292 | XCTAssertEqual(clientReceivedData.count, 1)
293 | XCTAssertEqual(serverReceivedData.count, 1)
294 | clientTransport.write(string: "c")
295 | }
296 | loop.call(withDelay: 6) {
297 | XCTAssertEqual(clientReceivedData.count, 1)
298 | XCTAssertEqual(serverReceivedData.count, 1)
299 | serverTransport.write(string: "3")
300 | }
301 | loop.call(withDelay: 7) {
302 | clientTransport.resume(reading: true)
303 | serverTransport.resume(reading: true)
304 | }
305 | loop.call(withDelay: 10) {
306 | loop.stop()
307 | }
308 |
309 | loop.runForever()
310 |
311 | XCTAssertEqual(serverReceivedData, ["a", "bc"])
312 | XCTAssertEqual(clientReceivedData, ["1", "23"])
313 | }
314 | }
315 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import EmbassyTests
3 |
4 | XCTMain([
5 | testCase(HeapSortTetsts.allTests),
6 | testCase(HTTPHeaderParserTests.allTests),
7 | testCase(MultiDictionaryTests.allTests),
8 | testCase(TCPSocketTests.allTests),
9 | testCase(SelectSelectorTests.allTests),
10 | testCase(SelectorEventLoopTests.allTests),
11 | testCase(TransportTests.allTests),
12 | testCase(HTTPServerTests.allTests),
13 | ])
14 |
--------------------------------------------------------------------------------
/run-docker.sh:
--------------------------------------------------------------------------------
1 | docker run -v "$PWD:/embassy" -it swiftdocker/swift /bin/bash
2 |
--------------------------------------------------------------------------------