├── .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 | Edge 3 |
Serverside non-blocking IO in Swift
4 | Ask questions in our Slack channel!
5 |

6 | 7 | 8 | # Lightning 9 | ##### (formerly Edge) 10 | 11 | ![Swift](http://img.shields.io/badge/swift-4.0.2-brightgreen.svg) 12 | [![Build Status](https://travis-ci.org/skylab-inc/Lightning.svg?branch=master)](https://travis-ci.org/skylab-inc/Lightning) 13 | [![codecov](https://codecov.io/gh/skylab-inc/Lightning/branch/master/graph/badge.svg)](https://codecov.io/gh/skylab-inc/Lightning) 14 | [![Slack Status](https://slackin-on-edge.herokuapp.com/badge.svg)](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 | --------------------------------------------------------------------------------