├── .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 | [![Build Status](https://travis-ci.org/envoy/Embassy.svg?branch=master)](https://travis-ci.org/envoy/Embassy) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg)](https://github.com/Carthage/Carthage) 5 | [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 6 | [![CocoaPods](https://img.shields.io/cocoapods/v/Embassy.svg)]() 7 | ![Swift Version](https://img.shields.io/badge/Swift-5.0-orange.svg) 8 | ![Plaform](https://img.shields.io/badge/Platform-macOS|iOS|Linux-lightgrey.svg) 9 | [![GitHub license](https://img.shields.io/github/license/envoy/Embassy.svg)](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 | --------------------------------------------------------------------------------