├── .codecov.yml
├── .gitignore
├── .swiftlint.yml
├── .travis.yml
├── Dockerfile
├── LICENSE.md
├── Package.resolved
├── Package.swift
├── README.md
├── Scripts
├── TravisCI
│ ├── code-coverage.sh
│ └── install-swift.sh
└── lipo
│ └── lipo.sh
├── Sources
├── CHTTPParser
│ ├── http_parser.c
│ └── include
│ │ └── http_parser.h
├── HTTP
│ ├── Error.swift
│ ├── Message.swift
│ ├── Method.swift
│ ├── Parser.swift
│ ├── Request.swift
│ ├── Response.swift
│ ├── Routing
│ │ ├── Endpoint.swift
│ │ ├── ErrorEndpoint.swift
│ │ ├── Filter.swift
│ │ ├── Path.swift
│ │ ├── RequestMiddleware.swift
│ │ ├── ResponseMiddleware.swift
│ │ └── Router.swift
│ ├── Serializable.swift
│ ├── Server.swift
│ ├── Status.swift
│ └── Version.swift
├── IOStream
│ ├── IOStream.swift
│ └── Pipe.swift
├── Lightning
│ └── Lightning.swift
├── POSIX
│ ├── FileDescriptor.swift
│ ├── Socket.swift
│ └── SystemError.swift
└── TCP
│ ├── Server.swift
│ └── Socket.swift
├── Tests
├── HTTPTests
│ ├── HTTPMessageTests.swift
│ ├── PerformanceTests.swift
│ ├── RequestParserTests.swift
│ ├── RequestSerializationTests.swift
│ ├── ResponseParserTests.swift
│ ├── ResponseSerializationTests.swift
│ ├── ServerTests.swift
│ └── XCTestManifests.swift
├── IOStreamTests
│ ├── PipeTests.swift
│ └── XCTestManifests.swift
├── LinuxMain.swift
├── RoutingTests
│ ├── RouterTests.swift
│ └── XCTestManifests.swift
└── TCPTests
│ ├── ConnectionTests.swift
│ └── XCTestManifests.swift
└── docker-compose.yml
/.codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | branch: master
3 |
4 | coverage:
5 | precision: 2
6 | round: down
7 | range: "70...100"
8 |
9 | ignore:
10 | - "Tests"
11 |
12 | comment:
13 | layout: "header, diff, changes, sunburst, uncovered, tree"
14 | behavior: default
15 |
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/linux,osx,xcode,swift
3 |
4 | ### Linux ###
5 | *~
6 |
7 | # temporary files which can be created if a process still has a handle open of a deleted file
8 | .fuse_hidden*
9 |
10 | # KDE directory preferences
11 | .directory
12 |
13 | # Linux trash folder which might appear on any partition or disk
14 | .Trash-*
15 |
16 |
17 | ### OSX ###
18 | .DS_Store
19 | .AppleDouble
20 | .LSOverride
21 |
22 | # Icon must end with two \r
23 | Icon
24 |
25 |
26 | # Thumbnails
27 | ._*
28 |
29 | # Files that might appear in the root of a volume
30 | .DocumentRevisions-V100
31 | .fseventsd
32 | .Spotlight-V100
33 | .TemporaryItems
34 | .Trashes
35 | .VolumeIcon.icns
36 |
37 | # Directories potentially created on remote AFP share
38 | .AppleDB
39 | .AppleDesktop
40 | Network Trash Folder
41 | Temporary Items
42 | .apdisk
43 |
44 |
45 | ### Xcode ###
46 | # Xcode
47 | #
48 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
49 |
50 | ## Build generated
51 | build/
52 | DerivedData/
53 |
54 | ## Various settings
55 | *.pbxuser
56 | !default.pbxuser
57 | *.mode1v3
58 | !default.mode1v3
59 | *.mode2v3
60 | !default.mode2v3
61 | *.perspectivev3
62 | !default.perspectivev3
63 | xcuserdata/
64 |
65 | ## Other
66 | *.moved-aside
67 | *.xccheckout
68 | *.xcscmblueprint
69 |
70 | ### Swift ###
71 | # Xcode
72 | #
73 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
74 |
75 | ## Build generated
76 | build/
77 | DerivedData/
78 |
79 | ## Various settings
80 | *.pbxuser
81 | !default.pbxuser
82 | *.mode1v3
83 | !default.mode1v3
84 | *.mode2v3
85 | !default.mode2v3
86 | *.perspectivev3
87 | !default.perspectivev3
88 | xcuserdata/
89 |
90 | ## Other
91 | *.xcodeproj
92 | #*.xcodeproj/*.plist
93 | *.moved-aside
94 | *.xcuserstate
95 |
96 | ## Obj-C/Swift specific
97 | *.hmap
98 | *.ipa
99 |
100 | ## Playgrounds
101 | timeline.xctimeline
102 | playground.xcworkspace
103 |
104 | # Swift Package Manager
105 | #
106 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
107 | Packages/
108 | .build/
109 |
110 | # CocoaPods
111 | #
112 | # We recommend against adding the Pods directory to your .gitignore. However
113 | # you should judge for yourself, the pros and cons are mentioned at:
114 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
115 | #
116 | # Pods/
117 |
118 | # Carthage
119 | #
120 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
121 | # Carthage/Checkouts
122 |
123 | Carthage/Build
124 |
125 | # fastlane
126 | #
127 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
128 | # screenshots whenever they are needed.
129 | # For more information about the recommended setup visit:
130 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
131 |
132 | fastlane/report.xml
133 | fastlane/Preview.html
134 | fastlane/screenshots
135 | fastlane/test_output
136 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | included:
2 | - Sources
3 | - Tests
4 | function_body_length:
5 | - 75 # warning
6 | - 100 # error
7 | type_body_length:
8 | - 300 # warning
9 | - 400 # error
10 | file_length:
11 | - 500
12 | variable_name:
13 | min_length: 0
14 | disabled_rules:
15 | - todo
16 | - force_try
17 | - cyclomatic_complexity
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Where's mah Swift at Travis!?
2 | language: generic
3 |
4 | matrix:
5 | include:
6 | - os: linux
7 | dist: trusty
8 | sudo: required
9 | - os: osx
10 | osx_image: xcode9.1
11 | sudo: required
12 |
13 | install:
14 | - ./Scripts/TravisCI/install-swift.sh
15 |
16 | script:
17 | - swift build
18 | - swift build -c release
19 | - swift test
20 |
21 | after_success:
22 | - ./Scripts/TravisCI/code-coverage.sh
23 |
24 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM swiftdocker/swift:4.0
2 |
3 | RUN apt-get update && apt-get install -y \
4 | libpq-dev
5 |
6 | # Create build directory
7 | RUN mkdir -p /usr/src
8 | WORKDIR /usr/src
9 |
10 | # Add swift source files
11 | ADD Sources Sources/
12 | ADD Tests Tests/
13 |
14 | # Add swift Package file
15 | ADD Package.swift .
16 |
17 | RUN chmod o+rw -R /usr/lib/swift/CoreFoundation/
18 |
19 | RUN swift test
20 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Tyler Cloutier
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.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Boilerplate",
6 | "repositoryURL": "https://github.com/crossroadlabs/Boilerplate.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "3411d6ade632d2a13613732b963f652a3eb13b2a",
10 | "version": "1.1.5"
11 | }
12 | },
13 | {
14 | "package": "PathToRegex",
15 | "repositoryURL": "https://github.com/skylab-inc/PathToRegex.git",
16 | "state": {
17 | "branch": "master",
18 | "revision": "e66d0ee87f7674a2ce3927554e78130787bec507",
19 | "version": null
20 | }
21 | },
22 | {
23 | "package": "PromiseKit",
24 | "repositoryURL": "https://github.com/mxcl/PromiseKit.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "6bab5e0c7f93947d9c0a7df0937add7454657f2c",
28 | "version": "4.5.0"
29 | }
30 | },
31 | {
32 | "package": "Regex",
33 | "repositoryURL": "https://github.com/crossroadlabs/Regex.git",
34 | "state": {
35 | "branch": "master",
36 | "revision": "5d0db3b1c11d74d1d661f69c8e7dcf3bd4b94317",
37 | "version": null
38 | }
39 | },
40 | {
41 | "package": "Result",
42 | "repositoryURL": "https://github.com/antitypical/Result.git",
43 | "state": {
44 | "branch": null,
45 | "revision": "7477584259bfce2560a19e06ad9f71db441fff11",
46 | "version": "3.2.4"
47 | }
48 | },
49 | {
50 | "package": "StreamKit",
51 | "repositoryURL": "https://github.com/skylab-inc/StreamKit.git",
52 | "state": {
53 | "branch": "master",
54 | "revision": "43238cc23188c02a90f51ce9bf58718611030abe",
55 | "version": null
56 | }
57 | }
58 | ]
59 | },
60 | "version": 1
61 | }
62 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "Lightning",
8 | products: [
9 | .library(
10 | name: "Lightning",
11 | targets: [
12 | "Lightning",
13 | "POSIX",
14 | "TCP",
15 | "HTTP",
16 | "IOStream",
17 | ]
18 | ),
19 | ],
20 | dependencies: [
21 | .package(url: "https://github.com/crossroadlabs/Regex.git", .branch("master")),
22 | .package(url: "https://github.com/skylab-inc/PathToRegex.git", .branch("master")),
23 | .package(url: "https://github.com/skylab-inc/StreamKit.git", .branch("master")),
24 | .package(url: "https://github.com/mxcl/PromiseKit.git", from: "4.5.0"),
25 | ],
26 | targets: [
27 | .target(name: "CHTTPParser"),
28 | .target(name: "POSIX"),
29 | .target(name: "TCP", dependencies: ["POSIX", "IOStream"]),
30 | .target(name: "HTTP", dependencies: [ "POSIX", "IOStream", "TCP", "CHTTPParser", "PromiseKit", "PathToRegex", "Regex"]),
31 | .target(name: "IOStream", dependencies: ["POSIX", "StreamKit"]),
32 | .target(name: "Lightning", dependencies: ["TCP", "IOStream", "HTTP"]),
33 | .testTarget(name: "HTTPTests", dependencies: ["HTTP"]),
34 | .testTarget(name: "IOStreamTests", dependencies: ["IOStream"]),
35 | .testTarget(name: "TCPTests", dependencies: ["TCP"]),
36 | .testTarget(name: "RoutingTests", dependencies: ["HTTP"]),
37 | ]
38 | )
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Serverside non-blocking IO in Swift
4 | Ask questions in our Slack channel!
5 |
6 |
7 |
8 | # Lightning
9 | ##### (formerly Edge)
10 |
11 | 
12 | [](https://travis-ci.org/skylab-inc/Lightning)
13 | [](https://codecov.io/gh/skylab-inc/Lightning)
14 | [](https://slackin-on-edge.herokuapp.com)
15 |
16 | #### Node
17 | Lightning is an HTTP Server and TCP Client/Server framework written in Swift and inspired by [Node.js](https://nodejs.org). It runs on both OS X and Linux. Like Node.js, Lightning uses an **event-driven, non-blocking I/O model**. In the same way that Node.js uses [libuv](http://libuv.org) to implement this model, Lightning uses [libdispatch](https://github.com/apple/swift-corelibs-libdispatch).
18 |
19 | This makes Lightning fast, efficient, and most crutially **single-threaded** by default. You simply do not need to worry about locks/mutexes/semaphores/etc if you have server-side state. Of course, Lightning applications can make use of libdispatch to easily offload heavy processing to a background thread if necessary.
20 |
21 | #### Reactive Programming
22 | Lightning's event API embraces Functional Reactive Programming by generalizing the familiar concept of promises. This API is called [StreamKit](https://github.com/skylab-inc/StreamKit).
23 |
24 | > StreamKit's architecture is inspired by both [ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa) and [RxSwift](https://github.com/ReactiveX/RxSwift).
25 |
26 | ##### Why did we reimplement?
27 | * Lightning should be easy to use out of the box.
28 | * Lightning is optimized for maximum performance, which requires careful tuning of the internals.
29 | * The modified API is meant to be more similar to the familiar concepts of Futures and Promises.
30 | * We don't want to be opinionated about any one framework. We want it to be easy to integate Lightning with either ReactiveCocoa or RxSwift.
31 |
32 | >FRP, greatly simplies management of asynchronous events. The general concept is that we can build a spout which pushes out asynchronous events as they happen. Then we hookup a pipeline of transformations that operate on events and pass the transformed values along. We can even do things like merge streams in interesting ways! Take a look at some of these [operations](http://rxmarbles.com) or watch [this talk](https://www.youtube.com/watch?v=XRYN2xt11Ek) about how FRP is used at Netflix.
33 |
34 | # Installation
35 |
36 | Lightning is available as a Swift 3/4 package. Simply add Lightning as a dependency to your Swift Package.
37 |
38 | Swift 3
39 | ```Swift
40 | import PackageDescription
41 |
42 | let package = Package(
43 | name: "MyProject",
44 | dependencies: [
45 | .Package(url: "https://github.com/skylab-inc/Lightning.git", majorVersion: 0, minor: 3)
46 | ]
47 | )
48 | ```
49 | Swift 4
50 | ```Swift
51 | // swift-tools-version:4.0
52 | // The swift-tools-version declares the minimum version of Swift required to build this package.
53 | import PackageDescription
54 |
55 | let package = Package(
56 | name: "MyProject",
57 | dependencies: [
58 | .package(url: "https://github.com/skylab-inc/Lightning.git", from: "0.3.0"),
59 | ]
60 | )
61 | ```
62 |
63 | # Usage
64 |
65 | ### Routing
66 | ```swift
67 | import Lightning
68 | import Foundation
69 |
70 | // Create an API router.
71 | let api = Router()
72 |
73 | // Add a GET "/users" endpoint.
74 | api.get("/users") { request in
75 | return Response(status: .ok)
76 | }
77 |
78 | // NOTE: Equivalent to `api.post("/auth/login")`
79 | let auth = api.subrouter("/auth")
80 | auth.post("/login") { request in
81 | return Response(status: .ok)
82 | }
83 |
84 | // Middleware to log all requests
85 | // NOTE: Middleware is a simple as a map function or closure!
86 | let app = Router()
87 | app.map { request in
88 | print(request)
89 | return request
90 | }
91 |
92 | // Mount the API router under "/v1.0".
93 | app.add("/v1.0", api)
94 |
95 | // NOTE: Warnings on all unhandled requests. No more hanging clients!
96 | app.any { _ in
97 | return Response(status: .notFound)
98 | }
99 |
100 | // Start the application.
101 | app.start(host: "0.0.0.0", port: 3000)
102 | ```
103 |
104 | ### Raw HTTP
105 | ```swift
106 | import Lightning
107 | import Foundation
108 |
109 | func handleRequest(request: Request) -> Response {
110 | print(String(bytes: request.body, encoding: .utf8)!)
111 | return try! Response(json: ["message": "Message received!"])
112 | }
113 |
114 | let server = HTTP.Server()
115 | server.listen(host: "0.0.0.0", port: 3000).startWithNext { client in
116 |
117 | let requestStream = client.read()
118 | requestStream.map(handleRequest).onNext{ response in
119 | client.write(response).start()
120 | }
121 |
122 | requestStream.onFailed { clientError in
123 | print("Oh no, there was an error! \(clientError)")
124 | }
125 |
126 | requestStream.onCompleted {
127 | print("Goodbye \(client)!")
128 | }
129 |
130 | requestStream.start()
131 | }
132 |
133 | RunLoop.runAll()
134 | ```
135 |
136 | ### TCP
137 | ```Swift
138 |
139 | import Lightning
140 | import Foundation
141 |
142 | let server = try! TCP.Server()
143 | try! server.bind(host: "0.0.0.0", port: 50000)
144 |
145 | server.listen().startWithNext { connection in
146 | let byteStream = connection.read()
147 | let strings = byteStream.map { String(bytes: $0, encoding: .utf8)! }
148 |
149 | strings.onNext { message in
150 | print("Client \(connection) says \"\(message)\"!")
151 | }
152 |
153 | strings.onFailed { error in
154 | print("Oh no, there was an error! \(error)")
155 | }
156 |
157 | strings.onCompleted {
158 | print("Goodbye \(connection)!")
159 | }
160 |
161 | strings.start()
162 | }
163 |
164 | RunLoop.runAll()
165 | ```
166 |
167 |
168 | ### Lightning is not Node.js
169 |
170 | Lightning is not meant to fulfill all of the roles of Node.js. Node.js is a JavaScript runtime, while Lightning is a TCP/Web server framework. The Swift compiler and package manager, combined with third-party Swift packages, make it unnecessary to build that functionality into Lightning.
171 |
--------------------------------------------------------------------------------
/Scripts/TravisCI/code-coverage.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euf -o pipefail
4 |
5 | UNAME=`uname`;
6 |
7 | if [[ $UNAME != "Darwin" ]];
8 | then
9 | echo "Code coverage for $UNAME is not yet supported. Skip coverage."
10 | exit 0;
11 | fi
12 |
13 | GENERATOR_OUTPUT=`swift package generate-xcodeproj`;
14 | PROJECT_NAME="${GENERATOR_OUTPUT/generated: .\//}";
15 | SCHEME_NAME="${PROJECT_NAME/.xcodeproj/}";
16 |
17 | rvm install 2.2.3
18 | gem install xcpretty
19 | WORKING_DIR=$(PWD) xcodebuild \
20 | -project $PROJECT_NAME \
21 | -scheme $SCHEME_NAME \
22 | -sdk macosx10.12 \
23 | -destination arch=x86_64 \
24 | -configuration Debug \
25 | -enableCodeCoverage YES \
26 | test | xcpretty
27 |
28 | bash <(curl -s https://codecov.io/bash)
29 |
--------------------------------------------------------------------------------
/Scripts/TravisCI/install-swift.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euf -o pipefail
4 |
5 | UNAME=`uname`;
6 | echo "Unix Platform: $UNAME";
7 |
8 | if [[ $UNAME != "Linux" ]] && [[ $UNAME != "Darwin" ]];
9 | then
10 | echo "$UNAME is not yet supported.";
11 | exit 1;
12 | fi
13 |
14 | if [[ $UNAME == "Linux" ]];
15 | then
16 | UBUNTU_VERSION=`lsb_release -r -s`;
17 |
18 | echo "Installing Swift on Ubuntu $UBUNTU_VERSION."
19 |
20 | if [[ $UBUNTU_VERSION != "16.04" ]] && [[ $UBUNTU_VERSION != "14.04" ]];
21 | then
22 | echo "Ubuntu version $UBUNTU_VERSION is not yet supported.";
23 | exit 1;
24 | fi
25 |
26 | OS="ubuntu$UBUNTU_VERSION";
27 | OS_STRIPPED=`echo $OS | tr -d .`;
28 | RELEASE_NAME_UPPER="swift-$SWIFT_VERSION-RELEASE";
29 | RELEASE_NAME_LOWER="swift-$SWIFT_VERSION-release";
30 | SWIFT_FILENAME="$RELEASE_NAME_UPPER-$OS";
31 |
32 | # Geez, Chris, what're you guys doin' here?
33 | URL="https://swift.org/builds/$RELEASE_NAME_LOWER/$OS_STRIPPED/$RELEASE_NAME_UPPER/$SWIFT_FILENAME.tar.gz";
34 | wget $URL
35 | tar -zxf "$SWIFT_FILENAME.tar.gz";
36 | sudo ln -s $PWD/$SWIFT_FILENAME/usr/bin/swift /usr/local/bin/swift
37 | sudo ln -s $PWD/$SWIFT_FILENAME/usr/bin/swift-build /usr/local/bin/swift-build
38 | sudo ln -s $PWD/$SWIFT_FILENAME/usr/bin/swift-build-tool /usr/local/bin/swift-build-tool
39 | sudo ln -s $PWD/$SWIFT_FILENAME/usr/bin/swift-package /usr/local/bin/swift-package
40 | sudo ln -s $PWD/$SWIFT_FILENAME/usr/bin/swift-run /usr/local/bin/swift-run
41 | sudo ln -s $PWD/$SWIFT_FILENAME/usr/bin/swift-test /usr/local/bin/swift-test
42 | fi
43 |
44 | echo `swift --version`;
45 |
--------------------------------------------------------------------------------
/Scripts/lipo/lipo.sh:
--------------------------------------------------------------------------------
1 | # Merge Script
2 |
3 | # 1
4 | # Set bash script to exit immediately if any commands fail.
5 | set -e
6 |
7 | for i in "Edge" "Drift" "TCP" "HTTP" "CHTTPParser" "CPOSIX" "POSIX" "POSIXExtensions" "Reflex" "RunLoop" "IOStream"; do
8 |
9 | # 2
10 | # Setup some constants for use later on.
11 | FRAMEWORK_NAME=$i
12 | SRCROOT="."
13 |
14 | # 3
15 | # If remnants from a previous build exist, delete them.
16 | if [ -d "${SRCROOT}/build" ]; then
17 | rm -rf "${SRCROOT}/build"
18 | fi
19 |
20 | # 4
21 | # Build the framework for device and for simulator (using
22 | # all needed architectures).
23 | xcodebuild -target "${FRAMEWORK_NAME}" -configuration Release -arch arm64 -arch armv7 -arch armv7s only_active_arch=no defines_module=yes -sdk "iphoneos"
24 | xcodebuild -target "${FRAMEWORK_NAME}" -configuration Release -arch x86_64 -arch i386 only_active_arch=no defines_module=yes -sdk "iphonesimulator"
25 |
26 | # 5
27 | # Remove .framework file if exists on Desktop from previous run.
28 | if [ -d "${HOME}/Desktop/${FRAMEWORK_NAME}.framework" ]; then
29 | rm -rf "${HOME}/Desktop/${FRAMEWORK_NAME}.framework"
30 | fi
31 |
32 | # 6
33 | # Copy the device version of framework to Desktop.
34 | cp -r "${SRCROOT}/build/Release-iphoneos/${FRAMEWORK_NAME}.framework" "${HOME}/Desktop/${FRAMEWORK_NAME}.framework"
35 |
36 | # 7
37 | # Replace the framework executable within the framework with
38 | # a new version created by merging the device and simulator
39 | # frameworks' executables with lipo.
40 | lipo -create -output "${HOME}/Desktop/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${SRCROOT}/build/Release-iphoneos/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${SRCROOT}/build/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}"
41 |
42 | # 8
43 | # Copy the Swift module mappings for the simulator into the
44 | # framework. The device mappings already exist from step 6.
45 | if ! [[ $FRAMEWORK_NAME == C* ]]; then
46 | cp -r "${SRCROOT}/build/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule/" "${HOME}/Desktop/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule"
47 | fi
48 |
49 | # 9
50 | # Delete the most recent build.
51 | if [ -d "${SRCROOT}/build" ]; then
52 | rm -rf "${SRCROOT}/build"
53 | fi
54 |
55 | done
56 |
--------------------------------------------------------------------------------
/Sources/CHTTPParser/include/http_parser.h:
--------------------------------------------------------------------------------
1 | /* Copyright Joyent, Inc. and other Node contributors. All rights reserved.
2 | *
3 | * Permission is hereby granted, free of charge, to any person obtaining a copy
4 | * of this software and associated documentation files (the "Software"), to
5 | * deal in the Software without restriction, including without limitation the
6 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | * sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in
11 | * all copies or substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19 | * IN THE SOFTWARE.
20 | */
21 | #ifndef http_parser_h
22 | #define http_parser_h
23 | #ifdef __cplusplus
24 | extern "C" {
25 | #endif
26 |
27 | /* Also update SONAME in the Makefile whenever you change these. */
28 | #define HTTP_PARSER_VERSION_MAJOR 2
29 | #define HTTP_PARSER_VERSION_MINOR 7
30 | #define HTTP_PARSER_VERSION_PATCH 1
31 |
32 | #include
33 | #if defined(_WIN32) && !defined(__MINGW32__) && \
34 | (!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__)
35 | #include
36 | #include
37 | typedef __int8 int8_t;
38 | typedef unsigned __int8 uint8_t;
39 | typedef __int16 int16_t;
40 | typedef unsigned __int16 uint16_t;
41 | typedef __int32 int32_t;
42 | typedef unsigned __int32 uint32_t;
43 | typedef __int64 int64_t;
44 | typedef unsigned __int64 uint64_t;
45 | #else
46 | #include
47 | #endif
48 |
49 | /* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run
50 | * faster
51 | */
52 | #ifndef HTTP_PARSER_STRICT
53 | # define HTTP_PARSER_STRICT 1
54 | #endif
55 |
56 | /* Maximium header size allowed. If the macro is not defined
57 | * before including this header then the default is used. To
58 | * change the maximum header size, define the macro in the build
59 | * environment (e.g. -DHTTP_MAX_HEADER_SIZE=). To remove
60 | * the effective limit on the size of the header, define the macro
61 | * to a very large number (e.g. -DHTTP_MAX_HEADER_SIZE=0x7fffffff)
62 | */
63 | #ifndef HTTP_MAX_HEADER_SIZE
64 | # define HTTP_MAX_HEADER_SIZE (80*1024)
65 | #endif
66 |
67 | typedef struct http_parser http_parser;
68 | typedef struct http_parser_settings http_parser_settings;
69 |
70 |
71 | /* Callbacks should return non-zero to indicate an error. The parser will
72 | * then halt execution.
73 | *
74 | * The one exception is on_headers_complete. In a HTTP_RESPONSE parser
75 | * returning '1' from on_headers_complete will tell the parser that it
76 | * should not expect a body. This is used when receiving a response to a
77 | * HEAD request which may contain 'Content-Length' or 'Transfer-Encoding:
78 | * chunked' headers that indicate the presence of a body.
79 | *
80 | * Returning `2` from on_headers_complete will tell parser that it should not
81 | * expect neither a body nor any futher responses on this connection. This is
82 | * useful for handling responses to a CONNECT request which may not contain
83 | * `Upgrade` or `Connection: upgrade` headers.
84 | *
85 | * http_data_cb does not return data chunks. It will be called arbitrarily
86 | * many times for each string. E.G. you might get 10 callbacks for "on_url"
87 | * each providing just a few characters more data.
88 | */
89 | typedef int (*http_data_cb) (http_parser*, const char *at, size_t length);
90 | typedef int (*http_cb) (http_parser*);
91 |
92 |
93 | /* Status Codes */
94 | #define HTTP_STATUS_MAP(XX) \
95 | XX(100, CONTINUE, Continue) \
96 | XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \
97 | XX(102, PROCESSING, Processing) \
98 | XX(200, OK, OK) \
99 | XX(201, CREATED, Created) \
100 | XX(202, ACCEPTED, Accepted) \
101 | XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \
102 | XX(204, NO_CONTENT, No Content) \
103 | XX(205, RESET_CONTENT, Reset Content) \
104 | XX(206, PARTIAL_CONTENT, Partial Content) \
105 | XX(207, MULTI_STATUS, Multi-Status) \
106 | XX(208, ALREADY_REPORTED, Already Reported) \
107 | XX(226, IM_USED, IM Used) \
108 | XX(300, MULTIPLE_CHOICES, Multiple Choices) \
109 | XX(301, MOVED_PERMANENTLY, Moved Permanently) \
110 | XX(302, FOUND, Found) \
111 | XX(303, SEE_OTHER, See Other) \
112 | XX(304, NOT_MODIFIED, Not Modified) \
113 | XX(305, USE_PROXY, Use Proxy) \
114 | XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \
115 | XX(308, PERMANENT_REDIRECT, Permanent Redirect) \
116 | XX(400, BAD_REQUEST, Bad Request) \
117 | XX(401, UNAUTHORIZED, Unauthorized) \
118 | XX(402, PAYMENT_REQUIRED, Payment Required) \
119 | XX(403, FORBIDDEN, Forbidden) \
120 | XX(404, NOT_FOUND, Not Found) \
121 | XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \
122 | XX(406, NOT_ACCEPTABLE, Not Acceptable) \
123 | XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \
124 | XX(408, REQUEST_TIMEOUT, Request Timeout) \
125 | XX(409, CONFLICT, Conflict) \
126 | XX(410, GONE, Gone) \
127 | XX(411, LENGTH_REQUIRED, Length Required) \
128 | XX(412, PRECONDITION_FAILED, Precondition Failed) \
129 | XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \
130 | XX(414, URI_TOO_LONG, URI Too Long) \
131 | XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \
132 | XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \
133 | XX(417, EXPECTATION_FAILED, Expectation Failed) \
134 | XX(421, MISDIRECTED_REQUEST, Misdirected Request) \
135 | XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \
136 | XX(423, LOCKED, Locked) \
137 | XX(424, FAILED_DEPENDENCY, Failed Dependency) \
138 | XX(426, UPGRADE_REQUIRED, Upgrade Required) \
139 | XX(428, PRECONDITION_REQUIRED, Precondition Required) \
140 | XX(429, TOO_MANY_REQUESTS, Too Many Requests) \
141 | XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \
142 | XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \
143 | XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \
144 | XX(501, NOT_IMPLEMENTED, Not Implemented) \
145 | XX(502, BAD_GATEWAY, Bad Gateway) \
146 | XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \
147 | XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \
148 | XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \
149 | XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \
150 | XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \
151 | XX(508, LOOP_DETECTED, Loop Detected) \
152 | XX(510, NOT_EXTENDED, Not Extended) \
153 | XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \
154 |
155 | enum http_status
156 | {
157 | #define XX(num, name, string) HTTP_STATUS_##name = num,
158 | HTTP_STATUS_MAP(XX)
159 | #undef XX
160 | };
161 |
162 |
163 | /* Request Methods */
164 | #define HTTP_METHOD_MAP(XX) \
165 | XX(0, DELETE, DELETE) \
166 | XX(1, GET, GET) \
167 | XX(2, HEAD, HEAD) \
168 | XX(3, POST, POST) \
169 | XX(4, PUT, PUT) \
170 | /* pathological */ \
171 | XX(5, CONNECT, CONNECT) \
172 | XX(6, OPTIONS, OPTIONS) \
173 | XX(7, TRACE, TRACE) \
174 | /* WebDAV */ \
175 | XX(8, COPY, COPY) \
176 | XX(9, LOCK, LOCK) \
177 | XX(10, MKCOL, MKCOL) \
178 | XX(11, MOVE, MOVE) \
179 | XX(12, PROPFIND, PROPFIND) \
180 | XX(13, PROPPATCH, PROPPATCH) \
181 | XX(14, SEARCH, SEARCH) \
182 | XX(15, UNLOCK, UNLOCK) \
183 | XX(16, BIND, BIND) \
184 | XX(17, REBIND, REBIND) \
185 | XX(18, UNBIND, UNBIND) \
186 | XX(19, ACL, ACL) \
187 | /* subversion */ \
188 | XX(20, REPORT, REPORT) \
189 | XX(21, MKACTIVITY, MKACTIVITY) \
190 | XX(22, CHECKOUT, CHECKOUT) \
191 | XX(23, MERGE, MERGE) \
192 | /* upnp */ \
193 | XX(24, MSEARCH, M-SEARCH) \
194 | XX(25, NOTIFY, NOTIFY) \
195 | XX(26, SUBSCRIBE, SUBSCRIBE) \
196 | XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \
197 | /* RFC-5789 */ \
198 | XX(28, PATCH, PATCH) \
199 | XX(29, PURGE, PURGE) \
200 | /* CalDAV */ \
201 | XX(30, MKCALENDAR, MKCALENDAR) \
202 | /* RFC-2068, section 19.6.1.2 */ \
203 | XX(31, LINK, LINK) \
204 | XX(32, UNLINK, UNLINK) \
205 |
206 | enum http_method
207 | {
208 | #define XX(num, name, string) HTTP_##name = num,
209 | HTTP_METHOD_MAP(XX)
210 | #undef XX
211 | };
212 |
213 |
214 | enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH };
215 |
216 |
217 | /* Flag values for http_parser.flags field */
218 | enum flags
219 | { F_CHUNKED = 1 << 0
220 | , F_CONNECTION_KEEP_ALIVE = 1 << 1
221 | , F_CONNECTION_CLOSE = 1 << 2
222 | , F_CONNECTION_UPGRADE = 1 << 3
223 | , F_TRAILING = 1 << 4
224 | , F_UPGRADE = 1 << 5
225 | , F_SKIPBODY = 1 << 6
226 | , F_CONTENTLENGTH = 1 << 7
227 | };
228 |
229 |
230 | /* Map for errno-related constants
231 | *
232 | * The provided argument should be a macro that takes 2 arguments.
233 | */
234 | #define HTTP_ERRNO_MAP(XX) \
235 | /* No error */ \
236 | XX(OK, "success") \
237 | \
238 | /* Callback-related errors */ \
239 | XX(CB_message_begin, "the on_message_begin callback failed") \
240 | XX(CB_url, "the on_url callback failed") \
241 | XX(CB_header_field, "the on_header_field callback failed") \
242 | XX(CB_header_value, "the on_header_value callback failed") \
243 | XX(CB_headers_complete, "the on_headers_complete callback failed") \
244 | XX(CB_body, "the on_body callback failed") \
245 | XX(CB_message_complete, "the on_message_complete callback failed") \
246 | XX(CB_status, "the on_status callback failed") \
247 | XX(CB_chunk_header, "the on_chunk_header callback failed") \
248 | XX(CB_chunk_complete, "the on_chunk_complete callback failed") \
249 | \
250 | /* Parsing-related errors */ \
251 | XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \
252 | XX(HEADER_OVERFLOW, \
253 | "too many header bytes seen; overflow detected") \
254 | XX(CLOSED_CONNECTION, \
255 | "data received after completed connection: close message") \
256 | XX(INVALID_VERSION, "invalid HTTP version") \
257 | XX(INVALID_STATUS, "invalid HTTP status code") \
258 | XX(INVALID_METHOD, "invalid HTTP method") \
259 | XX(INVALID_URL, "invalid URL") \
260 | XX(INVALID_HOST, "invalid host") \
261 | XX(INVALID_PORT, "invalid port") \
262 | XX(INVALID_PATH, "invalid path") \
263 | XX(INVALID_QUERY_STRING, "invalid query string") \
264 | XX(INVALID_FRAGMENT, "invalid fragment") \
265 | XX(LF_EXPECTED, "LF character expected") \
266 | XX(INVALID_HEADER_TOKEN, "invalid character in header") \
267 | XX(INVALID_CONTENT_LENGTH, \
268 | "invalid character in content-length header") \
269 | XX(UNEXPECTED_CONTENT_LENGTH, \
270 | "unexpected content-length header") \
271 | XX(INVALID_CHUNK_SIZE, \
272 | "invalid character in chunk size header") \
273 | XX(INVALID_CONSTANT, "invalid constant string") \
274 | XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\
275 | XX(STRICT, "strict mode assertion failed") \
276 | XX(PAUSED, "parser is paused") \
277 | XX(UNKNOWN, "an unknown error occurred")
278 |
279 |
280 | /* Define HPE_* values for each errno value above */
281 | #define HTTP_ERRNO_GEN(n, s) HPE_##n,
282 | enum http_errno {
283 | HTTP_ERRNO_MAP(HTTP_ERRNO_GEN)
284 | };
285 | #undef HTTP_ERRNO_GEN
286 |
287 |
288 | /* Get an http_errno value from an http_parser */
289 | #define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno)
290 |
291 |
292 | struct http_parser {
293 | /** PRIVATE **/
294 | unsigned int type : 2; /* enum http_parser_type */
295 | unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */
296 | unsigned int state : 7; /* enum state from http_parser.c */
297 | unsigned int header_state : 7; /* enum header_state from http_parser.c */
298 | unsigned int index : 7; /* index into current matcher */
299 | unsigned int lenient_http_headers : 1;
300 |
301 | uint32_t nread; /* # bytes read in various scenarios */
302 | uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */
303 |
304 | /** READ-ONLY **/
305 | unsigned short http_major;
306 | unsigned short http_minor;
307 | unsigned int status_code : 16; /* responses only */
308 | unsigned int method : 8; /* requests only */
309 | unsigned int http_errno : 7;
310 |
311 | /* 1 = Upgrade header was present and the parser has exited because of that.
312 | * 0 = No upgrade header present.
313 | * Should be checked when http_parser_execute() returns in addition to
314 | * error checking.
315 | */
316 | unsigned int upgrade : 1;
317 |
318 | /** PUBLIC **/
319 | void *data; /* A pointer to get hook to the "connection" or "socket" object */
320 | };
321 |
322 |
323 | struct http_parser_settings {
324 | http_cb on_message_begin;
325 | http_data_cb on_url;
326 | http_data_cb on_status;
327 | http_data_cb on_header_field;
328 | http_data_cb on_header_value;
329 | http_cb on_headers_complete;
330 | http_data_cb on_body;
331 | http_cb on_message_complete;
332 | /* When on_chunk_header is called, the current chunk length is stored
333 | * in parser->content_length.
334 | */
335 | http_cb on_chunk_header;
336 | http_cb on_chunk_complete;
337 | };
338 |
339 |
340 | enum http_parser_url_fields
341 | { UF_SCHEMA = 0
342 | , UF_HOST = 1
343 | , UF_PORT = 2
344 | , UF_PATH = 3
345 | , UF_QUERY = 4
346 | , UF_FRAGMENT = 5
347 | , UF_USERINFO = 6
348 | , UF_MAX = 7
349 | };
350 |
351 | struct http_parser_url_field_data {
352 | uint16_t off; /* Offset into buffer in which field starts */
353 | uint16_t len; /* Length of run in buffer */
354 | };
355 |
356 | /* Result structure for http_parser_parse_url().
357 | *
358 | * Callers should index into field_data[] with UF_* values iff field_set
359 | * has the relevant (1 << UF_*) bit set. As a courtesy to clients (and
360 | * because we probably have padding left over), we convert any port to
361 | * a uint16_t.
362 | */
363 | struct http_parser_url {
364 | uint16_t field_set; /* Bitmask of (1 << UF_*) values */
365 | uint16_t port; /* Converted UF_PORT string */
366 |
367 | struct http_parser_url_field_data field_data[UF_MAX];
368 | };
369 |
370 |
371 | /* Returns the library version. Bits 16-23 contain the major version number,
372 | * bits 8-15 the minor version number and bits 0-7 the patch level.
373 | * Usage example:
374 | *
375 | * unsigned long version = http_parser_version();
376 | * unsigned major = (version >> 16) & 255;
377 | * unsigned minor = (version >> 8) & 255;
378 | * unsigned patch = version & 255;
379 | * printf("http_parser v%u.%u.%u\n", major, minor, patch);
380 | */
381 | unsigned long http_parser_version(void);
382 |
383 | void http_parser_init(http_parser *parser, enum http_parser_type type);
384 |
385 |
386 | /* Initialize http_parser_settings members to 0
387 | */
388 | void http_parser_settings_init(http_parser_settings *settings);
389 |
390 |
391 | /* Executes the parser. Returns number of parsed bytes. Sets
392 | * `parser->http_errno` on error. */
393 | size_t http_parser_execute(http_parser *parser,
394 | const http_parser_settings *settings,
395 | const char *data,
396 | size_t len);
397 |
398 |
399 | /* If http_should_keep_alive() in the on_headers_complete or
400 | * on_message_complete callback returns 0, then this should be
401 | * the last message on the connection.
402 | * If you are the server, respond with the "Connection: close" header.
403 | * If you are the client, close the connection.
404 | */
405 | int http_should_keep_alive(const http_parser *parser);
406 |
407 | /* Returns a string version of the HTTP method. */
408 | const char *http_method_str(enum http_method m);
409 |
410 | /* Return a string name of the given error */
411 | const char *http_errno_name(enum http_errno err);
412 |
413 | /* Return a string description of the given error */
414 | const char *http_errno_description(enum http_errno err);
415 |
416 | /* Initialize all http_parser_url members to 0 */
417 | void http_parser_url_init(struct http_parser_url *u);
418 |
419 | /* Parse a URL; return nonzero on failure */
420 | int http_parser_parse_url(const char *buf, size_t buflen,
421 | int is_connect,
422 | struct http_parser_url *u);
423 |
424 | /* Pause or un-pause the parser; a nonzero value pauses */
425 | void http_parser_pause(http_parser *parser, int paused);
426 |
427 | /* Checks if this is the final chunk of the body. */
428 | int http_body_is_final(const http_parser *parser);
429 |
430 | #ifdef __cplusplus
431 | }
432 | #endif
433 | #endif
434 |
--------------------------------------------------------------------------------
/Sources/HTTP/Error.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Error.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 10/16/16.
6 | //
7 | //
8 |
9 | public protocol HTTPError: Error {
10 | var status: Status { get }
11 | }
12 |
13 | public enum ClientError: HTTPError {
14 | case badRequest
15 | case unauthorized
16 | case paymentRequired
17 | case forbidden
18 | case notFound
19 | case methodNotAllowed
20 | case notAcceptable
21 | case proxyAuthenticationRequired
22 | case requestTimeout
23 | case conflict
24 | case gone
25 | case lengthRequired
26 | case preconditionFailed
27 | case requestEntityTooLarge
28 | case requestURITooLong
29 | case unsupportedMediaType
30 | case requestedRangeNotSatisfiable
31 | case expectationFailed
32 | case imATeapot
33 | case authenticationTimeout
34 | case enhanceYourCalm
35 | case unprocessableEntity
36 | case locked
37 | case failedDependency
38 | case preconditionRequired
39 | case tooManyRequests
40 | case requestHeaderFieldsTooLarge
41 | }
42 |
43 | extension ClientError {
44 | public var status: Status {
45 | switch self {
46 | case .badRequest: return Status.badRequest
47 | case .unauthorized: return Status.unauthorized
48 | case .paymentRequired: return Status.paymentRequired
49 | case .forbidden: return Status.forbidden
50 | case .notFound: return Status.notFound
51 | case .methodNotAllowed: return Status.methodNotAllowed
52 | case .notAcceptable: return Status.notAcceptable
53 | case .proxyAuthenticationRequired: return Status.proxyAuthenticationRequired
54 | case .requestTimeout: return Status.requestTimeout
55 | case .conflict: return Status.conflict
56 | case .gone: return Status.gone
57 | case .lengthRequired: return Status.lengthRequired
58 | case .preconditionFailed: return Status.preconditionFailed
59 | case .requestEntityTooLarge: return Status.requestEntityTooLarge
60 | case .requestURITooLong: return Status.requestURITooLong
61 | case .unsupportedMediaType: return Status.unsupportedMediaType
62 | case .requestedRangeNotSatisfiable: return Status.requestedRangeNotSatisfiable
63 | case .expectationFailed: return Status.expectationFailed
64 | case .imATeapot: return Status.imATeapot
65 | case .authenticationTimeout: return Status.authenticationTimeout
66 | case .enhanceYourCalm: return Status.enhanceYourCalm
67 | case .unprocessableEntity: return Status.unprocessableEntity
68 | case .locked: return Status.locked
69 | case .failedDependency: return Status.failedDependency
70 | case .preconditionRequired: return Status.preconditionRequired
71 | case .tooManyRequests: return Status.tooManyRequests
72 | case .requestHeaderFieldsTooLarge: return Status.requestHeaderFieldsTooLarge
73 | }
74 | }
75 | }
76 |
77 | public enum ServerError: HTTPError {
78 | case internalServerError
79 | case notImplemented
80 | case badGateway
81 | case serviceUnavailable
82 | case gatewayTimeout
83 | case httpVersionNotSupported
84 | case variantAlsoNegotiates
85 | case insufficientStorage
86 | case loopDetected
87 | case notExtended
88 | case networkAuthenticationRequired
89 | }
90 |
91 | extension ServerError {
92 | public var status: Status {
93 | switch self {
94 | case .internalServerError: return Status.internalServerError
95 | case .notImplemented: return Status.notImplemented
96 | case .badGateway: return Status.badGateway
97 | case .serviceUnavailable: return Status.serviceUnavailable
98 | case .gatewayTimeout: return Status.gatewayTimeout
99 | case .httpVersionNotSupported: return Status.httpVersionNotSupported
100 | case .variantAlsoNegotiates: return Status.variantAlsoNegotiates
101 | case .insufficientStorage: return Status.insufficientStorage
102 | case .loopDetected: return Status.loopDetected
103 | case .notExtended: return Status.notExtended
104 | case .networkAuthenticationRequired: return Status.networkAuthenticationRequired
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/HTTP/Message.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Message.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 10/30/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol HTTPMessage {
12 | var version: Version { get set }
13 | var rawHeaders: [String] { get set }
14 | var headers: [String:String] { get }
15 | var cookies: [String] { get }
16 | var body: Data { get set }
17 | }
18 |
19 | public extension HTTPMessage {
20 |
21 | /// Groups the `rawHeaders` into key-value pairs. If there is an odd number
22 | /// of `rawHeaders`, the last value will be discarded.
23 | var rawHeaderPairs: [(String, String)] {
24 | return stride(from: 0, to: self.rawHeaders.count, by: 2).flatMap {
25 | let chunk = rawHeaders[$0.. Bool {
128 | return lhs.description == rhs.description
129 | }
130 |
--------------------------------------------------------------------------------
/Sources/HTTP/Parser.swift:
--------------------------------------------------------------------------------
1 | // Parser.swift
2 | //
3 | // The MIT License (MIT)
4 | //
5 | // Copyright (c) 2016 Edge
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in all
15 | // copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | // SOFTWARE.
24 |
25 | import CHTTPParser
26 | import Foundation
27 |
28 | typealias CParserPointer = UnsafeMutablePointer
29 | typealias RawParserPointer = UnsafeMutablePointer
30 |
31 | private func bridge(_ obj: T) -> UnsafeMutableRawPointer {
32 | return Unmanaged.passUnretained(obj).toOpaque()
33 | }
34 |
35 | private func bridge(_ ptr: UnsafeMutableRawPointer) -> T {
36 | return Unmanaged.fromOpaque(ptr).takeUnretainedValue()
37 | }
38 |
39 | struct ParseError: Error {
40 | let description: String
41 | }
42 |
43 | public class RequestParser {
44 | private let parser: FullMessageParser!
45 | var onRequest: ((Request) -> Void)?
46 |
47 | init(onRequest: ((Request) -> Void)? = nil) {
48 | self.onRequest = onRequest
49 | self.parser = FullMessageParser(type: HTTP_REQUEST)
50 | self.parser.onComplete = { [weak self] parseState in
51 | guard let uri = URL(string: parseState.uri) else {
52 | throw ParseError(description: "URL not valid.")
53 | }
54 |
55 | let request = Request(
56 | method: Method(code: parseState.method),
57 | uri: uri,
58 | version: Version(major: parseState.version.major, minor: parseState.version.minor),
59 | rawHeaders: parseState.rawHeaders,
60 | body: parseState.body
61 | )
62 | self?.onRequest?(request)
63 | }
64 | }
65 |
66 | public func parse(_ data: Data) throws {
67 | try parser.parse(data)
68 | }
69 |
70 | }
71 |
72 | public class ResponseParser {
73 | private let parser: FullMessageParser
74 | var onResponse: ((Response) -> Void)?
75 |
76 | init(onResponse: ((Response) -> Void)? = nil) {
77 | self.onResponse = onResponse
78 | self.parser = FullMessageParser(type: HTTP_RESPONSE)
79 | self.parser.onComplete = { [weak self] parseState in
80 | let response = Response(
81 | version: Version(major: parseState.version.major, minor: parseState.version.minor),
82 | status: Status(code: parseState.statusCode),
83 | rawHeaders: parseState.rawHeaders,
84 | body: parseState.body
85 | )
86 | self?.onResponse?(response)
87 | }
88 | }
89 |
90 | public func parse(_ data: Data) throws {
91 | try parser.parse(data)
92 | }
93 |
94 | }
95 |
96 | private final class FullMessageParser {
97 |
98 | struct ParseState {
99 |
100 | var version: (major: Int, minor: Int) = (0, 0)
101 | var rawHeaders: [String] = []
102 | var body: Data = Data()
103 |
104 | // Response
105 | var statusCode: Int! = nil
106 | var statusPhrase = ""
107 |
108 | // Request
109 | var method: Int! = nil
110 | var uri = ""
111 |
112 | }
113 |
114 | var state = ParseState()
115 | var onComplete: ((ParseState) throws -> ())?
116 | let rawParser: RawParser
117 |
118 | init(type: http_parser_type, onComplete: ((ParseState) throws -> ())? = nil) {
119 | self.onComplete = onComplete
120 | self.rawParser = RawParser(type: type)
121 | self.rawParser.delegate = self
122 | }
123 |
124 | func parse(_ data: Data) throws {
125 | try rawParser.parse(data)
126 | }
127 |
128 | }
129 |
130 | extension FullMessageParser: RawParserDelegate {
131 |
132 | func onMessageBegin() throws {
133 | // No state change
134 | }
135 |
136 | func onURL(data: UnsafeBufferPointer) throws {
137 | guard let uri = String(bytes: data, encoding: .utf8) else {
138 | throw ParseError(description: "URI could not be encoded as UTF8.")
139 | }
140 | state.uri += uri
141 | }
142 |
143 | func onStatus(data: UnsafeBufferPointer) throws {
144 | guard let partialStatusPhrase = String(bytes: data, encoding: .utf8) else {
145 | throw ParseError(description: "Status phrase could not be encoded as UTF8.")
146 | }
147 | state.statusPhrase += partialStatusPhrase
148 | }
149 |
150 | func onHeaderField(data: UnsafeBufferPointer) throws {
151 | guard let partialHeaderField = String(bytes: data, encoding: .utf8) else {
152 | throw ParseError(description: "Header field could not be encoded as UTF8.")
153 | }
154 | if state.rawHeaders.count % 2 == 0 {
155 | state.rawHeaders.append("")
156 | }
157 | state.rawHeaders[state.rawHeaders.count - 1] += partialHeaderField
158 | }
159 |
160 | func onHeaderValue(data: UnsafeBufferPointer) throws {
161 | guard let partialHeaderValue = String(bytes: data, encoding: .utf8) else {
162 | throw ParseError(description: "Header value could not be encoded as UTF8.")
163 | }
164 | if state.rawHeaders.count % 2 == 1 {
165 | state.rawHeaders.append("")
166 | }
167 | state.rawHeaders[state.rawHeaders.count - 1] += partialHeaderValue
168 | }
169 |
170 | func onHeadersComplete(
171 | method: Int,
172 | statusCode: Int,
173 | majorVersion: Int,
174 | minorVersion: Int
175 | ) throws -> HeadersCompleteDirective {
176 | state.statusCode = statusCode
177 | state.version = (major: majorVersion, minor: minorVersion)
178 | state.method = method
179 | return .none
180 | }
181 |
182 | func onBody(data: UnsafeBufferPointer) throws {
183 | state.body += Array(data)
184 | }
185 |
186 | func onMessageComplete() throws {
187 | try onComplete?(state)
188 | // Reset state
189 | state = ParseState()
190 | }
191 |
192 | func onChunkHeader() throws {
193 | // No state change
194 | }
195 |
196 | func onChunkComplete() throws {
197 | // No state change
198 | }
199 |
200 | }
201 |
202 | public enum HeadersCompleteDirective {
203 | case none
204 | case noBody
205 | case noBodyNoFurtherResponses
206 | }
207 |
208 | public protocol RawParserDelegate: class {
209 |
210 | func onMessageBegin() throws
211 | func onMessageComplete() throws
212 |
213 | func onURL(data: UnsafeBufferPointer) throws
214 | func onStatus(data: UnsafeBufferPointer) throws
215 | func onHeaderField(data: UnsafeBufferPointer) throws
216 | func onHeaderValue(data: UnsafeBufferPointer) throws
217 |
218 | func onHeadersComplete(
219 | method: Int,
220 | statusCode: Int,
221 | majorVersion: Int,
222 | minorVersion: Int
223 | ) throws -> HeadersCompleteDirective
224 |
225 | func onBody(data: UnsafeBufferPointer) throws
226 |
227 | func onChunkHeader() throws
228 | func onChunkComplete() throws
229 |
230 | }
231 |
232 | public final class RawParser {
233 |
234 | // Sneaky sneaky, I'm a meanie.
235 | // Here I create a constant private pointer to our "constant" http_parser
236 | // The can be therefore mutated through the pointer, but only within the RawParser.
237 | let parser = http_parser()
238 | private let parserPointer: UnsafeMutablePointer
239 |
240 | var type: http_parser_type
241 | fileprivate weak var delegate: RawParserDelegate?
242 |
243 | public init(type: http_parser_type, delegate: RawParserDelegate? = nil) {
244 | self.type = type
245 | self.delegate = delegate
246 | parserPointer = withUnsafeMutablePointer(to: &parser) { $0 }
247 | reset()
248 | }
249 |
250 | func reset() {
251 | http_parser_init(parserPointer, self.type)
252 |
253 | // Set self as the context. self must be a reference type.
254 | parserPointer.pointee.data = bridge(self)
255 | }
256 |
257 | public func parse(_ data: Data) throws {
258 | try data.withUnsafeBytes { (convertedPointer: UnsafePointer) in
259 | let bytesParsed = http_parser_execute(
260 | parserPointer,
261 | &requestSettings,
262 | convertedPointer,
263 | data.count
264 | )
265 | guard bytesParsed == data.count else {
266 | reset()
267 | let errorName = http_errno_name(http_errno(parser.http_errno))!
268 | let errorDescription = http_errno_description(http_errno(parser.http_errno))!
269 | let error = ParseError(
270 | description: "\(String(validatingUTF8: errorName)!):" +
271 | " \(String(validatingUTF8: errorDescription)!)"
272 | )
273 | throw error
274 | }
275 | }
276 | }
277 |
278 | }
279 |
280 | var requestSettings: http_parser_settings = {
281 | var settings = http_parser_settings()
282 | http_parser_settings_init(&settings)
283 |
284 | settings.on_message_begin = onMessageBegin
285 | settings.on_url = onURL
286 | settings.on_status = onStatus
287 | settings.on_header_field = onHeaderField
288 | settings.on_header_value = onHeaderValue
289 | settings.on_headers_complete = onHeadersComplete
290 | settings.on_body = onBody
291 | settings.on_message_complete = onMessageComplete
292 | settings.on_chunk_header = onChunkHeader
293 | settings.on_chunk_complete = onChunkComplete
294 |
295 | return settings
296 | }()
297 |
298 | // MARK: C function pointer spring boards
299 | private func onMessageBegin(_ parser: CParserPointer?) -> Int32 {
300 | let rawParser: RawParser = bridge(parser!.pointee.data)
301 | do {
302 | try rawParser.delegate?.onMessageBegin()
303 | } catch {
304 | return 1
305 | }
306 | return 0
307 | }
308 |
309 | private func onURL(_ parser: CParserPointer?, data: UnsafePointer?, length: Int) -> Int32 {
310 | let rawParser: RawParser = bridge(parser!.pointee.data)
311 | do {
312 | try data?.withMemoryRebound(to: UInt8.self, capacity: length) { convertedPointer in
313 | try rawParser
314 | .delegate?
315 | .onURL(data: UnsafeBufferPointer(start: convertedPointer, count: length))
316 | }
317 | } catch {
318 | return 1
319 | }
320 | return 0
321 | }
322 |
323 | private func onStatus(_ parser: CParserPointer?, data: UnsafePointer?, length: Int) -> Int32 {
324 | let rawParser: RawParser = bridge(parser!.pointee.data)
325 | do {
326 | try data?.withMemoryRebound(to: UInt8.self, capacity: length) { convertedPointer in
327 | try rawParser
328 | .delegate?
329 | .onStatus(data: UnsafeBufferPointer(start: convertedPointer, count: length))
330 | }
331 | } catch {
332 | return 1
333 | }
334 | return 0
335 | }
336 |
337 | private func onHeaderField(
338 | _ parser: CParserPointer?,
339 | data: UnsafePointer?,
340 | length: Int
341 | ) -> Int32 {
342 | let rawParser: RawParser = bridge(parser!.pointee.data)
343 | do {
344 | try data?.withMemoryRebound(to: UInt8.self, capacity: length) { convertedPointer in
345 | try rawParser
346 | .delegate?
347 | .onHeaderField(data: UnsafeBufferPointer(start: convertedPointer, count: length))
348 | }
349 | } catch {
350 | return 1
351 | }
352 | return 0
353 | }
354 |
355 | private func onHeaderValue(
356 | _ parser: CParserPointer?,
357 | data: UnsafePointer?,
358 | length: Int
359 | ) -> Int32 {
360 | let rawParser: RawParser = bridge(parser!.pointee.data)
361 | do {
362 | try data?.withMemoryRebound(to: UInt8.self, capacity: length) { convertedPointer in
363 | try rawParser
364 | .delegate?
365 | .onHeaderValue(data: UnsafeBufferPointer(start: convertedPointer, count: length))
366 | }
367 | } catch {
368 | return 1
369 | }
370 | return 0
371 | }
372 |
373 | private func onHeadersComplete(_ cParserPointer: CParserPointer?) -> Int32 {
374 | let rawParser: RawParser = bridge(cParserPointer!.pointee.data)
375 |
376 | let method = Int(rawParser.parser.method)
377 | let statusCode = Int(rawParser.parser.status_code)
378 | let majorVersion = Int(rawParser.parser.http_major)
379 | let minorVersion = Int(rawParser.parser.http_minor)
380 |
381 | do {
382 | let directive = try rawParser.delegate?.onHeadersComplete(
383 | method: method,
384 | statusCode: statusCode,
385 | majorVersion: majorVersion,
386 | minorVersion: minorVersion
387 | )
388 | switch directive {
389 | case .none: return 0
390 | case .none?: return 0
391 | case .noBody?: return 1
392 | case .noBodyNoFurtherResponses?: return 2
393 | }
394 | } catch {
395 | return 3 // on_headers_complete is a special snowflake
396 | }
397 | }
398 |
399 | private func onBody(_ cParserPointer: CParserPointer?, data: UnsafePointer?, length: Int) -> Int32 {
400 | let rawParser: RawParser = bridge(cParserPointer!.pointee.data)
401 | do {
402 | try data?.withMemoryRebound(to: UInt8.self, capacity: length) { convertedPointer in
403 | try rawParser
404 | .delegate?
405 | .onBody(data:UnsafeBufferPointer(start: convertedPointer, count: length))
406 | }
407 | } catch {
408 | return 1
409 | }
410 | return 0
411 | }
412 |
413 | private func onMessageComplete(_ cParserPointer: CParserPointer?) -> Int32 {
414 | let rawParser: RawParser = bridge(cParserPointer!.pointee.data)
415 | do {
416 | try rawParser.delegate?.onMessageComplete()
417 | } catch {
418 | return 1
419 | }
420 | return 0
421 | }
422 |
423 | private func onChunkHeader(_ cParserPointer: CParserPointer?) -> Int32 {
424 | let rawParser: RawParser = bridge(cParserPointer!.pointee.data)
425 | do {
426 | try rawParser.delegate?.onChunkHeader()
427 | } catch {
428 | return 1
429 | }
430 | return 0
431 | }
432 |
433 | private func onChunkComplete(_ cParserPointer: CParserPointer?) -> Int32 {
434 | let rawParser: RawParser = bridge(cParserPointer!.pointee.data)
435 | do {
436 | try rawParser.delegate?.onChunkComplete()
437 | } catch {
438 | return 1
439 | }
440 | return 0
441 | }
442 |
--------------------------------------------------------------------------------
/Sources/HTTP/Request.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Request.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 6/26/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | extension String {
12 | /// From https://github.com/IBM-Swift/Kitura/blob/bc0ac974229f92a7614c4e738343cfb663e2467e/Sources/Kitura/String%2BExtensions.swift
13 | /// Parses percent encoded string into query parameters
14 | var urlDecodedFieldValuePairs: [String : String] {
15 | var result: [String:String] = [:]
16 |
17 | for item in self.components(separatedBy: "&") {
18 | guard let range = item.range(of: "=") else {
19 | result[item] = nil
20 | continue
21 | }
22 |
23 | let key = String(item[.. Bool {
24 | if let method = method, request.method != method {
25 | return false
26 | }
27 | return true
28 | }
29 |
30 | func handle(
31 | requests: Signal,
32 | errors: Signal<(Request, Error)>,
33 | responses: Signal
34 | ) -> (
35 | handled: Signal,
36 | errored: Signal<(Request, Error)>,
37 | unhandled: Signal
38 | ) {
39 | let (shouldHandle, unhandled) = requests.partition(self.shouldHandleAndAddParams)
40 | let (handled, errored) = handle(requests: shouldHandle)
41 |
42 | let (mergedResponses, responsesInput) = Signal.pipe()
43 | responses.add(observer: responsesInput)
44 | handled.add(observer: responsesInput)
45 |
46 | let (mergedErrored, erroredInput) = Signal<(Request, Error)>.pipe()
47 | errors.add(observer: erroredInput)
48 | errored.add(observer: erroredInput)
49 |
50 | return (mergedResponses, mergedErrored, unhandled)
51 | }
52 |
53 | init(parent: Router? = nil, method: Method? = nil, _ handler: RequestHandler) {
54 | self.handler = handler
55 | self.method = method
56 | self.parent = parent
57 | }
58 |
59 | }
60 |
61 | extension Endpoint {
62 |
63 | func handle(requests: Signal) -> (Signal, Signal<(Request, Error)>) {
64 | let responses: Signal
65 | let (errors, errorsInput) = Signal<(Request, Error)>.pipe()
66 | switch handler {
67 | case .sync(let syncTransform):
68 | responses = requests.flatMap { request -> Response? in
69 | do {
70 | let response = try syncTransform(request)
71 | response.request = request
72 | return response
73 | } catch {
74 | errorsInput.sendNext((request, error))
75 | return nil
76 | }
77 | }
78 | case .async(let asyncTransform):
79 | responses = requests.flatMap { request -> Signal in
80 | return Signal { observer in
81 | asyncTransform(request).then {
82 | $0.request = request
83 | observer.sendNext($0)
84 | observer.sendCompleted()
85 | }.catch { error in
86 | errorsInput.sendNext((request, error))
87 | observer.sendNext(nil)
88 | observer.sendCompleted()
89 | }
90 | return nil
91 | }
92 | }.flatMap { (response: Response?) in response }
93 | }
94 | return (responses, errors)
95 | }
96 |
97 | }
98 |
99 | extension Endpoint: CustomStringConvertible {
100 | var description: String {
101 | if let method = method {
102 | return "\(method) '\(routePath)'"
103 | }
104 | return "ANY '\(routePath)'"
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/HTTP/Routing/ErrorEndpoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorEndpoint.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 12/18/17.
6 | //
7 |
8 | import Foundation
9 | import StreamKit
10 | import Regex
11 | import PathToRegex
12 |
13 | struct ErrorEndpoint: HandlerNode {
14 |
15 | let handler: ErrorHandler
16 | weak var parent: Router?
17 | let method: Method?
18 |
19 | func setParameters(on request: Request, match: Match) {
20 | request.parameters = [:]
21 | }
22 |
23 | func shouldHandleAndAddParams(request: Request, error: Error) -> Bool {
24 | if let method = method, request.method != method {
25 | return false
26 | }
27 | return true
28 | }
29 |
30 | func handle(
31 | requests: Signal,
32 | errors: Signal<(Request, Error)>,
33 | responses: Signal
34 | ) -> (
35 | handled: Signal,
36 | errored: Signal<(Request, Error)>,
37 | unhandled: Signal
38 | ) {
39 | let (shouldHandle, unhandled) = errors.partition(self.shouldHandleAndAddParams)
40 | let (handled, errored) = handle(errors: shouldHandle)
41 |
42 | let (mergedResponses, responsesInput) = Signal.pipe()
43 | responses.add(observer: responsesInput)
44 | handled.add(observer: responsesInput)
45 |
46 | let (mergedErrored, erroredInput) = Signal<(Request, Error)>.pipe()
47 | unhandled.add(observer: erroredInput)
48 | errored.add(observer: erroredInput)
49 |
50 | return (mergedResponses, mergedErrored, requests)
51 | }
52 |
53 | init(parent: Router? = nil, method: Method? = nil, _ handler: ErrorHandler) {
54 | self.handler = handler
55 | self.method = method
56 | self.parent = parent
57 | }
58 |
59 | }
60 |
61 | extension ErrorEndpoint {
62 |
63 | func handle(errors: Signal<(Request, Error)>) -> (Signal, Signal<(Request, Error)>) {
64 | let responses: Signal
65 | let (newErrors, newErrorsInput) = Signal<(Request, Error)>.pipe()
66 | switch handler {
67 | case .sync(let syncTransform):
68 | responses = errors.flatMap { (request, error) -> Response? in
69 | do {
70 | let response = try syncTransform(request, error)
71 | response.request = request
72 | return response
73 | } catch {
74 | newErrorsInput.sendNext((request, error))
75 | return nil
76 | }
77 | }
78 | case .async(let asyncTransform):
79 | responses = errors.flatMap { (request, error) -> Signal in
80 | return Signal { observer in
81 | asyncTransform(request, error).then {
82 | $0.request = request
83 | observer.sendNext($0)
84 | observer.sendCompleted()
85 | }.catch { error in
86 | newErrorsInput.sendNext((request, error))
87 | observer.sendNext(nil)
88 | observer.sendCompleted()
89 | }
90 | return nil
91 | }
92 | }.flatMap { (response: Response?) in response }
93 | }
94 | return (responses, newErrors)
95 | }
96 |
97 | }
98 |
99 | extension ErrorEndpoint: CustomStringConvertible {
100 | var description: String {
101 | if let method = method {
102 | return "\(method) '\(routePath)'"
103 | }
104 | return "ERROR '\(routePath)'"
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/HTTP/Routing/Filter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Filter.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 11/22/17.
6 | //
7 |
8 | import Foundation
9 | import StreamKit
10 | import Regex
11 | import PathToRegex
12 |
13 | struct Filter: FilterNode, CustomStringConvertible {
14 |
15 | weak var parent: Router?
16 | var predicate: (Request) -> Bool
17 |
18 | func filter(
19 | requests: Signal
20 | ) -> (
21 | requests: Signal,
22 | filtered: Signal
23 | ) {
24 | return requests.partition(predicate)
25 | }
26 |
27 | init(parent: Router? = nil, predicate: @escaping (Request) -> Bool) {
28 | self.parent = parent
29 | self.predicate = predicate
30 | }
31 |
32 | var description: String {
33 | return "FILTER '\(routePath)'"
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/HTTP/Routing/Path.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Path.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 11/22/17.
6 | //
7 |
8 | import Foundation
9 |
10 | enum Path {
11 |
12 | // See https://github.com/nodejs/node/blob/master/lib/path.js#L1238
13 | static func join(_ paths: String...) -> String {
14 | let joined = paths.filter { path in
15 | path.count > 0
16 | }.joined(separator: "/")
17 | return normalize(path: joined.count > 0 ? joined : ".")
18 | }
19 |
20 | static func normalize(path: String) -> String {
21 | return NSString(string: path).standardizingPath
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/HTTP/Routing/RequestMiddleware.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RequestMiddleware.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 11/22/17.
6 | //
7 |
8 | import Foundation
9 | import StreamKit
10 | import Regex
11 | import PathToRegex
12 |
13 | struct RequestMiddleware: HandlerNode {
14 |
15 | weak var parent: Router?
16 | let transform: RequestMapper
17 |
18 | func map(requests: Signal) -> Signal{
19 | let results: Signal
20 | switch transform {
21 | case .sync(let syncTransform):
22 | results = requests.map { request -> Request in
23 | syncTransform(request)
24 | }
25 | case .async(let asyncTransform):
26 | results = requests.flatMap { request -> Signal in
27 | return asyncTransform(request).asSignal()
28 | }
29 | }
30 | return results
31 | }
32 |
33 | func handle(
34 | requests: Signal,
35 | errors: Signal<(Request, Error)>,
36 | responses: Signal
37 | ) -> (
38 | handled: Signal,
39 | errored: Signal<(Request, Error)>,
40 | unhandled: Signal
41 | ) {
42 | let (unhandled, unhandledInput) = Signal.pipe()
43 | map(requests: requests).add(observer: unhandledInput)
44 | return (responses, errors, unhandled)
45 | }
46 |
47 | init(parent: Router, _ transform: RequestMapper) {
48 | self.transform = transform
49 | self.parent = parent
50 | }
51 |
52 | }
53 |
54 | extension RequestMiddleware: CustomStringConvertible {
55 |
56 | var description: String {
57 | return "REQUEST MIDDLEWARE '\(routePath)'"
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/HTTP/Routing/ResponseMiddleware.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResponseMiddleware.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 11/22/17.
6 | //
7 |
8 | import Foundation
9 | import StreamKit
10 | import Regex
11 | import PathToRegex
12 |
13 | struct ResponseMiddleware: HandlerNode {
14 |
15 | weak var parent: Router?
16 | let transform: ResponseMapper
17 |
18 | func map(responses: Signal) -> Signal{
19 | let results: Signal
20 | switch transform {
21 | case .sync(let syncTransform):
22 | results = responses.map { response -> Response in
23 | syncTransform(response)
24 | }
25 | case .async(let asyncTransform):
26 | results = responses.flatMap { response -> Signal in
27 | return asyncTransform(response).asSignal()
28 | }
29 | }
30 | return results
31 | }
32 |
33 | func handle(
34 | requests: Signal,
35 | errors: Signal<(Request, Error)>,
36 | responses: Signal
37 | ) -> (
38 | handled: Signal,
39 | errored: Signal<(Request, Error)>,
40 | unhandled: Signal
41 | ) {
42 | let (handled, handledInput) = Signal.pipe()
43 | map(responses: responses).add(observer: handledInput)
44 | return (handled, errors, requests)
45 | }
46 |
47 | init(parent: Router, _ transform: ResponseMapper) {
48 | self.transform = transform
49 | self.parent = parent
50 | }
51 |
52 | }
53 |
54 | extension ResponseMiddleware: CustomStringConvertible {
55 |
56 | var description: String {
57 | return "RESPONSE MIDDLEWARE '\(routePath)'"
58 | }
59 |
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/Sources/HTTP/Routing/Router.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import StreamKit
3 | import PromiseKit
4 | import PathToRegex
5 | import Regex
6 |
7 | /*
8 | let (matches, leftover) = requests.partition { request in
9 | return self.parent?.matches(path: request.uri.path) ?? false
10 | && request.method == self.method ?? request.method
11 | }
12 | */
13 | enum RequestMapper {
14 | case sync((Request) -> Request)
15 | case async((Request) -> Promise)
16 | }
17 |
18 | enum ResponseMapper {
19 | case sync((Response) -> Response)
20 | case async((Response) -> Promise)
21 | }
22 |
23 | enum RequestHandler {
24 | case sync((Request) throws -> Response)
25 | case async((Request) -> Promise)
26 | }
27 |
28 | enum ErrorHandler {
29 | case sync((Request, Error) throws -> Response)
30 | case async((Request, Error) -> Promise)
31 | }
32 |
33 | protocol HandlerNode: RouterNode, CustomStringConvertible {
34 | func handle(
35 | requests: Signal,
36 | errors: Signal<(Request, Error)>,
37 | responses: Signal
38 | ) -> (
39 | handled: Signal,
40 | errored: Signal<(Request, Error)>,
41 | unhandled: Signal
42 | )
43 | }
44 |
45 | protocol FilterNode: RouterNode, CustomStringConvertible {
46 | func filter(
47 | requests: Signal
48 | ) -> (
49 | requests: Signal,
50 | filtered: Signal
51 | )
52 | }
53 |
54 | protocol RouterNode {
55 |
56 | var parent: Router? { get }
57 | var path: String { get }
58 | var routePath: String { get }
59 |
60 | }
61 |
62 | extension RouterNode {
63 |
64 | var path: String {
65 | return "/"
66 | }
67 |
68 | var routePath: String {
69 | let parentRoutePath = parent?.routePath ?? "/"
70 | return Path.join(parentRoutePath, path)
71 | }
72 |
73 | var depth: Int {
74 | var p = parent
75 | var d = 0
76 | while p != nil {
77 | p = p!.parent
78 | d += 1
79 | }
80 | return d
81 | }
82 |
83 | func matches(urlPath: String) -> Bool {
84 | // Fast matching for the simple cases
85 | if routePath == "/" || routePath == "*" {
86 | return true
87 | }
88 | let routeRegex = try! Regex(path: urlPath)
89 | guard let _ = routeRegex.findFirst(in: urlPath) else {
90 | return false
91 | }
92 | return true
93 | }
94 |
95 | }
96 |
97 | enum RoutingNode: RouterNode, CustomStringConvertible {
98 |
99 | case filter(FilterNode)
100 | case handler(HandlerNode)
101 |
102 | var parent: Router? {
103 | switch self {
104 | case .filter(let node):
105 | return node.parent
106 | case .handler(let node):
107 | return node.parent
108 | }
109 | }
110 |
111 | var description: String {
112 | switch self {
113 | case .filter(let node):
114 | return node.description
115 | case .handler(let node):
116 | return node.description
117 | }
118 | }
119 |
120 | }
121 |
122 | public final class Router: HandlerNode {
123 |
124 | weak var parent: Router? = nil
125 | var nodes: [RoutingNode] = []
126 | var path: String
127 |
128 | public init() {
129 | self.path = "/"
130 | }
131 |
132 | func decodeParameter(_ parameter: String?) -> String? {
133 | return parameter?.removingPercentEncoding
134 | }
135 |
136 | func setParameters(on request: Request, match: Match, regex: Regex) {
137 | let valsArray = regex.groupNames.map { name in
138 | (name, decodeParameter(match.group(named: name)))
139 | }.map { tuple in
140 | (tuple.0, tuple.1 ?? "")
141 | }
142 | request.parameters = Dictionary(uniqueKeysWithValues: valsArray)
143 | }
144 |
145 | func shouldHandle(_ request: Request) -> Bool {
146 | if path == "/" || path == "*" {
147 | return true
148 | }
149 | let urlPath = String(request.uri.absoluteString.prefix(
150 | upTo: request.uri.absoluteString.index(of: "?") ?? request.uri.absoluteString.endIndex
151 | ))
152 | let regexPath = routePath
153 | let regex = try! Regex(path: regexPath, pathOptions: [])
154 | guard let match = regex.findFirst(in: urlPath) else {
155 | return false
156 | }
157 | setParameters(on: request, match: match, regex: regex)
158 | return true
159 | }
160 |
161 | private init(
162 | parent: Router,
163 | path: String?
164 | ) {
165 | self.parent = parent
166 | self.path = path ?? "/"
167 | }
168 |
169 | private func add(
170 | path: String? = nil,
171 | method: HTTP.Method? = nil,
172 | _ handler: RequestHandler
173 | ) {
174 | let subrouter = Router(parent: self, path: path)
175 | let endpoint = Endpoint(parent: subrouter, method: method, handler)
176 | subrouter.nodes.append(.handler(endpoint))
177 | nodes.append(.handler(subrouter))
178 | }
179 |
180 | private func add(
181 | path: String? = nil,
182 | method: HTTP.Method? = nil,
183 | _ handler: ErrorHandler
184 | ) {
185 | let subrouter = Router(parent: self, path: path)
186 | let endpoint = ErrorEndpoint(parent: subrouter, method: method, handler)
187 | subrouter.nodes.append(.handler(endpoint))
188 | nodes.append(.handler(subrouter))
189 | }
190 |
191 | func handle(
192 | requests: Signal,
193 | errors: Signal<(Request, Error)>,
194 | responses: Signal
195 | ) -> (
196 | handled: Signal,
197 | errored: Signal<(Request, Error)>,
198 | unhandled: Signal
199 | ) {
200 | let (unhandled, unhandledInput) = Signal.pipe()
201 | var (needsHandling, filtered) = requests.partition(self.shouldHandle)
202 |
203 | // Send filtered to the unhandled output requests of this router.
204 | filtered.add(observer: unhandledInput)
205 |
206 | // Handle as yet unhandled
207 | var handled = responses
208 | var errored = errors
209 | for node in nodes {
210 | switch node {
211 | case .filter(let node):
212 | let newlyFiltered: Signal
213 | // Partition requests that still need handling and the ones that are filtered
214 | // Send the ones that need handling onto the next node
215 | // Send the filtered ones to the unhandled output
216 | (needsHandling, newlyFiltered) = node.filter(requests: needsHandling)
217 | newlyFiltered.add(observer: unhandledInput)
218 | case .handler(let node):
219 | (handled, errored, needsHandling) = node.handle(
220 | requests: needsHandling,
221 | errors: errored,
222 | responses: handled
223 | )
224 | }
225 | }
226 | needsHandling.add(observer: unhandledInput)
227 | return (handled: handled, errored: errored, unhandled: unhandled)
228 | }
229 |
230 | }
231 |
232 | extension Router: CustomStringConvertible {
233 |
234 | public var description: String {
235 | return "ROUTER '\(routePath)'" + (nodes.count > 0 ? "\n" : "") + nodes.map {
236 | (0...depth).map { _ in "\t" } + $0.description
237 | }.joined(separator: "\n")
238 | }
239 |
240 | }
241 |
242 | extension Router: ServerDelegate {
243 |
244 | public func handle(requests: Signal) -> Signal {
245 | let (handled, errors, unhandled) = handle(
246 | requests: requests,
247 | errors: Signal<(Request, Error)>.empty,
248 | responses: Signal.empty
249 | )
250 | let errored = errors.map { (request, error) in
251 | // TODO: warn about unhandled error
252 | return Response(status: .internalServerError)
253 | }
254 | let notFound = unhandled.map { request in
255 | // TODO: warn about unhandled request
256 | return Response(status: .notFound)
257 | }
258 | let (responses, responsesInput) = Signal.pipe()
259 | handled.add(observer: responsesInput)
260 | notFound.add(observer: responsesInput)
261 | errored.add(observer: responsesInput)
262 | return responses
263 | }
264 |
265 | }
266 |
267 | /// Route building
268 | extension Router {
269 |
270 | public func add(_ subrouter: Router) {
271 | add(nil, subrouter)
272 | }
273 |
274 | public func add(_ path: String? = nil, _ subrouter: Router) {
275 | subrouter.path = path ?? "/"
276 | subrouter.parent = self
277 | nodes.append(.handler(subrouter))
278 | }
279 |
280 | public func subrouter(_ path: String) -> Router {
281 | let subrouter = Router(parent: self, path: path)
282 | nodes.append(.handler(subrouter))
283 | return subrouter
284 | }
285 |
286 | }
287 |
288 | /// Async Transforms
289 | extension Router {
290 |
291 | public func any(_ path: String? = nil, _ transform: @escaping (Request) -> Promise) {
292 | add(path: path, .async(transform))
293 | }
294 |
295 | public func get(_ path: String? = nil, _ transform: @escaping (Request) -> Promise) {
296 | add(path: path, method: .get, .async(transform))
297 | }
298 |
299 | public func post(_ path: String? = nil, _ transform: @escaping (Request) -> Promise) {
300 | add(path: path, method: .post, .async(transform))
301 | }
302 |
303 | public func put(_ path: String? = nil, _ transform: @escaping (Request) -> Promise) {
304 | add(path: path, method: .put, .async(transform))
305 | }
306 |
307 | public func delete(_ subpath: String? = nil, _ transform: @escaping (Request) -> Promise) {
308 | add(path: path, method: .delete, .async(transform))
309 | }
310 |
311 | public func any(_ path: String? = nil, _ transform: @escaping (Request, Error) -> Promise) {
312 | add(path: path, .async(transform))
313 | }
314 |
315 | public func get(_ path: String? = nil, _ transform: @escaping (Request, Error) -> Promise) {
316 | add(path: path, method: .get, .async(transform))
317 | }
318 |
319 | public func post(_ path: String? = nil, _ transform: @escaping (Request, Error) -> Promise) {
320 | add(path: path, method: .post, .async(transform))
321 | }
322 |
323 | public func put(_ path: String? = nil, _ transform: @escaping (Request, Error) -> Promise) {
324 | add(path: path, method: .put, .async(transform))
325 | }
326 |
327 | public func delete(_ subpath: String? = nil, _ transform: @escaping (Request, Error) -> Promise) {
328 | add(path: path, method: .delete, .async(transform))
329 | }
330 |
331 | }
332 |
333 | /// Sync transforms
334 | extension Router {
335 |
336 | public func any(_ path: String? = nil, _ transform: @escaping (Request) throws -> Response) {
337 | add(path: path, .sync(transform))
338 | }
339 |
340 | public func get(_ path: String? = nil, _ transform: @escaping (Request) throws -> Response) {
341 | add(path: path, method: .get, .sync(transform))
342 | }
343 |
344 | public func post(_ path: String? = nil, _ transform: @escaping (Request) throws -> Response) {
345 | add(path: path, method: .post, .sync(transform))
346 | }
347 |
348 | public func put(_ path: String? = nil, _ transform: @escaping (Request) throws -> Response) {
349 | add(path: path, method: .put, .sync(transform))
350 | }
351 |
352 | public func delete(_ path: String? = nil, _ transform: @escaping (Request) throws -> Response) {
353 | add(path: path, method: .delete, .sync(transform))
354 | }
355 |
356 | public func any(_ path: String? = nil, _ transform: @escaping (Request, Error) throws -> Response) {
357 | add(path: path, .sync(transform))
358 | }
359 |
360 | public func get(_ path: String? = nil, _ transform: @escaping (Request, Error) throws -> Response) {
361 | add(path: path, method: .get, .sync(transform))
362 | }
363 |
364 | public func post(_ path: String? = nil, _ transform: @escaping (Request, Error) throws -> Response) {
365 | add(path: path, method: .post, .sync(transform))
366 | }
367 |
368 | public func put(_ path: String? = nil, _ transform: @escaping (Request, Error) throws -> Response) {
369 | add(path: path, method: .put, .sync(transform))
370 | }
371 |
372 | public func delete(_ path: String? = nil, _ transform: @escaping (Request, Error) throws -> Response) {
373 | add(path: path, method: .delete, .sync(transform))
374 | }
375 |
376 | public func map(_ transform: @escaping (Request) -> Request) {
377 | nodes.append(.handler(RequestMiddleware(parent: self, .sync(transform))))
378 | }
379 |
380 | public func filter(_ predicate: @escaping (Request) -> Bool) {
381 | nodes.append(.filter(Filter(parent: self, predicate: predicate)))
382 | }
383 |
384 | public func mapResponses(_ transform: @escaping (Response) -> Response) {
385 | nodes.append(.handler(ResponseMiddleware(parent: self, .sync(transform))))
386 | }
387 |
388 | }
389 |
--------------------------------------------------------------------------------
/Sources/HTTP/Serializable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Serializable.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 7/1/16.
6 | //
7 | //
8 | import Foundation
9 |
10 | public protocol Serializable {
11 | var serialized: Data { get }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/HTTP/Server.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTP.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 6/22/16.
6 | //
7 | //
8 |
9 | import Dispatch
10 | import StreamKit
11 | import POSIX
12 | import Foundation
13 | import TCP
14 |
15 | public protocol ServerDelegate {
16 |
17 | func handle(requests: Signal) -> Signal
18 |
19 | }
20 |
21 | public final class Server {
22 |
23 | let delegate: ServerDelegate
24 | private var disposable: ActionDisposable? = nil
25 | let reuseAddress: Bool
26 | let reusePort: Bool
27 |
28 | public init(delegate: ServerDelegate? = nil, reuseAddress: Bool = false, reusePort: Bool = false) {
29 | self.delegate = delegate ?? Router()
30 | self.reuseAddress = reuseAddress
31 | self.reusePort = reusePort
32 | }
33 |
34 | deinit {
35 | self.stop()
36 | }
37 |
38 | public func stop() {
39 | disposable?.dispose()
40 | }
41 |
42 | public func parse(data dataStream: Source) -> Source {
43 | return Source { observer in
44 | let parser = RequestParser()
45 | parser.onRequest = { request in
46 | observer.sendNext(request)
47 | }
48 | dataStream.onNext { data in
49 | do {
50 | try parser.parse(data)
51 | } catch {
52 | // Respond with 400 error
53 | observer.sendFailed(ClientError.badRequest)
54 | }
55 | }
56 | dataStream.onCompleted {
57 | observer.sendCompleted()
58 | }
59 | dataStream.onFailed { error in
60 | observer.sendFailed(error)
61 | }
62 | dataStream.start()
63 | return ActionDisposable {
64 | dataStream.stop()
65 | }
66 | }
67 |
68 | }
69 |
70 | public func serialize(responses: Signal) -> Signal {
71 | return responses.map { $0.serialized }
72 | }
73 |
74 | func clients(host: String, port: POSIX.Port) -> Source {
75 | return Source { [reuseAddress, reusePort] observer in
76 | let tcpServer = try! TCP.Server(reuseAddress: reuseAddress, reusePort: reusePort)
77 | try! tcpServer.bind(host: host, port: port)
78 |
79 | let listen = tcpServer.listen()
80 | listen.onNext { socket in
81 | observer.sendNext(socket)
82 | }
83 |
84 | listen.onFailed { error in
85 | observer.sendFailed(error)
86 | }
87 |
88 | listen.start()
89 |
90 | return ActionDisposable {
91 | listen.stop()
92 | }
93 | }
94 | }
95 |
96 | public func listen(host: String, port: POSIX.Port) {
97 | let clients = self.clients(host: host, port: port)
98 | var connectedClients: [Socket] = []
99 | clients.onNext { socket in
100 | connectedClients.append(socket)
101 | let requestStream = self.parse(data: socket.read())
102 | let responses = self.delegate.handle(requests: requestStream.signal)
103 | let data = self.serialize(responses: responses)
104 | _ = socket.write(stream: data)
105 | requestStream.start()
106 | }
107 | disposable = ActionDisposable {
108 | clients.stop()
109 | for socket in connectedClients {
110 | socket.close()
111 | }
112 | }
113 | clients.start()
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/Sources/HTTP/Status.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Status.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 10/16/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public enum Status {
12 | case `continue`
13 | case switchingProtocols
14 | case processing
15 |
16 | case ok
17 | case created
18 | case accepted
19 | case nonAuthoritativeInformation
20 | case noContent
21 | case resetContent
22 | case partialContent
23 | case multiStatus
24 | case alreadyReported
25 | case imUsed
26 |
27 | case multipleChoices
28 | case movedPermanently
29 | case found
30 | case seeOther
31 | case notModified
32 | case useProxy
33 | case switchProxy
34 | case temporaryRedirect
35 | case permanentRedirect
36 |
37 | case badRequest
38 | case unauthorized
39 | case paymentRequired
40 | case forbidden
41 | case notFound
42 | case methodNotAllowed
43 | case notAcceptable
44 | case proxyAuthenticationRequired
45 | case requestTimeout
46 | case conflict
47 | case gone
48 | case lengthRequired
49 | case preconditionFailed
50 | case requestEntityTooLarge
51 | case requestURITooLong
52 | case unsupportedMediaType
53 | case requestedRangeNotSatisfiable
54 | case expectationFailed
55 | case imATeapot
56 | case authenticationTimeout
57 | case enhanceYourCalm
58 | case misdirectedRequest
59 | case unprocessableEntity
60 | case locked
61 | case failedDependency
62 | case unorderedCollection
63 | case upgradeRequired
64 | case preconditionRequired
65 | case tooManyRequests
66 | case requestHeaderFieldsTooLarge
67 | case unavailableForLegalReasons
68 |
69 | case internalServerError
70 | case notImplemented
71 | case badGateway
72 | case serviceUnavailable
73 | case gatewayTimeout
74 | case httpVersionNotSupported
75 | case variantAlsoNegotiates
76 | case insufficientStorage
77 | case loopDetected
78 | case bandwidthLimitExceeded
79 | case notExtended
80 | case networkAuthenticationRequired
81 |
82 | case other(code: Int, reasonPhrase: String)
83 | }
84 |
85 | extension Status {
86 | public init(code: Int, reasonPhrase: String? = nil) {
87 | if let reasonPhrase = reasonPhrase {
88 | self = .other(code: code, reasonPhrase: reasonPhrase)
89 | } else {
90 | switch code {
91 | case Status.`continue`.code: self = .`continue`
92 | case Status.switchingProtocols.code: self = .switchingProtocols
93 | case Status.processing.code: self = .processing
94 |
95 | case Status.ok.code: self = .ok
96 | case Status.created.code: self = .created
97 | case Status.accepted.code: self = .accepted
98 | case Status.nonAuthoritativeInformation.code: self = .nonAuthoritativeInformation
99 | case Status.noContent.code: self = .noContent
100 | case Status.resetContent.code: self = .resetContent
101 | case Status.partialContent.code: self = .partialContent
102 | case Status.alreadyReported.code: self = .alreadyReported
103 | case Status.multiStatus.code: self = .multiStatus
104 | case Status.imUsed.code: self = .imUsed
105 |
106 | case Status.multipleChoices.code: self = .multipleChoices
107 | case Status.movedPermanently.code: self = .movedPermanently
108 | case Status.found.code: self = .found
109 | case Status.seeOther.code: self = .seeOther
110 | case Status.notModified.code: self = .notModified
111 | case Status.useProxy.code: self = .useProxy
112 | case Status.switchProxy.code: self = .switchProxy
113 | case Status.temporaryRedirect.code: self = .temporaryRedirect
114 | case Status.permanentRedirect.code: self = .permanentRedirect
115 |
116 | case Status.badRequest.code: self = .badRequest
117 | case Status.unauthorized.code: self = .unauthorized
118 | case Status.paymentRequired.code: self = .paymentRequired
119 | case Status.forbidden.code: self = .forbidden
120 | case Status.notFound.code: self = .notFound
121 | case Status.methodNotAllowed.code: self = .methodNotAllowed
122 | case Status.notAcceptable.code: self = .notAcceptable
123 | case Status.proxyAuthenticationRequired.code: self = .proxyAuthenticationRequired
124 | case Status.requestTimeout.code: self = .requestTimeout
125 | case Status.conflict.code: self = .conflict
126 | case Status.gone.code: self = .gone
127 | case Status.lengthRequired.code: self = .lengthRequired
128 | case Status.preconditionFailed.code: self = .preconditionFailed
129 | case Status.requestEntityTooLarge.code: self = .requestEntityTooLarge
130 | case Status.requestURITooLong.code: self = .requestURITooLong
131 | case Status.unsupportedMediaType.code: self = .unsupportedMediaType
132 | case Status.requestedRangeNotSatisfiable.code: self = .requestedRangeNotSatisfiable
133 | case Status.expectationFailed.code: self = .expectationFailed
134 | case Status.imATeapot.code: self = .imATeapot
135 | case Status.authenticationTimeout.code: self = .authenticationTimeout
136 | case Status.enhanceYourCalm.code: self = .enhanceYourCalm
137 | case Status.misdirectedRequest.code: self = .misdirectedRequest
138 | case Status.unprocessableEntity.code: self = .unprocessableEntity
139 | case Status.locked.code: self = .locked
140 | case Status.failedDependency.code: self = .failedDependency
141 | case Status.unorderedCollection.code: self = .unorderedCollection
142 | case Status.upgradeRequired.code: self = .upgradeRequired
143 | case Status.preconditionRequired.code: self = .preconditionRequired
144 | case Status.tooManyRequests.code: self = .tooManyRequests
145 | case Status.requestHeaderFieldsTooLarge.code: self = .requestHeaderFieldsTooLarge
146 | case Status.unavailableForLegalReasons.code: self = .unavailableForLegalReasons
147 |
148 | case Status.internalServerError.code: self = .internalServerError
149 | case Status.notImplemented.code: self = .notImplemented
150 | case Status.badGateway.code: self = .badGateway
151 | case Status.serviceUnavailable.code: self = .serviceUnavailable
152 | case Status.gatewayTimeout.code: self = .gatewayTimeout
153 | case Status.httpVersionNotSupported.code: self = .httpVersionNotSupported
154 | case Status.variantAlsoNegotiates.code: self = .variantAlsoNegotiates
155 | case Status.insufficientStorage.code: self = .insufficientStorage
156 | case Status.loopDetected.code: self = .loopDetected
157 | case Status.bandwidthLimitExceeded.code: self = .bandwidthLimitExceeded
158 | case Status.notExtended.code: self = .notExtended
159 | case Status.networkAuthenticationRequired.code: self = .networkAuthenticationRequired
160 |
161 | default: self = .other(code: code, reasonPhrase: "¯\\_(ツ)_/¯")
162 | }
163 | }
164 | }
165 | }
166 |
167 | extension Status {
168 | public var code: Int {
169 | switch self {
170 | case .`continue`: return 100
171 | case .switchingProtocols: return 101
172 | case .processing: return 102
173 |
174 | case .ok: return 200
175 | case .created: return 201
176 | case .accepted: return 202
177 | case .nonAuthoritativeInformation: return 203
178 | case .noContent: return 204
179 | case .resetContent: return 205
180 | case .partialContent: return 206
181 | case .multiStatus: return 207
182 | case .alreadyReported: return 208
183 | case .imUsed: return 226
184 |
185 | case .multipleChoices: return 300
186 | case .movedPermanently: return 301
187 | case .found: return 302
188 | case .seeOther: return 303
189 | case .notModified: return 304
190 | case .useProxy: return 305
191 | case .switchProxy: return 306
192 | case .temporaryRedirect: return 307
193 | case .permanentRedirect: return 308
194 |
195 | case .badRequest: return 400
196 | case .unauthorized: return 401
197 | case .paymentRequired: return 402
198 | case .forbidden: return 403
199 | case .notFound: return 404
200 | case .methodNotAllowed: return 405
201 | case .notAcceptable: return 406
202 | case .proxyAuthenticationRequired: return 407
203 | case .requestTimeout: return 408
204 | case .conflict: return 409
205 | case .gone: return 410
206 | case .lengthRequired: return 411
207 | case .preconditionFailed: return 412
208 | case .requestEntityTooLarge: return 413
209 | case .requestURITooLong: return 414
210 | case .unsupportedMediaType: return 415
211 | case .requestedRangeNotSatisfiable: return 416
212 | case .expectationFailed: return 417
213 | case .imATeapot: return 418
214 | case .authenticationTimeout: return 419
215 | case .enhanceYourCalm: return 420
216 | case .misdirectedRequest: return 421
217 | case .unprocessableEntity: return 422
218 | case .locked: return 423
219 | case .failedDependency: return 424
220 | case .unorderedCollection: return 425
221 | case .upgradeRequired: return 426
222 | case .preconditionRequired: return 428
223 | case .tooManyRequests: return 429
224 | case .requestHeaderFieldsTooLarge: return 431
225 | case .unavailableForLegalReasons: return 451
226 |
227 | case .internalServerError: return 500
228 | case .notImplemented: return 501
229 | case .badGateway: return 502
230 | case .serviceUnavailable: return 503
231 | case .gatewayTimeout: return 504
232 | case .httpVersionNotSupported: return 505
233 | case .variantAlsoNegotiates: return 506
234 | case .insufficientStorage: return 507
235 | case .loopDetected: return 508
236 | case .bandwidthLimitExceeded: return 509
237 | case .notExtended: return 510
238 | case .networkAuthenticationRequired: return 511
239 |
240 | case .other(let code, _): return code
241 | }
242 | }
243 | }
244 |
245 | extension Status {
246 | public var reasonPhrase: String {
247 | switch self {
248 | case .`continue`: return "Continue"
249 | case .switchingProtocols: return "Switching Protocols"
250 | case .processing: return "Processing"
251 |
252 | case .ok: return "OK"
253 | case .created: return "Created"
254 | case .accepted: return "Accepted"
255 | case .nonAuthoritativeInformation: return "Non-Authoritative Information"
256 | case .noContent: return "No Content"
257 | case .resetContent: return "Reset Content"
258 | case .partialContent: return "Partial Content"
259 | case .multiStatus: return "Multi-Status"
260 | case .alreadyReported: return "Already Reported"
261 | case .imUsed: return "IM Used"
262 |
263 | case .multipleChoices: return "Multiple Choices"
264 | case .movedPermanently: return "Moved Permanently"
265 | case .found: return "Found"
266 | case .seeOther: return "See Other"
267 | case .notModified: return "Not Modified"
268 | case .useProxy: return "Use Proxy"
269 | case .switchProxy: return "Switch Proxy"
270 | case .temporaryRedirect: return "Temporary Redirect"
271 | case .permanentRedirect: return "Permanent Redirect"
272 |
273 | case .badRequest: return "Bad Request"
274 | case .unauthorized: return "Unauthorized"
275 | case .paymentRequired: return "Payment Required"
276 | case .forbidden: return "Forbidden"
277 | case .notFound: return "Not Found"
278 | case .methodNotAllowed: return "Method Not Allowed"
279 | case .notAcceptable: return "Not Acceptable"
280 | case .proxyAuthenticationRequired: return "Proxy Authentication Required"
281 | case .requestTimeout: return "Request Timeout"
282 | case .conflict: return "Conflict"
283 | case .gone: return "Gone"
284 | case .lengthRequired: return "Length Required"
285 | case .preconditionFailed: return "Precondition Failed"
286 | case .requestEntityTooLarge: return "Request Entity Too Large"
287 | case .requestURITooLong: return "Request URI Too Long"
288 | case .unsupportedMediaType: return "Unsupported Media Type"
289 | case .requestedRangeNotSatisfiable: return "Requested Range Not Satisfiable"
290 | case .expectationFailed: return "Expectation Failed"
291 | case .imATeapot: return "I'm a teapot"
292 | case .authenticationTimeout: return "Authentication Timeout"
293 | case .enhanceYourCalm: return "Enhance Your Calm"
294 | case .misdirectedRequest: return "Misdirected Request"
295 | case .unprocessableEntity: return "Unprocessable Entity"
296 | case .locked: return "Locked"
297 | case .failedDependency: return "Failed Dependency"
298 | case .unorderedCollection: return "Unordered Collection"
299 | case .upgradeRequired: return "Upgrade Required"
300 | case .preconditionRequired: return "Precondition Required"
301 | case .tooManyRequests: return "Too Many Requests"
302 | case .requestHeaderFieldsTooLarge: return "Request Header Fields Too Large"
303 | case .unavailableForLegalReasons: return "Unavailable For Legal Reasons"
304 |
305 | case .internalServerError: return "Internal Server Error"
306 | case .notImplemented: return "Not Implemented"
307 | case .badGateway: return "Bad Gateway"
308 | case .serviceUnavailable: return "Service Unavailable"
309 | case .gatewayTimeout: return "Gateway Timeout"
310 | case .httpVersionNotSupported: return "HTTP Version Not Supported"
311 | case .variantAlsoNegotiates: return "Variant Also Negotiates"
312 | case .insufficientStorage: return "Insufficient Storage"
313 | case .loopDetected: return "Loop Detected"
314 | case .bandwidthLimitExceeded: return "Bandwidth Limit Exceeded"
315 | case .notExtended: return "Not Extended"
316 | case .networkAuthenticationRequired: return "Network Authentication Required"
317 |
318 | case .other(_, let reasonPhrase): return reasonPhrase
319 | }
320 | }
321 | }
322 |
323 | extension Status: Hashable {
324 | public var hashValue: Int {
325 | return code
326 | }
327 | }
328 |
329 | public func == (lhs: Status, rhs: Status) -> Bool {
330 | return lhs.hashValue == rhs.hashValue
331 | }
332 |
--------------------------------------------------------------------------------
/Sources/HTTP/Version.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Version.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 10/16/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Version {
12 | public var major: Int
13 | public var minor: Int
14 |
15 | public init(major: Int, minor: Int) {
16 | self.major = major
17 | self.minor = minor
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/IOStream/IOStream.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IOStream.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 5/1/16.
6 | //
7 | //
8 | import Foundation
9 | import Dispatch
10 | import StreamKit
11 | import POSIX
12 | // swiftlint:disable variable_name
13 | #if os(Linux)
14 | import Glibc
15 | let empty_off_t = Glibc.off_t()
16 | let INT32_MAX = Glibc.INT32_MAX
17 | #else
18 | import Darwin
19 | let empty_off_t = Darwin.off_t()
20 | let INT32_MAX = Darwin.INT32_MAX
21 | #endif
22 | // swiftlint:enable variable_name
23 |
24 | public protocol IOStream: class {
25 | var fd: FileDescriptor { get }
26 | var channel: DispatchIO { get }
27 | }
28 |
29 | public protocol WritableIOStream: IOStream {
30 | func write(buffer: Data) -> Source
31 | }
32 |
33 | public extension WritableIOStream {
34 |
35 | func write(stream: Signal) -> Signal {
36 | return Signal { observer in
37 | var unwrittenByteCounts = [Int:Int]()
38 | var globalIndex = 0
39 | stream.onNext { data in
40 | let dataIndex = globalIndex
41 | globalIndex += 1
42 |
43 | let writeSource = self.write(buffer: data)
44 | writeSource.onNext { unwrittenBytes in
45 | unwrittenByteCounts[dataIndex] = unwrittenBytes
46 | observer.sendNext(unwrittenByteCounts.values.reduce(0, +))
47 | }
48 | writeSource.onCompleted {
49 | unwrittenByteCounts[dataIndex] = nil
50 | }
51 | writeSource.onFailed { _ in
52 | unwrittenByteCounts[dataIndex] = nil
53 | }
54 | writeSource.start()
55 | }
56 | return nil
57 | }
58 | }
59 |
60 | func write(buffer: Data) -> Source {
61 | return Source { observer in
62 | let writeChannel = DispatchIO(
63 | type: .stream,
64 | io: self.channel,
65 | queue: .main
66 | ) { error in
67 | if let systemError = SystemError(errorNumber: error) {
68 | observer.sendFailed(systemError)
69 | }
70 | }
71 |
72 | // Allocate dispatch data
73 | // TODO: This does not seem right.
74 | // Work around crash for now.
75 | let dispatchData = buffer.withUnsafeBytes {
76 | return DispatchData(
77 | bytes: UnsafeRawBufferPointer(start: $0, count: buffer.count)
78 | )
79 | }
80 |
81 | // Schedule write operation
82 | writeChannel.write(
83 | offset: empty_off_t,
84 | data: dispatchData,
85 | queue: .main
86 | ) { done, data, error in
87 |
88 | if let data = data {
89 | // Report number of unwritten bytes
90 | observer.sendNext(data.count)
91 | }
92 |
93 | if let systemError = SystemError(errorNumber: error) {
94 | // If there was an error emit the error.
95 | observer.sendFailed(systemError)
96 | }
97 |
98 | if done {
99 | if error == 0 {
100 | // If the done param is set and there is no error,
101 | // all data has been written, emit writing end.
102 | // DO NOT emit end otherwise!
103 | observer.sendCompleted()
104 | }
105 |
106 | // Must be an unrecoverable error, close the channel.
107 | // TODO: Maybe don't close if you want half-open channel
108 | // NOTE: This will be done by onCompleted or onError
109 | // dispatch_io_close(self.channel, 0)
110 | writeChannel.close()
111 | }
112 | }
113 |
114 | return ActionDisposable {
115 | writeChannel.close()
116 | }
117 | }
118 | }
119 | }
120 |
121 | public protocol ReadableIOStream: IOStream {
122 | func read(minBytes: Int) -> Source
123 | }
124 |
125 | public extension ReadableIOStream {
126 |
127 | func read(minBytes: Int = 1) -> Source {
128 |
129 | return Source { observer in
130 |
131 | let readChannel = DispatchIO(type: .stream, io: self.channel, queue: .main) { error in
132 | if let systemError = SystemError(errorNumber: error) {
133 | observer.sendFailed(systemError)
134 | }
135 | }
136 |
137 | readChannel.setLimit(lowWater: minBytes)
138 | readChannel.read(
139 | offset: empty_off_t,
140 | length: size_t(INT32_MAX),
141 | queue: .main
142 | ) { done, data, error in
143 |
144 | // Deliver data if it is non-empty
145 | if let data = data, !data.isEmpty {
146 | data.enumerateBytes { (buffer, byteIndex, stop) in
147 | observer.sendNext(Data(buffer))
148 | }
149 | }
150 |
151 | if let systemError = SystemError(errorNumber: error) {
152 | // If there was an error emit the error.
153 | observer.sendFailed(systemError)
154 | }
155 |
156 | if done {
157 | if error == 0 {
158 | // If the done param is set and there is no error,
159 | // all data has been read, emit end.
160 | // DO NOT emit end otherwise!
161 | observer.sendCompleted()
162 | }
163 |
164 | // It's done close the channel
165 | // TODO: Maybe don't close if you want half-open channel
166 | // NOTE: This will be done by onCompleted or onError
167 | // dispatch_io_close(readChannel, 0)
168 | readChannel.close()
169 | }
170 | }
171 | return ActionDisposable {
172 | readChannel.close()
173 | }
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/Sources/IOStream/Pipe.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Pipe.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 5/2/16.
6 | //
7 | //
8 |
9 | import Dispatch
10 | import POSIX
11 | import StreamKit
12 |
13 | public final class Pipe: WritableIOStream, ReadableIOStream {
14 |
15 | public let fd: FileDescriptor
16 | public let channel: DispatchIO
17 | public let channelErrorSignal: Signal<()>
18 |
19 | public init(fd: StandardFileDescriptor) {
20 | self.fd = fd
21 | let (channelErrorSignal, observer) = Signal<()>.pipe()
22 | self.channelErrorSignal = channelErrorSignal
23 | self.channel = DispatchIO(
24 | type: .stream,
25 | fileDescriptor: fd.rawValue,
26 | queue: .main
27 | ) { error in
28 | if let systemError = SystemError(errorNumber: error) {
29 | observer.sendFailed(systemError)
30 | } else {
31 | observer.sendCompleted()
32 | }
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/Lightning/Lightning.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Edge.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 6/5/16.
6 | //
7 | //
8 |
9 | #if os(Linux)
10 | import Glibc
11 | #else
12 | import Darwin
13 | #endif
14 | import Dispatch
15 |
16 | public struct Edge {
17 |
18 | private init() {}
19 |
20 | public static func run(ignoreSigPipe: Bool = true) {
21 | if ignoreSigPipe {
22 | #if os(Linux)
23 | Glibc.signal(SIGPIPE, SIG_IGN)
24 | #else
25 | Darwin.signal(SIGPIPE, SIG_IGN)
26 | #endif
27 | }
28 | dispatchMain()
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/POSIX/FileDescriptor.swift:
--------------------------------------------------------------------------------
1 | #if os(Linux)
2 | import Glibc
3 | #else
4 | import Darwin
5 | #endif
6 |
7 | public protocol FileDescriptor {
8 |
9 | var rawValue: Int32 { get }
10 |
11 | func close()
12 |
13 | }
14 |
15 | extension FileDescriptor {
16 |
17 | public func close() {
18 | #if os(Linux)
19 | _ = Glibc.close(rawValue)
20 | #else
21 | _ = Darwin.close(rawValue)
22 | #endif
23 |
24 | }
25 | }
26 |
27 | public enum StandardFileDescriptor: Int32, FileDescriptor {
28 | case invalid = -1
29 | case stdin = 0
30 | case stdout = 1
31 | case stderr = 2
32 | }
33 |
34 | public struct SocketFileDescriptor: CustomDebugStringConvertible, FileDescriptor {
35 |
36 | public let rawValue: Int32
37 | public let addressFamily: AddressFamily
38 | public let socketType: SocketType
39 | public let blocking: Bool
40 |
41 | public init(
42 | socketType: SocketType,
43 | addressFamily: AddressFamily,
44 | blocking: Bool = false
45 | ) throws {
46 | self.rawValue = socket(addressFamily.rawValue, socketType.rawValue, 0)
47 | if self.rawValue == StandardFileDescriptor.invalid.rawValue {
48 | throw SystemError(errorNumber: errno)!
49 | }
50 |
51 | if !blocking {
52 | let flags = fcntl(self.rawValue, F_GETFL, 0)
53 | let error = fcntl(self.rawValue, F_SETFL, flags | O_NONBLOCK)
54 | if error == -1 {
55 | throw SystemError(errorNumber: errno)!
56 | }
57 | }
58 |
59 | self.addressFamily = addressFamily
60 | self.socketType = socketType
61 | self.blocking = blocking
62 | }
63 |
64 | public init(
65 | rawValue: Int32,
66 | socketType: SocketType,
67 | addressFamily: AddressFamily,
68 | blocking: Bool
69 | ) {
70 | self.rawValue = rawValue
71 | self.addressFamily = addressFamily
72 | self.socketType = socketType
73 | self.blocking = blocking
74 | }
75 |
76 | public var debugDescription: String {
77 | return "\(String(rawValue))"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/POSIX/Socket.swift:
--------------------------------------------------------------------------------
1 | #if os(Linux)
2 | import Glibc
3 | let sockStream = Int32(SOCK_STREAM.rawValue)
4 | let sockDgram = Int32(SOCK_DGRAM.rawValue)
5 | let sockSeqPacket = Int32(SOCK_SEQPACKET.rawValue)
6 | let sockRaw = Int32(SOCK_RAW.rawValue)
7 | let sockRDM = Int32(SOCK_RDM.rawValue)
8 | #else
9 | import Darwin.C
10 | let sockStream = SOCK_STREAM
11 | let sockDgram = SOCK_DGRAM
12 | let sockSeqPacket = SOCK_SEQPACKET
13 | let sockRaw = SOCK_RAW
14 | let sockRDM = SOCK_RDM
15 | #endif
16 |
17 | public typealias Port = UInt16
18 |
19 | public struct SocketType {
20 |
21 | public static let stream = SocketType(rawValue: sockStream)
22 | public static let datagram = SocketType(rawValue: sockDgram)
23 | public static let seqPacket = SocketType(rawValue: sockSeqPacket)
24 | public static let raw = SocketType(rawValue: sockRaw)
25 | public static let reliableDatagram = SocketType(rawValue: sockRDM)
26 |
27 | public let rawValue: Int32
28 |
29 | public init(rawValue: Int32) {
30 | self.rawValue = rawValue
31 | }
32 | }
33 |
34 | public struct AddressFamily {
35 | public static let unix = AddressFamily(rawValue: AF_UNIX)
36 | public static let inet = AddressFamily(rawValue: AF_INET)
37 | public static let inet6 = AddressFamily(rawValue: AF_INET6)
38 | public static let ipx = AddressFamily(rawValue: AF_IPX)
39 | public static let netlink = AddressFamily(rawValue: AF_APPLETALK)
40 |
41 | public let rawValue: Int32
42 |
43 | public init(rawValue: Int32) {
44 | self.rawValue = rawValue
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/POSIX/SystemError.swift:
--------------------------------------------------------------------------------
1 | #if os(Linux)
2 | import Glibc
3 | #else
4 | import Darwin.C
5 | #endif
6 |
7 | public enum SystemError : Error {
8 | case operationNotPermitted
9 | case noSuchFileOrDirectory
10 | case noSuchProcess
11 | case interruptedSystemCall
12 | case inputOutputError
13 | case deviceNotConfigured
14 | case argumentListTooLong
15 | case executableFormatError
16 | case badFileDescriptor
17 | case noChildProcesses
18 | case resourceDeadlockAvoided
19 | case cannotAllocateMemory
20 | case permissionDenied
21 | case badAddress
22 |
23 | case blockDeviceRequired
24 |
25 | case deviceOrResourceBusy
26 | case fileExists
27 | case crossDeviceLink
28 | case operationNotSupportedByDevice
29 | case notADirectory
30 | case isADirectory
31 | case invalidArgument
32 | case tooManyOpenFilesInSystem
33 | case tooManyOpenFiles
34 | case inappropriateInputOutputControlForDevice
35 | case textFileBusy
36 | case fileTooLarge
37 | case noSpaceLeftOnDevice
38 | case illegalSeek
39 | case readOnlyFileSystem
40 | case tooManyLinks
41 | case brokenPipe
42 |
43 | /* math software */
44 | case numericalArgumentOutOfDomain
45 | case resultTooLarge
46 |
47 | /* non-blocking and interrupt i/o */
48 | case resourceTemporarilyUnavailable
49 | case operationWouldBlock
50 | case operationNowInProgress
51 | case operationAlreadyInProgress
52 |
53 | /* ipc/network software -- argument errors */
54 | case socketOperationOnNonSocket
55 | case destinationAddressRequired
56 | case messageTooLong
57 | case protocolWrongTypeForSocket
58 | case protocolNotAvailable
59 | case protocolNotSupported
60 |
61 | case socketTypeNotSupported
62 |
63 | case operationNotSupported
64 |
65 | case protocolFamilyNotSupported
66 |
67 | case addressFamilyNotSupportedByProtocolFamily
68 | case addressAlreadyInUse
69 | case cannotAssignRequestedAddress
70 |
71 | /* ipc/network software -- operational errors */
72 | case networkIsDown
73 | case networkIsUnreachable
74 | case networkDroppedConnectionOnReset
75 | case softwareCausedConnectionAbort
76 | case connectionResetByPeer
77 | case noBufferSpaceAvailable
78 | case socketIsAlreadyConnected
79 | case socketIsNotConnected
80 |
81 | case cannotSendAfterSocketShutdown
82 | case tooManyReferences
83 |
84 | case operationTimedOut
85 | case connectionRefused
86 |
87 | case tooManyLevelsOfSymbolicLinks
88 | case fileNameTooLong
89 |
90 | case hostIsDown
91 |
92 | case noRouteToHost
93 | case directoryNotEmpty
94 |
95 | /* quotas & mush */
96 | case tooManyUsers
97 |
98 | case diskQuotaExceeded
99 |
100 | /* Network File System */
101 | case staleFileHandle
102 | case objectIsRemote
103 |
104 | case noLocksAvailable
105 | case functionNotImplemented
106 |
107 | case valueTooLargeForDefinedDataType
108 |
109 | case operationCanceled
110 |
111 | case identifierRemoved
112 | case noMessageOfDesiredType
113 | case illegalByteSequence
114 |
115 | case badMessage
116 | case multihopAttempted
117 | case noDataAvailable
118 | case linkHasBeenSevered
119 | case outOfStreamsResources
120 | case deviceNotAStream
121 | case protocolError
122 | case timerExpired
123 |
124 | case stateNotRecoverable
125 | case previousOwnerDied
126 |
127 | case other(errorNumber: Int32)
128 | }
129 |
130 | extension SystemError {
131 | public init?(errorNumber: Int32) {
132 | switch errorNumber {
133 | case 0: return nil
134 |
135 | case EPERM: self = .operationNotPermitted
136 | case ENOENT: self = .noSuchFileOrDirectory
137 | case ESRCH: self = .noSuchProcess
138 | case EINTR: self = .interruptedSystemCall
139 | case EIO: self = .inputOutputError
140 | case ENXIO: self = .deviceNotConfigured
141 | case E2BIG: self = .argumentListTooLong
142 | case ENOEXEC: self = .executableFormatError
143 | case EBADF: self = .badFileDescriptor
144 | case ECHILD: self = .noChildProcesses
145 | case EDEADLK: self = .resourceDeadlockAvoided
146 | case ENOMEM: self = .cannotAllocateMemory
147 | case EACCES: self = .permissionDenied
148 | case EFAULT: self = .badAddress
149 |
150 | case ENOTBLK: self = .blockDeviceRequired
151 |
152 | case EBUSY: self = .deviceOrResourceBusy
153 | case EEXIST: self = .fileExists
154 | case EXDEV: self = .crossDeviceLink
155 | case ENODEV: self = .operationNotSupportedByDevice
156 | case ENOTDIR: self = .notADirectory
157 | case EISDIR: self = .isADirectory
158 | case EINVAL: self = .invalidArgument
159 | case ENFILE: self = .tooManyOpenFilesInSystem
160 | case EMFILE: self = .tooManyOpenFiles
161 | case ENOTTY: self = .inappropriateInputOutputControlForDevice
162 | case ETXTBSY: self = .textFileBusy
163 | case EFBIG: self = .fileTooLarge
164 | case ENOSPC: self = .noSpaceLeftOnDevice
165 | case ESPIPE: self = .illegalSeek
166 | case EROFS: self = .readOnlyFileSystem
167 | case EMLINK: self = .tooManyLinks
168 | case EPIPE: self = .brokenPipe
169 |
170 | /* math software */
171 | case EDOM: self = .numericalArgumentOutOfDomain
172 | case ERANGE: self = .resultTooLarge
173 |
174 | /* non-blocking and interrupt i/o */
175 | case EAGAIN: self = .resourceTemporarilyUnavailable
176 | case EWOULDBLOCK: self = .operationWouldBlock
177 | case EINPROGRESS: self = .operationNowInProgress
178 | case EALREADY: self = .operationAlreadyInProgress
179 |
180 | /* ipc/network software -- argument errors */
181 | case ENOTSOCK: self = .socketOperationOnNonSocket
182 | case EDESTADDRREQ: self = .destinationAddressRequired
183 | case EMSGSIZE: self = .messageTooLong
184 | case EPROTOTYPE: self = .protocolWrongTypeForSocket
185 | case ENOPROTOOPT: self = .protocolNotAvailable
186 | case EPROTONOSUPPORT: self = .protocolNotSupported
187 |
188 | case ESOCKTNOSUPPORT: self = .socketTypeNotSupported
189 |
190 | case ENOTSUP: self = .operationNotSupported
191 |
192 | case EPFNOSUPPORT: self = .protocolFamilyNotSupported
193 |
194 | case EAFNOSUPPORT: self = .addressFamilyNotSupportedByProtocolFamily
195 | case EADDRINUSE: self = .addressAlreadyInUse
196 | case EADDRNOTAVAIL: self = .cannotAssignRequestedAddress
197 |
198 | /* ipc/network software -- operational errors */
199 | case ENETDOWN: self = .networkIsDown
200 | case ENETUNREACH: self = .networkIsUnreachable
201 | case ENETRESET: self = .networkDroppedConnectionOnReset
202 | case ECONNABORTED: self = .softwareCausedConnectionAbort
203 | case ECONNRESET: self = .connectionResetByPeer
204 | case ENOBUFS: self = .noBufferSpaceAvailable
205 | case EISCONN: self = .socketIsAlreadyConnected
206 | case ENOTCONN: self = .socketIsNotConnected
207 |
208 | case ESHUTDOWN: self = .cannotSendAfterSocketShutdown
209 | case ETOOMANYREFS: self = .tooManyReferences
210 |
211 | case ETIMEDOUT: self = .operationTimedOut
212 | case ECONNREFUSED: self = .connectionRefused
213 |
214 | case ELOOP: self = .tooManyLevelsOfSymbolicLinks
215 | case ENAMETOOLONG: self = .fileNameTooLong
216 |
217 | case EHOSTDOWN: self = .hostIsDown
218 |
219 | case EHOSTUNREACH: self = .noRouteToHost
220 | case ENOTEMPTY: self = .directoryNotEmpty
221 |
222 | /* quotas & mush */
223 | case EUSERS: self = .tooManyUsers
224 |
225 | case EDQUOT: self = .diskQuotaExceeded
226 |
227 | /* Network File System */
228 | case ESTALE: self = .staleFileHandle
229 | case EREMOTE: self = .objectIsRemote
230 |
231 | case ENOLCK: self = .noLocksAvailable
232 | case ENOSYS: self = .functionNotImplemented
233 |
234 | case EOVERFLOW: self = .valueTooLargeForDefinedDataType
235 |
236 | case ECANCELED: self = .operationCanceled
237 |
238 | case EIDRM: self = .identifierRemoved
239 | case ENOMSG: self = .noMessageOfDesiredType
240 | case EILSEQ: self = .illegalByteSequence
241 |
242 | case EBADMSG: self = .badMessage
243 | case EMULTIHOP: self = .multihopAttempted
244 | case ENODATA: self = .noDataAvailable
245 | case ENOLINK: self = .linkHasBeenSevered
246 | case ENOSR: self = .outOfStreamsResources
247 | case ENOSTR: self = .deviceNotAStream
248 | case EPROTO: self = .protocolError
249 | case ETIME: self = .timerExpired
250 |
251 | case ENOTRECOVERABLE: self = .stateNotRecoverable
252 | case EOWNERDEAD: self = .previousOwnerDied
253 | default: self = .other(errorNumber: errorNumber)
254 | }
255 | }
256 | }
257 |
258 | extension SystemError {
259 | public var errorNumber: Int32 {
260 | switch self {
261 | case .operationNotPermitted: return EPERM
262 | case .noSuchFileOrDirectory: return ENOENT
263 | case .noSuchProcess: return ESRCH
264 | case .interruptedSystemCall: return EINTR
265 | case .inputOutputError: return EIO
266 | case .deviceNotConfigured: return ENXIO
267 | case .argumentListTooLong: return E2BIG
268 | case .executableFormatError: return ENOEXEC
269 | case .badFileDescriptor: return EBADF
270 | case .noChildProcesses: return ECHILD
271 | case .resourceDeadlockAvoided: return EDEADLK
272 | case .cannotAllocateMemory: return ENOMEM
273 | case .permissionDenied: return EACCES
274 | case .badAddress: return EFAULT
275 |
276 | case .blockDeviceRequired: return ENOTBLK
277 |
278 | case .deviceOrResourceBusy: return EBUSY
279 | case .fileExists: return EEXIST
280 | case .crossDeviceLink: return EXDEV
281 | case .operationNotSupportedByDevice: return ENODEV
282 | case .notADirectory: return ENOTDIR
283 | case .isADirectory: return EISDIR
284 | case .invalidArgument: return EINVAL
285 | case .tooManyOpenFilesInSystem: return ENFILE
286 | case .tooManyOpenFiles: return EMFILE
287 | case .inappropriateInputOutputControlForDevice: return ENOTTY
288 | case .textFileBusy: return ETXTBSY
289 | case .fileTooLarge: return EFBIG
290 | case .noSpaceLeftOnDevice: return ENOSPC
291 | case .illegalSeek: return ESPIPE
292 | case .readOnlyFileSystem: return EROFS
293 | case .tooManyLinks: return EMLINK
294 | case .brokenPipe: return EPIPE
295 |
296 | /* math software */
297 | case .numericalArgumentOutOfDomain: return EDOM
298 | case .resultTooLarge: return ERANGE
299 |
300 | /* non-blocking and interrupt i/o */
301 | case .resourceTemporarilyUnavailable: return EAGAIN
302 | case .operationWouldBlock: return EWOULDBLOCK
303 | case .operationNowInProgress: return EINPROGRESS
304 | case .operationAlreadyInProgress: return EALREADY
305 |
306 | /* ipc/network software -- argument errors */
307 | case .socketOperationOnNonSocket: return ENOTSOCK
308 | case .destinationAddressRequired: return EDESTADDRREQ
309 | case .messageTooLong: return EMSGSIZE
310 | case .protocolWrongTypeForSocket: return EPROTOTYPE
311 | case .protocolNotAvailable: return ENOPROTOOPT
312 | case .protocolNotSupported: return EPROTONOSUPPORT
313 |
314 | case .socketTypeNotSupported: return ESOCKTNOSUPPORT
315 |
316 | case .operationNotSupported: return ENOTSUP
317 |
318 | case .protocolFamilyNotSupported: return EPFNOSUPPORT
319 |
320 | case .addressFamilyNotSupportedByProtocolFamily: return EAFNOSUPPORT
321 | case .addressAlreadyInUse: return EADDRINUSE
322 | case .cannotAssignRequestedAddress: return EADDRNOTAVAIL
323 |
324 | /* ipc/network software -- operational errors */
325 | case .networkIsDown: return ENETDOWN
326 | case .networkIsUnreachable: return ENETUNREACH
327 | case .networkDroppedConnectionOnReset: return ENETRESET
328 | case .softwareCausedConnectionAbort: return ECONNABORTED
329 | case .connectionResetByPeer: return ECONNRESET
330 | case .noBufferSpaceAvailable: return ENOBUFS
331 | case .socketIsAlreadyConnected: return EISCONN
332 | case .socketIsNotConnected: return ENOTCONN
333 |
334 | case .cannotSendAfterSocketShutdown: return ESHUTDOWN
335 | case .tooManyReferences: return ETOOMANYREFS
336 |
337 | case .operationTimedOut: return ETIMEDOUT
338 | case .connectionRefused: return ECONNREFUSED
339 |
340 | case .tooManyLevelsOfSymbolicLinks: return ELOOP
341 | case .fileNameTooLong: return ENAMETOOLONG
342 |
343 | case .hostIsDown: return EHOSTDOWN
344 |
345 | case .noRouteToHost: return EHOSTUNREACH
346 | case .directoryNotEmpty: return ENOTEMPTY
347 |
348 | /* quotas & mush */
349 | case .tooManyUsers: return EUSERS
350 |
351 | case .diskQuotaExceeded: return EDQUOT
352 |
353 | /* Network File System */
354 | case .staleFileHandle: return ESTALE
355 | case .objectIsRemote: return EREMOTE
356 |
357 | case .noLocksAvailable: return ENOLCK
358 | case .functionNotImplemented: return ENOSYS
359 |
360 | case .valueTooLargeForDefinedDataType: return EOVERFLOW
361 |
362 | case .operationCanceled: return ECANCELED
363 |
364 | case .identifierRemoved: return EIDRM
365 | case .noMessageOfDesiredType: return ENOMSG
366 | case .illegalByteSequence: return EILSEQ
367 |
368 | case .badMessage: return EBADMSG
369 | case .multihopAttempted: return EMULTIHOP
370 | case .noDataAvailable: return ENODATA
371 | case .linkHasBeenSevered: return ENOLINK
372 | case .outOfStreamsResources: return ENOSR
373 | case .deviceNotAStream: return ENOSTR
374 | case .protocolError: return EPROTO
375 | case .timerExpired: return ETIME
376 |
377 | case .stateNotRecoverable: return ENOTRECOVERABLE
378 | case .previousOwnerDied: return EOWNERDEAD
379 |
380 | case .other(let errorNumber): return errorNumber
381 | }
382 | }
383 | }
384 |
385 | extension SystemError : Equatable {}
386 |
387 | public func == (lhs: SystemError, rhs: SystemError) -> Bool {
388 | return lhs.errorNumber == rhs.errorNumber
389 | }
390 |
391 | extension SystemError {
392 | public static func description(for errorNumber: Int32) -> String {
393 | return String(cString: strerror(errorNumber))
394 | }
395 | }
396 |
397 | extension SystemError : CustomStringConvertible {
398 | public var description: String {
399 | return SystemError.description(for: errorNumber)
400 | }
401 | }
402 |
403 | extension SystemError {
404 | public static var lastOperationError: SystemError? {
405 | return SystemError(errorNumber: errno)
406 | }
407 | }
408 |
409 | public func ensureLastOperationSucceeded() throws {
410 | if let error = SystemError.lastOperationError {
411 | throw error
412 | }
413 | }
414 |
--------------------------------------------------------------------------------
/Sources/TCP/Server.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Server.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 4/30/16.
6 | //
7 | //
8 | #if os(Linux)
9 | import Glibc
10 | #else
11 | import Darwin
12 | #endif
13 |
14 | import Dispatch
15 | import StreamKit
16 | import POSIX
17 | import IOStream
18 |
19 | public final class Server {
20 |
21 | public static let defaultReuseAddress = false
22 | public static let defaultReusePort = false
23 |
24 | private let fd: SocketFileDescriptor
25 | private let listeningSource: DispatchSourceRead
26 |
27 | public convenience init(reuseAddress: Bool = defaultReuseAddress, reusePort: Bool = defaultReusePort) throws {
28 | let fd = try SocketFileDescriptor(
29 | socketType: SocketType.stream,
30 | addressFamily: AddressFamily.inet
31 | )
32 | try self.init(fd: fd, reuseAddress: reuseAddress, reusePort: reusePort)
33 | }
34 |
35 | public init(fd: SocketFileDescriptor, reuseAddress: Bool = defaultReuseAddress, reusePort: Bool = defaultReusePort) throws {
36 | self.fd = fd
37 | if reuseAddress {
38 | // Set SO_REUSEADDR
39 | var reuseAddr = 1
40 | let error = setsockopt(
41 | self.fd.rawValue,
42 | SOL_SOCKET,
43 | SO_REUSEADDR,
44 | &reuseAddr,
45 | socklen_t(MemoryLayout.stride)
46 | )
47 | if error != 0 {
48 | throw SystemError(errorNumber: errno)!
49 | }
50 | }
51 |
52 | if reusePort {
53 | // Set SO_REUSEPORT
54 | var reusePort = 1
55 | let error = setsockopt(
56 | self.fd.rawValue,
57 | SOL_SOCKET,
58 | SO_REUSEPORT,
59 | &reusePort,
60 | socklen_t(MemoryLayout.stride)
61 | )
62 | if let systemError = SystemError(errorNumber: error) {
63 | throw systemError
64 | }
65 | }
66 |
67 | self.listeningSource = DispatchSource.makeReadSource(
68 | fileDescriptor: self.fd.rawValue,
69 | queue: .main
70 | )
71 | }
72 |
73 | public func bind(host: String, port: Port) throws {
74 | var addrInfoPointer: UnsafeMutablePointer? = nil
75 |
76 | #if os(Linux)
77 | var hints = Glibc.addrinfo(
78 | ai_flags: 0,
79 | ai_family: fd.addressFamily.rawValue,
80 | ai_socktype: Int32(SOCK_STREAM.rawValue),
81 | ai_protocol: Int32(IPPROTO_TCP),
82 | ai_addrlen: 0,
83 | ai_addr: nil,
84 | ai_canonname: nil,
85 | ai_next: nil
86 | )
87 | #else
88 | var hints = Darwin.addrinfo(
89 | ai_flags: 0,
90 | ai_family: fd.addressFamily.rawValue,
91 | ai_socktype: SOCK_STREAM,
92 | ai_protocol: IPPROTO_TCP,
93 | ai_addrlen: 0,
94 | ai_canonname: nil,
95 | ai_addr: nil,
96 | ai_next: nil
97 | )
98 | #endif
99 |
100 | let ret = getaddrinfo(host, String(port), &hints, &addrInfoPointer)
101 | if let systemError = SystemError(errorNumber: ret) {
102 | throw systemError
103 | }
104 |
105 | let addressInfo = addrInfoPointer!.pointee
106 |
107 | #if os(Linux)
108 | let bindRet = Glibc.bind(
109 | fd.rawValue,
110 | addressInfo.ai_addr,
111 | socklen_t(MemoryLayout.stride)
112 | )
113 | #else
114 | let bindRet = Darwin.bind(
115 | fd.rawValue,
116 | addressInfo.ai_addr,
117 | socklen_t(MemoryLayout.stride)
118 | )
119 | #endif
120 | freeaddrinfo(addrInfoPointer)
121 |
122 | if bindRet != 0 {
123 | throw SystemError(errorNumber: errno)!
124 | }
125 | }
126 |
127 | public func listen(backlog: Int = 32) -> Source {
128 | return Source { [listeningSource = self.listeningSource, fd = self.fd] observer in
129 | #if os(Linux)
130 | let ret = Glibc.listen(fd.rawValue, Int32(backlog))
131 | #else
132 | let ret = Darwin.listen(fd.rawValue, Int32(backlog))
133 | #endif
134 | if ret != 0 {
135 | observer.sendFailed(SystemError(errorNumber: errno)!)
136 | return nil
137 | }
138 | listeningSource.setEventHandler {
139 |
140 | var socketAddress = sockaddr()
141 | var sockLen = socklen_t(MemoryLayout.size)
142 |
143 | // Accept connections
144 | let numPendingConnections: UInt = listeningSource.data
145 | for _ in 0...stride)
71 | )
72 | if error == -1, let systemError = SystemError.lastOperationError {
73 | throw systemError
74 | }
75 | }
76 | do {
77 | let error = setsockopt(
78 | self.socketFD.rawValue,
79 | SOL_SOCKET,
80 | SO_SNDTIMEO,
81 | &timeout,
82 | socklen_t(MemoryLayout.stride)
83 | )
84 | if let systemError = SystemError(errorNumber: error) {
85 | throw systemError
86 | }
87 | }
88 |
89 | if reuseAddress {
90 | // Set SO_REUSEADDR
91 | var reuseAddr = 1
92 | let error = setsockopt(
93 | self.socketFD.rawValue,
94 | SOL_SOCKET,
95 | SO_REUSEADDR,
96 | &reuseAddr,
97 | socklen_t(MemoryLayout.stride)
98 | )
99 | if let systemError = SystemError(errorNumber: error) {
100 | throw systemError
101 | }
102 | }
103 |
104 | if reusePort {
105 | // Set SO_REUSEPORT
106 | var reusePort = 1
107 | let error = setsockopt(
108 | self.socketFD.rawValue,
109 | SOL_SOCKET,
110 | SO_REUSEPORT,
111 | &reusePort,
112 | socklen_t(MemoryLayout.stride)
113 | )
114 | if let systemError = SystemError(errorNumber: error) {
115 | throw systemError
116 | }
117 | }
118 |
119 | // Create the dispatch source for listening
120 | self.channel = DispatchIO(
121 | type: .stream,
122 | fileDescriptor: fd.rawValue,
123 | queue: .main
124 | ) { error in
125 | // Close the file descriptor for the channel
126 | fd.close()
127 |
128 | // Throw any error
129 | if let systemError = SystemError(errorNumber: error) {
130 | try! { throw systemError }()
131 | }
132 | }
133 | }
134 |
135 | public func close() {
136 | channel.close()
137 | }
138 |
139 | public func connect(host: String, port: Port) -> Promise<()> {
140 | return Promise { [socketFD, fd, channel = self.channel] resolve, reject in
141 | var addrInfoPointer: UnsafeMutablePointer? = nil
142 |
143 | #if os(Linux)
144 | var hints = addrinfo(
145 | ai_flags: 0,
146 | ai_family: socketFD.addressFamily.rawValue,
147 | ai_socktype: Int32(SOCK_STREAM.rawValue),
148 | ai_protocol: 0,
149 | ai_addrlen: 0,
150 | ai_addr: nil,
151 | ai_canonname: nil,
152 | ai_next: nil
153 | )
154 | #else
155 | var hints = addrinfo(
156 | ai_flags: 0,
157 | ai_family: socketFD.addressFamily.rawValue,
158 | ai_socktype: SOCK_STREAM,
159 | ai_protocol: 0,
160 | ai_addrlen: 0,
161 | ai_canonname: nil,
162 | ai_addr: nil,
163 | ai_next: nil
164 | )
165 | #endif
166 |
167 | let ret = getaddrinfo(host, String(port), &hints, &addrInfoPointer)
168 | if let systemError = SystemError(errorNumber: ret) {
169 | reject(systemError)
170 | return
171 | }
172 |
173 | let addressInfo = addrInfoPointer!.pointee
174 | #if os(Linux)
175 | let connectRet = Glibc.connect(
176 | fd.rawValue,
177 | addressInfo.ai_addr,
178 | socklen_t(MemoryLayout.stride)
179 | )
180 | #else
181 | let connectRet = Darwin.connect(
182 | fd.rawValue,
183 | addressInfo.ai_addr,
184 | socklen_t(MemoryLayout.stride)
185 | )
186 | #endif
187 | freeaddrinfo(addrInfoPointer)
188 |
189 | // Blocking, connect immediately or throw error
190 | if socketFD.blocking {
191 | if connectRet != 0 {
192 | reject(SystemError(errorNumber: errno)!)
193 | } else {
194 | resolve(())
195 | }
196 | return
197 | }
198 |
199 | // Non-blocking, check for immediate connection
200 | if connectRet == 0 {
201 | resolve(())
202 | return
203 | }
204 |
205 | // Non-blocking, dispatch connection, check errno for connection error.
206 | let error = SystemError(errorNumber: errno)
207 | if case SystemError.operationNowInProgress? = error {
208 | // Wait for channel to be writable. Then we are connected.
209 | channel.write(offset: off_t(), data: .empty, queue: .main) { done, data, error in
210 | var result = 0
211 | var resultLength = socklen_t(MemoryLayout.stride)
212 | let ret = getsockopt(fd.rawValue, SOL_SOCKET, SO_ERROR, &result, &resultLength)
213 | if let systemError = SystemError(errorNumber: ret) {
214 | reject(systemError)
215 | return
216 | }
217 | if let systemError = SystemError(errorNumber: Int32(result)) {
218 | reject(systemError)
219 | return
220 | }
221 | if let systemError = SystemError(errorNumber: error) {
222 | reject(systemError)
223 | return
224 | }
225 | resolve(())
226 | }
227 | } else if let error = error {
228 | reject(error)
229 | }
230 | }
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/Tests/HTTPTests/HTTPMessageTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPMessageTests.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 10/30/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | @testable import HTTP
12 |
13 | class HTTPMessageTests: XCTestCase {
14 |
15 | struct TestMessageType: HTTPMessage {
16 | var version = Version(major: 1, minor: 1)
17 | var rawHeaders: [String] = []
18 | var cookies: [String] {
19 | return lowercasedRawHeaderPairs.filter { (key, value) in
20 | key == "set-cookie"
21 | }.map { $0.1 }
22 | }
23 | var body: Data = Data()
24 | }
25 |
26 | func testHeaders() {
27 | var testMessage = TestMessageType()
28 | testMessage.rawHeaders = [
29 | "Date", "Sun, 30 Oct 2016 09:06:40 GMT",
30 | "Expires", "-1",
31 | "Cache-Control", "private, max-age=0",
32 | "Content-Type", "application/json",
33 | "Content-Type", "text/html; charset=ISO-8859-1",
34 | "P3P", "CP=\"See https://www.google.com/support/accounts/answer/151657?hl=en.\"",
35 | "Server", "gws",
36 | "Server", "gws", // Duplicate servers for test purposes.
37 | "X-XSS-Protection", "1; mode=block",
38 | "X-Frame-Options", "SAMEORIGIN",
39 | "Set-Cookie", "NID=89=c6V5PAWCEOXgvA6TQrNSR8Pnih2iX3Aa3rIQS005IG6WS8RHH" +
40 | "_3YTmymtEk5yMxLkz19C_qr2zBNspy7zwubAVo38-kIdjbArSJcXCBbjCcn_hJ" +
41 | "TEi9grq_ZgHxZTZ5V2YLnH3uxx6U4EA; expires=Mon, 01-May-2017 09:06:40 GMT;" +
42 | " path=/; domain=.google.com; HttpOnly",
43 | "Accept-Ranges", "none",
44 | "Vary", "Accept-Encoding",
45 | "Transfer-Encoding", "chunked"
46 | ]
47 | let expectedHeaders = [
48 | "date": "Sun, 30 Oct 2016 09:06:40 GMT",
49 | "expires": "-1",
50 | "cache-control": "private, max-age=0",
51 | "content-type": "text/html; charset=ISO-8859-1",
52 | "p3p": "CP=\"See https://www.google.com/support/accounts/answer/151657?hl=en.\"",
53 | "server": "gws, gws",
54 | "x-xss-protection": "1; mode=block",
55 | "x-frame-options": "SAMEORIGIN",
56 | "accept-ranges": "none",
57 | "vary": "Accept-Encoding",
58 | "transfer-encoding": "chunked"
59 | ]
60 | XCTAssert(
61 | testMessage.headers == expectedHeaders,
62 | "Actual headers, \(testMessage.headers), did not match expected."
63 | )
64 | let expectedCookies = [
65 | "NID=89=c6V5PAWCEOXgvA6TQrNSR8Pnih2iX3Aa3rIQS005IG6WS8RHH" +
66 | "_3YTmymtEk5yMxLkz19C_qr2zBNspy7zwubAVo38-kIdjbArSJcXCBbjCcn_hJ" +
67 | "TEi9grq_ZgHxZTZ5V2YLnH3uxx6U4EA; expires=Mon, 01-May-2017 09:06:40 GMT;" +
68 | " path=/; domain=.google.com; HttpOnly"
69 | ]
70 | XCTAssert(
71 | testMessage.cookies == expectedCookies,
72 | "Actual cookies, \(testMessage.cookies), did not match expected."
73 | )
74 |
75 | }
76 |
77 | }
78 |
79 | extension HTTPMessageTests {
80 | static var allTests = [
81 | ("testHeaders", testHeaders),
82 | ]
83 | }
84 |
--------------------------------------------------------------------------------
/Tests/HTTPTests/PerformanceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServerTests.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 10/30/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import PromiseKit
12 | @testable import HTTP
13 |
14 | func generateRandomBytes(_ num: Int) -> Data? {
15 | var data = Data(count: num)
16 | let result = data.withUnsafeMutableBytes {
17 | SecRandomCopyBytes(kSecRandomDefault, data.count, $0)
18 | }
19 | if result == errSecSuccess {
20 | return data
21 | } else {
22 | print("Problem generating random bytes")
23 | return nil
24 | }
25 | }
26 | let dataSize = 10_000_000
27 | let oneMBData = generateRandomBytes(dataSize)!
28 | let rootUrl = "http://localhost:3000"
29 |
30 | class PerformanceTests: XCTestCase {
31 |
32 | struct TestError: Error {
33 |
34 | }
35 |
36 | private func postData(path: String) -> Promise<()> {
37 | let session = URLSession(configuration: .default)
38 |
39 | let urlString = rootUrl + path
40 | let url = URL(string: urlString)!
41 | var req = URLRequest(url: url)
42 |
43 | req.httpMethod = "POST"
44 | req.httpBody = oneMBData
45 |
46 | return Promise { resolve, reject in
47 | session.dataTask(with: req) { (data, urlResp, err) in
48 | if let err = err {
49 | reject(err)
50 | }
51 | resolve(())
52 | }.resume()
53 | }
54 | }
55 |
56 | private func getData(path: String) -> Promise {
57 | let session = URLSession(configuration: .default)
58 |
59 | let urlString = rootUrl + path
60 | let url = URL(string: urlString)!
61 | var req = URLRequest(url: url)
62 |
63 | req.httpMethod = "GET"
64 |
65 | return Promise { resolve, reject in
66 | session.dataTask(with: req) { (data, urlResp, err) in
67 | if let err = err {
68 | XCTFail("Error on response: \(err)")
69 | reject(err)
70 | }
71 | guard let data = data else {
72 | XCTFail("No data returned")
73 | reject(TestError())
74 | return
75 | }
76 | resolve(data)
77 | }.resume()
78 | }
79 | }
80 |
81 | private func emptyGet(path: String) -> Promise<()> {
82 | let session = URLSession(configuration: .default)
83 |
84 | let urlString = rootUrl + path
85 | let url = URL(string: urlString)!
86 | var req = URLRequest(url: url)
87 |
88 | req.httpMethod = "GET"
89 |
90 | return Promise { resolve, reject in
91 | session.dataTask(with: req) { (data, urlResp, err) in
92 | if let err = err {
93 | XCTFail("Error on response: \(err)")
94 | reject(err)
95 | }
96 | resolve(())
97 | }.resume()
98 | }
99 | }
100 |
101 | func testPerformanceReceivingData() {
102 | self.measureMetrics(XCTestCase.defaultPerformanceMetrics, automaticallyStartMeasuring: false) {
103 | let app = Router()
104 |
105 | app.post("/post") { request -> Response in
106 | let count = request.body.count
107 | XCTAssertEqual(count, dataSize)
108 | return Response()
109 | }
110 |
111 | let server = HTTP.Server(delegate: app, reusePort: true)
112 | server.listen(host: "0.0.0.0", port: 3000)
113 |
114 | let expectSuccess = expectation(description: "Request was not successful.")
115 |
116 | self.startMeasuring()
117 |
118 | postData(path: "/post").then {
119 | expectSuccess.fulfill()
120 | }.catch { error in
121 | XCTFail(error.localizedDescription)
122 | }
123 |
124 | waitForExpectations(timeout: 5) { error in
125 | self.stopMeasuring()
126 | server.stop()
127 | }
128 | }
129 | }
130 |
131 | func testPerformanceSendingData() {
132 | self.measureMetrics(XCTestCase.defaultPerformanceMetrics, automaticallyStartMeasuring: false) {
133 | let app = Router()
134 |
135 | app.get("/get") { request -> Response in
136 | return Response(body: oneMBData)
137 | }
138 |
139 | let server = HTTP.Server(delegate: app, reusePort: true)
140 | server.listen(host: "0.0.0.0", port: 3000)
141 |
142 | let expectSuccess = expectation(description: "Request was not successful.")
143 |
144 | self.startMeasuring()
145 |
146 | getData(path: "/get").then { data in
147 | expectSuccess.fulfill()
148 | XCTAssertEqual(dataSize, data.count)
149 | }.catch { error in
150 | XCTFail(error.localizedDescription)
151 | }
152 |
153 | waitForExpectations(timeout: 5) { error in
154 | self.stopMeasuring()
155 | server.stop()
156 | }
157 | }
158 | }
159 |
160 | func testPerformanceConcurrentRequests() {
161 | self.measureMetrics(XCTestCase.defaultPerformanceMetrics, automaticallyStartMeasuring: false) {
162 | let app = Router()
163 |
164 | app.get("/get") { request -> Response in
165 | return Response()
166 | }
167 |
168 | let server = HTTP.Server(delegate: app, reusePort: true)
169 | server.listen(host: "0.0.0.0", port: 3000)
170 |
171 | self.startMeasuring()
172 |
173 | for _ in 0..<150 {
174 | let expectSuccess = self.expectation(description: "Request was not successful.")
175 | DispatchQueue.global().async {
176 | self.emptyGet(path: "/get").then {
177 | expectSuccess.fulfill()
178 | }.catch { error in
179 | XCTFail(error.localizedDescription)
180 | }
181 | }
182 | }
183 |
184 | waitForExpectations(timeout: 5) { error in
185 | self.stopMeasuring()
186 | server.stop()
187 | }
188 | }
189 | }
190 |
191 | }
192 |
193 | extension PerformanceTests {
194 | static var allTests = [
195 | ("testPerformanceSendingData", testPerformanceSendingData),
196 | ]
197 | }
198 |
199 |
--------------------------------------------------------------------------------
/Tests/HTTPTests/RequestParserTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import HTTP
3 |
4 | class RequestParserTests: XCTestCase {
5 |
6 | func testInvalidMethod() {
7 | let expectParseError = expectation(description: "Invalid request did not throw an error.")
8 | let parser = RequestParser()
9 | let data = ("INVALID / HTTP/1.1\r\n" + "\r\n")
10 | do {
11 | try parser.parse(Data(data.utf8))
12 | } catch {
13 | expectParseError.fulfill()
14 | }
15 | waitForExpectations(timeout: 0)
16 | }
17 |
18 | func testShortRequests() {
19 | let methods = [
20 | "DELETE",
21 | "GET",
22 | "HEAD",
23 | "POST",
24 | "PUT",
25 | "CONNECT",
26 | "OPTIONS",
27 | "TRACE",
28 | "COPY",
29 | "LOCK",
30 | "MKCOL",
31 | "MOVE",
32 | "PROPFIND",
33 | "PROPPATCH",
34 | "SEARCH",
35 | "UNLOCK",
36 | "BIND",
37 | "REBIND",
38 | "UNBIND",
39 | "ACL",
40 | "REPORT",
41 | "MKACTIVITY",
42 | "CHECKOUT",
43 | "MERGE",
44 | "M-SEARCH",
45 | "NOTIFY",
46 | "SUBSCRIBE",
47 | "UNSUBSCRIBE",
48 | "PATCH",
49 | "PURGE",
50 | "MKCALENDAR",
51 | "LINK",
52 | "UNLINK"
53 | ]
54 | for (i, method) in methods.enumerated() {
55 | var numberParsed = 0
56 | let data = ("\(method) / HTTP/1.1\r\n" + "\r\n")
57 | let parser = RequestParser { request in
58 | numberParsed += 1
59 | XCTAssert(request.method == Method(code: i))
60 | XCTAssert(request.uri.path == "/")
61 | XCTAssert(request.version.major == 1)
62 | XCTAssert(request.version.minor == 1)
63 | XCTAssert(request.rawHeaders.count == 0)
64 | }
65 | do {
66 | try parser.parse(Data(data.utf8))
67 | } catch {
68 | XCTFail("Parsing error \(error) for method \(method)")
69 | }
70 | XCTAssert(
71 | numberParsed == 1,
72 | "Parse produced incorrect number of requests: \(numberParsed)"
73 | )
74 | }
75 | }
76 |
77 | func testDiscontinuousShortRequest() {
78 | var numberParsed = 0
79 | let parser = RequestParser { request in
80 | numberParsed += 1
81 | XCTAssert(request.method == .get)
82 | XCTAssert(request.uri.path == "/")
83 | XCTAssert(request.version.major == 1)
84 | XCTAssert(request.version.minor == 1)
85 | XCTAssert(request.rawHeaders.count == 0)
86 | }
87 | let dataArray = [
88 | "GET / HT",
89 | "TP/1.",
90 | "1\r\n",
91 | "\r\n"
92 | ]
93 | do {
94 | for data in dataArray {
95 | try parser.parse(Data(data.utf8))
96 | }
97 | } catch {
98 | XCTFail("Parsing error \(error).")
99 | }
100 | XCTAssert(numberParsed == 1, "Parse produced incorrect number of requests: \(numberParsed)")
101 | }
102 |
103 | func testMediumRequest() {
104 | var numberParsed = 0
105 | let data = ("GET / HTTP/1.1\r\n" +
106 | "Host: swift.org\r\n" +
107 | "\r\n")
108 | let parser = RequestParser { request in
109 | numberParsed += 1
110 | XCTAssert(request.method == .get)
111 | XCTAssert(request.uri.path == "/")
112 | XCTAssert(request.version.major == 1)
113 | XCTAssert(request.version.minor == 1)
114 | XCTAssert(request.rawHeaders[0] == "Host")
115 | XCTAssert(request.rawHeaders[1] == "swift.org")
116 | }
117 | do {
118 | try parser.parse(Data(data.utf8))
119 | } catch {
120 | XCTFail("Parsing error \(error).")
121 | }
122 | XCTAssert(numberParsed == 1, "Parse produced incorrect number of requests: \(numberParsed)")
123 | }
124 |
125 | func testDiscontinuousMediumRequest() {
126 | var numberParsed = 0
127 | let parser = RequestParser { request in
128 | numberParsed += 1
129 | XCTAssert(request.method == .get)
130 | XCTAssert(request.uri.path == "/")
131 | XCTAssert(request.version.major == 1)
132 | XCTAssert(request.version.minor == 1)
133 | XCTAssert(request.rawHeaders[0] == "Host")
134 | XCTAssert(request.rawHeaders[1] == "swift.org")
135 | }
136 | let dataArray = [
137 | "GET / HTT",
138 | "P/1.1\r\n",
139 | "Hos",
140 | "t: swift.or",
141 | "g\r\n",
142 | "Conten",
143 | "t-Type: appl",
144 | "ication/json\r\n",
145 | "\r",
146 | "\n"
147 | ]
148 | do {
149 | for data in dataArray {
150 | try parser.parse(Data(data.utf8))
151 | }
152 | } catch {
153 | XCTFail("Parsing error \(error).")
154 | }
155 | XCTAssert(numberParsed == 1, "Parse produced incorrect number of requests: \(numberParsed)")
156 | }
157 |
158 | func testDiscontinuousMediumRequestMultipleCookie() {
159 | var numberParsed = 0
160 | let parser = RequestParser { request in
161 | numberParsed += 1
162 | XCTAssert(request.method == .get)
163 | XCTAssert(request.uri.path == "/")
164 | XCTAssert(request.version.major == 1)
165 | XCTAssert(request.version.minor == 1)
166 | XCTAssert(request.rawHeaders[0] == "Host")
167 | XCTAssert(request.rawHeaders[1] == "swift.org")
168 | XCTAssert(request.rawHeaders[2] == "Cookie")
169 | XCTAssert(request.rawHeaders[3] == "server=swift")
170 | XCTAssert(request.rawHeaders[4] == "Cookie")
171 | XCTAssert(request.rawHeaders[5] == "lang=swift")
172 | }
173 | let dataArray = [
174 | "GET / HTT",
175 | "P/1.1\r\n",
176 | "Hos",
177 | "t: swift.or",
178 | "g\r\n",
179 | "C",
180 | "ookie: serv",
181 | "er=swift\r\n",
182 | "C",
183 | "ookie: lan",
184 | "g=swift\r\n",
185 | "\r",
186 | "\n"
187 | ]
188 | do {
189 | for data in dataArray {
190 | try parser.parse(Data(data.utf8))
191 | }
192 | } catch {
193 | XCTFail("Parsing error \(error).")
194 | }
195 | XCTAssert(numberParsed == 1, "Parse produced incorrect number of requests: \(numberParsed)")
196 | }
197 |
198 | func testCompleteRequest() {
199 | var numberParsed = 0
200 | let parser = RequestParser { request in
201 | numberParsed += 1
202 | XCTAssert(request.method == .post)
203 | XCTAssert(request.uri.path == "/")
204 | XCTAssert(request.version.major == 1)
205 | XCTAssert(request.version.minor == 1)
206 | XCTAssert(request.rawHeaders[0] == "Content-Length")
207 | XCTAssert(request.rawHeaders[1] == "5")
208 | }
209 | let data = ("POST / HTTP/1.1\r\n" +
210 | "Content-Length: 5\r\n" +
211 | "\r\n" +
212 | "Swift")
213 | do {
214 | try parser.parse(Data(data.utf8))
215 | } catch {
216 | XCTFail("Parsing error \(error).")
217 | }
218 | XCTAssert(numberParsed == 1, "Parse produced incorrect number of requests: \(numberParsed)")
219 | }
220 |
221 | func testDiscontinuousCompleteRequest() {
222 | var numberParsed = 0
223 | let parser = RequestParser { request in
224 | numberParsed += 1
225 | XCTAssert(request.method == .post)
226 | XCTAssert(request.uri.path == "/profile")
227 | XCTAssert(request.version.major == 1)
228 | XCTAssert(request.version.minor == 1)
229 | XCTAssert(request.rawHeaders[0] == "Content-Length")
230 | XCTAssert(request.rawHeaders[1] == "5")
231 | }
232 | let dataArray = [
233 | "PO",
234 | "ST /pro",
235 | "file HTT",
236 | "P/1.1\r\n",
237 | "Cont",
238 | "ent-Length: 5",
239 | "\r\n",
240 | "\r",
241 | "\n",
242 | "Swi",
243 | "ft"
244 | ]
245 | do {
246 | for data in dataArray {
247 | try parser.parse(Data(data.utf8))
248 | }
249 | } catch {
250 | XCTFail("Parsing error \(error).")
251 | }
252 | XCTAssert(numberParsed == 1, "Parse produced incorrect number of requests: \(numberParsed)")
253 | }
254 |
255 | func testMultipleShortRequestsInTheSameStream() {
256 | var numberParsed = 0
257 | let parser = RequestParser { request in
258 | numberParsed += 1
259 | if numberParsed == 1 {
260 | XCTAssert(request.method == .get)
261 | XCTAssert(request.uri.path == "/")
262 | XCTAssert(request.version.major == 1)
263 | XCTAssert(request.version.minor == 1)
264 | XCTAssert(request.rawHeaders.count == 0)
265 | } else if numberParsed == 2 {
266 | XCTAssert(request.method == .head)
267 | XCTAssert(request.uri.path == "/profile")
268 | XCTAssert(request.version.major == 1)
269 | XCTAssert(request.version.minor == 1)
270 | XCTAssert(request.rawHeaders.count == 0)
271 | }
272 | }
273 | let dataArray = [
274 | "GET / HT",
275 | "TP/1.",
276 | "1\r\n",
277 | "\r\n",
278 | "HEAD /profile HT",
279 | "TP/1.",
280 | "1\r\n",
281 | "\r\n"
282 | ]
283 | do {
284 | for data in dataArray {
285 | try parser.parse(Data(data.utf8))
286 | }
287 | } catch {
288 | XCTFail("Parsing error \(error).")
289 | }
290 | XCTAssert(numberParsed == 2, "Parse produced incorrect number of requests: \(numberParsed)")
291 | }
292 |
293 | func testMultipleShortRequestsInSingleMessage() {
294 | var numberParsed = 0
295 | let parser = RequestParser { request in
296 | numberParsed += 1
297 | if numberParsed == 1 {
298 | XCTAssert(request.method == .get)
299 | XCTAssert(request.uri.path == "/")
300 | XCTAssert(request.version.major == 1)
301 | XCTAssert(request.version.minor == 1)
302 | XCTAssert(request.rawHeaders.count == 0)
303 | } else if numberParsed == 2 {
304 | XCTAssert(request.method == .head)
305 | XCTAssert(request.uri.path == "/profile")
306 | XCTAssert(request.version.major == 1)
307 | XCTAssert(request.version.minor == 1)
308 | XCTAssert(request.rawHeaders.count == 0)
309 | }
310 | }
311 | let data = "GET / HTTP/1.1\r\n\r\nHEAD /profile HTTP/1.1\r\n\r\n"
312 | do {
313 | try parser.parse(Data(data.utf8))
314 | } catch {
315 | XCTFail("Parsing error \(error).")
316 | }
317 | XCTAssert(numberParsed == 2, "Parse produced incorrect number of requests: \(numberParsed)")
318 | }
319 |
320 | func testManyRequests() {
321 | let data = ("POST / HTTP/1.1\r\n" +
322 | "Content-Length: 5\r\n" +
323 | "\r\n" +
324 | "Swift")
325 | self.measure {
326 | var numberParsed = 0
327 | let messageNumber = 10000
328 | let parser = RequestParser { request in
329 | numberParsed += 1
330 | XCTAssert(request.method == .post)
331 | XCTAssert(request.uri.path == "/")
332 | XCTAssert(request.version.major == 1)
333 | XCTAssert(request.version.minor == 1)
334 | XCTAssert(request.rawHeaders[0] == "Content-Length")
335 | XCTAssert(request.rawHeaders[1] == "5")
336 | }
337 | for _ in 0 ..< messageNumber {
338 | do {
339 | try parser.parse(Data(data.utf8))
340 | } catch {
341 | XCTFail("Parsing error \(error).")
342 | }
343 | }
344 | XCTAssert(
345 | numberParsed == messageNumber,
346 | "Parse produced incorrect number of requests: \(numberParsed)"
347 | )
348 | }
349 | }
350 | }
351 |
352 | extension RequestParserTests {
353 | static var allTests: [(String, (RequestParserTests) -> () throws -> Void)] {
354 | return [
355 | ("testInvalidMethod", testInvalidMethod),
356 | ("testShortRequests", testShortRequests),
357 | ("testDiscontinuousShortRequest", testDiscontinuousShortRequest),
358 | ("testMediumRequest", testMediumRequest),
359 | ("testDiscontinuousMediumRequest", testDiscontinuousMediumRequest),
360 | ("testDiscontinuousMediumRequestMultipleCookie",
361 | testDiscontinuousMediumRequestMultipleCookie),
362 | ("testCompleteRequest", testCompleteRequest),
363 | ("testDiscontinuousCompleteRequest", testDiscontinuousCompleteRequest),
364 | ("testMultipleShortRequestsInTheSameStream", testMultipleShortRequestsInTheSameStream),
365 | ("testMultipleShortRequestsInSingleMessage", testMultipleShortRequestsInSingleMessage),
366 | ("testManyRequests", testManyRequests),
367 | ]
368 | }
369 | }
370 |
--------------------------------------------------------------------------------
/Tests/HTTPTests/RequestSerializationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RequestSerializationTests.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 10/30/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | @testable import HTTP
12 |
13 | class RequestSerializationTests: XCTestCase {
14 |
15 | func testBasicSerialization() {
16 | let expected = "GET / HTTP/1.1\r\n\r\n"
17 | let request = Request(
18 | method: .get,
19 | uri: URL(string: "/")!,
20 | version: Version(major: 1, minor: 1),
21 | rawHeaders: [],
22 | body: Data()
23 | )
24 | let actual = String(bytes: request.serialized, encoding: .utf8)!
25 | XCTAssert(expected == actual, "Actual request, \(actual), did not match expected.")
26 | }
27 |
28 | func testHeaderSerialization() {
29 | let expected = "GET / HTTP/1.1\r\nAccept: */*\r\n" +
30 | "Host: www.google.com\r\nConnection: Keep-Alive\r\n\r\n"
31 | let request = Request(
32 | method: .get,
33 | uri: URL(string: "/")!,
34 | version: Version(major: 1, minor: 1),
35 | rawHeaders: ["Accept", "*/*", "Host", "www.google.com", "Connection", "Keep-Alive"],
36 | body: Data()
37 | )
38 | let actual = String(bytes: request.serialized, encoding: .utf8)!
39 | XCTAssert(expected == actual, "Actual request, \(actual), did not match expected.")
40 | }
41 |
42 | func testDefaultParameters() {
43 | let expected = "GET / HTTP/1.1\r\n\r\n"
44 | let request = Request(
45 | method: .get,
46 | uri: URL(string: "/")!
47 | )
48 | let actual = String(bytes: request.serialized, encoding: .utf8)!
49 | XCTAssert(expected == actual, "Actual request, \(actual), did not match expected.")
50 | }
51 |
52 | }
53 |
54 | extension RequestSerializationTests {
55 | static var allTests = [
56 | ("testBasicSerialization", testBasicSerialization),
57 | ("testHeaderSerialization", testHeaderSerialization),
58 | ("testDefaultParameters", testDefaultParameters),
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/Tests/HTTPTests/ResponseParserTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import HTTP
3 |
4 | class ResponseParserTests: XCTestCase {
5 |
6 | let status = [
7 | 100: "Continue",
8 | 101: "Switching Protocols",
9 | 102: "Processing", // RFC 2518, obsoleted by RFC 4918
10 | 200: "OK",
11 | 201: "Created",
12 | 202: "Accepted",
13 | 203: "Non-Authoritative Information",
14 | 204: "No Content",
15 | 205: "Reset Content",
16 | 206: "Partial Content",
17 | 207: "Multi-Status", // RFC 4918
18 | 208: "Already Reported",
19 | 226: "IM Used",
20 | 300: "Multiple Choices",
21 | 301: "Moved Permanently",
22 | 302: "Found",
23 | 303: "See Other",
24 | 304: "Not Modified",
25 | 305: "Use Proxy",
26 | 307: "Temporary Redirect",
27 | 308: "Permanent Redirect", // RFC 7238
28 | 400: "Bad Request",
29 | 401: "Unauthorized",
30 | 402: "Payment Required",
31 | 403: "Forbidden",
32 | 404: "Not Found",
33 | 405: "Method Not Allowed",
34 | 406: "Not Acceptable",
35 | 407: "Proxy Authentication Required",
36 | 408: "Request Timeout",
37 | 409: "Conflict",
38 | 410: "Gone",
39 | 411: "Length Required",
40 | 412: "Precondition Failed",
41 | 413: "Request Entity Too Large",
42 | 414: "Request URI Too Long",
43 | 415: "Unsupported Media Type",
44 | 416: "Requested Range Not Satisfiable",
45 | 417: "Expectation Failed",
46 | 418: "I'm a teapot", // RFC 2324
47 | 421: "Misdirected Request",
48 | 422: "Unprocessable Entity", // RFC 4918
49 | 423: "Locked", // RFC 4918
50 | 424: "Failed Dependency", // RFC 4918
51 | 425: "Unordered Collection", // RFC 4918
52 | 426: "Upgrade Required", // RFC 2817
53 | 428: "Precondition Required", // RFC 6585
54 | 429: "Too Many Requests", // RFC 6585
55 | 431: "Request Header Fields Too Large", // RFC 6585
56 | 451: "Unavailable For Legal Reasons",
57 | 500: "Internal Server Error",
58 | 501: "Not Implemented",
59 | 502: "Bad Gateway",
60 | 503: "Service Unavailable",
61 | 504: "Gateway Timeout",
62 | 505: "HTTP Version Not Supported",
63 | 506: "Variant Also Negotiates", // RFC 2295
64 | 507: "Insufficient Storage", // RFC 4918
65 | 508: "Loop Detected",
66 | 509: "Bandwidth Limit Exceeded",
67 | 510: "Not Extended", // RFC 2774
68 | 511: "Network Authentication Required" // RFC 6585
69 | ]
70 | let contentLengthOptional = Set([
71 | 100,
72 | 101,
73 | 102,
74 | 204,
75 | 304,
76 | ])
77 |
78 | func testInvalidResponse() {
79 | let parser = ResponseParser()
80 | let data = ("FTP/1.1 200 OK\r\n\r\n")
81 | XCTAssertThrowsError(
82 | try parser.parse(Data(data.utf8)),
83 | "Invalid request did not throw an error."
84 | )
85 | }
86 |
87 | func testShortResponse() {
88 | for (code, reasonPhrase) in status {
89 | var numberParsed = 0
90 | let contentLength = contentLengthOptional.contains(code) ? "" : "Content-Length: 0\r\n"
91 | let data = "HTTP/1.1 \(code) \(reasonPhrase)\r\n\(contentLength)\r\n"
92 | let parser = ResponseParser { response in
93 | numberParsed += 1
94 | XCTAssert(response.status == Status(code: code))
95 | XCTAssert(
96 | response.status.reasonPhrase == reasonPhrase,
97 | "Reason phrase mismatch: \(response.status.code)," +
98 | " \(response.status.reasonPhrase) vs \(reasonPhrase)"
99 | )
100 | XCTAssert(response.version.major == 1)
101 | XCTAssert(response.version.minor == 1)
102 | XCTAssert(response.headers.count == (contentLength == "" ? 0 : 1))
103 | }
104 | do {
105 | try parser.parse(Data(data.utf8))
106 | } catch {
107 | XCTFail("Parsing error \(error) for \(data)")
108 | }
109 | XCTAssert(
110 | numberParsed == 1,
111 | "Parse produced incorrect number of requests" +
112 | ", \(numberParsed), for message: \(data)"
113 | )
114 | }
115 | }
116 |
117 | }
118 |
119 | extension ResponseParserTests {
120 | static var allTests: [(String, (ResponseParserTests) -> () throws -> Void)] {
121 | return [
122 | ("testInvalidResponse", testInvalidResponse),
123 | ("testShortResponse", testShortResponse),
124 | ]
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Tests/HTTPTests/ResponseSerializationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResponseSerializationTests.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 10/30/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | @testable import HTTP
12 |
13 | class ResponseSerializationTests: XCTestCase {
14 |
15 | func testBasicSerialization() {
16 | let expected = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
17 | let response = Response(
18 | version: Version(major: 1, minor: 1),
19 | status: .ok,
20 | rawHeaders: [],
21 | body: Data()
22 | )
23 | let actual = String(bytes: response.serialized, encoding: .utf8)!
24 | XCTAssert(expected == actual, "Actual response, \(actual), did not match expected.")
25 | }
26 |
27 | func testHeaderSerialization() {
28 | let expected =
29 | "HTTP/1.1 200 OK\r\n" +
30 | "Date: Sun, 30 Oct 2016 09:06:40 GMT\r\n" +
31 | "Content-Type: text/html; charset=ISO-8859-1\r\n" +
32 | "Content-Length: 0\r\n" +
33 | "\r\n"
34 | let response = Response(
35 | version: Version(major: 1, minor: 1),
36 | status: .ok,
37 | rawHeaders: [
38 | "Date", "Sun, 30 Oct 2016 09:06:40 GMT",
39 | "Content-Type", "text/html; charset=ISO-8859-1"
40 | ],
41 | body: Data()
42 | )
43 | let actual = String(bytes: response.serialized, encoding: .utf8)!
44 | XCTAssert(expected == actual, "Actual request, \(actual), did not match expected.")
45 | }
46 |
47 | func testDefaultParameters() {
48 | let expected = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
49 | let response = Response(status: .ok)
50 | let actual = String(bytes: response.serialized, encoding: .utf8)!
51 | XCTAssert(expected == actual, "Actual response, \(actual), did not match expected.")
52 | }
53 |
54 | func testJSONSerialization() {
55 | let expected = "HTTP/1.1 200 OK\r\n" +
56 | "Content-Type: application/json\r\n" +
57 | "Content-Length: 31\r\n" +
58 | "\r\n{\"message\":\"Message received!\"}"
59 | let response = try! Response(json: ["message": "Message received!"])
60 | let actual = String(bytes: response.serialized, encoding: .utf8)!
61 | XCTAssert(expected == actual, "Actual response, \(actual), did not match expected.")
62 | }
63 |
64 | }
65 |
66 | extension ResponseSerializationTests {
67 | static var allTests = [
68 | ("testBasicSerialization", testBasicSerialization),
69 | ("testHeaderSerialization", testHeaderSerialization),
70 | ("testDefaultParameters", testDefaultParameters),
71 | ("testJSONSerialization", testJSONSerialization),
72 | ]
73 | }
74 |
--------------------------------------------------------------------------------
/Tests/HTTPTests/ServerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServerTests.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 10/30/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | @testable import HTTP
12 |
13 | class ServerTests: XCTestCase {
14 |
15 | private func sendRequest(path: String, method: String) {
16 | let json = ["message": "Message to server!"]
17 | let jsonResponse = ["message": "Message received!"]
18 | let session = URLSession(configuration: .default)
19 | let rootUrl = "http://localhost:3001"
20 | let responseExpectation = expectation(
21 | description: "Did not receive a response for path: \(path)"
22 | )
23 | let urlString = rootUrl + path
24 | let url = URL(string: urlString)!
25 | var req = URLRequest(url: url)
26 | req.httpMethod = method
27 | req.addValue("application/json", forHTTPHeaderField: "Content-Type")
28 | if method == "POST" {
29 | do {
30 | req.httpBody = try JSONSerialization.data(withJSONObject: json)
31 | } catch let error {
32 | XCTFail(String(describing: error))
33 | }
34 | }
35 | session.dataTask(with: req) { (data, urlResp, err) in
36 | responseExpectation.fulfill()
37 | if let err = err {
38 | XCTFail("Error on response: \(err)")
39 | }
40 | guard let data = data else {
41 | XCTFail("No data returned")
42 | return
43 | }
44 | guard let stringBody = try? JSONSerialization.jsonObject(with: data) else {
45 | XCTFail("Problem deserializing body")
46 | return
47 | }
48 | guard let body = stringBody as? [String:String] else {
49 | XCTFail("Body not well formed json")
50 | return
51 | }
52 | XCTAssert(body == jsonResponse, "Received body \(body) != json \(jsonResponse)")
53 | }.resume()
54 | }
55 |
56 | func testServer() {
57 | let json = ["message": "Message to server!"]
58 | let jsonResponse = ["message": "Message received!"]
59 |
60 | let postRequestExpectation = expectation(description: "Did not receive a POST request.")
61 | let getRequestExpectation = expectation(description: "Did not receive a GET request.")
62 | func handleRequest(request: Request) -> Response {
63 | if request.method == .post {
64 | let data = Data(request.body)
65 | guard let stringBody = try? JSONSerialization.jsonObject(with: data) else {
66 | XCTFail("Problem deserializing body")
67 | fatalError()
68 | }
69 | guard let body = stringBody as? [String:String] else {
70 | XCTFail("Body not well formed json")
71 | fatalError()
72 | }
73 | XCTAssert(body == json, "Received body \(body) != json \(json)")
74 | postRequestExpectation.fulfill()
75 | } else if request.method == .get {
76 | getRequestExpectation.fulfill()
77 | }
78 | return try! Response(json: jsonResponse)
79 | }
80 |
81 | let server = HTTP.Server(reusePort: true)
82 | server.clients(host: "0.0.0.0", port: 3001).startWithNext { client in
83 |
84 | let requestStream = server.parse(data: client
85 | .read())
86 | .map(handleRequest)
87 |
88 | requestStream.onNext { response in
89 | let writeStream = client.write(buffer: response.serialized)
90 | writeStream.onFailed { err in
91 | XCTFail(String(describing: err))
92 | }
93 | writeStream.start()
94 | }
95 |
96 | requestStream.onFailed { clientError in
97 | XCTFail("ClientError: \(clientError)")
98 | }
99 |
100 | requestStream.onCompleted {
101 |
102 | }
103 |
104 | requestStream.start()
105 | }
106 |
107 | sendRequest(path: "", method: "POST")
108 | sendRequest(path: "", method: "GET")
109 |
110 | waitForExpectations(timeout: 1) { error in
111 | server.stop()
112 | }
113 | }
114 |
115 | }
116 |
117 | extension ServerTests {
118 | static var allTests = [
119 | ("testServer", testServer),
120 | ]
121 | }
122 |
--------------------------------------------------------------------------------
/Tests/HTTPTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !os(macOS)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(RequestParserTests.allTests),
7 | testCase(RequestSerializationTests.allTests),
8 | testCase(ResponseSerializationTests.allTests),
9 | testCase(RequestParserTests.allTests),
10 | testCase(HTTPMessageTests.allTests),
11 | testCase(ServerTests.allTests),
12 | ]
13 | }
14 | #endif
15 |
--------------------------------------------------------------------------------
/Tests/IOStreamTests/PipeTests.swift:
--------------------------------------------------------------------------------
1 | @testable import IOStream
2 | import XCTest
3 |
4 | class PipeTests: XCTestCase {
5 |
6 | func testPipes() {
7 |
8 | // let receiveExpectation = expectation(description: "Did not receive data from stdin.")
9 | let stdin = Pipe(fd: .stdin)
10 | let stdout = Pipe(fd: .stdout)
11 |
12 | // Write to stdout
13 | let outStream = stdout.write(buffer: Data("Send it in!\n".utf8))
14 | outStream.onFailed { err in
15 | XCTFail(String(describing: err))
16 | }
17 | outStream.start()
18 |
19 | // Read from stdin
20 | let inStream = stdin.read()
21 | inStream.onNext { data in
22 | // TODO: Some pipe magic to write data to stdin during the test.
23 | // receiveExpectation.fulfill()
24 | }
25 | inStream.onFailed { err in
26 | XCTFail(String(describing: err))
27 | }
28 | inStream.start()
29 |
30 | }
31 |
32 | }
33 |
34 | extension PipeTests {
35 | static var allTests = [
36 | ("testPipes", testPipes),
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/IOStreamTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !os(macOS)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(PipeTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import HTTPTests
4 | import TCPTests
5 | import IOStreamTests
6 |
7 | var tests = [XCTestCaseEntry]()
8 | tests += HTTPTests.allTests()
9 | tests += TCPTests.allTests()
10 | tests += IOStreamTests.allTests()
11 | XCTMain(tests)
12 |
--------------------------------------------------------------------------------
/Tests/RoutingTests/RouterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServerTests.swift
3 | // Edge
4 | //
5 | // Created by Tyler Fleming Cloutier on 10/30/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | @testable import HTTP
12 |
13 | class TestError: Error {}
14 |
15 | class RouterTests: XCTestCase {
16 |
17 | private func sendRequest(path: String, method: String, status: Int = 200, queryString: String = "") {
18 | let session = URLSession(configuration: .default)
19 | let jsonResponse = ["message": "Message received!"]
20 | let rootUrl = "http://localhost:3000"
21 | let jsonRequest = ["message": "Message to server!"]
22 | let responseExpectation = expectation(
23 | description: "Did not receive a response for path: \(path)"
24 | )
25 | let urlString = (rootUrl +
26 | path.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed)! +
27 | (queryString != "" ? "?" + queryString : ""))
28 | let url = URL(string: urlString)!
29 | var req = URLRequest(url: url)
30 | req.httpMethod = method
31 | req.addValue("application/json", forHTTPHeaderField: "Content-Type")
32 | if method == "POST" {
33 | req.httpBody = try! JSONSerialization.data(withJSONObject: jsonRequest)
34 | }
35 | session.dataTask(with: req) { (data, urlResp, err) in
36 | responseExpectation.fulfill()
37 | if let err = err {
38 | XCTFail("Error on response: \(err)")
39 | }
40 | guard let data = data else {
41 | XCTFail("No data returned")
42 | return
43 | }
44 | if method == "POST" {
45 | guard let stringBody = try? JSONSerialization.jsonObject(with: data) else {
46 | XCTFail("Problem deserializing body")
47 | fatalError()
48 | }
49 | guard let body = stringBody as? [String:String] else {
50 | XCTFail("Body not well formed json")
51 | fatalError()
52 | }
53 | XCTAssert(body == jsonResponse, "Received body \(body) != json \(jsonResponse)")
54 | }
55 | }.resume()
56 | }
57 |
58 | func testRouting() {
59 | let requestExpectation = expectation(description: "Did not receive any request.")
60 | let userExpectation = expectation(description: "Did not receive a user request.")
61 | let loginExpectation = expectation(description: "Did not receive a login request.")
62 |
63 | var someRequest = false
64 | let jsonResponse = ["message": "Message received!"]
65 |
66 | let app = Router()
67 | app.map { request in
68 | if !someRequest {
69 | requestExpectation.fulfill()
70 | someRequest = true
71 | }
72 | return request
73 | }
74 |
75 | let api = Router()
76 | let comics = Router()
77 |
78 | func middleware1(request: Request) -> Request {
79 | return request
80 | }
81 |
82 | func middleware2(request: Request) -> Request {
83 | return request
84 | }
85 |
86 | // Authentication
87 | let authentication = Router()
88 | authentication.map(middleware1)
89 | authentication.map(middleware2)
90 | authentication.filter { _ in true }
91 |
92 | authentication.post("/login") { request -> Response in
93 | loginExpectation.fulfill()
94 | return try! Response(json: jsonResponse)
95 | }
96 |
97 | authentication.post("/register") { request in
98 | return Response(status: .ok)
99 | }
100 |
101 | // Users
102 | let users = Router()
103 |
104 | users.get { request -> Response in
105 | userExpectation.fulfill()
106 | return try! Response(json: jsonResponse)
107 | }
108 |
109 | api.add(authentication)
110 | api.add("/users", users)
111 | api.add("/comics", comics)
112 |
113 | authentication.post("/login2") { _ in
114 | return try! Response(json: jsonResponse)
115 | }
116 |
117 | let notFound = Router()
118 | notFound.any { request in
119 | return Response(status: .notFound)
120 | }
121 |
122 | app.add("/v1.0", api)
123 | app.add(notFound)
124 |
125 | let server = HTTP.Server(delegate: app, reusePort: true)
126 | server.listen(host: "0.0.0.0", port: 3000)
127 |
128 | sendRequest(path: "/v1.0/users", method: "GET")
129 | sendRequest(path: "/v1.0/login", method: "POST")
130 | sendRequest(path: "/v1.0/login2", method: "POST")
131 | sendRequest(path: "/v1.0/login3", method: "GET")
132 |
133 | waitForExpectations(timeout: 1) { error in
134 | server.stop()
135 | }
136 | }
137 |
138 | func testRouteMatching() {
139 | let app = Router()
140 | let routeMatch = expectation(description: "Did not receive any request.")
141 | var count = 0
142 |
143 | app.get("/foo/:bar/*") { request -> Response in
144 | count += 1
145 | if count == 2 {
146 | routeMatch.fulfill()
147 | }
148 | XCTAssertNotNil(request.parameters["bar"])
149 | XCTAssertNotNil(request.parameters["0"])
150 | return Response(status: .ok)
151 | }
152 |
153 | let server = HTTP.Server(delegate: app, reusePort: true)
154 | server.listen(host: "0.0.0.0", port: 3000)
155 |
156 | sendRequest(path: "/foo/users/", method: "GET")
157 | sendRequest(path: "/foo/users/asdf", method: "GET")
158 |
159 | waitForExpectations(timeout: 1) { error in
160 | server.stop()
161 | }
162 | }
163 |
164 | func testParameters() {
165 | let sub = Router()
166 | let expectParams = self.expectation(description: "Expect to hit the API with params.")
167 | sub.get("/far") { request -> Response in
168 | expectParams.fulfill()
169 | XCTAssertEqual(request.parameters["bar"], "users are")
170 | return Response()
171 | }
172 |
173 | let app = Router()
174 | app.add("/foo/:bar", sub)
175 |
176 | let server = HTTP.Server(delegate: app, reusePort: true)
177 | server.listen(host: "0.0.0.0", port: 3000)
178 |
179 | sendRequest(path: "/foo/users are/far", method: "GET")
180 |
181 | waitForExpectations(timeout: 1) { error in
182 | server.stop()
183 | }
184 |
185 | }
186 |
187 | func testQueryParameters() {
188 | let sub = Router()
189 | let expectQueryParams = self.expectation(description: "Expect to hit the API with query params.")
190 | sub.get("/far") { request -> Response in
191 | expectQueryParams.fulfill()
192 | XCTAssertEqual(request.queryParameters["bar"], "true")
193 | return Response()
194 | }
195 |
196 | let app = Router()
197 | app.add("/foo/:bar", sub)
198 |
199 | let server = HTTP.Server(delegate: app, reusePort: true)
200 | server.listen(host: "0.0.0.0", port: 3000)
201 |
202 | sendRequest(path: "/foo/users/far", method: "GET", queryString: "bar=true")
203 |
204 | waitForExpectations(timeout: 1) { error in
205 | server.stop()
206 | }
207 |
208 | }
209 |
210 | func testMiddleware() {
211 | let a = Router()
212 | let b = Router()
213 | b.filter { request in
214 | return request.uri.path == "/test"
215 | }
216 | b.map { request in
217 | XCTAssert(request.uri.path == "/test", "Filter did not work.")
218 | return Request(
219 | method: request.method,
220 | uri: request.uri,
221 | version: request.version,
222 | rawHeaders: request.rawHeaders,
223 | body: Data("Hehe, changin' the body.".utf8)
224 | )
225 | }
226 |
227 | a.any { request -> Response in
228 | XCTAssert(request.body.count == 0, "Body was transformed but should not have been.")
229 | return Response(status: .notFound)
230 | }
231 |
232 | b.get { request -> Response in
233 | XCTAssert(
234 | "Hehe, changin' the body." == String(
235 | data: Data(request.body),
236 | encoding: .utf8
237 | )!,
238 | "Body did not match expected transformed body."
239 | )
240 | return Response(status: .ok)
241 | }
242 |
243 | sendRequest(path: "/test", method: "GET")
244 | sendRequest(path: "/not_test", method: "GET", status: 404)
245 |
246 | a.add(b)
247 |
248 | let server = HTTP.Server(delegate: a, reusePort: true)
249 | server.listen(host: "0.0.0.0", port: 3000)
250 | waitForExpectations(timeout: 1) { error in
251 | server.stop()
252 | }
253 | }
254 |
255 | func testErrorHandling() {
256 | func willHalt() -> Bool {
257 | return true
258 | }
259 | let expectRequest = self.expectation(description: "Expect request")
260 | let expectError = self.expectation(description: "Expect error")
261 | let expectSecondError = self.expectation(description: "Expect second error")
262 | let app = Router()
263 | app.get("/foo") { request in
264 | expectRequest.fulfill()
265 | if willHalt() {
266 | throw TestError()
267 | }
268 | return Response()
269 | }
270 | app.any { (request, error) -> Response in
271 | expectError.fulfill()
272 | XCTAssertNotNil(error as? TestError)
273 | if willHalt() {
274 | throw TestError()
275 | }
276 | return Response()
277 | }
278 | app.any { (request, error) -> Response in
279 | expectSecondError.fulfill()
280 | XCTAssertNotNil(error as? TestError)
281 | return Response(status: .ok)
282 | }
283 |
284 | let server = HTTP.Server(delegate: app, reusePort: true)
285 | server.listen(host: "0.0.0.0", port: 3000)
286 |
287 | sendRequest(path: "/foo", method: "GET", queryString: "bar=true")
288 |
289 | waitForExpectations(timeout: 1) { error in
290 | server.stop()
291 | }
292 | }
293 |
294 | }
295 |
296 | extension RouterTests {
297 | static var allTests = [
298 | ("testRouting", testRouting),
299 | ("testRouteMatching", testRouteMatching),
300 | ("testParameters", testParameters),
301 | ("testQueryParameters", testQueryParameters),
302 | ("testErrorHandling", testErrorHandling),
303 | ("testMiddleware", testMiddleware)
304 | ]
305 | }
306 |
--------------------------------------------------------------------------------
/Tests/RoutingTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !os(macOS)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(RouterTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/Tests/TCPTests/ConnectionTests.swift:
--------------------------------------------------------------------------------
1 | @testable import TCP
2 | import XCTest
3 |
4 | class ConnectionTests: XCTestCase {
5 |
6 | func testClientServer() throws {
7 |
8 | let interruptExpectation = expectation(
9 | description: "Did not receive an interrupted read."
10 | )
11 | let receiveMessageExpectation = expectation(description: "Did not receive any message.")
12 | let completeWriteExpectation = expectation(description: "Did not complete write.")
13 |
14 | let server = try Server(reusePort: true)
15 |
16 | while (try? server.bind(host: "localhost", port: 50000)) == nil {
17 | RunLoop.current.run(until: Date(timeIntervalSinceNow: 1))
18 | }
19 |
20 | let connections = server.listen()
21 | connections.startWithNext { connection in
22 | let strings = connection
23 | .read()
24 | .map { String(bytes: $0, encoding: .utf8)! }
25 |
26 | strings.onNext { message in
27 | receiveMessageExpectation.fulfill()
28 | XCTAssert(message == "This is a test", "Incorrect message.")
29 | strings.stop()
30 | }
31 |
32 | strings.onInterrupted {
33 | interruptExpectation.fulfill()
34 | }
35 |
36 | strings.onFailed { error in
37 | XCTFail("Read failed with error: \(error)")
38 | }
39 |
40 | strings.onCompleted {
41 | XCTFail("Completed instead of interrupt.")
42 | }
43 |
44 | strings.start()
45 | }
46 |
47 | let socket = try Socket(reusePort: true)
48 | socket.connect(host: "localhost", port: 50000).then {
49 | let buffer = Data("This is a test".utf8)
50 | let write = socket.write(buffer: buffer)
51 | write.onCompleted {
52 | completeWriteExpectation.fulfill()
53 | }
54 | write.onFailed { error in
55 | XCTFail("Write failed with error: \(error)")
56 | }
57 | write.start()
58 | }.catch { error in
59 | XCTFail("Connection failed with error: \(error)")
60 | }
61 |
62 | waitForExpectations(timeout: 1)
63 | connections.stop()
64 | }
65 |
66 | func testResourceCleanUp() {
67 | // Create two servers consecutively
68 | try! testClientServer()
69 | try! testClientServer()
70 | }
71 |
72 | }
73 |
74 | extension ConnectionTests {
75 | static var allTests = [
76 | ("testClientServer", testClientServer),
77 | ("testResourceCleanUp", testResourceCleanUp),
78 | ]
79 | }
80 |
--------------------------------------------------------------------------------
/Tests/TCPTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !os(macOS)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(ConnectionTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | services:
4 | test:
5 | image: swiftdocker/swift
6 | volumes:
7 | - .:/test
8 | working_dir: /test
9 | privileged: true
10 | command: bash -c "swift build --clean && swift build && swift build -c release && swift test"
11 |
--------------------------------------------------------------------------------