├── Tests ├── LinuxMain.swift └── PerfectNIOTests │ ├── XCTestManifests.swift │ └── PerfectNIOTests.swift ├── Sources ├── PerfectNIO │ ├── RouteDescription.swift │ ├── HTTPRequestInfo.swift │ ├── HTTPOutput │ │ ├── ErrorOutput.swift │ │ ├── JSONOutput.swift │ │ ├── TextOutput.swift │ │ ├── BytesOutput.swift │ │ ├── MustacheOutput.swift │ │ ├── HTTPOutput.swift │ │ ├── FileOutput.swift │ │ └── CompressedOutput.swift │ ├── HTTPHead.swift │ ├── RouteCRUD.swift │ ├── HTTPRequest.swift │ ├── HandlerState.swift │ ├── ServerRegistry.swift │ ├── PerfectNIO.swift │ ├── RouteDictionary.swift │ ├── QueryDecoder.swift │ ├── RequestDecoder.swift │ ├── RouteServer.swift │ ├── NIOHTTPHandler.swift │ ├── MimeReader.swift │ ├── WebSocketHandler.swift │ └── RouteRegistry.swift └── PerfectNIOExe │ └── main.swift ├── Package.swift ├── .gitignore ├── LICENSE └── README.md /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import PerfectNIOTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += PerfectNIOTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/PerfectNIOTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !os(macOS) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(PerfectNIOTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/RouteDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteDescription.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2019-05-02. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Routes: CustomStringConvertible { 11 | public var description: String { 12 | return registry.routes.keys.joined(separator: "\n") 13 | } 14 | } 15 | 16 | public struct RouteDescription { 17 | let uri: String 18 | } 19 | 20 | extension RouteDescription: CustomStringConvertible { 21 | public var description: String { 22 | return uri 23 | } 24 | } 25 | 26 | public extension Routes where InType == HTTPRequest, OutType == HTTPOutput { 27 | var describe: [RouteDescription] { 28 | return registry.routes.map { 29 | RouteDescription(uri: $0.key) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HTTPRequestInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequestInfo.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2019-01-25. 6 | // 7 | 8 | import Foundation 9 | import NIOHTTP1 10 | 11 | public struct HTTPRequestOptions: OptionSet { 12 | public typealias RawValue = UInt8 13 | public let rawValue: RawValue 14 | public init(rawValue: RawValue) { 15 | self.rawValue = rawValue 16 | } 17 | public static let isTLS = HTTPRequestOptions(rawValue: 1<<0) 18 | public static let mayCompress = HTTPRequestOptions(rawValue: 1<<1) 19 | } 20 | 21 | public struct HTTPRequestInfo { 22 | public let head: HTTPRequestHead 23 | public let options: HTTPRequestOptions 24 | public init(head: HTTPRequestHead, options: HTTPRequestOptions) { 25 | self.head = head 26 | self.options = options 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HTTPOutput/ErrorOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorOutput.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-11-19. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import Foundation 20 | import NIOHTTP1 21 | 22 | /// Output which can be thrown 23 | public class ErrorOutput: BytesOutput, Error, CustomStringConvertible { 24 | public let description: String 25 | /// Construct a ErrorOutput with a simple text message 26 | public init(status: HTTPResponseStatus, description: String? = nil) { 27 | self.description = description ?? status.reasonPhrase 28 | let chars = Array(self.description.utf8) 29 | let headers = HTTPHeaders([("Content-Type", "text/plain")]) 30 | super.init(head: HTTPHead(status: status, headers: headers), body: chars) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HTTPOutput/JSONOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPOutput.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-11-19. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import Foundation 20 | import NIOHTTP1 21 | 22 | /// JSON output from an Encodable 23 | public class JSONOutput: BytesOutput { 24 | public init(_ encodable: E, head: HTTPHead? = nil) throws { 25 | let body = Array(try JSONEncoder().encode(encodable)) 26 | let useHeaders = HTTPHeaders([("content-type", "application/json")]) 27 | super.init(head: HTTPHead(headers: useHeaders).merged(with: head), body: body) 28 | } 29 | } 30 | 31 | /// Convert Encodable to JSON output 32 | public extension Routes where OutType: Encodable { 33 | func json() -> Routes { 34 | return map { try JSONOutput($0) } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HTTPOutput/TextOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPOutput.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-11-19. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import Foundation 20 | import NIOHTTP1 21 | 22 | /// Plain text output from a CustomStringConvertible 23 | public class TextOutput: BytesOutput { 24 | public init(_ c: C, status: HTTPResponseStatus? = nil, headers: [(String, String)] = []) { 25 | let body = Array("\(c)".utf8) 26 | let useHeaders = HTTPHeaders([("content-type", "text/plain")] + headers) 27 | super.init(head: HTTPHead(status: status, headers: useHeaders), body: body) 28 | } 29 | } 30 | 31 | /// Converts CustomStringConvertible to plain text output 32 | public extension Routes where OutType: CustomStringConvertible { 33 | func text() -> Routes { 34 | return map { TextOutput($0) } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/PerfectNIOExe/main.swift: -------------------------------------------------------------------------------- 1 | 2 | import PerfectNIO 3 | import Foundation 4 | 5 | let index = root { 6 | try FileOutput(localPath: "./webroot/index.html") as HTTPOutput 7 | } 8 | 9 | class EchoSocket { 10 | var socket: WebSocket 11 | var closed = false 12 | init(socket: WebSocket) { 13 | self.socket = socket 14 | self.socket.options = [.manualClose] 15 | } 16 | deinit { 17 | print("death") 18 | } 19 | func process(message msg: WebSocketMessage) { 20 | switch msg { 21 | case .close: 22 | if !closed { 23 | _ = socket.writeMessage(.close) 24 | } 25 | closed = true 26 | case .ping: 27 | _ = socket.writeMessage(.pong) 28 | case .pong: 29 | () 30 | case .text(let text): 31 | _ = socket.writeMessage(.text(text)) 32 | case .binary(let binary): 33 | _ = socket.writeMessage(.binary(binary)) 34 | } 35 | } 36 | func loop() { 37 | guard !closed else { 38 | return 39 | } 40 | socket.readMessage().whenSuccess { 41 | msg in 42 | self.process(message: msg) 43 | self.loop() 44 | } 45 | } 46 | } 47 | let socket = root().echo.webSocket(protocol: "echo") { 48 | request -> WebSocketHandler in 49 | return { 50 | socket in 51 | EchoSocket(socket: socket).loop() 52 | } 53 | } 54 | 55 | let address = try SocketAddress(ipAddress: "0.0.0.0", port: 42000) 56 | let server = try root().dir(index, socket).bind(address: address).listen() 57 | try server.wait() 58 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HTTPHead.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHead.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2019-01-14. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import Foundation 20 | import NIOHTTP1 21 | 22 | func +(lhs: HTTPHeaders, rhs: HTTPHeaders) -> HTTPHeaders { 23 | return HTTPHeaders(lhs.map {$0} + rhs.map {$0}) 24 | } 25 | 26 | public struct HTTPHead { 27 | /// Optional HTTP status 28 | public var status: HTTPResponseStatus? 29 | /// HTTP headers 30 | public var headers: HTTPHeaders 31 | public init(status: HTTPResponseStatus? = nil, headers: HTTPHeaders) { 32 | self.status = status 33 | self.headers = headers 34 | } 35 | public func merged(with: HTTPHead?) -> HTTPHead { 36 | guard let with = with else { 37 | return self 38 | } 39 | let status: HTTPResponseStatus? 40 | if with.status != .ok { 41 | status = with.status 42 | } else { 43 | status = self.status 44 | } 45 | let headers = self.headers + with.headers 46 | return HTTPHead(status: status, headers: headers) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HTTPOutput/BytesOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPOutput.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-11-19. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import Foundation 20 | import NIO 21 | import NIOHTTP1 22 | 23 | /// Raw byte output 24 | public class BytesOutput: HTTPOutput { 25 | private let head: HTTPHead? 26 | private var bodyBytes: [UInt8]? 27 | public init(head: HTTPHead? = nil, 28 | body: [UInt8]) { 29 | let headers = HTTPHeaders([("Content-Length", "\(body.count)")]) 30 | self.head = HTTPHead(headers: headers).merged(with: head) 31 | bodyBytes = body 32 | } 33 | public override func head(request: HTTPRequestInfo) -> HTTPHead? { 34 | return head 35 | } 36 | public override func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) { 37 | if let b = bodyBytes { 38 | bodyBytes = nil 39 | var buf = allocator.buffer(capacity: b.count) 40 | buf.writeBytes(b) 41 | promise.succeed(IOData.byteBuffer(buf)) 42 | } else { 43 | promise.succeed(nil) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PerfectNIO", 7 | platforms: [ 8 | .macOS(.v10_15) 9 | ], 10 | products: [ 11 | .executable(name: "PerfectNIOExe", targets: ["PerfectNIOExe"]), 12 | .library(name: "PerfectNIO", targets: ["PerfectNIO"]), 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/PerfectlySoft/Perfect-Mustache.git", from: "4.0.0"), 16 | .package(url: "https://github.com/PerfectlySoft/PerfectLib.git", from: "4.0.0"), 17 | .package(url: "https://github.com/PerfectlySoft/Perfect-CRUD.git", from: "2.0.0"), 18 | .package(url: "https://github.com/PerfectlySoft/Perfect-MIME.git", from: "1.0.0"), 19 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"), 20 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"), 21 | .package(url: "https://github.com/PerfectlySoft/Perfect-CZlib-src.git", from: "0.0.0"), 22 | 23 | // tests only 24 | .package(url: "https://github.com/PerfectlySoft/Perfect-CURL.git", from: "5.0.0"), 25 | ], 26 | targets: [ 27 | .target(name: "PerfectNIOExe", dependencies: [ 28 | "PerfectNIO", 29 | ]), 30 | .target(name: "PerfectNIO", dependencies: [ 31 | "PerfectLib", 32 | "PerfectCRUD", 33 | "PerfectMIME", 34 | "PerfectMustache", 35 | "NIOHTTP1", 36 | "NIOSSL", 37 | "NIOWebSocket", 38 | "PerfectCZlib" 39 | ]), 40 | .testTarget(name: "PerfectNIOTests", dependencies: [ 41 | "PerfectNIO", 42 | "PerfectCURL" 43 | ]), 44 | ] 45 | ) 46 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/RouteCRUD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteCRUD.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-10-28. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import Foundation 20 | import PerfectCRUD 21 | import Dispatch 22 | import NIO 23 | 24 | public typealias DCP = DatabaseConfigurationProtocol 25 | 26 | let foreignEventsQueue = DispatchQueue(label: "foreignEventsQueue", attributes: .concurrent) 27 | 28 | public extension Routes { 29 | func db(_ provide: @autoclosure @escaping () throws -> Database, 30 | _ call: @escaping (OutType, Database) throws -> NewOut) -> Routes { 31 | return self.async { 32 | i, promise in 33 | do { 34 | let db = try provide() 35 | promise.succeed(try call(i, db)) 36 | } catch { 37 | promise.fail(error) 38 | } 39 | } 40 | } 41 | func table(_ provide: @autoclosure @escaping () throws -> Database, 42 | _ type: T.Type, 43 | _ call: @escaping (OutType, Table>) throws -> NewOut) -> Routes { 44 | return self.async { 45 | i, promise in 46 | do { 47 | let table = try provide().table(type) 48 | promise.succeed(try call(i, table)) 49 | } catch { 50 | promise.fail(error) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HTTPRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2019-02-11. 6 | // 7 | 8 | import NIO 9 | import NIOHTTP1 10 | import Foundation 11 | 12 | /// Client content which has been read and parsed (if needed). 13 | public enum HTTPRequestContentType { 14 | /// There was no content provided by the client. 15 | case none 16 | /// A multi-part form/file upload. 17 | case multiPartForm(MimeReader) 18 | /// A url-encoded form. 19 | case urlForm(QueryDecoder) 20 | /// Some other sort of content. 21 | case other([UInt8]) 22 | } 23 | 24 | public protocol HTTPRequest { 25 | var channel: Channel? { get } 26 | var method: HTTPMethod { get } 27 | var uri: String { get } 28 | var headers: HTTPHeaders { get } 29 | var uriVariables: [String:String] { get set } 30 | var path: String { get } 31 | var searchArgs: QueryDecoder? { get } 32 | var contentType: String? { get } 33 | var contentLength: Int { get } 34 | var contentRead: Int { get } 35 | var contentConsumed: Int { get } 36 | var localAddress: SocketAddress? { get } 37 | var remoteAddress: SocketAddress? { get } 38 | func readSomeContent() -> EventLoopFuture<[ByteBuffer]> 39 | func readContent() -> EventLoopFuture 40 | } 41 | 42 | public extension HTTPRequest { 43 | /// Returns all the cookie name/value pairs parsed from the request. 44 | var cookies: [String:String] { 45 | guard let cookie = self.headers["cookie"].first else { 46 | return [:] 47 | } 48 | return Dictionary(cookie.split(separator: ";").compactMap { 49 | let d = $0.split(separator: "=") 50 | guard d.count == 2 else { return nil } 51 | let d2 = d.map { String($0.filter { $0 != Character(" ") }).stringByDecodingURL ?? "" } 52 | return (d2[0], d2[1]) 53 | }, uniquingKeysWith: {$1}) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HTTPOutput/MustacheOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MustacheOutput.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2019-01-14. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import Foundation 20 | import PerfectMustache 21 | import NIOHTTP1 22 | import NIO 23 | import NIOHTTP1 24 | 25 | public class MustacheOutput: HTTPOutput { 26 | private let head: HTTPHead? 27 | private var bodyBytes: [UInt8]? 28 | public init(templatePath: String, 29 | inputs: [String:Any], 30 | contentType: String) throws { 31 | let context = MustacheEvaluationContext(templatePath: templatePath, map: inputs) 32 | let collector = MustacheEvaluationOutputCollector() 33 | let result = try context.formulateResponse(withCollector: collector) 34 | let body = Array(result.utf8) 35 | bodyBytes = body 36 | head = HTTPHead(headers: HTTPHeaders([ 37 | ("Content-Type", contentType), 38 | ("Content-Length", "\(body.count)") 39 | ])) 40 | } 41 | public override func head(request: HTTPRequestInfo) -> HTTPHead? { 42 | return head 43 | } 44 | public override func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) { 45 | if let b = bodyBytes { 46 | bodyBytes = nil 47 | var buf = allocator.buffer(capacity: b.count) 48 | buf.writeBytes(b) 49 | promise.succeed(IOData.byteBuffer(buf)) 50 | } else { 51 | promise.succeed(nil) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | .DS_Store 71 | *.xcodeproj/ 72 | Packages/ 73 | Package.resolved 74 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HTTPOutput/HTTPOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPOutput.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-11-19. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import Foundation 20 | import NIOHTTP1 21 | import NIO 22 | 23 | /// Indicates how the `body` func data, and possibly content-length, should be handled 24 | public enum HTTPOutputResponseHint { 25 | /// content size is known and all content is available 26 | /// not chunked. calling `body` will deliver the one available block (or nil) 27 | case fixed 28 | /// content size is known but all content is not yet available 29 | /// e.g. the content might be too large to reasonably fit in memory at once 30 | /// chunked 31 | case multi 32 | /// content size is not known. 33 | /// stream while `body()` returns data 34 | /// e.g. compressed `.multi` output 35 | /// chunked 36 | case stream 37 | } 38 | 39 | /// The response output for the client 40 | open class HTTPOutput { 41 | /// Indicates how the `body` func data, and possibly content-length, should be handled 42 | var kind: HTTPOutputResponseHint = .fixed // !FIX! is this utilized? 43 | public init() {} 44 | /// Optional HTTP head 45 | open func head(request: HTTPRequestInfo) -> HTTPHead? { 46 | return nil 47 | } 48 | /// Produce body data 49 | /// Set nil on last chunk 50 | /// Call promise.fail upon failure 51 | open func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) { 52 | promise.succeed(nil) 53 | } 54 | /// Called when the request has completed either successfully or with a failure. 55 | /// Sub-classes can override to take special actions or perform cleanup operations. 56 | /// Inherited implimenation does nothing. 57 | open func closed() { 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HandlerState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HandlerState.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-10-12. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import NIOHTTP1 20 | import NIO 21 | 22 | final class HandlerState { 23 | var request: NIOHTTPHandler 24 | var responseHead = HTTPHead(status: .ok, headers: HTTPHeaders()) 25 | var currentComponent: String? { 26 | guard range.lowerBound < uri.endIndex else { 27 | return nil 28 | } 29 | return String(uri[range]) 30 | } 31 | var trailingComponents: String? { 32 | guard range.lowerBound < uri.endIndex else { 33 | return nil 34 | } 35 | return String(uri[range.lowerBound...]) 36 | } 37 | let uri: [Character] 38 | var range: Range.Index> 39 | var content: HTTPRequestContentType? 40 | init(request: NIOHTTPHandler, uri: String) { 41 | self.request = request 42 | self.uri = Array(uri) 43 | let si = self.uri.startIndex 44 | range = si..<(si+1) 45 | advanceComponent() 46 | } 47 | func readContent() -> EventLoopFuture { 48 | if let c = content { 49 | return request.channel!.eventLoop.makeSucceededFuture(c) 50 | } 51 | return request.readContent().map { 52 | self.content = $0 53 | return $0 54 | } 55 | } 56 | func advanceComponent() { 57 | var genRange = range.endIndex 58 | while genRange < uri.endIndex && uri[genRange] == "/" { 59 | genRange = uri.index(after: genRange) 60 | } 61 | guard genRange < uri.endIndex else { 62 | range = uri.endIndex.. String { 50 | // return "\(address)" 51 | // } 52 | // static func get(_ key: SocketAddress) -> ServerInfo? { 53 | // return GlobalServerMap.serverInfoAccessQueue.sync { GlobalServerMap.serverMap[key] } 54 | // } 55 | // static func set(_ key: SocketAddress, _ newValue: ServerInfo) { 56 | // GlobalServerMap.serverInfoAccessQueue.sync { GlobalServerMap.serverMap[key] = newValue } 57 | // } 58 | //} 59 | 60 | 61 | 62 | //enum ServerRegistry { 63 | // static func addServer(hostName: String, 64 | // address: SocketAddress, 65 | // tls: TLSConfiguration?) throws -> EventLoopFuture { 66 | // 67 | // } 68 | //} 69 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/PerfectNIO.swift: -------------------------------------------------------------------------------- 1 | // 2 | //===----------------------------------------------------------------------===// 3 | // 4 | // This source file is part of the Perfect.org open source project 5 | // 6 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 7 | // Licensed under Apache License v2.0 8 | // 9 | // See http://perfect.org/licensing.html for license information 10 | // 11 | //===----------------------------------------------------------------------===// 12 | // 13 | 14 | import Foundation 15 | 16 | @_exported import NIO 17 | @_exported import NIOHTTP1 18 | @_exported import NIOSSL 19 | //@_exported import CNIOOpenSSL 20 | 21 | public typealias ComponentGenerator = IndexingIterator<[String]> 22 | 23 | extension String { 24 | var components: [String] { 25 | return self.split(separator: "/").map(String.init) 26 | } 27 | var componentGenerator: ComponentGenerator { 28 | return self.split(separator: "/").map(String.init).makeIterator() 29 | } 30 | // need url decoding component generator 31 | func appending(component name: String) -> String { 32 | let name = name.components.joined(separator: "/") 33 | if name.isEmpty { 34 | return self 35 | } 36 | if hasSuffix("/") { 37 | return self + name 38 | } 39 | return self + "/" + name.split(separator: "/").joined(separator: "/") 40 | } 41 | var cleanedPath: String { 42 | return "/" + components.joined(separator: "/") 43 | } 44 | var componentBase: String? { 45 | return self.components.first 46 | } 47 | var componentName: String? { 48 | return self.components.last 49 | } 50 | var ext: String { 51 | if self.first != "." { 52 | return "." + self 53 | } 54 | return self 55 | } 56 | var splitQuery: (String, String?) { 57 | guard let r = self.range(of: "?") else { 58 | return (self.cleanedPath, nil) 59 | } 60 | return (String(self[self.startIndex..) -> (String, String) { 70 | guard let r = self.range(of: eqChar, range: range) else { 71 | return (String(self[range]), "") 72 | } 73 | return (String(self[range.lowerBound..>) throws -> Future> 26 | init(_ registry: Routes) throws 27 | subscript(_ method: HTTPMethod, _ uri: String) -> ResolveFunc? { get } 28 | } 29 | 30 | extension RouteRegistry { 31 | var withMethods: RouteRegistry { 32 | return .init(routes: routes.flatMap { 33 | item -> [RouteRegistry.Tuple] in 34 | let (method, path) = item.0.splitMethod 35 | if nil == method { 36 | let r = item.1 37 | return HTTPMethod.allCases.map { 38 | ("\($0)://\(path)", r) 39 | } 40 | } 41 | return [item] 42 | }) 43 | } 44 | } 45 | 46 | class RouteFinderRegExp: RouteFinder { 47 | typealias Matcher = (NSRegularExpression, ResolveFunc) 48 | let matchers: [HTTPMethod:[Matcher]] 49 | required init(_ registry: Routes) throws { 50 | let full = registry.registry.withMethods.routes 51 | var m = [HTTPMethod:[Matcher]]() 52 | try full.forEach { 53 | let (p1, fnc) = $0 54 | let (meth, path) = p1.splitMethod 55 | let method = meth ?? .GET 56 | let matcher: Matcher = (try RouteFinderRegExp.regExp(for: path), fnc) 57 | let fnd = m[method] ?? [] 58 | m[method] = fnd + [matcher] 59 | } 60 | matchers = m 61 | } 62 | subscript(_ method: HTTPMethod, _ uri: String) -> ResolveFunc? { 63 | guard let matchers = self.matchers[method] else { 64 | return nil 65 | } 66 | let uriRange = NSRange(location: 0, length: uri.count) 67 | for i in 0.. NSRegularExpression { 77 | let c = path.components 78 | let strs = c.map { 79 | comp -> String in 80 | switch comp { 81 | case "*": 82 | return "/([^/]*)" 83 | case "**": 84 | return "/(.*)" 85 | default: 86 | return "/" + comp 87 | } 88 | } 89 | return try NSRegularExpression(pattern: "^" + strs.joined(separator: "") + "$", options: NSRegularExpression.Options.caseInsensitive) 90 | } 91 | } 92 | 93 | class RouteFinderDictionary: RouteFinder { 94 | let dict: [String:ResolveFunc] 95 | required init(_ registry: Routes) throws { 96 | dict = Dictionary(registry.registry.withMethods.routes.filter { 97 | !($0.0.components.contains("*") || $0.0.components.contains("**")) 98 | }, uniquingKeysWith: {return $1}) 99 | } 100 | subscript(_ method: HTTPMethod, _ uri: String) -> ResolveFunc? { 101 | let key = method.name + "://" + uri 102 | return dict[key] 103 | } 104 | } 105 | 106 | class RouteFinderDual: RouteFinder { 107 | let alpha: RouteFinder 108 | let beta: RouteFinder 109 | required init(_ registry: Routes) throws { 110 | alpha = try RouteFinderDictionary(registry) 111 | beta = try RouteFinderRegExp(registry) 112 | } 113 | 114 | subscript(_ method: HTTPMethod, _ uri: String) -> ResolveFunc? { 115 | return alpha[method, uri] ?? beta[method, uri] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/QueryDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryDecoder.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-11-11. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import Foundation 20 | 21 | private let ampChar = "&".utf8.first! 22 | private let eqChar = "=".utf8.first! 23 | 24 | //private extension String { 25 | // init?(_ slice: ArraySlice) { 26 | // self.init(bytes: slice, encoding: .utf8) 27 | // } 28 | //} 29 | 30 | public struct QueryDecoder { 31 | typealias A = Array 32 | typealias I = A.Index 33 | typealias R = Range 34 | struct RangeTriple { 35 | let start: I 36 | let middle: I 37 | let end: I 38 | } 39 | let collection: A 40 | var ranges: [RangeTriple] = [] 41 | var lookup: [String:[Int]] = [:] 42 | 43 | public init(_ c: [UInt8]) { 44 | collection = c 45 | build() 46 | } 47 | 48 | private func decodedString(_ bytes: ArraySlice) -> String { 49 | return String(bytes: bytes, encoding: .utf8)?.stringByDecodingURL ?? "" 50 | } 51 | 52 | public subscript(_ key: String) -> [String] { 53 | return get(key).map { self.decodedString($0) } 54 | } 55 | 56 | public func map(_ call: ((String,String)) throws -> T) rethrows -> [T] { 57 | return try mapBytes { 58 | return try call(($0.0, self.decodedString($0.1))) 59 | } 60 | } 61 | 62 | public func mapBytes(_ call: ((String,ArraySlice)) throws -> T) rethrows -> [T] { 63 | return try ranges.map {try call(triple2Tuple($0))} 64 | } 65 | 66 | public func get(_ key: String) -> [ArraySlice] { 67 | guard let fnd = lookup[key] else { 68 | return [] 69 | } 70 | return fnd.map { triple2Value(ranges[$0]) } 71 | } 72 | 73 | func triple2Tuple(_ triple: RangeTriple) -> (String, ArraySlice) { 74 | let nameSlice: ArraySlice 75 | let valueSlice: ArraySlice 76 | if triple.middle == collection.endIndex { 77 | nameSlice = collection[triple.start...] 78 | valueSlice = ArraySlice(repeating: 0, count: 0) 79 | } else { 80 | nameSlice = collection[triple.start.. ArraySlice { 87 | let valueSlice: ArraySlice 88 | if triple.middle == collection.endIndex { 89 | valueSlice = ArraySlice(repeating: 0, count: 0) 90 | } else { 91 | valueSlice = collection[triple.middle.. 138 | if triple.middle == collection.endIndex { 139 | nameSlice = collection[triple.start...] 140 | } else if collection[triple.middle - 1] != eqChar { 141 | nameSlice = collection[triple.start...allocate(capacity: Int(SHA1_RESULTLEN)) 29 | defer { bytes.deallocate() } 30 | let src = Array(self) 31 | var ctx = SHA1_CTX() 32 | c_nio_sha1_init(&ctx) 33 | c_nio_sha1_loop(&ctx, src, src.count) 34 | c_nio_sha1_result(&ctx, bytes) 35 | var r = [UInt8]() 36 | for idx in 0.. HTTPHead? { 87 | let eTag = getETag() 88 | var headers = [("Accept-Ranges", "bytes")] 89 | if let ifNoneMatch = request.head.headers["if-none-match"].first, 90 | ifNoneMatch == eTag { 91 | // region is nil. no body 92 | return HTTPHead(status: HTTPResponseStatus.notModified, headers: HTTPHeaders(headers)) 93 | } 94 | let contentType = MIMEType.forExtension(path.filePathExtension) 95 | headers.append(("Content-Type", contentType)) 96 | if let rangeRequest = request.head.headers["range"].first, let range = parseRangeHeader(fromHeader: rangeRequest, max: size).first { 97 | headers.append(("Content-Length", "\(range.count)")) 98 | headers.append(("Content-Range", "bytes \(range.startIndex)-\(range.endIndex-1)/\(size)")) 99 | region = FileRegion(fileHandle: file, readerIndex: range.startIndex, endIndex: range.endIndex) 100 | } else { 101 | headers.append(("Content-Length", "\(size)")) 102 | region = FileRegion(fileHandle: file, readerIndex: 0, endIndex: size) 103 | } 104 | return HTTPHead(status: .ok, headers: HTTPHeaders(headers)) 105 | } 106 | public override func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) { 107 | if let r = region { 108 | region = nil 109 | promise.succeed(.fileRegion(r)) 110 | } else { 111 | promise.succeed(nil) 112 | } 113 | } 114 | 115 | func getETag() -> String { 116 | let eTagStr = path + "\(modDate)" 117 | let eTag = eTagStr.utf8.sha1 118 | let eTagReStr = eTag.map { $0.hexString }.joined(separator: "") 119 | return eTagReStr 120 | } 121 | 122 | // bytes=0-3/7-9/10-15 123 | func parseRangeHeader(fromHeader header: String, max: Int) -> [Range] { 124 | let initialSplit = header.split(separator: "=") 125 | guard initialSplit.count == 2 && String(initialSplit[0]) == "bytes" else { 126 | return [Range]() 127 | } 128 | let ranges = initialSplit[1] 129 | return ranges.split(separator: "/").compactMap { self.parseOneRange(fromString: String($0), max: max) } 130 | } 131 | 132 | // 0-3 133 | // 0- 134 | func parseOneRange(fromString string: String, max: Int) -> Range? { 135 | let split = string.split(separator: "-", omittingEmptySubsequences: false).map { String($0) } 136 | guard split.count == 2 else { 137 | return nil 138 | } 139 | if split[1].isEmpty { 140 | guard let lower = Int(split[0]), 141 | lower <= max else { 142 | return nil 143 | } 144 | return Range(uncheckedBounds: (lower, max)) 145 | } 146 | guard let lower = Int(split[0]), 147 | let upperRaw = Int(split[1]) else { 148 | return nil 149 | } 150 | let upper = Swift.min(max, upperRaw+1) 151 | guard lower <= upper else { 152 | return nil 153 | } 154 | return Range(uncheckedBounds: (lower, upper)) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/HTTPOutput/CompressedOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompressedOutput.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2019-01-14. 6 | // 7 | // some of this code taken from NIO HTTPResponseCompressor, 8 | // which didn't itself quite fit how things are operating here 9 | // therefore… 10 | //===----------------------------------------------------------------------===// 11 | // 12 | // This source file is part of the SwiftNIO open source project 13 | // 14 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 15 | // Licensed under Apache License v2.0 16 | // 17 | // See LICENSE.txt for license information 18 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 19 | // 20 | // SPDX-License-Identifier: Apache-2.0 21 | // 22 | //===----------------------------------------------------------------------===// 23 | 24 | import Foundation 25 | import NIOHTTP1 26 | import PerfectCZlib 27 | import NIO 28 | 29 | internal extension String { 30 | /// Test if this `Collection` starts with the unicode scalars of `needle`. 31 | /// 32 | /// - note: This will be faster than `String.startsWith` as no unicode normalisations are performed. 33 | /// 34 | /// - parameters: 35 | /// - needle: The `Collection` of `Unicode.Scalar`s to match at the beginning of `self` 36 | /// - returns: If `self` started with the elements contained in `needle`. 37 | func startsWithSameUnicodeScalars(string needle: S) -> Bool { 38 | return self.unicodeScalars.starts(with: needle.unicodeScalars) 39 | } 40 | } 41 | 42 | /// Given a header value, extracts the q value if there is one present. If one is not present, 43 | /// returns the default q value, 1.0. 44 | private func qValueFromHeader(_ text: String) -> Float { 45 | let headerParts = text.split(separator: ";", maxSplits: 1, omittingEmptySubsequences: false) 46 | guard headerParts.count > 1 && headerParts[1].count > 0 else { 47 | return 1 48 | } 49 | 50 | // We have a Q value. 51 | let qValue = Float(headerParts[1].split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)[1]) ?? 0 52 | if qValue < 0 || qValue > 1 || qValue.isNaN { 53 | return 0 54 | } 55 | return qValue 56 | } 57 | 58 | public class CompressedOutput: HTTPOutput { 59 | private static let fileIO = sharedNonBlockingFileIO 60 | 61 | fileprivate enum CompressionAlgorithm: String { 62 | case gzip = "gzip" 63 | case deflate = "deflate" 64 | } 65 | private var stream = z_stream() 66 | private var algorithm: CompressionAlgorithm? // needed? 67 | private var sourceContent: HTTPOutput 68 | private let minCompressLength: Int 69 | private var done = false 70 | private let chunkSize = 32 * 1024 71 | private var consumingRegion: FileRegion? 72 | private let noCompressMimes = ["image/", "video/", "audio/"] 73 | public init(source: HTTPOutput) { 74 | sourceContent = source 75 | minCompressLength = 1024 * 14 // !FIX! 76 | super.init() 77 | kind = .stream 78 | } 79 | deinit { 80 | deinitializeEncoder() 81 | } 82 | 83 | public override func head(request: HTTPRequestInfo) -> HTTPHead? { 84 | guard let algo = compressionAlgorithm(request.head) else { 85 | return sourceContent.head(request: request) 86 | } 87 | let newRequest = HTTPRequestInfo(head: request.head, 88 | options: request.options.union(.mayCompress)) 89 | let sourceHead = sourceContent.head(request: newRequest) 90 | if let contentLengthStr = sourceHead?.headers["content-length"].first, 91 | let contentLength = Int(contentLengthStr), 92 | contentLength < minCompressLength { 93 | return sourceHead 94 | } 95 | if let contentTypeStr = sourceHead?.headers["content-type"].first, 96 | let _ = noCompressMimes.first(where: { contentTypeStr.hasPrefix($0) }) { 97 | return sourceHead 98 | } 99 | var head: HTTPHead 100 | if let t = sourceHead { 101 | head = t 102 | } else { 103 | head = HTTPHead(headers: HTTPHeaders()) 104 | } 105 | algorithm = algo 106 | initializeEncoder(encoding: algo) 107 | head.headers.remove(name: "content-length") 108 | head.headers.add(name: "Content-Encoding", value: algo.rawValue) 109 | return head 110 | } 111 | public override func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) { 112 | guard let _ = self.algorithm else { 113 | return sourceContent.body(promise: promise, allocator: allocator) 114 | } 115 | guard !done else { 116 | return promise.succeed(nil) 117 | } 118 | let eventLoop = promise.futureResult.eventLoop 119 | if let region = consumingRegion { 120 | let readSize = min(chunkSize, region.readableBytes) 121 | let newRegion = FileRegion(fileHandle: region.fileHandle, 122 | readerIndex: region.readerIndex, 123 | endIndex: region.readerIndex + readSize) 124 | let readP = CompressedOutput.fileIO.read(fileRegion: newRegion, 125 | allocator: allocator, 126 | eventLoop: eventLoop) 127 | readP.whenSuccess { 128 | bytes in 129 | self.consumingRegion?.moveReaderIndex(forwardBy: bytes.readableBytes) 130 | if self.consumingRegion?.readableBytes == 0 { 131 | self.consumingRegion = nil 132 | } 133 | promise.succeed(IOData.byteBuffer(self.compress(bytes, allocator: allocator))) 134 | } 135 | readP.whenFailure { promise.fail($0) } 136 | } else { 137 | let newp = eventLoop.makePromise(of: IOData?.self) 138 | sourceContent.body(promise: newp, allocator: allocator) 139 | newp.futureResult.whenSuccess { 140 | data in 141 | if let data = data { 142 | switch data { 143 | case .byteBuffer(let bytes): 144 | promise.succeed(IOData.byteBuffer(self.compress(bytes, allocator: allocator))) 145 | case .fileRegion(let region): 146 | self.consumingRegion = region 147 | self.body(promise: promise, allocator: allocator) 148 | } 149 | } else { 150 | self.done = true 151 | promise.succeed(IOData.byteBuffer(self.compress(nil, allocator: allocator))) 152 | } 153 | } 154 | newp.futureResult.whenFailure { 155 | promise.fail($0) 156 | } 157 | } 158 | } 159 | private func compressionAlgorithm(_ head: HTTPRequestHead) -> CompressionAlgorithm? { 160 | let acceptHeaders = head.headers["accept-encoding"] 161 | var gzipQValue: Float = -1 162 | var deflateQValue: Float = -1 163 | var anyQValue: Float = -1 164 | for fullHeader in acceptHeaders { 165 | for acceptHeader in fullHeader.replacingOccurrences(of: " ", with: "").split(separator: ",").map(String.init) { 166 | if acceptHeader.startsWithSameUnicodeScalars(string: "gzip") || acceptHeader.startsWithSameUnicodeScalars(string: "x-gzip") { 167 | gzipQValue = qValueFromHeader(acceptHeader) 168 | } else if acceptHeader.startsWithSameUnicodeScalars(string: "deflate") { 169 | deflateQValue = qValueFromHeader(acceptHeader) 170 | } else if acceptHeader.startsWithSameUnicodeScalars(string: "*") { 171 | anyQValue = qValueFromHeader(acceptHeader) 172 | } 173 | } 174 | } 175 | if gzipQValue > 0 || deflateQValue > 0 { 176 | return gzipQValue >= deflateQValue ? .gzip : .deflate 177 | } else if anyQValue > 0 { 178 | return .gzip 179 | } 180 | return nil 181 | } 182 | /// Set up the encoder for compressing data according to a specific 183 | /// algorithm. 184 | private func initializeEncoder(encoding: CompressionAlgorithm) { 185 | // zlib docs say: The application must initialize zalloc, zfree and opaque before calling the init function. 186 | stream.zalloc = nil 187 | stream.zfree = nil 188 | stream.opaque = nil 189 | 190 | let windowBits: Int32 191 | switch encoding { 192 | case .deflate: 193 | windowBits = 15 194 | case .gzip: 195 | windowBits = 16 + 15 196 | } 197 | 198 | let rc = deflateInit2_(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, windowBits, 8, Z_DEFAULT_STRATEGY, ZLIB_VERSION, Int32(MemoryLayout.size)) 199 | precondition(rc == Z_OK, "Unexpected return from zlib init: \(rc)") 200 | } 201 | 202 | private func deinitializeEncoder() { 203 | // We deliberately discard the result here because we just want to free up 204 | // the pending data. 205 | deflateEnd(&stream) 206 | } 207 | // pass nil to flush 208 | private func compress(_ bytes: ByteBuffer?, allocator: ByteBufferAllocator) -> ByteBuffer { 209 | defer { 210 | stream.next_out = nil 211 | stream.avail_out = 0 212 | stream.next_in = nil 213 | stream.avail_in = 0 214 | } 215 | let readable = bytes?.readableBytes ?? 0 216 | let needed = Int(deflateBound(&stream, UInt(readable))) + (readable == 0 ? 4096 : 0) 217 | var dest = allocator.buffer(capacity: needed) 218 | 219 | if var bytes = bytes { 220 | dest.writeWithUnsafeMutableBytes(minimumWritableBytes: needed) { 221 | outputPtr in 222 | let typedOutputPtr = UnsafeMutableBufferPointer(start: outputPtr.baseAddress!.assumingMemoryBound(to: UInt8.self), 223 | count: needed) 224 | stream.next_out = typedOutputPtr.baseAddress 225 | stream.avail_out = UInt32(needed) 226 | bytes.readWithUnsafeMutableReadableBytes { 227 | dataPtr in 228 | let typedDataPtr = UnsafeMutableBufferPointer(start: dataPtr.baseAddress!.assumingMemoryBound(to: UInt8.self), 229 | count: readable) 230 | stream.next_in = typedDataPtr.baseAddress 231 | stream.avail_in = UInt32(readable) 232 | let rc = deflate(&stream, Z_NO_FLUSH) 233 | if rc != Z_OK || stream.avail_in != 0 { 234 | debugPrint("deflate rc \(rc)") 235 | } 236 | return readable - Int(stream.avail_in) 237 | } 238 | return needed - Int(stream.avail_out) 239 | } 240 | } else { 241 | stream.next_in = nil 242 | stream.avail_in = 0 243 | var rc = Z_OK 244 | while rc != Z_ERRNO { 245 | dest.writeWithUnsafeMutableBytes(minimumWritableBytes: needed) { 246 | outputPtr in 247 | let typedOutputPtr = UnsafeMutableBufferPointer(start: outputPtr.baseAddress!.assumingMemoryBound(to: UInt8.self), 248 | count: needed) 249 | stream.next_out = typedOutputPtr.baseAddress 250 | stream.avail_out = UInt32(needed) 251 | rc = deflate(&stream, Z_FINISH) 252 | return needed - Int(stream.avail_out) 253 | } 254 | if rc == Z_STREAM_END { 255 | break 256 | } 257 | dest.reserveCapacity(dest.capacity + needed) 258 | } 259 | } 260 | return dest 261 | } 262 | } 263 | 264 | /// Compresses eligible output 265 | public extension Routes where OutType: HTTPOutput { 266 | func compressed() -> Routes { 267 | return map { CompressedOutput(source: $0) } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/RequestDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestDecoder.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-10-28. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import Foundation 20 | import NIOHTTP1 21 | import PerfectLib 22 | import PerfectMIME 23 | import struct Foundation.UUID 24 | 25 | public struct FileUpload: Codable { 26 | public let contentType: MIMEType 27 | public let fileName: String 28 | public let fileSize: Int 29 | public let tmpFileName: String 30 | } 31 | 32 | enum RequestParamValue { 33 | case string(String), file(FileUpload) 34 | } 35 | 36 | typealias RequestTuples = (String, RequestParamValue) 37 | 38 | /// Extensions on HTTPRequest which permit the request body to be decoded to a Codable type. 39 | public extension HTTPRequest { 40 | /// Decode the request body into the desired type, or throw an error. 41 | func decode(_ type: A.Type, content: HTTPRequestContentType) throws -> A { 42 | let postTuples: [RequestTuples] 43 | switch content { 44 | case .none: 45 | postTuples = [] 46 | case .multiPartForm(let mime): 47 | postTuples = mime.bodySpecs.filter { 48 | spec in 49 | // prune empty file uploads 50 | if nil == spec.file, 51 | spec.fileName == "", 52 | spec.contentType == "application/octet-stream" { 53 | return false 54 | } 55 | return true 56 | }.map { 57 | spec in 58 | let value: RequestParamValue 59 | if spec.file != nil { 60 | value = .file(FileUpload(contentType: MIMEType(spec.contentType), 61 | fileName: spec.fileName, 62 | fileSize: spec.fileSize, 63 | tmpFileName: spec.tmpFileName)) 64 | } else { 65 | value = .string(spec.fieldValue) 66 | } 67 | return (spec.fieldName, value) 68 | } 69 | case .urlForm(let t): 70 | postTuples = t.map {($0.0, .string($0.1))} 71 | case .other(let body): 72 | return try JSONDecoder().decode(A.self, from: Data(body)) 73 | } 74 | return try A.init(from: 75 | RequestDecoder(params: 76 | uriVariables.map {($0.key, .string($0.value))} 77 | + (searchArgs?.map {($0.0, .string($0.1))} ?? []) + postTuples)) 78 | } 79 | } 80 | 81 | private enum SpecialType { 82 | case uint8Array, int8Array, data, uuid, date 83 | init?(_ type: Any.Type) { 84 | switch type { 85 | case is [Int8].Type: 86 | self = .int8Array 87 | case is [UInt8].Type: 88 | self = .uint8Array 89 | case is Data.Type: 90 | self = .data 91 | case is UUID.Type: 92 | self = .uuid 93 | case is Date.Type: 94 | self = .date 95 | default: 96 | return nil 97 | } 98 | } 99 | } 100 | 101 | class RequestReader: KeyedDecodingContainerProtocol { 102 | typealias Key = K 103 | var codingPath: [CodingKey] = [] 104 | var allKeys: [Key] = [] 105 | let parent: RequestDecoder 106 | let params: [RequestTuples] 107 | init(_ p: RequestDecoder, params: [RequestTuples]) { 108 | parent = p 109 | self.params = params 110 | } 111 | func getValue(_ key: Key) throws -> T { 112 | let str: RequestParamValue 113 | let keyStr = key.stringValue 114 | if let v = params.first(where: {$0.0 == keyStr}) { 115 | str = v.1 116 | } else { 117 | throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, debugDescription: "Key \(keyStr) not found.")) 118 | } 119 | let finalStr: String 120 | switch str { 121 | case .string(let string): 122 | finalStr = string 123 | case .file(let file): 124 | finalStr = "\(file.tmpFileName);\(file.contentType);\(file.fileName);\(file.fileSize)" 125 | } 126 | guard let ret = T.init(finalStr) else { 127 | throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Could not convert to \(T.self).") 128 | } 129 | return ret 130 | } 131 | func contains(_ key: Key) -> Bool { 132 | return nil != params.first(where: {$0.0 == key.stringValue}) 133 | } 134 | func decodeNil(forKey key: Key) throws -> Bool { 135 | return !contains(key) 136 | } 137 | func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { 138 | return try getValue(key) 139 | } 140 | func decode(_ type: Int.Type, forKey key: Key) throws -> Int { 141 | return try getValue(key) 142 | } 143 | func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { 144 | return try getValue(key) 145 | } 146 | func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { 147 | return try getValue(key) 148 | } 149 | func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { 150 | return try getValue(key) 151 | } 152 | func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { 153 | return try getValue(key) 154 | } 155 | func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { 156 | return try getValue(key) 157 | } 158 | func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { 159 | return try getValue(key) 160 | } 161 | func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { 162 | return try getValue(key) 163 | } 164 | func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { 165 | return try getValue(key) 166 | } 167 | func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { 168 | return try getValue(key) 169 | } 170 | func decode(_ type: Float.Type, forKey key: Key) throws -> Float { 171 | return try getValue(key) 172 | } 173 | func decode(_ type: Double.Type, forKey key: Key) throws -> Double { 174 | return try getValue(key) 175 | } 176 | func decode(_ type: String.Type, forKey key: Key) throws -> String { 177 | return try getValue(key) 178 | } 179 | func decode(_ t: T.Type, forKey key: Key) throws -> T where T : Decodable { 180 | if let special = SpecialType(t) { 181 | switch special { 182 | case .uint8Array, .int8Array, .data: 183 | throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, 184 | debugDescription: "The data type \(t) is not supported for GET requests.")) 185 | case .uuid: 186 | let str: String = try getValue(key) 187 | guard let uuid = UUID(uuidString: str) else { 188 | throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Could not convert to \(t).") 189 | } 190 | return uuid as! T 191 | case .date: 192 | // !FIX! need to support better formats 193 | let str: String = try getValue(key) 194 | guard let date = Date(fromISO8601: str) else { 195 | throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Could not convert to \(t).") 196 | } 197 | return date as! T 198 | } 199 | } else if t == FileUpload.self { 200 | let keyStr = key.stringValue 201 | if let v = params.first(where: {$0.0 == keyStr}), 202 | case .file(let f) = v.1 { 203 | return f as! T 204 | } 205 | } 206 | throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, 207 | debugDescription: "The data type \(t) is not supported for GET requests.")) 208 | } 209 | func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { 210 | fatalError("Unimplimented") 211 | } 212 | func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { 213 | fatalError("Unimplimented") 214 | } 215 | func superDecoder() throws -> Decoder { 216 | fatalError("Unimplimented") 217 | } 218 | func superDecoder(forKey key: Key) throws -> Decoder { 219 | fatalError("Unimplimented") 220 | } 221 | } 222 | 223 | class RequestUnkeyedReader: UnkeyedDecodingContainer, SingleValueDecodingContainer { 224 | let codingPath: [CodingKey] = [] 225 | var count: Int? = 1 226 | var isAtEnd: Bool { return currentIndex != 0 } 227 | var currentIndex: Int = 0 228 | let parent: RequestDecoder 229 | var decodedType: Any.Type? 230 | var typeDecoder: RequestDecoder? 231 | init(parent p: RequestDecoder) { 232 | parent = p 233 | } 234 | func advance(_ t: Any.Type) { 235 | currentIndex += 1 236 | decodedType = t 237 | } 238 | func decodeNil() -> Bool { 239 | return false 240 | } 241 | func decode(_ type: Bool.Type) throws -> Bool { 242 | advance(type) 243 | return false 244 | } 245 | func decode(_ type: Int.Type) throws -> Int { 246 | advance(type) 247 | return 0 248 | } 249 | func decode(_ type: Int8.Type) throws -> Int8 { 250 | advance(type) 251 | return 0 252 | } 253 | func decode(_ type: Int16.Type) throws -> Int16 { 254 | advance(type) 255 | return 0 256 | } 257 | func decode(_ type: Int32.Type) throws -> Int32 { 258 | advance(type) 259 | return 0 260 | } 261 | func decode(_ type: Int64.Type) throws -> Int64 { 262 | advance(type) 263 | return 0 264 | } 265 | func decode(_ type: UInt.Type) throws -> UInt { 266 | advance(type) 267 | return 0 268 | } 269 | func decode(_ type: UInt8.Type) throws -> UInt8 { 270 | advance(type) 271 | return 0 272 | } 273 | func decode(_ type: UInt16.Type) throws -> UInt16 { 274 | advance(type) 275 | return 0 276 | } 277 | func decode(_ type: UInt32.Type) throws -> UInt32 { 278 | advance(type) 279 | return 0 280 | } 281 | func decode(_ type: UInt64.Type) throws -> UInt64 { 282 | advance(type) 283 | return 0 284 | } 285 | func decode(_ type: Float.Type) throws -> Float { 286 | advance(type) 287 | return 0 288 | } 289 | func decode(_ type: Double.Type) throws -> Double { 290 | advance(type) 291 | return 0 292 | } 293 | func decode(_ type: String.Type) throws -> String { 294 | advance(type) 295 | return "" 296 | } 297 | func decode(_ type: T.Type) throws -> T { 298 | advance(type) 299 | return try T(from: parent) 300 | } 301 | func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { 302 | fatalError("Unimplimented") 303 | } 304 | func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { 305 | fatalError("Unimplimented") 306 | } 307 | func superDecoder() throws -> Decoder { 308 | currentIndex += 1 309 | return parent 310 | } 311 | } 312 | 313 | class RequestDecoder: Decoder { 314 | var codingPath: [CodingKey] = [] 315 | var userInfo: [CodingUserInfoKey : Any] = [:] 316 | let params: [RequestTuples] 317 | init(params: [RequestTuples]) { 318 | self.params = params 319 | } 320 | func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { 321 | return KeyedDecodingContainer(RequestReader(self, params: params)) 322 | } 323 | func unkeyedContainer() throws -> UnkeyedDecodingContainer { 324 | return RequestUnkeyedReader(parent: self) 325 | } 326 | func singleValueContainer() throws -> SingleValueDecodingContainer { 327 | return RequestUnkeyedReader(parent: self) 328 | } 329 | } 330 | 331 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/RouteServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteServer.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-10-24. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import NIO 20 | import NIOHTTP1 21 | import NIOSSL 22 | import Foundation 23 | 24 | var sharedNonBlockingFileIO: NonBlockingFileIO = { 25 | let threadPool = NIOThreadPool(numberOfThreads: NonBlockingFileIO.defaultThreadPoolSize) 26 | threadPool.start() 27 | return NonBlockingFileIO(threadPool: threadPool) 28 | }() 29 | 30 | /// Routes which have been bound to a port and have started listening for connections. 31 | public protocol ListeningRoutes { 32 | /// Stop listening for requests 33 | @discardableResult 34 | func stop() -> ListeningRoutes 35 | /// Wait, perhaps forever, until the routes have stopped listening for requests. 36 | func wait() throws 37 | } 38 | 39 | /// Routes which have been bound to an address but are not yet listening for requests. 40 | public protocol BoundRoutes { 41 | /// The address the server is bound to. 42 | var address: SocketAddress { get } 43 | /// Start listening 44 | func listen() throws -> ListeningRoutes 45 | } 46 | 47 | class NIOBoundRoutes: BoundRoutes { 48 | private let childGroup: EventLoopGroup 49 | let acceptGroup: MultiThreadedEventLoopGroup 50 | private let channel: Channel 51 | public let address: SocketAddress 52 | init(registry: Routes, 53 | address: SocketAddress, 54 | threadGroup: EventLoopGroup?, 55 | tls: TLSConfiguration?) throws { 56 | 57 | let ag = MultiThreadedEventLoopGroup(numberOfThreads: 1) 58 | acceptGroup = ag 59 | childGroup = threadGroup ?? ag 60 | let finder = try RouteFinderDual(registry) 61 | self.address = address 62 | 63 | let sslContext: NIOSSLContext? 64 | if let tls = tls { 65 | sslContext = try NIOSSLContext(configuration: tls) 66 | } else { 67 | sslContext = nil 68 | } 69 | var bs = ServerBootstrap(group: acceptGroup, childGroup: childGroup) 70 | .serverChannelOption(ChannelOptions.backlog, value: 256) 71 | .serverChannelOption(ChannelOptions.maxMessagesPerRead, value: 1) 72 | .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) 73 | if threadGroup == nil { 74 | bs = bs.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT), value: 1) 75 | } 76 | channel = try bs 77 | .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) 78 | .childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) 79 | .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1) 80 | .childChannelOption(ChannelOptions.autoRead, value: true) 81 | .childChannelOption(ChannelOptions.allowRemoteHalfClosure, value: true) 82 | .childChannelOption(ChannelOptions.recvAllocator, value: AdaptiveRecvByteBufferAllocator(minimum: 1024, initial: 4096, maximum: 65536)) 83 | .childChannelInitializer { 84 | channel in 85 | NIOBoundRoutes.configureHTTPServerPipeline(pipeline: channel.pipeline, sslContext: sslContext) 86 | .flatMap { 87 | _ in 88 | channel.pipeline.addHandler(NIOHTTPHandler(finder: finder, isTLS: sslContext != nil), name: "NIOHTTPHandler") 89 | } 90 | }.bind(to: address).wait() 91 | } 92 | public func listen() throws -> ListeningRoutes { 93 | return NIOListeningRoutes(channel: channel) 94 | } 95 | private static func configureHTTPServerPipeline(pipeline: ChannelPipeline, sslContext: NIOSSLContext?) -> EventLoopFuture { 96 | var handlers: [ChannelHandler] = [] 97 | if let sslContext = sslContext { 98 | let handler = try! NIOSSLServerHandler(context: sslContext) 99 | handlers.append(handler) 100 | handlers.append(SSLFileRegionHandler()) 101 | } 102 | let responseEncoder = HTTPResponseEncoder() 103 | let requestDecoder = HTTPRequestDecoder(leftOverBytesStrategy: .dropBytes) 104 | 105 | return pipeline.addHandlers(handlers) 106 | .flatMap { pipeline.addHandler(responseEncoder, name: "HTTPResponseEncoder", position: .last) } 107 | .flatMap { pipeline.addHandler(ByteToMessageHandler(requestDecoder), name: "HTTPRequestDecoder", position: .last) } 108 | .flatMap { pipeline.addHandler(HTTPServerPipelineHandler(), name: "HTTPServerPipelineHandler", position: .last) } 109 | .flatMap { pipeline.addHandler(HTTPServerProtocolErrorHandler(), name: "HTTPServerProtocolErrorHandler", position: .last) } 110 | } 111 | } 112 | 113 | private final class SSLFileRegionHandler: ChannelOutboundHandler { 114 | typealias OutboundIn = IOData 115 | typealias OutboundOut = IOData 116 | 117 | private static let fileIO = sharedNonBlockingFileIO 118 | private typealias BufferedWrite = (data: IOData, promise: EventLoopPromise?) 119 | private var buffer = MarkedCircularBuffer(initialCapacity: 96) 120 | 121 | func write(context: ChannelHandlerContext, 122 | data: NIOAny, 123 | promise: EventLoopPromise?) { 124 | buffer.append((data: unwrapOutboundIn(data), promise: promise)) 125 | } 126 | func flush(context: ChannelHandlerContext) { 127 | buffer.mark() 128 | flushBuffer(context: context) 129 | } 130 | func flushBuffer(context: ChannelHandlerContext) { 131 | guard buffer.hasMark else { 132 | return context.flush() 133 | } 134 | guard let item = buffer.popFirst() else { 135 | return 136 | } 137 | let unwrapped = item.data 138 | let promise = item.promise 139 | 140 | switch unwrapped { 141 | case .fileRegion(let region): 142 | SSLFileRegionHandler.fileIO.readChunked(fileRegion: region, 143 | allocator: ByteBufferAllocator(), 144 | eventLoop: context.eventLoop) { 145 | context.writeAndFlush(self.wrapOutboundOut(.byteBuffer($0))) 146 | }.always { 147 | _ in 148 | self.flushBuffer(context: context) 149 | }.cascade(to: promise) 150 | case .byteBuffer(_): 151 | context.write(wrapOutboundOut(unwrapped), promise: promise) 152 | flushBuffer(context: context) 153 | } 154 | } 155 | } 156 | 157 | class NIOListeningRoutes: ListeningRoutes { 158 | private let channel: Channel 159 | private let f: EventLoopFuture 160 | private static var globalInitialized: Bool = { 161 | var sa = sigaction() 162 | // !FIX! re-evaluate which of these are required 163 | #if os(Linux) 164 | sa.__sigaction_handler.sa_handler = SIG_IGN 165 | #else 166 | sa.__sigaction_u.__sa_handler = SIG_IGN 167 | #endif 168 | sa.sa_flags = 0 169 | sigaction(SIGPIPE, &sa, nil) 170 | var rlmt = rlimit() 171 | #if os(Linux) 172 | getrlimit(Int32(RLIMIT_NOFILE.rawValue), &rlmt) 173 | rlmt.rlim_cur = rlmt.rlim_max 174 | setrlimit(Int32(RLIMIT_NOFILE.rawValue), &rlmt) 175 | #else 176 | getrlimit(RLIMIT_NOFILE, &rlmt) 177 | rlmt.rlim_cur = rlim_t(OPEN_MAX) 178 | setrlimit(RLIMIT_NOFILE, &rlmt) 179 | #endif 180 | return true 181 | }() 182 | init(channel: Channel) { 183 | _ = NIOListeningRoutes.globalInitialized 184 | self.channel = channel 185 | f = channel.closeFuture 186 | } 187 | @discardableResult 188 | public func stop() -> ListeningRoutes { 189 | channel.close(promise: nil) 190 | return self 191 | } 192 | public func wait() throws { 193 | try f.wait() 194 | } 195 | } 196 | 197 | public extension Routes where InType == HTTPRequest, OutType == HTTPOutput { 198 | func bind(port: Int, tls: TLSConfiguration? = nil) throws -> BoundRoutes { 199 | let address = try SocketAddress(ipAddress: "0.0.0.0", port: port) 200 | return try bind(address: address, tls: tls) 201 | } 202 | func bind(address: SocketAddress, tls: TLSConfiguration? = nil) throws -> BoundRoutes { 203 | return try NIOBoundRoutes(registry: self, 204 | address: address, 205 | threadGroup: MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount), 206 | tls: tls) 207 | } 208 | func bind(count: Int, address: SocketAddress, tls: TLSConfiguration? = nil) throws -> [BoundRoutes] { 209 | if count == 1 { 210 | return [try bind(address: address)] 211 | } 212 | return try (0.. { return method(.GET) } 395 | var POST: Routes { return method(.POST) } 396 | var PUT: Routes { return method(.PUT) } 397 | var DELETE: Routes { return method(.DELETE) } 398 | var OPTIONS: Routes { return method(.OPTIONS) } 399 | func method(_ method: HTTPMethod, _ methods: HTTPMethod...) -> Routes { 400 | let methods = [method] + methods 401 | return .init(.init(routes: 402 | registry.routes.flatMap { 403 | route in 404 | return methods.map { 405 | ($0.name + "://" + route.0.splitMethod.1, route.1) 406 | } 407 | } 408 | )) 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/NIOHTTPHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NIOHTTPHandler.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-10-30. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import NIO 20 | import NIOHTTP1 21 | 22 | public extension Routes { 23 | /// Run the call asynchronously on a non-event loop thread. 24 | /// Caller must succeed or fail the given promise to continue the request. 25 | func async(_ call: @escaping (OutType, EventLoopPromise) -> ()) -> Routes { 26 | return applyFuncs { 27 | input in 28 | return input.flatMap { 29 | box in 30 | let p: EventLoopPromise = input.eventLoop.makePromise() 31 | foreignEventsQueue.async { call(box.value, p) } 32 | return p.futureResult.map { return RouteValueBox(box.state, $0) } 33 | } 34 | } 35 | } 36 | } 37 | 38 | final class NIOHTTPHandler: ChannelInboundHandler, HTTPRequest { 39 | public typealias InboundIn = HTTPServerRequestPart 40 | public typealias OutboundOut = HTTPServerResponsePart 41 | enum State { 42 | case none, head, body, end 43 | } 44 | var method: HTTPMethod { return head?.method ?? .GET } 45 | var uri: String { return head?.uri ?? "" } 46 | var headers: HTTPHeaders { return head?.headers ?? .init() } 47 | var uriVariables: [String:String] = [:] 48 | var path: String = "" 49 | var searchArgs: QueryDecoder? 50 | var contentType: String? = nil 51 | var contentLength = 0 52 | var contentRead = 0 53 | var contentConsumed = 0 { 54 | didSet { 55 | assert(contentConsumed <= contentRead && contentConsumed <= contentLength) 56 | } 57 | } 58 | var localAddress: SocketAddress? { return channel?.localAddress } 59 | var remoteAddress: SocketAddress? { return channel?.remoteAddress } 60 | 61 | let finder: RouteFinder 62 | var head: HTTPRequestHead? 63 | var channel: Channel? 64 | var pendingBytes: [ByteBuffer] = [] 65 | var pendingPromise: EventLoopPromise<[ByteBuffer]>? 66 | var readState = State.none 67 | var writeState = State.none 68 | var forceKeepAlive: Bool? = nil 69 | var upgraded = false 70 | let isTLS: Bool 71 | init(finder: RouteFinder, isTLS: Bool) { 72 | self.finder = finder 73 | self.isTLS = isTLS 74 | } 75 | deinit { 76 | // print("~NIOHTTPHandler") 77 | } 78 | 79 | func runRequest() { 80 | guard let requestHead = self.head else { 81 | return 82 | } 83 | let requestInfo = HTTPRequestInfo(head: requestHead, options: isTLS ? .isTLS : []) 84 | guard let fnc = finder[requestHead.method, path] else { 85 | 86 | // !FIX! routes need pre-request error handlers, 404 87 | let error = ErrorOutput(status: .notFound, description: "No route for URI.") 88 | let head = HTTPHead(headers: HTTPHeaders()).merged(with: error.head(request: requestInfo)) 89 | return write(head: head, body: error) 90 | } 91 | let state = HandlerState(request: self, uri: path) 92 | let f = channel!.eventLoop.makeSucceededFuture(RouteValueBox(state, self as HTTPRequest)) 93 | let p = try! fnc(f) 94 | p.whenSuccess { 95 | let body = $0.value 96 | let head = state.responseHead.merged(with: body.head(request: requestInfo)) 97 | self.write(head: head, body: body) 98 | } 99 | p.whenFailure { 100 | error in 101 | var body: HTTPOutput 102 | switch error { 103 | case let error as TerminationType: 104 | switch error { 105 | case .error(let e): 106 | body = e 107 | case .criteriaFailed: 108 | body = BytesOutput(head: state.responseHead, body: []) 109 | case .internalError: 110 | body = ErrorOutput(status: .internalServerError, description: "Internal server error.") 111 | } 112 | case let error as ErrorOutput: 113 | body = error 114 | default: 115 | body = ErrorOutput(status: .internalServerError, description: "Internal server error: \(error)") 116 | } 117 | let head = state.responseHead.merged(with: body.head(request: requestInfo)) 118 | self.write(head: head, body: body) 119 | } 120 | } 121 | func channelActive(context ctx: ChannelHandlerContext) { 122 | channel = ctx.channel 123 | // print("channelActive") 124 | } 125 | func channelInactive(context ctx: ChannelHandlerContext) { 126 | // print("~channelInactive") 127 | } 128 | func channelRead(context ctx: ChannelHandlerContext, data: NIOAny) { 129 | let reqPart = unwrapInboundIn(data) 130 | switch reqPart { 131 | case .head(let head): 132 | http(head: head, ctx: ctx) 133 | case .body(let body): 134 | http(body: body, ctx: ctx) 135 | case .end(let headers): 136 | http(end: headers, ctx: ctx) 137 | } 138 | } 139 | func errorCaught(context ctx: ChannelHandlerContext, error: Error) { 140 | // we don't have any recognized errors to be caught here 141 | ctx.close(promise: nil) 142 | } 143 | func http(head: HTTPRequestHead, ctx: ChannelHandlerContext) { 144 | assert(contentLength == 0) 145 | readState = .head 146 | self.head = head 147 | let (path, args) = head.uri.splitQuery 148 | self.path = path 149 | if let args = args { 150 | searchArgs = QueryDecoder(Array(args.utf8)) 151 | } 152 | contentType = head.headers["content-type"].first 153 | contentLength = Int(head.headers["content-length"].first ?? "0") ?? 0 154 | } 155 | func http(body: ByteBuffer, ctx: ChannelHandlerContext) { 156 | let onlyHead = readState == .head 157 | 158 | readState = .body 159 | let readable = body.readableBytes 160 | if contentRead + readable > contentLength { 161 | // should this close the invalid request? 162 | // or does NIO take care of this case? 163 | let diff = contentLength - contentRead 164 | if diff > 0, let s = body.getSlice(at: 0, length: diff) { 165 | pendingBytes.append(s) 166 | } 167 | contentRead = contentLength 168 | } else { 169 | contentRead += readable 170 | pendingBytes.append(body) 171 | } 172 | if contentRead == contentLength { 173 | readState = .end 174 | } 175 | if let p = pendingPromise { 176 | pendingPromise = nil 177 | p.succeed(consumeContent()) 178 | } 179 | if onlyHead { 180 | runRequest() 181 | } 182 | } 183 | func http(end: HTTPHeaders?, ctx: ChannelHandlerContext) { 184 | if case .head = readState { 185 | runRequest() 186 | } 187 | } 188 | 189 | func reset() { 190 | // !FIX! this. it's prone to break when future mutable properties are added 191 | writeState = .none 192 | readState = .none 193 | head = nil 194 | contentLength = 0 195 | contentConsumed = 0 196 | contentRead = 0 197 | forceKeepAlive = nil 198 | 199 | uriVariables = [:] 200 | path = "" 201 | searchArgs = nil 202 | contentType = nil 203 | } 204 | 205 | func userInboundEventTriggered(context ctx: ChannelHandlerContext, event: Any) { 206 | switch event { 207 | case let evt as ChannelEvent where evt == ChannelEvent.inputClosed: 208 | // The remote peer half-closed the channel. At this time, any 209 | // outstanding response will now get the channel closed, and 210 | // if we are idle or waiting for a request body to finish we 211 | // will close the channel immediately. 212 | switch readState { 213 | case .none, .body: 214 | ctx.close(promise: nil) 215 | case .end, .head: 216 | forceKeepAlive = false 217 | } 218 | default: 219 | ctx.fireUserInboundEventTriggered(event) 220 | } 221 | } 222 | 223 | func channelReadComplete(context ctx: ChannelHandlerContext) { 224 | return 225 | } 226 | } 227 | 228 | // reading 229 | extension NIOHTTPHandler { 230 | func consumeContent() -> [ByteBuffer] { 231 | let cpy = pendingBytes 232 | pendingBytes = [] 233 | let sum = cpy.reduce(0) { $0 + $1.readableBytes } 234 | contentConsumed += sum 235 | return cpy 236 | } 237 | func readSomeContent() -> EventLoopFuture<[ByteBuffer]> { 238 | precondition(nil != self.channel) 239 | let channel = self.channel! 240 | let promise: EventLoopPromise<[ByteBuffer]> = channel.eventLoop.makePromise() 241 | readSomeContent(promise) 242 | return promise.futureResult 243 | } 244 | func readSomeContent(_ promise: EventLoopPromise<[ByteBuffer]>) { 245 | guard contentConsumed < contentLength else { 246 | return promise.succeed([]) 247 | } 248 | let content = consumeContent() 249 | if !content.isEmpty { 250 | return promise.succeed(content) 251 | } 252 | pendingPromise = promise 253 | } 254 | // content can only be read once 255 | func readContent() -> EventLoopFuture { 256 | if contentLength == 0 || contentConsumed == contentLength { 257 | return channel!.eventLoop.makeSucceededFuture(.none) 258 | } 259 | let ret: EventLoopFuture 260 | let ct = contentType ?? "application/octet-stream" 261 | if ct.hasPrefix("multipart/form-data") { 262 | let p: EventLoopPromise = channel!.eventLoop.makePromise() 263 | readContent(multi: MimeReader(ct), p) 264 | ret = p.futureResult 265 | } else { 266 | let p: EventLoopPromise<[UInt8]> = channel!.eventLoop.makePromise() 267 | readContent(p) 268 | if ct.hasPrefix("application/x-www-form-urlencoded") { 269 | ret = p.futureResult.map { 270 | .urlForm(QueryDecoder($0)) 271 | } 272 | } else { 273 | ret = p.futureResult.map { .other($0) } 274 | } 275 | } 276 | return ret 277 | } 278 | 279 | func readContent(multi: MimeReader, _ promise: EventLoopPromise) { 280 | if contentConsumed < contentRead { 281 | consumeContent().forEach { 282 | multi.addToBuffer(bytes: $0.getBytes(at: 0, length: $0.readableBytes) ?? []) 283 | } 284 | } 285 | if contentConsumed == contentLength { 286 | return promise.succeed(.multiPartForm(multi)) 287 | } 288 | readSomeContent().whenSuccess { 289 | buffers in 290 | buffers.forEach { 291 | multi.addToBuffer(bytes: $0.getBytes(at: 0, length: $0.readableBytes) ?? []) 292 | } 293 | self.readContent(multi: multi, promise) 294 | } 295 | } 296 | 297 | func readContent(_ promise: EventLoopPromise<[UInt8]>) { 298 | // fast track 299 | if contentRead == contentLength { 300 | var a: [UInt8] = [] 301 | consumeContent().forEach { 302 | a.append(contentsOf: $0.getBytes(at: 0, length: $0.readableBytes) ?? []) 303 | } 304 | return promise.succeed(a) 305 | } 306 | readContent(accum: [], promise) 307 | } 308 | 309 | func readContent(accum: [UInt8], _ promise: EventLoopPromise<[UInt8]>) { 310 | readSomeContent().whenSuccess { 311 | buffers in 312 | var a: [UInt8] 313 | if buffers.count == 1 && accum.isEmpty { 314 | a = buffers.first!.getBytes(at: 0, length: buffers.first!.readableBytes) ?? [] 315 | } else { 316 | a = accum 317 | buffers.forEach { 318 | a.append(contentsOf: $0.getBytes(at: 0, length: $0.readableBytes) ?? []) 319 | } 320 | } 321 | if self.contentConsumed == self.contentLength { 322 | promise.succeed(a) 323 | } else { 324 | self.readContent(accum: a, promise) 325 | } 326 | } 327 | } 328 | } 329 | 330 | // writing 331 | extension NIOHTTPHandler { 332 | func write(head: HTTPHead, body: HTTPOutput) { 333 | writeHead(head) 334 | writeBody(body) 335 | } 336 | private func writeHead(_ output: HTTPHead) { 337 | guard let head = head else { 338 | return // … 339 | } 340 | writeState = .head 341 | let headers = output.headers 342 | var h = HTTPResponseHead(version: head.version, 343 | status: output.status ?? .ok, 344 | headers: headers) 345 | if !self.headers.contains(name: "keep-alive") && !self.headers.contains(name: "close") { 346 | switch (head.isKeepAlive, head.version.major, head.version.minor) { 347 | case (true, 1, 0): 348 | // HTTP/1.0 and the request has 'Connection: keep-alive', we should mirror that 349 | h.headers.add(name: "Connection", value: "keep-alive") 350 | case (false, 1, let n) where n >= 1: 351 | // HTTP/1.1 (or treated as such) and the request has 'Connection: close', we should mirror that 352 | h.headers.add(name: "Connection", value: "close") 353 | default: 354 | () 355 | } 356 | } 357 | channel?.write(wrapOutboundOut(.head(h)), promise: nil) 358 | } 359 | private func writeBody(_ body: HTTPOutput) { 360 | guard let channel = self.channel, 361 | writeState != .end else { 362 | return 363 | } 364 | let promiseBytes = channel.eventLoop.makePromise(of: IOData?.self) 365 | promiseBytes.futureResult.whenSuccess { 366 | let writeDonePromise: EventLoopPromise = channel.eventLoop.makePromise() 367 | if let bytes = $0 { 368 | writeDonePromise.futureResult.whenSuccess { 369 | _ = channel.eventLoop.submit { 370 | self.writeBody(body) 371 | } 372 | } 373 | if bytes.readableBytes > 0 { 374 | channel.writeAndFlush(self.wrapOutboundOut(.body(bytes)), promise: writeDonePromise) 375 | } else { 376 | writeDonePromise.succeed(()) 377 | } 378 | } else { 379 | let keepAlive = self.forceKeepAlive ?? self.head?.isKeepAlive ?? false 380 | self.reset() 381 | if !self.upgraded { 382 | body.closed() 383 | writeDonePromise.futureResult.whenComplete { 384 | _ in 385 | if !keepAlive { 386 | channel.close(promise: nil) 387 | } 388 | } 389 | channel.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: writeDonePromise) 390 | } else { 391 | channel.flush() 392 | } 393 | } 394 | writeDonePromise.futureResult.whenFailure { 395 | error in 396 | channel.close(promise: nil) 397 | body.closed() 398 | } 399 | } 400 | promiseBytes.futureResult.whenFailure { 401 | error in 402 | channel.close(promise: nil) 403 | body.closed() 404 | } 405 | body.body(promise: promiseBytes, allocator: channel.allocator) 406 | } 407 | 408 | // func userInboundEventTriggered(context ctx: ChannelHandlerContext, event: Any) { 409 | // if event is IdleStateHandler.IdleStateEvent { 410 | // _ = ctx.close() 411 | // } else { 412 | // ctx.fireUserInboundEventTriggered(event) 413 | // } 414 | // } 415 | } 416 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/MimeReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MimeReader.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 7/6/15. 6 | // Copyright (C) 2015 PerfectlySoft, Inc. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | import Foundation 21 | #if os(Linux) 22 | import LinuxBridge 23 | let S_IRUSR = __S_IREAD 24 | let S_IRGRP = (S_IRUSR >> 3) 25 | let S_IWGRP = (SwiftGlibc.S_IWUSR >> 3) 26 | let S_IROTH = (S_IRGRP >> 3) 27 | let S_IWOTH = (S_IWGRP >> 3) 28 | #else 29 | import Darwin 30 | #endif 31 | import PerfectLib 32 | 33 | enum MimeReadState { 34 | case stateNone 35 | case stateBoundary // next thing to be read will be a boundry 36 | case stateHeader // read header lines until data starts 37 | case stateFieldValue // read a simple value; name has already been set 38 | case stateFile // read file data until boundry 39 | case stateDone 40 | } 41 | 42 | let kMultiPartForm = "multipart/form-data" 43 | let kBoundary = "boundary" 44 | 45 | let kContentDisposition = "Content-Disposition" 46 | let kContentType = "Content-Type" 47 | 48 | let kPerfectTempPrefix = "perfect_upload_" 49 | 50 | let mime_cr: UInt8 = 13 51 | let mime_lf: UInt8 = 10 52 | let mime_dash: UInt8 = 45 53 | 54 | /// This class is responsible for reading multi-part POST form data, including handling file uploads. 55 | /// Data can be given for parsing in little bits at a time by calling the `addTobuffer` function. 56 | /// Any file uploads which are encountered will be written to the temporary directory indicated when the `MimeReader` is created. 57 | /// Temporary files will be deleted when this object is deinitialized. 58 | public final class MimeReader { 59 | 60 | /// Array of BodySpecs representing each part that was parsed. 61 | public var bodySpecs = [BodySpec]() 62 | 63 | var (multi, gotFile) = (false, false) 64 | var buffer = [UInt8]() 65 | let tempDirectory: String 66 | var state: MimeReadState = .stateNone 67 | 68 | /// The boundary identifier. 69 | public var boundary = "" 70 | 71 | /// This class represents a single part of a multi-part POST submission 72 | public class BodySpec { 73 | /// The name of the form field. 74 | public var fieldName = "" 75 | /// The value for the form field. 76 | /// Having a fieldValue and a file are mutually exclusive. 77 | public var fieldValue = "" 78 | var fieldValueTempBytes: [UInt8]? 79 | /// The content-type for the form part. 80 | public var contentType = "" 81 | /// The client-side file name as submitted by the form. 82 | public var fileName = "" 83 | /// The size of the file which was submitted. 84 | public var fileSize = 0 85 | /// The name of the temporary file which stores the file upload on the server-side. 86 | public var tmpFileName = "" 87 | /// The File object for the local temporary file. 88 | public var file: File? 89 | 90 | init() { 91 | 92 | } 93 | 94 | /// Clean up the BodySpec, possibly closing and deleting any associated temporary file. 95 | public func cleanup() { 96 | if let f = file { 97 | if f.exists { 98 | f.delete() 99 | } 100 | file = nil 101 | } 102 | } 103 | 104 | deinit { 105 | cleanup() 106 | } 107 | } 108 | 109 | /// Initialize given a Content-type header line. 110 | /// - parameter contentType: The Content-type header line. 111 | /// - parameter tempDir: The path to the directory in which to store temporary files. Defaults to "/tmp/". 112 | public init(_ contentType: String, tempDir: String = "/tmp/") { 113 | tempDirectory = tempDir 114 | if contentType.hasPrefix(kMultiPartForm) { 115 | multi = true 116 | if let range = contentType.range(of: kBoundary) { 117 | 118 | let startIndex = contentType.index(range.lowerBound, offsetBy: kBoundary.count+1) 119 | let endIndex = contentType.endIndex 120 | 121 | let boundaryString = String(contentType[startIndex...Index) -> Bool { 135 | var gen = boundary.utf8.makeIterator() 136 | var pos = start 137 | var next = gen.next() 138 | while let char = next { 139 | 140 | if pos == byts.endIndex || char != byts[pos] { 141 | return false 142 | } 143 | 144 | pos += 1 145 | next = gen.next() 146 | } 147 | return next == nil // got to the end is success 148 | } 149 | 150 | func isField(name nam: String, bytes: [UInt8], start: Array.Index) -> Array.Index { 151 | var check = start 152 | let end = bytes.endIndex 153 | var gen = nam.utf8.makeIterator() 154 | while check != end { 155 | if bytes[check] == 58 { // : 156 | return check 157 | } 158 | let gened = gen.next() 159 | 160 | if gened == nil { 161 | break 162 | } 163 | 164 | if tolower(Int32(gened!)) != tolower(Int32(bytes[check])) { 165 | break 166 | } 167 | 168 | check = check.advanced(by: 1) 169 | } 170 | return end 171 | } 172 | 173 | func pullValue(name nam: String, from: String) -> String { 174 | var accum = "" 175 | let option = String.CompareOptions.caseInsensitive 176 | if let nameRange = from.range(of: nam + "=", options: option) { 177 | var start = nameRange.upperBound 178 | let end = from.endIndex 179 | 180 | if from[start] == "\"" { 181 | start = from.index(after: start) 182 | } 183 | 184 | while start < end { 185 | if from[start] == "\"" || from[start] == ";" { 186 | break; 187 | } 188 | accum.append(from[start]) 189 | start = from.index(after: start) 190 | } 191 | } 192 | return accum 193 | } 194 | 195 | @discardableResult 196 | func internalAddToBuffer(bytes byts: [UInt8]) -> MimeReadState { 197 | 198 | var clearBuffer = true 199 | var position = byts.startIndex 200 | let end = byts.endIndex 201 | 202 | while position != end { 203 | switch state { 204 | case .stateDone, .stateNone: 205 | return .stateNone 206 | case .stateBoundary: 207 | if position.distance(to: end) < boundary.count + 2 { 208 | buffer = Array(byts[position.. 1 { 229 | let b1 = byts[eolPos] 230 | let b2 = byts[eolPos.advanced(by: 1)] 231 | if b1 == mime_cr && b2 == mime_lf { 232 | break 233 | } 234 | eolPos = eolPos.advanced(by: 1) 235 | } 236 | if eolPos.distance(to: end) <= 1 { // no eol 237 | buffer = Array(byts[position.. 1 && byts[position] == mime_cr && byts[position.advanced(by: 1)] == mime_lf { 263 | position = position.advanced(by: 2) 264 | if spec.fileName.count > 0 { 265 | openTempFile(spec: spec) 266 | state = .stateFile 267 | } else { 268 | state = .stateFieldValue 269 | spec.fieldValueTempBytes = [UInt8]() 270 | } 271 | } 272 | } 273 | case .stateFieldValue: 274 | let spec = bodySpecs.last! 275 | while position != end { 276 | if byts[position] == mime_cr { 277 | if position.distance(to: end) == 1 { 278 | buffer = Array(byts[position.., length: Int) { 400 | if isMultiPart { 401 | if self.buffer.count != 0 { 402 | for i in 0.. EventLoopFuture 56 | func writeMessage(_ message: WebSocketMessage) -> EventLoopFuture 57 | } 58 | 59 | public typealias WebSocketHandler = (WebSocket) -> () 60 | 61 | public extension Routes { 62 | func webSocket(protocol: String, _ callback: @escaping (OutType) throws -> WebSocketHandler) -> Routes { 63 | return applyFuncs { 64 | return $0.flatMapThrowing { 65 | RouteValueBox($0.state, WebSocketUpgradeHTTPOutput(request: $0.state.request, handler: try callback($0.value))) 66 | } 67 | } 68 | } 69 | } 70 | 71 | fileprivate extension HTTPHeaders { 72 | func nonListHeader(_ name: String) -> String? { 73 | let fields = self[canonicalForm: name] 74 | guard fields.count == 1 else { 75 | return nil 76 | } 77 | return String(fields[0]) 78 | } 79 | } 80 | 81 | public final class WebSocketUpgradeHTTPOutput: HTTPOutput { 82 | private let magicWebSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 83 | let request: HTTPRequest 84 | let handler: WebSocketHandler 85 | var failed = false 86 | public init(request: HTTPRequest, handler: @escaping WebSocketHandler) { 87 | self.request = request 88 | self.handler = handler 89 | } 90 | public override func head(request: HTTPRequestInfo) -> HTTPHead? { 91 | var extraHeaders = HTTPHeaders() 92 | // The version must be 13. 93 | guard let key = request.head.headers.nonListHeader("Sec-WebSocket-Key"), 94 | let version = request.head.headers.nonListHeader("Sec-WebSocket-Version"), 95 | version == "13" else { 96 | failed = true 97 | return HTTPHead(status: .badRequest, headers: extraHeaders) 98 | } 99 | let acceptValue: String 100 | do { 101 | var hasher = SHA1() 102 | hasher.update(string: key) 103 | hasher.update(string: magicWebSocketGUID) 104 | acceptValue = String(base64Encoding: hasher.finish()) 105 | } 106 | extraHeaders.replaceOrAdd(name: "Upgrade", value: "websocket") 107 | extraHeaders.add(name: "Sec-WebSocket-Accept", value: acceptValue) 108 | extraHeaders.replaceOrAdd(name: "Connection", value: "upgrade") 109 | return HTTPHead(status: .switchingProtocols, headers: extraHeaders) 110 | } 111 | 112 | public override func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) { 113 | guard !failed, let channel = request.channel, let request = self.request as? NIOHTTPHandler else { 114 | return promise.succeed(nil) 115 | } 116 | request.upgraded = true 117 | channel.pipeline.removeHandler(name: "HTTPResponseEncoder") 118 | .flatMap { 119 | _ in 120 | channel.pipeline.removeHandler(name: "HTTPRequestDecoder") 121 | }.flatMap { 122 | _ in 123 | channel.pipeline.removeHandler(name: "HTTPServerPipelineHandler") 124 | }.flatMap { 125 | _ in 126 | channel.pipeline.removeHandler(name: "HTTPServerProtocolErrorHandler") 127 | }.flatMap { 128 | _ in 129 | channel.pipeline.removeHandler(name: "NIOHTTPHandler") 130 | }.flatMap { 131 | _ in 132 | channel.pipeline.addHandlers([ WebSocketFrameEncoder(), 133 | ByteToMessageHandler(WebSocketFrameDecoder()), 134 | WebSocketProtocolErrorHandler(), 135 | NIOWebSocketHandler(channel: channel, socketHandler: self.handler)], 136 | position: .last) 137 | // }.then { 138 | // channel.setOption(option: ChannelOptions.autoRead, value: false) // !FIX! this made it stop reading altogether. rather have messages be pulled 139 | }.whenComplete { 140 | _ in 141 | promise.succeed(nil) 142 | } 143 | } 144 | } 145 | 146 | public struct WebSocketError: Error, CustomStringConvertible { 147 | public let description: String 148 | init(_ description: String) { 149 | self.description = description 150 | } 151 | } 152 | 153 | private struct NIOWebSocket: WebSocket { 154 | let handler: NIOWebSocketHandler // !FIX! does this cause retain cycle? 155 | var options: [WebSocketOption] { 156 | get { return handler.options } 157 | set { handler.options = newValue } 158 | } 159 | func readMessage() -> EventLoopFuture { 160 | return handler.issueRead() 161 | } 162 | func writeMessage(_ message: WebSocketMessage) -> EventLoopFuture { 163 | return handler.writeMessage(message) 164 | } 165 | } 166 | 167 | private final class NIOWebSocketHandler: ChannelInboundHandler { 168 | enum CloseState { 169 | case open, closed, sentClose, receivedClose 170 | } 171 | typealias InboundIn = WebSocketFrame 172 | typealias OutboundOut = WebSocketFrame 173 | let channel: Channel 174 | fileprivate var sentClose = false 175 | fileprivate var socketHandler: WebSocketHandler 176 | private var waitingPromise: EventLoopPromise? 177 | private var waitingMessages: [WebSocketMessage] = [] 178 | var options: [WebSocketOption] = [] { 179 | didSet { 180 | for option in options { 181 | switch option { 182 | case .manualClose: 183 | manualClose = true 184 | case .manualPing: 185 | manualPing = true 186 | case .pingInterval(let time): 187 | pingInterval = time 188 | case .responseTimeout(let time): 189 | responseTimeout = time 190 | } 191 | } 192 | } 193 | } 194 | var closeState = CloseState.open 195 | var manualClose = false 196 | var manualPing = false 197 | var pingInterval: Int = 0 198 | var responseTimeout: Int = 0 199 | 200 | init(channel: Channel, socketHandler: @escaping WebSocketHandler) { 201 | self.channel = channel 202 | self.socketHandler = socketHandler 203 | } 204 | func issueRead() -> EventLoopFuture { 205 | if !waitingMessages.isEmpty { 206 | return channel.eventLoop.makeSucceededFuture(waitingMessages.removeFirst()) 207 | } 208 | if let p = waitingPromise { 209 | return p.futureResult 210 | } 211 | let p = channel.eventLoop.makePromise(of: WebSocketMessage.self) 212 | waitingPromise = p 213 | channel.read() 214 | return p.futureResult 215 | } 216 | func writeMessage(_ message: WebSocketMessage) -> EventLoopFuture { 217 | switch message { 218 | case .close: 219 | switch closeState { 220 | case .open: 221 | closeState = .sentClose 222 | case .closed: 223 | return channel.eventLoop.makeFailedFuture(WebSocketError("Connection not open.")) 224 | case .sentClose: 225 | return channel.eventLoop.makeFailedFuture(WebSocketError("Close already sent.")) 226 | case .receivedClose: 227 | closeState = .closed 228 | } 229 | let stupidEmptyBuffer = channel.allocator.buffer(capacity: 0) 230 | let closeFrame = WebSocketFrame(fin: true, opcode: .connectionClose, data: stupidEmptyBuffer) 231 | let fut = channel.writeAndFlush(wrapOutboundOut(closeFrame)) 232 | if case .closed = closeState { 233 | return fut.flatMap { self.channel.close(mode: .all) } 234 | } 235 | return fut 236 | case .ping: 237 | let stupidEmptyBuffer = channel.allocator.buffer(capacity: 0) 238 | let closeFrame = WebSocketFrame(fin: true, opcode: .ping, data: stupidEmptyBuffer) 239 | return channel.writeAndFlush(wrapOutboundOut(closeFrame)) 240 | case .pong: 241 | let stupidEmptyBuffer = channel.allocator.buffer(capacity: 0) 242 | let closeFrame = WebSocketFrame(fin: true, opcode: .pong, data: stupidEmptyBuffer) 243 | return channel.writeAndFlush(wrapOutboundOut(closeFrame)) 244 | case .text(let text): 245 | let bytes = Array(text.utf8) 246 | var buffer = channel.allocator.buffer(capacity: bytes.count) 247 | buffer.writeBytes(bytes) 248 | let frame = WebSocketFrame(fin: true, opcode: .text, data: buffer) 249 | return channel.writeAndFlush(wrapOutboundOut(frame)) 250 | case .binary(let bytes): 251 | var buffer = channel.allocator.buffer(capacity: bytes.count) 252 | buffer.writeBytes(bytes) 253 | let frame = WebSocketFrame(fin: true, opcode: .binary, data: buffer) 254 | return channel.writeAndFlush(wrapOutboundOut(frame)) 255 | } 256 | } 257 | func handlerAdded(context ctx: ChannelHandlerContext) { 258 | socketHandler(NIOWebSocket(handler: self)) 259 | } 260 | func handlerRemoved(context ctx: ChannelHandlerContext) { 261 | 262 | } 263 | func errorCaught(context ctx: ChannelHandlerContext, error: Error) { 264 | // we don't have any recognized errors to be caught here 265 | ctx.close(promise: nil) 266 | } 267 | func channelRead(context ctx: ChannelHandlerContext, data: NIOAny) { 268 | let frame = unwrapInboundIn(data) 269 | switch frame.opcode { 270 | case .connectionClose: 271 | switch closeState { 272 | case .open: 273 | closeState = .receivedClose 274 | if !manualClose { 275 | writeMessage(.close).whenComplete {_ in 276 | self.queueMessage(.close) 277 | } 278 | } else { 279 | queueMessage(.close) 280 | } 281 | case .sentClose: 282 | closeState = .closed 283 | ctx.close(mode: .all).whenComplete {_ in 284 | self.queueMessage(.close) 285 | } 286 | case .closed, .receivedClose: 287 | closeOnError(context: ctx) 288 | } 289 | case .ping: 290 | if !manualPing { 291 | pong(context: ctx, frame: frame) 292 | } 293 | queueMessage(.ping) 294 | case .text: 295 | var data = frame.unmaskedData 296 | let text = data.readString(length: data.readableBytes) ?? "" 297 | queueMessage(.text(text)) 298 | case .binary: 299 | var data = frame.unmaskedData 300 | let binary = data.readBytes(length: data.readableBytes) ?? [] 301 | queueMessage(.binary(binary)) 302 | case .continuation: 303 | () 304 | case .pong: 305 | queueMessage(.pong) 306 | default: 307 | closeOnError(context: ctx) 308 | } 309 | } 310 | private func queueMessage(_ msg: WebSocketMessage) { 311 | waitingMessages.append(msg) 312 | checkWaitingPromise() 313 | } 314 | @discardableResult 315 | private func checkWaitingPromise() -> Bool { 316 | guard !waitingMessages.isEmpty, let p = waitingPromise else { 317 | return false 318 | } 319 | waitingPromise = nil 320 | p.succeed(waitingMessages.removeFirst()) 321 | return true 322 | } 323 | func channelReadComplete(context ctx: ChannelHandlerContext) { 324 | ctx.flush() 325 | } 326 | private func receivedClose(context ctx: ChannelHandlerContext, frame: WebSocketFrame) { 327 | // if awaitingClose { 328 | ctx.close(promise: nil) 329 | // } else { 330 | // var data = frame.unmaskedData 331 | // let closeDataCode = data.readSlice(length: 2) ?? ctx.channel.allocator.buffer(capacity: 0) 332 | // let closeFrame = WebSocketFrame(fin: true, opcode: .connectionClose, data: closeDataCode) 333 | // _ = ctx.write(wrapOutboundOut(closeFrame)).map { () in 334 | // ctx.close(promise: nil) 335 | // } 336 | // } 337 | closeState = .closed 338 | } 339 | 340 | private func pong(context ctx: ChannelHandlerContext, frame: WebSocketFrame) { 341 | var frameData = frame.data 342 | let maskingKey = frame.maskKey 343 | if let maskingKey = maskingKey { 344 | frameData.webSocketUnmask(maskingKey) 345 | } 346 | let responseFrame = WebSocketFrame(fin: true, opcode: .pong, data: frameData) 347 | ctx.write(wrapOutboundOut(responseFrame), promise: nil) 348 | } 349 | 350 | private func closeOnError(context ctx: ChannelHandlerContext) { 351 | var data = ctx.channel.allocator.buffer(capacity: 2) 352 | data.write(webSocketErrorCode: .protocolError) 353 | let frame = WebSocketFrame(fin: true, opcode: .connectionClose, data: data) 354 | ctx.write(wrapOutboundOut(frame)).whenComplete { 355 | _ in 356 | ctx.close(mode: .output, promise: nil) 357 | } 358 | closeState = .closed 359 | } 360 | } 361 | 362 | //===----------------------------------------------------------------------===// 363 | // 364 | // This source file is part of the SwiftNIO open source project 365 | // 366 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 367 | // Licensed under Apache License v2.0 368 | // 369 | // See LICENSE.txt for license information 370 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 371 | // 372 | // SPDX-License-Identifier: Apache-2.0 373 | // 374 | //===----------------------------------------------------------------------===// 375 | 376 | // The base64 unicode table. 377 | private let base64Table: [UnicodeScalar] = [ 378 | "A", "B", "C", "D", "E", "F", "G", "H", 379 | "I", "J", "K", "L", "M", "N", "O", "P", 380 | "Q", "R", "S", "T", "U", "V", "W", "X", 381 | "Y", "Z", "a", "b", "c", "d", "e", "f", 382 | "g", "h", "i", "j", "k", "l", "m", "n", 383 | "o", "p", "q", "r", "s", "t", "u", "v", 384 | "w", "x", "y", "z", "0", "1", "2", "3", 385 | "4", "5", "6", "7", "8", "9", "+", "/", 386 | ] 387 | 388 | private extension String { 389 | /// Base64 encode an array of UInt8 to a string, without the use of Foundation. 390 | /// 391 | /// This function performs the world's most naive Base64 encoding: no attempts to use a larger 392 | /// lookup table or anything intelligent like that, just shifts and masks. This works fine, for 393 | /// now: the purpose of this encoding is to avoid round-tripping through Data, and the perf gain 394 | /// from avoiding that is more than enough to outweigh the silliness of this code. 395 | init(base64Encoding array: Array) { 396 | // In Base64, 3 bytes become 4 output characters, and we pad to the nearest multiple 397 | // of four. 398 | var outputString = String() 399 | outputString.reserveCapacity(((array.count + 2) / 3) * 4) 400 | 401 | var bytes = array.makeIterator() 402 | while let firstByte = bytes.next() { 403 | let secondByte = bytes.next() 404 | let thirdByte = bytes.next() 405 | outputString.unicodeScalars.append(String.encode(firstByte: firstByte)) 406 | outputString.unicodeScalars.append(String.encode(firstByte: firstByte, secondByte: secondByte)) 407 | outputString.unicodeScalars.append(String.encode(secondByte: secondByte, thirdByte: thirdByte)) 408 | outputString.unicodeScalars.append(String.encode(thirdByte: thirdByte)) 409 | } 410 | 411 | self = outputString 412 | } 413 | 414 | private static func encode(firstByte: UInt8) -> UnicodeScalar { 415 | let index = firstByte >> 2 416 | return base64Table[Int(index)] 417 | } 418 | 419 | private static func encode(firstByte: UInt8, secondByte: UInt8?) -> UnicodeScalar { 420 | var index = (firstByte & 0b00000011) << 4 421 | if let secondByte = secondByte { 422 | index += (secondByte & 0b11110000) >> 4 423 | } 424 | return base64Table[Int(index)] 425 | } 426 | 427 | private static func encode(secondByte: UInt8?, thirdByte: UInt8?) -> UnicodeScalar { 428 | guard let secondByte = secondByte else { 429 | // No second byte means we are just emitting padding. 430 | return "=" 431 | } 432 | var index = (secondByte & 0b00001111) << 2 433 | if let thirdByte = thirdByte { 434 | index += (thirdByte & 0b11000000) >> 6 435 | } 436 | return base64Table[Int(index)] 437 | } 438 | 439 | private static func encode(thirdByte: UInt8?) -> UnicodeScalar { 440 | guard let thirdByte = thirdByte else { 441 | // No third byte means just padding. 442 | return "=" 443 | } 444 | let index = thirdByte & 0b00111111 445 | return base64Table[Int(index)] 446 | } 447 | } 448 | 449 | 450 | private struct SHA1 { 451 | private var sha1Ctx: SHA1_CTX 452 | 453 | /// Create a brand-new hash context. 454 | init() { 455 | self.sha1Ctx = SHA1_CTX() 456 | c_nio_sha1_init(&self.sha1Ctx) 457 | } 458 | 459 | /// Feed the given string into the hash context as a sequence of UTF-8 bytes. 460 | /// 461 | /// - parameters: 462 | /// - string: The string that will be UTF-8 encoded and fed into the 463 | /// hash context. 464 | mutating func update(string: String) { 465 | let buffer = Array(string.utf8) 466 | buffer.withUnsafeBufferPointer { 467 | self.update($0) 468 | } 469 | } 470 | 471 | /// Feed the bytes into the hash context. 472 | /// 473 | /// - parameters: 474 | /// - bytes: The bytes to feed into the hash context. 475 | mutating func update(_ bytes: UnsafeBufferPointer) { 476 | c_nio_sha1_loop(&self.sha1Ctx, bytes.baseAddress!, bytes.count) 477 | } 478 | 479 | /// Complete the hashing. 480 | /// 481 | /// - returns: A 20-byte array of bytes. 482 | mutating func finish() -> [UInt8] { 483 | var hashResult: [UInt8] = Array(repeating: 0, count: 20) 484 | hashResult.withUnsafeMutableBufferPointer { 485 | $0.baseAddress!.withMemoryRebound(to: Int8.self, capacity: 20) { 486 | c_nio_sha1_result(&self.sha1Ctx, $0) 487 | } 488 | } 489 | return hashResult 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /Sources/PerfectNIO/RouteRegistry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteRegistry.swift 3 | // PerfectNIO 4 | // 5 | // Created by Kyle Jessup on 2018-10-23. 6 | // 7 | //===----------------------------------------------------------------------===// 8 | // 9 | // This source file is part of the Perfect.org open source project 10 | // 11 | // Copyright (c) 2015 - 2019 PerfectlySoft Inc. and the Perfect project authors 12 | // Licensed under Apache License v2.0 13 | // 14 | // See http://perfect.org/licensing.html for license information 15 | // 16 | //===----------------------------------------------------------------------===// 17 | // 18 | 19 | import NIO 20 | import NIOHTTP1 21 | 22 | /// An error occurring during process of building a set of routes. 23 | public enum RouteError: Error, CustomStringConvertible { 24 | case duplicatedRoutes([String]) 25 | public var description: String { 26 | switch self { 27 | case .duplicatedRoutes(let r): 28 | return "Duplicated routes: \(r.joined(separator: ", "))" 29 | } 30 | } 31 | } 32 | 33 | // Internal structure of a route set. 34 | // This is exposed to users only through struct `Routes`. 35 | struct RouteRegistry: CustomStringConvertible { 36 | typealias ResolveFunc = (InType) throws -> OutType 37 | typealias Tuple = (String,ResolveFunc) 38 | let routes: [String:ResolveFunc] 39 | public var description: String { 40 | return routes.keys.sorted().joined(separator: "\n") 41 | } 42 | init(_ routes: [String:ResolveFunc]) { 43 | self.routes = routes 44 | } 45 | init(checkedRoutes routes: [Tuple]) throws { 46 | var check = Set() 47 | try routes.forEach { 48 | let key = $0.0 49 | guard !check.contains(key) else { 50 | throw RouteError.duplicatedRoutes([key]) 51 | } 52 | check.insert(key) 53 | } 54 | self.init(Dictionary(uniqueKeysWithValues: routes)) 55 | } 56 | init(routes: [Tuple]) { 57 | self.init(Dictionary(uniqueKeysWithValues: routes)) 58 | } 59 | func append(_ registry: RouteRegistry) -> RouteRegistry { 60 | let a = routes.flatMap { 61 | (t: Tuple) -> [RouteRegistry.Tuple] in 62 | let (itemPath, itemFnc) = t 63 | return registry.routes.map { 64 | (t: RouteRegistry.Tuple) -> RouteRegistry.Tuple in 65 | let (subPath, subFnc) = t 66 | let (meth, path) = subPath.splitMethod 67 | let newPath = nil == meth ? 68 | itemPath.appending(component: path) : 69 | meth!.name + "://" + itemPath.splitMethod.1.appending(component: path) 70 | return (newPath, { try subFnc(itemFnc($0)) }) 71 | } 72 | } 73 | return .init(routes: a) 74 | } 75 | func validate() throws { 76 | let paths = routes.map { $0.0 }.sorted() 77 | var dups = Set() 78 | var last: String? 79 | paths.forEach { 80 | s in 81 | if s == last { 82 | dups.insert(s) 83 | } 84 | last = s 85 | } 86 | guard dups.isEmpty else { 87 | throw RouteError.duplicatedRoutes(Array(dups)) 88 | } 89 | } 90 | } 91 | 92 | // The value used in all route Futures. 93 | struct RouteValueBox { 94 | let state: HandlerState 95 | let value: ValueType 96 | init(_ state: HandlerState, _ value: ValueType) { 97 | self.state = state 98 | self.value = value 99 | } 100 | } 101 | 102 | typealias Future = EventLoopFuture 103 | 104 | /// Main routes object. 105 | /// Created by calling `root()` or by chaining a function from an existing route. 106 | @dynamicMemberLookup 107 | public struct Routes { 108 | typealias Registry = RouteRegistry>, Future>> 109 | let registry: Registry 110 | init(_ registry: Registry) { 111 | self.registry = registry 112 | } 113 | func applyPaths(_ call: (String) -> String) -> Routes { 114 | return .init(.init(routes: registry.routes.map { (call($0.key), $0.value) })) 115 | } 116 | func applyFuncs(_ call: @escaping (Future>) -> Future>) -> Routes { 117 | return .init(.init(routes: registry.routes.map { 118 | let (path, fnc) = $0 119 | return (path, { call(try fnc($0)) }) 120 | })) 121 | } 122 | func apply(paths: (String) -> String, funcs call: @escaping (Future>) -> Future>) -> Routes { 123 | return .init(.init(routes: registry.routes.map { 124 | let (path, fnc) = $0 125 | return (paths(path), { call(try fnc($0)) }) 126 | })) 127 | } 128 | } 129 | 130 | /// Create a root route accepting/returning the HTTPRequest. 131 | /// `root()` 132 | public func root() -> Routes { 133 | return .init(.init(["/":{$0}])) 134 | } 135 | 136 | /// Create a root route accepting the HTTPRequest and returning some new value. 137 | /// `root { r in … }` 138 | public func root(_ call: @escaping (HTTPRequest) throws -> NewOut) -> Routes { 139 | return .init(.init(["/":{$0.flatMapThrowing{RouteValueBox($0.state, try call($0.value))}}])) 140 | } 141 | 142 | /// Create a root route returning some new value. 143 | /// `root { ... }` 144 | public func root(_ call: @escaping () throws -> NewOut) -> Routes { 145 | return .init(.init(["/":{$0.flatMapThrowing{RouteValueBox($0.state, try call())}}])) 146 | } 147 | 148 | /// Create a root route accepting and returning some new value. 149 | /// `root("/", Foo.self)` 150 | public func root(path: String, _ type: NewOut.Type) -> Routes { 151 | return .init(.init([path:{$0}])) 152 | } 153 | 154 | public extension Routes { 155 | /// Add a function mapping the input to the output. 156 | func map(_ call: @escaping (OutType) throws -> NewOut) -> Routes { 157 | return applyFuncs { 158 | return $0.flatMapThrowing { 159 | return RouteValueBox($0.state, try call($0.value)) 160 | } 161 | } 162 | } 163 | /// Add a function mapping the input to the output. 164 | func map(_ call: @escaping () throws -> NewOut) -> Routes { 165 | return applyFuncs { 166 | return $0.flatMapThrowing { 167 | return RouteValueBox($0.state, try call()) 168 | } 169 | } 170 | } 171 | /// Map the values of a Collection to a new Array. 172 | func map(_ call: @escaping (OutType.Element) throws -> NewOut) -> Routes> where OutType: Collection { 173 | return applyFuncs { 174 | return $0.flatMapThrowing { 175 | return RouteValueBox($0.state, try $0.value.map(call)) 176 | } 177 | } 178 | } 179 | } 180 | 181 | public extension Routes { 182 | /// Create and return a new route path. 183 | /// The new route accepts and returns the same types as the existing set. 184 | /// This adds an additional path component to the route set. 185 | subscript(dynamicMember name: String) -> Routes { 186 | return path(name) 187 | } 188 | /// Create and return a new route path. 189 | /// The new route accepts the input value and returns a new value. 190 | /// This adds an additional path component to the route set. 191 | subscript(dynamicMember name: String) -> (@escaping (OutType) throws -> NewOut) -> Routes { 192 | return {self.path(name, $0)} 193 | } 194 | /// Create and return a new route path. 195 | /// The new route accepts nothing and returns a new value. 196 | /// This adds an additional path component to the route set. 197 | subscript(dynamicMember name: String) -> (@escaping () throws -> NewOut) -> Routes { 198 | return { call in self.path(name, { _ in return try call()})} 199 | } 200 | } 201 | 202 | public extension Routes { 203 | /// Create and return a new route path. 204 | /// The new route accepts and returns the same types as the existing set. 205 | /// This adds an additional path component to the route set. 206 | func path(_ name: String) -> Routes { 207 | return apply( 208 | paths: {$0.appending(component: name)}, 209 | funcs: { 210 | $0.flatMapThrowing { 211 | $0.state.advanceComponent() 212 | return $0 213 | } 214 | } 215 | ) 216 | } 217 | /// Create and return a new route path. 218 | /// The new route accepts the input value and returns a new value. 219 | /// This adds an additional path component to the route set. 220 | func path(_ name: String, _ call: @escaping (OutType) throws -> NewOut) -> Routes { 221 | return apply( 222 | paths: {$0.appending(component: name)}, 223 | funcs: { 224 | $0.flatMapThrowing { 225 | $0.state.advanceComponent() 226 | return RouteValueBox($0.state, try call($0.value)) 227 | } 228 | } 229 | ) 230 | } 231 | /// Create and return a new route path. 232 | /// The new route accepts nothing and returns a new value. 233 | /// This adds an additional path component to the route set. 234 | func path(_ name: String, _ call: @escaping () throws -> NewOut) -> Routes { 235 | return apply( 236 | paths: {$0.appending(component: name)}, 237 | funcs: { 238 | $0.flatMapThrowing { 239 | $0.state.advanceComponent() 240 | return RouteValueBox($0.state, try call()) 241 | } 242 | } 243 | ) 244 | } 245 | } 246 | 247 | public extension Routes { 248 | /// Adds the indicated file extension to the route set. 249 | func ext(_ ext: String) -> Routes { 250 | let ext = ext.ext 251 | return applyPaths { $0 + ext } 252 | } 253 | /// Adds the indicated file extension to the route set 254 | /// and sets the response's content type. 255 | func ext(_ ext: String, 256 | contentType: String) -> Routes { 257 | let ext = ext.ext 258 | return apply( 259 | paths: {$0 + ext}, 260 | funcs: { 261 | $0.flatMapThrowing { 262 | $0.state.responseHead.headers.add(name: "content-type", value: contentType) 263 | return $0 264 | } 265 | } 266 | ) 267 | } 268 | /// Adds the indicated file extension to the route set. 269 | /// Optionally set the response's content type. 270 | /// The given function accepts the input value and returns a new value. 271 | func ext(_ ext: String, 272 | contentType: String? = nil, 273 | _ call: @escaping (OutType) throws -> NewOut) -> Routes { 274 | let ext = ext.ext 275 | return apply( 276 | paths: {$0 + ext}, 277 | funcs: { 278 | $0.flatMapThrowing { 279 | if let c = contentType { 280 | $0.state.responseHead.headers.add(name: "content-type", value: c) 281 | } 282 | return RouteValueBox($0.state, try call($0.value)) 283 | } 284 | } 285 | ) 286 | } 287 | } 288 | 289 | public extension Routes { 290 | /// Adds a wildcard path component to the route set. 291 | /// The given function accepts the input value and the value for that wildcard path component, as given by the HTTP client, 292 | /// and returns a new value. 293 | func wild(_ call: @escaping (OutType, String) throws -> NewOut) -> Routes { 294 | return apply( 295 | paths: {$0.appending(component: "*")}, 296 | funcs: { 297 | $0.flatMapThrowing { 298 | let c = $0.state.currentComponent ?? "-error-" 299 | $0.state.advanceComponent() 300 | return RouteValueBox($0.state, try call($0.value, c)) 301 | } 302 | } 303 | ) 304 | } 305 | /// Adds a wildcard path component to the route set. 306 | /// Gives the wildcard path component a variable name and the path component value is added as a request urlVariable. 307 | func wild(name: String) -> Routes { 308 | return apply( 309 | paths: {$0.appending(component: "*")}, 310 | funcs: { 311 | $0.flatMapThrowing { 312 | $0.state.request.uriVariables[name] = $0.state.currentComponent ?? "-error-" 313 | $0.state.advanceComponent() 314 | return $0 315 | } 316 | } 317 | ) 318 | } 319 | /// Adds a trailing-wildcard to the route set. 320 | /// The given function accepts the input value and the value for the remaining path components, as given by the HTTP client, 321 | /// and returns a new value. 322 | func trailing(_ call: @escaping (OutType, String) throws -> NewOut) -> Routes { 323 | return apply( 324 | paths: {$0.appending(component: "**")}, 325 | funcs: { 326 | $0.flatMapThrowing { 327 | let c = $0.state.trailingComponents ?? "" 328 | $0.state.advanceComponent() 329 | return RouteValueBox($0.state, try call($0.value, c)) 330 | } 331 | } 332 | ) 333 | } 334 | } 335 | 336 | public extension Routes { 337 | /// Adds the current HTTPRequest as a parameter to the function. 338 | func request(_ call: @escaping (OutType, HTTPRequest) throws -> NewOut) -> Routes { 339 | return applyFuncs { 340 | $0.flatMapThrowing { 341 | return RouteValueBox($0.state, try call($0.value, $0.state.request)) 342 | } 343 | } 344 | } 345 | /// Reads the client content body and delivers it to the provided function. 346 | func readBody(_ call: @escaping (OutType, HTTPRequestContentType) throws -> NewOut) -> Routes { 347 | return applyFuncs { 348 | $0.flatMap { 349 | box in 350 | return box.state.request.readContent().flatMapThrowing { 351 | return RouteValueBox(box.state, try call(box.value, $0)) 352 | } 353 | } 354 | } 355 | } 356 | } 357 | 358 | public extension Routes { 359 | /// The caller can inspect the given input value and choose to return an HTTP error code. 360 | /// If any code outside of 200..<300 is return the request is aborted. 361 | func statusCheck(_ handler: @escaping (OutType) throws -> HTTPResponseStatus) -> Routes { 362 | return applyFuncs { 363 | $0.flatMapThrowing { 364 | box in 365 | let status = try handler(box.value) 366 | box.state.responseHead.status = status 367 | switch status.code { 368 | case 200..<300: 369 | return box 370 | default: 371 | throw TerminationType.criteriaFailed 372 | } 373 | } 374 | } 375 | } 376 | /// The caller can choose to return an HTTP error code. 377 | /// If any code outside of 200..<300 is return the request is aborted. 378 | func statusCheck(_ handler: @escaping () throws -> HTTPResponseStatus) -> Routes { 379 | return statusCheck { _ in try handler() } 380 | } 381 | } 382 | 383 | public extension Routes { 384 | /// Read the client content body and then attempt to decode it as the indicated `Decodable` type. 385 | /// Both the original input value and the newly decoded object are delivered to the provided function. 386 | func decode(_ type: Type.Type, 387 | _ handler: @escaping (OutType, Type) throws -> NewOut) -> Routes { 388 | return readBody { ($0, $1) }.request { 389 | return try handler($0.0, try $1.decode(Type.self, content: $0.1)) 390 | } 391 | } 392 | /// Read the client content body and then attempt to decode it as the indicated `Decodable` type. 393 | /// The newly decoded object is delivered to the provided function. 394 | func decode(_ type: Type.Type, 395 | _ handler: @escaping (Type) throws -> NewOut) -> Routes { 396 | return decode(type) { try handler($1) } 397 | } 398 | /// Read the client content body and then attempt to decode it as the indicated `Decodable` type. 399 | /// The newly decoded object becomes the route set's new output value. 400 | func decode(_ type: Type.Type) -> Routes { 401 | return decode(type) { $1 } 402 | } 403 | } 404 | /* broke with swift 5.2 405 | @_functionBuilder 406 | public struct RouteBuilder { 407 | public typealias RouteType = Routes 408 | 409 | public static func buildExpression(_ expression: RouteType) -> [RouteType] { 410 | return [expression] 411 | } 412 | 413 | public static func buildBlock(_ children: RouteType...) -> [RouteType] { 414 | return children 415 | } 416 | } 417 | 418 | /// These extensions append new route sets to an existing set. 419 | public extension Routes { 420 | /// Append new routes to the set given a new output type. 421 | /// At times, Swift's type inference can fail to discern what the programmer intends when calling functions like this. 422 | /// Calling the second version of this method, the one accepting a `type: NewOut.Type` as the first parameter, 423 | /// can often clarify your intentions to the compiler. If you experience a compilation error with this function, try the other. 424 | func dir(@RouteBuilder makeChildren: (Routes) throws -> [Routes]) throws -> Routes { 425 | return try dir(makeChildren(root(path: "/", OutType.self))) 426 | } 427 | 428 | /// Append new routes to the set given a new output type. 429 | /// The first `type` argument to this function serves to help type inference. 430 | func dir(type: NewOut.Type, @RouteBuilder makeChildren: (Routes) throws -> [Routes]) throws -> Routes { 431 | return try dir(makeChildren(root(path: "/", OutType.self))) 432 | } 433 | 434 | /// Append new routes to the set given a new output type. 435 | /// At times, Swift's type inference can fail to discern what the programmer intends when calling functions like this. 436 | /// Calling the second version of this method, the one accepting a `type: NewOut.Type` as the first parameter, 437 | /// can often clarify your intentions to the compiler. If you experience a compilation error with this function, try the other. 438 | func dir(@RouteBuilder makeChildren: (Routes) throws -> Routes) throws -> Routes { 439 | return try dir([makeChildren(root(path: "/", OutType.self))]) 440 | } 441 | 442 | /// Append new routes to the set given a new output type. 443 | /// The first `type` argument to this function serves to help type inference. 444 | func dir(type: NewOut.Type, @RouteBuilder makeChildren: (Routes) throws -> Routes) throws -> Routes { 445 | return try dir([makeChildren(root(path: "/", OutType.self))]) 446 | } 447 | } 448 | 449 | public func root(@RouteBuilder makeChildren: (Routes) throws -> [Routes]) throws -> Routes { 450 | return try root().dir(makeChildren(root())) 451 | } 452 | 453 | public func root(type: NewOut.Type, @RouteBuilder makeChildren: (Routes) throws -> [Routes]) throws -> Routes { 454 | return try root().dir(makeChildren(root())) 455 | } 456 | */ 457 | 458 | // non-function builder versions 459 | /// These extensions append new route sets to an existing set. 460 | public extension Routes { 461 | /// Append new routes to the set given a new output type. 462 | /// At times, Swift's type inference can fail to discern what the programmer intends when calling functions like this. 463 | /// Calling the second version of this method, the one accepting a `type: NewOut.Type` as the first parameter, 464 | /// can often clarify your intentions to the compiler. If you experience a compilation error with this function, try the other. 465 | func dir(makeChildren: (Routes) throws -> [Routes]) throws -> Routes { 466 | return try dir(makeChildren(root(path: "/", OutType.self))) 467 | } 468 | 469 | /// Append new routes to the set given a new output type. 470 | /// The first `type` argument to this function serves to help type inference. 471 | func dir(type: NewOut.Type, makeChildren: (Routes) throws -> [Routes]) throws -> Routes { 472 | return try dir(makeChildren(root(path: "/", OutType.self))) 473 | } 474 | 475 | /// Append new routes to the set given a new output type. 476 | /// At times, Swift's type inference can fail to discern what the programmer intends when calling functions like this. 477 | /// Calling the second version of this method, the one accepting a `type: NewOut.Type` as the first parameter, 478 | /// can often clarify your intentions to the compiler. If you experience a compilation error with this function, try the other. 479 | func dir(makeChildren: (Routes) throws -> Routes) throws -> Routes { 480 | return try dir([makeChildren(root(path: "/", OutType.self))]) 481 | } 482 | 483 | /// Append new routes to the set given a new output type. 484 | /// The first `type` argument to this function serves to help type inference. 485 | func dir(type: NewOut.Type, makeChildren: (Routes) throws -> Routes) throws -> Routes { 486 | return try dir([makeChildren(root(path: "/", OutType.self))]) 487 | } 488 | } 489 | 490 | public func root(makeChildren: (Routes) throws -> [Routes]) throws -> Routes { 491 | return try root().dir(makeChildren(root())) 492 | } 493 | 494 | public func root(type: NewOut.Type, makeChildren: (Routes) throws -> [Routes]) throws -> Routes { 495 | return try root().dir(makeChildren(root())) 496 | } 497 | // -- 498 | 499 | /// These extensions append new route sets to an existing set. 500 | public extension Routes { 501 | /// Append new routes to this set given an array. 502 | func dir(_ registries: [Routes]) throws -> Routes { 503 | let reg = try RouteRegistry(checkedRoutes: registries.flatMap { $0.registry.routes }) 504 | return .init(registry.append(reg)) 505 | } 506 | /// Append a new route set to this set. 507 | func dir(_ registry: Routes, _ registries: Routes...) throws -> Routes { 508 | return try dir([registry] + registries) 509 | } 510 | } 511 | 512 | public extension Routes { 513 | /// If the output type is an `Optional`, this function permits it to be safely unwraped. 514 | /// If it can not be unwrapped the request is terminated. 515 | /// The provided function is called with the unwrapped value. 516 | func unwrap(_ call: @escaping (U) throws -> NewOut) -> Routes where OutType == Optional { 517 | return map { 518 | guard let unwrapped = $0 else { 519 | throw ErrorOutput(status: .internalServerError, description: "Assertion failed") 520 | } 521 | return try call(unwrapped) 522 | } 523 | } 524 | } 525 | -------------------------------------------------------------------------------- /Tests/PerfectNIOTests/PerfectNIOTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOHTTP1 3 | import NIO 4 | import NIOSSL 5 | import PerfectCRUD 6 | import PerfectCURL 7 | import PerfectLib 8 | import struct Foundation.UUID 9 | @testable import PerfectNIO 10 | 11 | protocol APIResponse: Codable {} 12 | let userCount = 10 13 | 14 | final class PerfectNIOTests: XCTestCase { 15 | func testRoot1() { 16 | do { 17 | let route = root { "OK" }.text() 18 | let server = try route.bind(port: 42000).listen() 19 | defer { 20 | try? server.stop().wait() 21 | } 22 | let req = try CURLRequest("http://localhost:42000/").perform() 23 | XCTAssertEqual(req.bodyString, "OK") 24 | } catch { 25 | XCTFail("\(error)") 26 | } 27 | } 28 | func testRoot2() { 29 | do { 30 | let route1 = root { "OK2" }.text() 31 | let route2 = root { "OK1" }.foo { $0 }.text() 32 | let server = try root().dir(route1, route2).bind(port: 42000).listen() 33 | defer { 34 | try? server.stop().wait() 35 | } 36 | let resp1 = try CURLRequest("http://localhost:42000/foo").perform().bodyString 37 | XCTAssertEqual(resp1, "OK1") 38 | let resp2 = try CURLRequest("http://localhost:42000/").perform().bodyString 39 | XCTAssertEqual(resp2, "OK2") 40 | } catch { 41 | XCTFail("\(error)") 42 | } 43 | } 44 | func testDir1() { 45 | do { 46 | let route = try root().dir {[ 47 | $0.foo1 { "OK1" }, 48 | $0.foo2 { "OK2" }, 49 | $0.foo3 { "OK3" } 50 | ]}.text() 51 | let server = try route.bind(port: 42000).listen() 52 | defer { 53 | try? server.stop().wait() 54 | } 55 | let resp1 = try CURLRequest("http://localhost:42000/foo1").perform().bodyString 56 | XCTAssertEqual(resp1, "OK1") 57 | let resp2 = try CURLRequest("http://localhost:42000/foo2").perform().bodyString 58 | XCTAssertEqual(resp2, "OK2") 59 | let resp3 = try CURLRequest("http://localhost:42000/foo3").perform().bodyString 60 | XCTAssertEqual(resp3, "OK3") 61 | } catch { 62 | XCTFail("\(error)") 63 | } 64 | } 65 | func testDuplicates() { 66 | do { 67 | let route = try root().dir{[ 68 | $0.foo1 { "OK1" }, 69 | $0.foo1 { "OK2" }, 70 | $0.foo3 { "OK3" }, 71 | ]}.text() 72 | let server = try route.bind(port: 42000).listen() 73 | defer { 74 | try? server.stop().wait() 75 | } 76 | XCTAssert(false) 77 | } catch { 78 | XCTAssert(true) 79 | } 80 | } 81 | func testUriVars() { 82 | struct Req: Codable { 83 | let id: UUID 84 | let action: String 85 | } 86 | do { 87 | let route = root().v1 88 | .wild(name: "id") 89 | .wild(name: "action") 90 | .decode(Req.self) { return "\($1.id) - \($1.action)" } 91 | .text() 92 | let id = UUID().uuidString 93 | let action = "share" 94 | let uri1 = "/v1/\(id)/\(action)" 95 | let server = try route.bind(port: 42000).listen() 96 | defer { 97 | try? server.stop().wait() 98 | } 99 | let resp1 = try CURLRequest("http://localhost:42000\(uri1)").perform().bodyString 100 | XCTAssertEqual(resp1, "\(id) - \(action)") 101 | } catch { 102 | XCTAssert(true) 103 | } 104 | } 105 | func testWildCard() { 106 | do { 107 | let route = root().wild { $1 }.foo.text() 108 | let server = try route.bind(port: 42000).listen() 109 | defer { 110 | try? server.stop().wait() 111 | } 112 | let req = try CURLRequest("http://localhost:42000/OK/foo").perform() 113 | XCTAssertEqual(req.bodyString, "OK") 114 | } catch { 115 | XCTFail("\(error)") 116 | } 117 | } 118 | func testTrailingWildCard() { 119 | do { 120 | let route = root().foo.trailing { $1 }.text() 121 | let server = try route.bind(port: 42000).listen() 122 | defer { 123 | try? server.stop().wait() 124 | } 125 | let req = try CURLRequest("http://localhost:42000/foo/OK/OK").perform() 126 | XCTAssertEqual(req.bodyString, "OK/OK") 127 | } catch { 128 | XCTFail("\(error)") 129 | } 130 | } 131 | func testMap1() { 132 | do { 133 | let route: Routes = try root().dir {[ 134 | $0.a { 1 }.map { "\($0)" }.text(), 135 | $0.b { [1,2,3] }.map { (i: Int) -> String in "\(i)" }.json() 136 | ]} 137 | let server = try route.bind(port: 42000).listen() 138 | defer { 139 | try? server.stop().wait() 140 | } 141 | let req1 = try CURLRequest("http://localhost:42000/a").perform() 142 | XCTAssertEqual(req1.bodyString, "1") 143 | let req2 = try CURLRequest("http://localhost:42000/b").perform().bodyJSON(Array.self) 144 | XCTAssertEqual(req2, ["1","2","3"]) 145 | } catch { 146 | XCTFail("\(error)") 147 | } 148 | } 149 | func testStatusCheck1() { 150 | do { 151 | let route: Routes = try root().dir {[ 152 | $0.a.statusCheck { .internalServerError }.map { "BAD" }.text(), 153 | $0.b.statusCheck { _ in .internalServerError }.map { "BAD" }.text(), 154 | $0.c.statusCheck { _ in .ok }.map { "OK" }.text() 155 | ]} 156 | let server = try route.bind(port: 42000).listen() 157 | defer { 158 | try? server.stop().wait() 159 | } 160 | let req1 = try CURLRequest("http://localhost:42000/a").perform() 161 | XCTAssertEqual(req1.responseCode, 500) 162 | let req2 = try CURLRequest("http://localhost:42000/b").perform() 163 | XCTAssertEqual(req2.responseCode, 500) 164 | let req3 = try CURLRequest("http://localhost:42000/c").perform().bodyString 165 | XCTAssertEqual(req3, "OK") 166 | } catch { 167 | XCTFail("\(error)") 168 | } 169 | } 170 | func testMethods1() { 171 | do { 172 | let route: Routes = try root().dir {[ 173 | $0.GET.foo1 { "GET OK" }, 174 | $0.POST.foo2 { "POST OK" } 175 | ]}.text() 176 | let server = try route.bind(port: 42000).listen() 177 | defer { 178 | try? server.stop().wait() 179 | } 180 | let req1 = try CURLRequest("http://localhost:42000/foo1").perform().bodyString 181 | XCTAssertEqual(req1, "GET OK") 182 | let req2 = try CURLRequest("http://localhost:42000/foo2", .postString("")).perform().bodyString 183 | XCTAssertEqual(req2, "POST OK") 184 | let req3 = try CURLRequest("http://localhost:42000/foo1", .postString("")).perform().responseCode 185 | XCTAssertEqual(req3, 404) 186 | } catch { 187 | XCTFail("\(error)") 188 | } 189 | } 190 | func testReadBody1() { 191 | do { 192 | let route = try root(type: String.self) {[ 193 | $0.multi.readBody { 194 | (req, cont) -> String in 195 | switch cont { 196 | case .multiPartForm(_): 197 | return "OK" 198 | case .none, .urlForm, .other: 199 | throw ErrorOutput(status: .badRequest) 200 | } 201 | }, 202 | $0.url.readBody { 203 | (req, cont) -> String in 204 | switch cont { 205 | case .urlForm(_): 206 | return "OK" 207 | case .none, .multiPartForm, .other: 208 | throw ErrorOutput(status: .badRequest) 209 | } 210 | }, 211 | $0.other.readBody { 212 | (req, cont) -> String in 213 | switch cont { 214 | case .other(_): 215 | return "OK" 216 | case .none, .multiPartForm, .urlForm: 217 | throw ErrorOutput(status: .badRequest) 218 | } 219 | } 220 | ]}.POST.text() 221 | let server = try route.bind(port: 42000).listen() 222 | defer { 223 | try? server.stop().wait() 224 | } 225 | let req1 = try CURLRequest("http://localhost:42000/multi", .postField(.init(name: "foo", value: "bar"))).perform().bodyString 226 | XCTAssertEqual(req1, "OK") 227 | let req2 = try CURLRequest("http://localhost:42000/url", .postString("foo=bar")).perform().bodyString 228 | XCTAssertEqual(req2, "OK") 229 | let req3 = try CURLRequest("http://localhost:42000/other", .addHeader(.contentType, "application/octet-stream"), .postData([1,2,3,4,5])).perform().bodyString 230 | XCTAssertEqual(req3, "OK") 231 | } catch { 232 | XCTFail("\(error)") 233 | } 234 | } 235 | func testDecodeBody1() { 236 | struct Foo: Codable { 237 | let id: UUID 238 | let date: Date 239 | } 240 | do { 241 | let route = try root().POST.dir {[ 242 | $0.1.decode(Foo.self), 243 | $0.2.decode(Foo.self) { $1 }, 244 | $0.3.decode(Foo.self) { $0 } 245 | ]}.json() 246 | let server = try route.bind(port: 42000).listen() 247 | defer { 248 | try? server.stop().wait() 249 | } 250 | let foo = Foo(id: UUID(), date: Date()) 251 | let fooData = Array(try JSONEncoder().encode(foo)) 252 | for i in 1...3 { 253 | let req = try CURLRequest("http://localhost:42000/\(i)", .addHeader(.contentType, "application/json"), .postData(fooData)).perform().bodyJSON(Foo.self) 254 | XCTAssertEqual(req.id, foo.id) 255 | XCTAssertEqual(req.date, foo.date) 256 | } 257 | } catch { 258 | XCTFail("\(error)") 259 | } 260 | } 261 | func testPathExt1() { 262 | struct Foo: Codable, CustomStringConvertible { 263 | var description: String { 264 | return "foo-data \(id)/\(date)" 265 | } 266 | let id: UUID 267 | let date: Date 268 | } 269 | do { 270 | let fooRoute = root().foo { Foo(id: UUID(), date: Date()) } 271 | let route = try root().dir( 272 | fooRoute.ext("json").json(), 273 | fooRoute.ext("txt").text()) 274 | let server = try route.bind(port: 42000).listen() 275 | defer { 276 | try? server.stop().wait() 277 | } 278 | let req1 = try? CURLRequest("http://localhost:42000/foo.json").perform().bodyJSON(Foo.self) 279 | XCTAssertNotNil(req1) 280 | let req2 = try CURLRequest("http://localhost:42000/foo.txt").perform().bodyString 281 | XCTAssert(req2.hasPrefix("foo-data ")) 282 | } catch { 283 | XCTFail("\(error)") 284 | } 285 | } 286 | 287 | func testQueryDecoder() { 288 | let q = QueryDecoder(Array("a=1&b=2&c=3&d=4&b=5&e&f=&g=1234567890&h".utf8)) 289 | XCTAssertEqual(q["not"], []) 290 | XCTAssertEqual(q["a"], ["1"]) 291 | XCTAssertEqual(q["b"], ["2", "5"]) 292 | XCTAssertEqual(q["c"], ["3"]) 293 | XCTAssertEqual(q["d"], ["4"]) 294 | XCTAssertEqual(q["e"], [""]) 295 | XCTAssertEqual(q["f"], [""]) 296 | XCTAssertEqual(q["g"], ["1234567890"]) 297 | XCTAssertEqual(q["h"], [""]) 298 | // print("\(q.lookup)") 299 | // print("\(q.ranges)") 300 | } 301 | func testQueryDecoderSpeed() { 302 | func printTupes(_ t: QueryDecoder) { 303 | for c in "abcdefghijklmnopqrstuvwxyz" { 304 | let key = "abc" + String(c) 305 | let _ = t.get(key) 306 | // print(fnd) 307 | } 308 | } 309 | let body = Array("abca=abcdefghijklmnopqrstuvwxyz&abcb=abcdefghijklmnopqrstuvwxyz&abcc=abcdefghijklmnopqrstuvwxyz&abcd=abcdefghijklmnopqrstuvwxyz&abce=abcdefghijklmnopqrstuvwxyz&abcf=abcdefghijklmnopqrstuvwxyz&abcg=abcdefghijklmnopqrstuvwxyz&abch=abcdefghijklmnopqrstuvwxyz&abci=abcdefghijklmnopqrstuvwxyz&abcj=abcdefghijklmnopqrstuvwxyz&abck=abcdefghijklmnopqrstuvwxyz&abcl=abcdefghijklmnopqrstuvwxyz&abcm=abcdefghijklmnopqrstuvwxyz&abcn=abcdefghijklmnopqrstuvwxyz&abco=abcdefghijklmnopqrstuvwxyz&abcp=abcdefghijklmnopqrstuvwxyz&abcq=abcdefghijklmnopqrstuvwxyz&abca=abcdefghijklmnopqrstuvwxyz&abcs=abcdefghijklmnopqrstuvwxyz&abct=abcdefghijklmnopqrstuvwxyz&abcu=abcdefghijklmnopqrstuvwxyz&abcv=abcdefghijklmnopqrstuvwxyz&abcw=abcdefghijklmnopqrstuvwxyz&abcx=abcdefghijklmnopqrstuvwxyz&abcy=abcdefghijklmnopqrstuvwxyz&abcz=abcdefghijklmnopqrstuvwxyz".utf8) 310 | self.measure { 311 | for _ in 0..<20000 { 312 | let q = QueryDecoder(body) 313 | printTupes(q) 314 | } 315 | } 316 | } 317 | 318 | func testTLS1() { 319 | do { 320 | let route = root { "OK" }.text() 321 | let tls = TLSConfiguration.forServer( 322 | certificateChain: [.certificate(serverCert)], 323 | privateKey: .privateKey(serverKey)) 324 | let server = try route.bind(port: 42000, tls: tls).listen() 325 | defer { 326 | try? server.stop().wait() 327 | } 328 | let req1 = try CURLRequest("https://localhost:42000/", .sslVerifyPeer(false), .sslVerifyHost(false)).perform().bodyString 329 | XCTAssertEqual(req1, "OK") 330 | } catch { 331 | XCTFail("\(error)") 332 | } 333 | } 334 | 335 | func testGetRequest1() { 336 | do { 337 | let route = root().foo { "OK" }.request { $1.path }.text() 338 | let server = try route.bind(port: 42000).listen() 339 | defer { 340 | try? server.stop().wait() 341 | } 342 | let req1 = try CURLRequest("http://localhost:42000/foo").perform().bodyString 343 | XCTAssertEqual(req1, "/foo") 344 | } catch { 345 | XCTFail("\(error)") 346 | } 347 | } 348 | 349 | func testAsync1() { 350 | do { 351 | let route = root().async { 352 | (req: HTTPRequest, p: EventLoopPromise) in 353 | sleep(1) 354 | p.succeed("OK") 355 | }.text() 356 | let server = try route.bind(port: 42000).listen() 357 | defer { 358 | try? server.stop().wait() 359 | } 360 | let req1 = try CURLRequest("http://localhost:42000/").perform().bodyString 361 | XCTAssertEqual(req1, "OK") 362 | } catch { 363 | XCTFail("\(error)") 364 | } 365 | } 366 | 367 | func testAsync2() { 368 | struct MyError: Error {} 369 | do { 370 | let route = root().async { 371 | (req: HTTPRequest, p: EventLoopPromise) in 372 | sleep(1) 373 | p.fail(MyError()) 374 | }.text() 375 | let server = try route.bind(port: 42000).listen() 376 | defer { try! server.stop().wait() } 377 | _ = try CURLRequest("http://localhost:42000/", .failOnError).perform() 378 | XCTAssert(false) 379 | } catch { 380 | XCTAssert(true) 381 | } 382 | } 383 | 384 | func testStream1() { 385 | do { 386 | class StreamOutput: HTTPOutput { 387 | var counter = 0 388 | override init() { 389 | super.init() 390 | kind = .multi 391 | } 392 | override func head(request: HTTPRequestInfo) -> HTTPHead? { 393 | return HTTPHead(headers: HTTPHeaders([("content-length", "16384")])) 394 | } 395 | override func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) { 396 | if counter > 15 { 397 | promise.succeed(nil) 398 | } else { 399 | let toSend = String(repeating: "\(counter % 10)", count: 1024) 400 | counter += 1 401 | let ary = Array(toSend.utf8) 402 | var buf = allocator.buffer(capacity: ary.count) 403 | buf.writeBytes(ary) 404 | promise.succeed(.byteBuffer(buf)) 405 | } 406 | } 407 | } 408 | let route = root() { return StreamOutput() as HTTPOutput } 409 | let server = try route.bind(port: 42000).listen() 410 | defer { 411 | try? server.stop().wait() 412 | } 413 | let req = try CURLRequest("http://localhost:42000/").perform() 414 | XCTAssertEqual(req.bodyString.count, 16384) 415 | } catch { 416 | XCTFail("\(error)") 417 | } 418 | } 419 | 420 | func testStream2() { 421 | do { 422 | class StreamOutput: HTTPOutput { 423 | var counter = 0 424 | override init() { 425 | super.init() 426 | kind = .stream 427 | } 428 | override func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) { 429 | if counter > 15 { 430 | promise.succeed(nil) 431 | } else { 432 | let toSend = String(repeating: "\(counter % 10)", count: 1024) 433 | counter += 1 434 | let ary = Array(toSend.utf8) 435 | var buf = allocator.buffer(capacity: ary.count) 436 | buf.writeBytes(ary) 437 | promise.succeed(.byteBuffer(buf)) 438 | } 439 | } 440 | } 441 | let route = root() { return StreamOutput() as HTTPOutput } 442 | let server = try route.bind(port: 42000).listen() 443 | defer { 444 | try? server.stop().wait() 445 | } 446 | let req = try CURLRequest("http://localhost:42000/").perform() 447 | XCTAssertEqual(req.bodyString.count, 16384) 448 | } catch { 449 | XCTFail("\(error)") 450 | } 451 | } 452 | 453 | func testUnwrap() { 454 | do { 455 | let route = try root().dir {[ 456 | $0.a { nil }, 457 | $0.b { "OK" } 458 | ]}.unwrap { $0 }.text() 459 | let server = try route.bind(port: 42000).listen() 460 | defer { 461 | try? server.stop().wait() 462 | } 463 | let req1 = try CURLRequest("http://localhost:42000/b").perform().bodyString 464 | XCTAssertEqual(req1, "OK") 465 | let req2 = try CURLRequest("http://localhost:42000/a").perform() 466 | XCTAssertEqual(req2.responseCode, 500) 467 | } catch { 468 | XCTFail("\(error)") 469 | } 470 | } 471 | 472 | func testAuthEg() { 473 | //let userCount = 10 474 | //protocol APIResponse {} - protocol can't be nested. real thing is up top ^ 475 | struct AuthenticatedRequest { 476 | init?(_ request: HTTPRequest) { 477 | // would check auth and return nil if invalid 478 | } 479 | } 480 | struct User: Codable, APIResponse { 481 | let id: UUID 482 | } 483 | struct FriendList: Codable, APIResponse { 484 | let users: [User] 485 | } 486 | struct AppHandlers { 487 | static func userInfo(user: User) -> User { 488 | // just echo it back 489 | return user 490 | } 491 | static func friendList(for user: User) -> FriendList { 492 | // make up some friends 493 | return FriendList(users: (0.., allocator: ByteBufferAllocator) { 561 | if counter > 15 { 562 | promise.succeed(nil) 563 | } else { 564 | let toSend = String(repeating: "\(counter % 10)", count: 1024) 565 | counter += 1 566 | let ary = Array(toSend.utf8) 567 | var buf = allocator.buffer(capacity: ary.count) 568 | buf.writeBytes(ary) 569 | promise.succeed(.byteBuffer(buf)) 570 | } 571 | } 572 | } 573 | let route = root() { return StreamOutput() as HTTPOutput }.compressed() 574 | let server = try route.bind(port: 42000).listen() 575 | defer { 576 | try? server.stop().wait() 577 | } 578 | do { 579 | let req = try CURLRequest("http://localhost:42000/", .acceptEncoding("gzip, deflate")).perform() 580 | XCTAssertEqual(req.bodyString.count, 16384) 581 | XCTAssert(req.headers.contains(where: { $0.0 == .contentEncoding && $0.1 == "gzip" })) 582 | } 583 | do { 584 | let req = try CURLRequest("http://localhost:42000/", .acceptEncoding("deflate")).perform() 585 | XCTAssertEqual(req.bodyString.count, 16384) 586 | XCTAssert(req.headers.contains(where: { $0.0 == .contentEncoding && $0.1 == "deflate" })) 587 | } 588 | } catch { 589 | XCTFail("\(error)") 590 | } 591 | } 592 | 593 | func testCompress3() { 594 | do { 595 | let tmpFilePath = "/tmp/test.txt" 596 | let file = File(tmpFilePath) 597 | defer { file.delete() } 598 | do { 599 | var bytes: [UInt8] = [] 600 | for i in 0..<16 { 601 | let toSend = String(repeating: "\(i % 10)", count: 1024) 602 | bytes.append(contentsOf: Array(toSend.utf8)) 603 | } 604 | try file.open(.truncate, permissions: [.readUser, .writeUser]) 605 | try file.write(bytes: bytes) 606 | file.close() 607 | } 608 | let route = root().test { 609 | try FileOutput(localPath: tmpFilePath) as HTTPOutput 610 | }.ext("txt").compressed() 611 | let server = try route.bind(port: 42000).listen() 612 | defer { 613 | try? server.stop().wait() 614 | } 615 | let resp = try CURLRequest("http://localhost:42000/test.txt", .acceptEncoding("gzip, deflate")).perform() 616 | XCTAssertEqual(resp.bodyBytes.count, 16384) 617 | XCTAssert(resp.headers.contains(where: { $0.0 == .contentEncoding && $0.1 == "gzip" })) 618 | } catch { 619 | XCTFail("\(error)") 620 | } 621 | } 622 | 623 | func testCompress4() { 624 | do { 625 | let tmpFilePath = "/tmp/test.gif" 626 | let file = File(tmpFilePath) 627 | defer { file.delete() } 628 | do { 629 | var bytes: [UInt8] = [] 630 | for i in 0..<16 { 631 | let toSend = String(repeating: "\(i % 10)", count: 1024) 632 | bytes.append(contentsOf: Array(toSend.utf8)) 633 | } 634 | try file.open(.truncate, permissions: [.readUser, .writeUser]) 635 | try file.write(bytes: bytes) 636 | file.close() 637 | } 638 | let route = root().test { 639 | try FileOutput(localPath: tmpFilePath) as HTTPOutput 640 | }.ext("gif").compressed() 641 | let server = try route.bind(port: 42000).listen() 642 | defer { 643 | try? server.stop().wait() 644 | } 645 | let resp = try CURLRequest("http://localhost:42000/test.gif", .acceptEncoding("gzip, deflate")).perform() 646 | XCTAssertEqual(resp.bodyBytes.count, 16384) 647 | XCTAssert(!resp.headers.contains(where: { $0.0 == .contentEncoding && $0.1 == "gzip" })) 648 | } catch { 649 | XCTFail("\(error)") 650 | } 651 | } 652 | 653 | func testFileOutput() { 654 | do { 655 | let tmpFilePath = "/tmp/test.txt" 656 | let file = File(tmpFilePath) 657 | defer { file.delete() } 658 | do { 659 | var bytes: [UInt8] = [] 660 | for i in 0..<16 { 661 | let toSend = String(repeating: "\(i % 10)", count: 1024) 662 | bytes.append(contentsOf: Array(toSend.utf8)) 663 | } 664 | try file.open(.truncate, permissions: [.readUser, .writeUser]) 665 | try file.write(bytes: bytes) 666 | file.close() 667 | } 668 | let route = root().test { 669 | try FileOutput(localPath: tmpFilePath) as HTTPOutput 670 | }.ext("txt") 671 | let server = try route.bind(port: 42000).listen() 672 | defer { 673 | try? server.stop().wait() 674 | } 675 | let resp = try CURLRequest("http://localhost:42000/test.txt").perform().bodyBytes 676 | XCTAssertEqual(resp.count, 16384) 677 | } catch { 678 | XCTFail("\(error)") 679 | } 680 | } 681 | 682 | func testMustacheOutput() { 683 | do { 684 | let expectedOutput = "key1: value1
key2: value2" 685 | let tmpFilePath = "/tmp/test.mustache" 686 | let file = File(tmpFilePath) 687 | defer { file.delete() } 688 | do { 689 | try file.open(.truncate, permissions: [.readUser, .writeUser]) 690 | try file.write(string: "key1: {{key1}}
key2: {{key2}}") 691 | file.close() 692 | } 693 | let route = root().test { 694 | try MustacheOutput(templatePath: tmpFilePath, 695 | inputs: ["key1":"value1", "key2":"value2"], 696 | contentType: "text/html") as HTTPOutput 697 | }.ext("html") 698 | let server = try route.bind(port: 42000).listen() 699 | defer { 700 | try? server.stop().wait() 701 | } 702 | let resp = try CURLRequest("http://localhost:42000/test.html").perform().bodyString 703 | XCTAssertEqual(resp, expectedOutput) 704 | } catch { 705 | XCTFail("\(error)") 706 | } 707 | } 708 | 709 | func testAddress() { 710 | do { 711 | let port = 42000 712 | let route = root().address { $0.localAddress?.port }.unwrap { "\($0)" }.text() 713 | let address = try SocketAddress(ipAddress: "127.0.0.1", port: port) 714 | let server = try route.bind(address: address).listen() 715 | defer { 716 | try? server.stop().wait() 717 | } 718 | let req1 = try CURLRequest("http://localhost:\(port)/address").perform().bodyString 719 | XCTAssertEqual(req1, "\(port)") 720 | } catch { 721 | XCTFail("\(error)") 722 | } 723 | } 724 | 725 | func testDescribe() { 726 | let expected = Set(["/b/*/foo2", 727 | "POST:///c/foo3", 728 | "HEAD:///d/foo4", 729 | "/a/foo1", 730 | "GET:///d/foo4"]) 731 | let routes = try! root().dir {[ 732 | $0.a.foo1 { "foo" }, 733 | $0.b.wild(name: "p1").foo2 { "foo" }, 734 | $0.POST.c.foo3 { "foo" }, 735 | $0.method(.GET, .HEAD).d.foo4 { "foo" } 736 | ]}.text() 737 | for desc in routes.describe { 738 | let uri = desc.uri 739 | XCTAssert(expected.contains(uri)) 740 | } 741 | } 742 | 743 | static var allTests = [ 744 | ("testRoot1", testRoot1), 745 | ("testRoot2", testRoot2), 746 | ("testDir1", testDir1), 747 | ("testDuplicates", testDuplicates), 748 | ("testUriVars", testUriVars), 749 | ("testWildCard", testWildCard), 750 | ("testTrailingWildCard", testTrailingWildCard), 751 | ("testMap1", testMap1), 752 | ("testStatusCheck1", testStatusCheck1), 753 | ("testMethods1", testMethods1), 754 | ("testReadBody1", testReadBody1), 755 | ("testDecodeBody1", testDecodeBody1), 756 | ("testPathExt1", testPathExt1), 757 | ("testQueryDecoder", testQueryDecoder), 758 | ("testQueryDecoderSpeed", testQueryDecoderSpeed), 759 | ("testTLS1", testTLS1), 760 | ("testGetRequest1", testGetRequest1), 761 | ("testAsync1", testAsync1), 762 | ("testAsync2", testAsync2), 763 | ("testStream1", testStream1), 764 | ("testStream2", testStream2), 765 | ("testUnwrap", testUnwrap), 766 | ("testAuthEg", testAuthEg), 767 | ("testCompress1", testCompress1), 768 | ("testCompress2", testCompress2), 769 | ("testCompress3", testCompress3), 770 | ("testFileOutput", testFileOutput), 771 | ("testMustacheOutput", testMustacheOutput), 772 | ("testAddress", testAddress), 773 | ("testDescribe", testDescribe) 774 | ] 775 | } 776 | 777 | let serverCert = try! NIOSSLCertificate(bytes: 778 | Array(""" 779 | -----BEGIN CERTIFICATE----- 780 | MIICpDCCAYwCCQCW58Rktc4bnjANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAkx 781 | MjcuMC4wLjEwHhcNMTgwMzE2MTM1MzI1WhcNMTgwNDE1MTM1MzI1WjAUMRIwEAYD 782 | VQQDDAkxMjcuMC4wLjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDX 783 | TJ3iM/BWQy+jYQbZhyPaIcLVt/a7g2eaKtE55aiGXovbSXmpgfCi4TuyrmT+I8wJ 784 | 6bv668LaDZcNLhBP6ad7rtnVGorCJNqT845//+ghN75Y7oYi7+0Jx7ctKmizQ+b7 785 | tyfozlMXn1al6kIpdVe3yKuzpzvBz/Vxj+UA88R3kn1QErPRbmcDYYp0LQUHSwn9 786 | KzOkScr+lbn9q/b1gr9bV6afts5Xzyo7mBwk0yQTKCcVAuoveuufq1DB6dcGHN36 787 | /stfT3EX65pK2Rdn1bFHVBJr4sGRCqlV5sn6cOwfl5yiSvLlgeqR7XqQjUZWwMg8 788 | mfzeiDzZoVgy8+BAvb21AgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAJ55mM4IiNLs 789 | Tr096FOsTVmlXw1OANt03ClVqQlXAS1b5eWMTXwqJFoRex5UzucFoW7M375QpCT4 790 | ei1F8GlXlybx8P7uYOGfvXYU2NFenmAIEHhzsx9LJRfPdb/IGgGfr9TfyIngVc9K 791 | 8OFPTbvBWIONeao3z9r0v4eXRtdnLn/7Qk+o6mTvlNe6IJsAcXWreqcfrzvAOwXD 792 | 7xmtwEs1C6EPrgA/GJq3QhD/HDkVxUyjQbc75HU+Ze8zecvoNsBvpRswg9BKa9xl 793 | hU4SF5sARed3pySfEhoGAQD7N24QZX8uYo6/DqpBNJ48oJuDQh6mbwmpzise3gRx 794 | 8QvZfOf/dSY= 795 | -----END CERTIFICATE----- 796 | """.utf8), format: .pem) 797 | 798 | let serverKey = try! NIOSSLPrivateKey(bytes: 799 | Array(""" 800 | -----BEGIN RSA PRIVATE KEY----- 801 | MIIEpQIBAAKCAQEA10yd4jPwVkMvo2EG2Ycj2iHC1bf2u4NnmirROeWohl6L20l5 802 | qYHwouE7sq5k/iPMCem7+uvC2g2XDS4QT+mne67Z1RqKwiTak/OOf//oITe+WO6G 803 | Iu/tCce3LSpos0Pm+7cn6M5TF59WpepCKXVXt8irs6c7wc/1cY/lAPPEd5J9UBKz 804 | 0W5nA2GKdC0FB0sJ/SszpEnK/pW5/av29YK/W1emn7bOV88qO5gcJNMkEygnFQLq 805 | L3rrn6tQwenXBhzd+v7LX09xF+uaStkXZ9WxR1QSa+LBkQqpVebJ+nDsH5ecokry 806 | 5YHqke16kI1GVsDIPJn83og82aFYMvPgQL29tQIDAQABAoIBAQDBSeqwyvppJ3Zc 807 | Ul6I6leYnRjDMJ6VZ/qaIPin5vPudnFPFN7h/GNih51F5GWM9+xVtf7q3cCYbP0A 808 | eytv4xBW7Ppp5KNQey+1BkMXzVLEh7wfMT1Bnm8Lib59EQbgcgSsVZnB24IjwgxT 809 | dkWh3NQ8ji8AYhI3BRGQu6PXwAHRag+eLwWmHaaXGfXgDUerCPC2I7oNcix/payK 810 | rfEztEesjT54ByICewAqusRyByWXEc3Hm6qayc0uGR8UzfRfL3Q2g9arKKDH8Kob 811 | 374ponjL1OWv/FI9EauhLsdRxnjeIeHZSX3WQPjEnp8odAvCcdf/nMJClNQw2zXw 812 | t80ytYgBAoGBAPVJXjtSzNP3ZIXDIx0VMucIdrcMJhiGu3qUtCyrBsKe9W6IaDZd 813 | 7eJ8hvKV2Y7ycIUv17X227PbL8uqMVe855ALbidIQuFV1mqTO8dJeNiqbqfi42aL 814 | xyeHKW9+rdiDi55GEQgNeCSUd8VHO/DdcfCneuvKDgWo3QtzzfcyfUIBAoGBAOCz 815 | 81Ad4qHDVButh/bro5vsEP4Xq7SVYuqPQBQKwMm78LJtUcLSdbmYjEKakDzZbuAl 816 | xl5Zl5LBkgOIfEmJk+XbBy3NvNsUioGza7hWKD2aSo6s0tgDtfYmUta038t2gwdH 817 | ccHyERQhq+e8Z7x8cCWp48axmbfEtBoVejuySBO1AoGBAOtVryE/ueGMtFdZ97CJ 818 | jEL5bd0FvO8/JVTgo1VP6baEiHm6SjIPQJNSYq8QcqGhna9LTaz54aTYIS1IZvsE 819 | 9S7QqKjrva8wif3KsUntBhLqwiw1lXPnm/YiyfB9HBJlc2kxVFnjgmemQpt2Ut4v 820 | uIfqSBc9zuJDN4ErZGtNd7wBAoGBAL/9gVtm7YlBl8++SXnUhIpo/WvdVbyKF2ZK 821 | 13lIZsj3aAVMGpvXrvbRPKZ74dnb/jxOilt7OWMPOW8DYw6CGng+2LduHnsh5eZE 822 | Iznxg5h/CE03pT8kjIiw3f7NtJnnvLSveqc36RfGXVc3R3to53mG2zOd87VswGW5 823 | DCONhMAxAoGAaokOEY3wwEK34KOWDRrAEpMH5DQagecCB3L+QclkdratcqtGIt62 824 | Z8TvyV3f6Wl89pcpI1y5RZm8cbUF2rvlHjJ8WLSBEcR5vFnRCOplAQZdmg9Tmv/6 825 | toWGTsOXMHUr1s3T2Lh4UtWW+kMSNU16Es+DcGP2Rq3VJ3juuywdkCQ= 826 | -----END RSA PRIVATE KEY----- 827 | """.utf8), format: .pem) 828 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Swift 5.0 4 | 5 | 6 | Platforms macOS | Linux 7 | 8 | 9 | License Apache 10 | 11 |

12 | 13 | # Perfect 4 NIO 14 | 15 | This project is a work in progress and should be considered **alpha quality** until this sentence is removed. 16 | 17 | Package.swift Usage 18 | 19 | ### Intro 20 | 21 | Perfect 4 NIO is a Swift based API server. It provides the ability to serve HTTP/S endpoints by creating one or more URI based routes and binding them to a port. Each route is built as a series of operations, each accepting and returning some sort of value. A route finally terminates and outputs to the client by returning an `HTTPOutput` object. 22 | 23 | ### Simple Routing 24 | 25 | ```swift 26 | root { "Hello, world!" }.text() 27 | ``` 28 | 29 | This simple route would be applied to the root `/` of the server. It accepts nothing, Void, but returns a String. That string would be returned to the client with the text/plain content type. 30 | 31 | However, that bit of code produces an unused value. To serve a route you must first bind it to a port, ask it to listen for requests, then (optionally) wait until the process is terminated. 32 | 33 | ```swift 34 | try root { "Hello, world!" }.text().bind(port: 8080).listen().wait() 35 | ``` 36 | 37 | This will create a route and bind it to port 8080. It will then serve HTTP clients on that port until the process exits. 38 | 39 | Each of these steps can be broken up as nessesary. 40 | 41 | ```swift 42 | let route = root { "Hello, world!" } 43 | let textOutput = route.text() 44 | let boundServer = try textOutput.bind(port: 8080) 45 | let listeningServer = try boundServer.listen() 46 | try listeningServer.wait() 47 | ``` 48 | 49 | ### Root 50 | 51 | The `root` function is used to create a route beginning with `/`. The root is, by default, a function accepting an `HTTPRequest` and returning an `HTTPRequest`; an identity function. There are a few other variants of the `root` func. These are listed here: root. 52 | 53 | The type of object returned by `root` is a `Routes` object. Routes is defined simply as: 54 | 55 | ```swift 56 | public struct Routes {} 57 | ``` 58 | 59 | A `Routes` object encompasses one or more paths with their associated functions. For a particular route, all enclosed functions accept `InType` and return `OutType`. 60 | 61 | ### Paths 62 | 63 | A route can have additional path components added to it by using Swift 4.2 dynamic member lookup. 64 | 65 | ```swift 66 | let route = root().hello { "Hello, world!" } 67 | ``` 68 | 69 | Now the route serves itself on `/hello`. 70 | 71 | ```swift 72 | let route = root().hello.world { "Hello, world!" } 73 | ``` 74 | 75 | Now the route serves itself on `/hello/world`. 76 | 77 | Equivalently, you may use the `path` func to achieve the same thing. 78 | 79 | ```swift 80 | let route = root().path("hello").path("world") { "Hello, world!" } 81 | ``` 82 | or 83 | 84 | ```swift 85 | let route = root().path("hello/world") { "Hello, world!" } 86 | ``` 87 | 88 | This may be required in cases where your desired path component string conflicts with a built-in func (*\*list these somewhere simply*) or contains characters which are invalid for Swift identifiers. You may also simply prefer it stylistically, or may be using variable path names. These are all good reasons why one might want to use the `path` func over dynamic member lookup. 89 | 90 | All further examples in this document use dynamic member lookup. 91 | 92 | Note that paths which begin with a number, or consist wholly of numbers, are valid when using dynamic member lookup, even though they would normally not be when used as a property or func. This is a bit of a digression, but, for example: 93 | 94 | ```swift 95 | let route = root().1234 { "This is cool" }.text() 96 | 97 | struct MyTotallyUnrelatedStruct { 98 | func 1234() -> String { ... } // compilation error 99 | } 100 | ``` 101 | 102 | ### Combining Routes 103 | 104 | Most servers will want to service more than one URI. Routes can be combined in various ways. Combined routes behave as though they were one route. Combined routes can be bound and can listen for connections the same as an individual route can. 105 | 106 | Routes are combined using the `dir` func. Dir will append the given routes to the receiver and return a new route object containing all of the routes. 107 | 108 | ```swift 109 | let helloRoute = root().hello { "Hello, world!" } 110 | let byeRoute = root().bye { "Bye, world!" } 111 | 112 | let combinedRoutes = try root().v1.dir(helloRoute, byeRoute).text() 113 | 114 | try combinedRoutes.bind(port: 8080).listen().wait() 115 | ``` 116 | 117 | The above creates two routes which can be accessed at the URIs `/v1/hello` and `/v1/bye`. These two routes are combined and then the `text()` func is applied to them so that they return a text/plain content type. 118 | 119 | Dir will ensure that you are not adding any duplicate routes and will throw an Error if you are. 120 | 121 | Dir can be called and passed either a variadic number of routes, an array of routes, or a closure which accepts a stand-in route and returns an array of routes to append. Let's look closer at this last case. 122 | 123 | ```swift 124 | let foos = try root().v1.dir{[ 125 | $0.foo1 { "OK1" }, 126 | $0.foo2 { "OK2" }, 127 | $0.foo3 { "OK3" }, 128 | ]}.text() 129 | ``` 130 | 131 | This produces the following routes: `/v1/foo1`, `/v1/foo2`, and `/v1/foo3`, which each have the `text()` func applied to them. 132 | 133 | It's important to note that because routes are strongly typed, all routes that are passed to `dir` must accept whatever type of value the preceeding function returns. Any misuse will be caught at compilation time. 134 | 135 | The following case passes the current request's `path` URI to two different routes which each modify the value in some way and then pass it down the line. 136 | 137 | ```swift 138 | let route = try root { $0.path }.v1.dir {[ 139 | $0.upper { $0.uppercased() }, 140 | $0.lower { $0.lowercased() } 141 | ]}.text() 142 | ``` 143 | 144 | The above produces the following routes: `/v1/upper` and `/v1/lower`. 145 | 146 | ### HTTP Method 147 | 148 | Unless otherwise indicated, a route will serve for any HTTP method (GET, POST, etc.). Calling one of the method properties on a route will force it to serve only with the method indicated. 149 | 150 | ```swift 151 | let route = try root().dir {[ 152 | $0.GET.foo1 { "GET OK" }, 153 | $0.POST.foo2 { "POST OK" }, 154 | ]}.text() 155 | ``` 156 | 157 | Above, two routes are added, both with a URI of `/`. However, one accepts only GET and the other only POST. 158 | 159 | If you wish a route to serve more than one HTTP method, the `method` func will facilitate this. 160 | 161 | ```swift 162 | let route = root().method(.GET, .POST).foo { "GET or POST OK" }.text() 163 | ``` 164 | 165 | This will creat a route `/foo` which will answer to either GET or POST. 166 | 167 | Applying a method like this to routes which have already had methods applied to them will remove the old method and apply the new. 168 | 169 | ### Route Operations 170 | 171 | A variety of operations can be applied to a route. These operations include: 172 | 173 | #### map 174 | Transform an output in some way producing a new output or a sequence of output values. 175 | 176 | Definitions: 177 | 178 | ```swift 179 | public extension Routes { 180 | /// Add a function mapping the input to the output. 181 | func map(_ call: @escaping (OutType) throws -> NewOut) -> Routes 182 | /// Add a function mapping the input to the output. 183 | func map(_ call: @escaping () throws -> NewOut) -> Routes 184 | /// Map the values of a Collection to a new Array. 185 | func map(_ call: @escaping (OutType.Element) throws -> NewOut) -> Routes> where OutType: Collection 186 | } 187 | ``` 188 | 189 | Example: 190 | 191 | ```swift 192 | let route = try root().dir {[ 193 | $0.a { 1 }.map { "\($0)" }.text(), 194 | $0.b { [1,2,3] }.map { (i: Int) -> String in "\(i)" }.json() 195 | ]} 196 | ``` 197 | 198 | #### ext 199 | Apply a file extension to the routes. 200 | 201 | Definitions: 202 | 203 | ```swift 204 | public extension Routes { 205 | /// Adds the indicated file extension to the route set. 206 | func ext(_ ext: String) -> Routes 207 | /// Adds the indicated file extension to the route set. 208 | /// Optionally set the response's content type. 209 | /// The given function accepts the input value and returns a new value. 210 | func ext(_ ext: String, 211 | contentType: String? = nil, 212 | _ call: @escaping (OutType) throws -> NewOut) -> Routes 213 | } 214 | ``` 215 | 216 | Example: 217 | 218 | The following returns a `Foo` object to the client and makes the object available as either `json` or `text` by adding an appropriate file extension to the route URI. 219 | 220 | ```swift 221 | struct Foo: Codable, CustomStringConvertible { 222 | var description: String { 223 | return "foo-data \(id)/\(date)" 224 | } 225 | let id: UUID 226 | let date: Date 227 | } 228 | let fooRoute = root().foo { Foo(id: UUID(), date: Date()) } 229 | let route = try root().dir( 230 | fooRoute.ext("json").json(), 231 | fooRoute.ext("txt").text()) 232 | ``` 233 | 234 | This will produce the routes `/foo.json` and `/foo.txt`. 235 | 236 | #### wild 237 | Apply a wildcard path segment. 238 | 239 | Definitions: 240 | 241 | ```swift 242 | public extension Routes { 243 | /// Adds a wildcard path component to the route set. 244 | /// The given function accepts the input value and the value for that wildcard path component, as given by the HTTP client, 245 | /// and returns a new value. 246 | func wild(_ call: @escaping (OutType, String) throws -> NewOut) -> Routes 247 | /// Adds a wildcard path component to the route set. 248 | /// Gives the wildcard path component a variable name and the path component value is added as a request urlVariable. 249 | func wild(name: String) -> Routes 250 | } 251 | ``` 252 | 253 | Example: 254 | 255 | ```swift 256 | let route = root().wild { $1 }.foo.text() 257 | ``` 258 | 259 | Above, the route `/*/foo` is created. The "*" can be any string, which is then echoed back to the client. 260 | 261 | WIldcard path components can also be given a name. This will make the component available through the `HTTPRequest.uriVariables` property, or for use during Decodable `decode` operations. 262 | 263 | Example: 264 | 265 | ```swift 266 | struct Req: Codable { 267 | let id: UUID 268 | let action: String 269 | } 270 | let route = root().v1 271 | .wild(name: "id") 272 | .wild(name: "action") 273 | .decode(Req.self) { return "\($1.id) - \($1.action)" } 274 | .text() 275 | ``` 276 | 277 | The above creates the route `/v1/*/*`. The "id" and "action" wildcards are saved and used during decoding of the `Req` object. The `Req` properties are then echoed back to the client. 278 | 279 | #### trailing 280 | Apply a trailing wildcard path segment 281 | 282 | Definitions: 283 | 284 | ```swift 285 | public extension Routes { 286 | /// Adds a trailing-wildcard to the route set. 287 | /// The given function accepts the input value and the value for the remaining path components, as given by the HTTP client, 288 | /// and returns a new value. 289 | func trailing(_ call: @escaping (OutType, String) throws -> NewOut) -> Routes 290 | } 291 | ``` 292 | 293 | Example: 294 | 295 | ```swift 296 | let route = root().foo.trailing { $1 }.text() 297 | ``` 298 | 299 | The route `/foo/**` is created, with "**" matching any subsequent path components. The remaining path components String is available as the second argument (the first argument is still the current `HTTPRequest` object), and is then echoed back to the client. If a client accessed the URI `/foo/OK/OK`, the String "OK/OK" would be made available as the trailing wildcard value. 300 | 301 | #### request 302 | Access the HTTPRequest object. 303 | 304 | While all routes start off by receiving the current HTTPRequest object, it can often be more convenient to begin passing other values down your route pipeline but then go back to the request object for some cause. 305 | 306 | Definitions: 307 | 308 | ```swift 309 | public extension Routes { 310 | /// Adds the current HTTPRequest as a parameter to the function. 311 | func request(_ call: @escaping (OutType, HTTPRequest) throws -> NewOut) -> Routes 312 | } 313 | ``` 314 | 315 | Example: 316 | 317 | ```swift 318 | let route = root().foo { "OK" }.request { $1.path }.text() 319 | ``` 320 | 321 | This route URI is `/foo`. It echos back the request path by first discarding the HTTPRequest but then grabbing it again using the `request` func. The request is always provided as the second argument. 322 | 323 | #### readBody 324 | Read the client body data and deliver it to the provided callback. 325 | 326 | Definitions: 327 | 328 | ```swift 329 | public extension Routes { 330 | func readBody(_ call: @escaping (OutType, HTTPRequestContentType) throws -> NewOut) -> Routes 331 | } 332 | ``` 333 | 334 | Example: 335 | 336 | ```swift 337 | let route = root().POST.readBody { 338 | (req: HTTPRequest, content: HTTPRequestContentType) -> String in 339 | switch content { 340 | case .urlForm: return "url-encoded" 341 | case .multiPartForm: return "multi-part" 342 | case .other: return "other" 343 | case .none: return "none" 344 | } 345 | }.text() 346 | ``` 347 | 348 | This example accepts a POST request at `/`. It reads the submitted body and returns a String describing what type the data was. 349 | 350 | #### statusCheck 351 | Assert some condition by returning either 'OK' (200..<300 status code) or failing. 352 | 353 | Definitions: 354 | 355 | ```swift 356 | public extension Routes { 357 | /// The caller can inspect the given input value and choose to return an HTTP error code. 358 | /// If any code outside of 200..<300 is return the request is aborted. 359 | func statusCheck(_ handler: @escaping (OutType) throws -> HTTPResponseStatus) -> Routes 360 | /// The caller can choose to return an HTTP error code. 361 | /// If any code outside of 200..<300 is return the request is aborted. 362 | func statusCheck(_ handler: @escaping () throws -> HTTPResponseStatus) -> Routes 363 | } 364 | ``` 365 | 366 | Example: 367 | 368 | ```swift 369 | let route = try root().dir {[ 370 | $0.a, 371 | $0.b 372 | ]}.statusCheck { 373 | req in 374 | guard req.path != "/b" else { 375 | return .internalServerError 376 | } 377 | return .ok 378 | }.map { req in "OK" }.text() 379 | ``` 380 | 381 | This route will serve the URIs `/a` and `/b`. However, the request will be deliberately failed with `.internalServerError` if the `/b` URI is accessed. After the function given to `statusCheck` is called, the route continues with the previous value. You can see this in the call to `map` where its first parameter reverts back to the current request after the status check. 382 | 383 | #### decode 384 | Read and decode the client body as a Decodable object. 385 | 386 | Decode offers a few variants to fit different use cases. 387 | 388 | Definitions: 389 | 390 | ```swift 391 | public extension Routes { 392 | /// Read the client content body and then attempt to decode it as the indicated `Decodable` type. 393 | /// Both the original input value and the newly decoded object are delivered to the provided function. 394 | func decode(_ type: Type.Type, 395 | _ handler: @escaping (OutType, Type) throws -> NewOut) -> Routes 396 | /// Read the client content body and then attempt to decode it as the indicated `Decodable` type. 397 | /// The newly decoded object is delivered to the provided function. 398 | func decode(_ type: Type.Type, 399 | _ handler: @escaping (Type) throws -> NewOut) -> Routes 400 | /// Read the client content body and then attempt to decode it as the indicated `Decodable` type. 401 | /// The newly decoded object becomes the route set's new output value. 402 | func decode(_ type: Type.Type) -> Routes 403 | /// Decode the request body into the desired type, or throw an error. 404 | /// This function would be used after the content body has already been read. 405 | func decode(_ type: A.Type, content: HTTPRequestContentType) throws -> A 406 | } 407 | ``` 408 | 409 | Example: 410 | 411 | ```swift 412 | struct Foo: Codable { 413 | let id: UUID 414 | let date: Date 415 | } 416 | let route = try root().POST.dir{[ 417 | $0.1.decode(Foo.self), 418 | $0.2.decode(Foo.self) { $1 }, 419 | $0.3.decode(Foo.self) { $0 }, 420 | ]}.json() 421 | ``` 422 | 423 | This example will serve the URIs `/1`, `/2`, and `/3`. It decodes the POST body in three different ways. #1 decodes the body and returns it as the new value (discarding the HTTPRequest). #2 decodes the body and calls the closure with the previous value, the HTTPRequest, and with the newly decoded Foo object. This case simply returns the Foo as the new value. #3 decodes the body and accepts it in a closure which accepts only one argument. This also is simply returned as the new value. Finally, regardless of which URI was hit, the value is converted to json and returns to the client. 424 | 425 | #### unwrap 426 | Unwrap an Optional value, or fail the request if the value is nil. 427 | 428 | Definitions: 429 | 430 | ```swift 431 | public extension Routes { 432 | /// If the output type is an `Optional`, this function permits it to be safely unwraped. 433 | /// If it can not be unwrapped the request is terminated. 434 | /// The provided function is called with the unwrapped value. 435 | func unwrap(_ call: @escaping (U) throws -> NewOut) -> Routes where OutType == Optional 436 | } 437 | ``` 438 | 439 | Example: 440 | 441 | ```swift 442 | let route = try root().dir {[ 443 | $0.a { nil }, 444 | $0.b { "OK" } 445 | ]}.unwrap { $0 }.text() 446 | ``` 447 | 448 | The above creates `/a` and `/b`. `/a` returns a nil `String?` while `/b` returns "OK". Either route's value will go through the `unwrap` func. If the value is nil, the request will be failed with an `.internalServerError`. (KRJ: address this. needs to be more flexible wrt response status code.) 449 | 450 | #### async 451 | Execute a task asynchronously, out of the NIO event loop. 452 | 453 | When performing lengthy or blocking operations, such as external URL requests or database operations, it is vital that the operation be moved out of the NIO event loop. This `async` func lets you do just that. The activity moves into a new thread out of the NIO event loop within which you have free reign. When your activity has completed, signal the provided EventLoopPromise with your return value by calling either `success` or `fail`. 454 | 455 | Definitions: 456 | 457 | ```swift 458 | public extension Routes { 459 | /// Run the call asynchronously on a non-event loop thread. 460 | /// Caller must succeed or fail the given promise to continue the request. 461 | func async(_ call: @escaping (OutType, EventLoopPromise) -> ()) -> Routes 462 | } 463 | ``` 464 | 465 | Example: 466 | 467 | ```swift 468 | let route = root().async { 469 | (req: HTTPRequest, p: EventLoopPromise) in 470 | sleep(1) 471 | p.succeed(result: "OK") 472 | }.text() 473 | ``` 474 | 475 | The above spins off an asynchronous activity (in this case, sleeping for 1 second) and then signals that it is complete. The value that it submits to the promise, "OK", is sent to the client. 476 | 477 | It's important to note that subsequent activities for the route will occur on the NIO event loop. 478 | 479 | #### text 480 | Use a `CustomStringConvertible` as the output with a text/plain content type. 481 | 482 | Definitions: 483 | 484 | ```swift 485 | public extension Routes where OutType: CustomStringConvertible { 486 | func text() -> Routes 487 | } 488 | ``` 489 | 490 | Example: 491 | 492 | ```swift 493 | let route = root { 1 }.text() 494 | ``` 495 | 496 | Above, the route `/` is created which serves the stringified number 1. 497 | 498 | #### json 499 | Use an `Encodable` as the output with the application/json content type. 500 | 501 | Definitions: 502 | 503 | ```swift 504 | public extension Routes where OutType: Encodable { 505 | func json() -> Routes 506 | } 507 | ``` 508 | 509 | Example: 510 | 511 | ```swift 512 | struct Foo: Codable { 513 | let id: UUID 514 | let date: Date 515 | } 516 | let route = root().foo { Foo(id: UUID(), date: Date()) }.json() 517 | ``` 518 | 519 | This example create a route `/foo` which returns a Foo object. The Foo is converted to JSON and sent to the client. 520 | 521 | #### compressed 522 | 523 | Outgoing client content can be compressed using either gzip or deflate algorithms by calling the `compressed()` function on any route returning HTTPOutput. 524 | 525 | ```swift 526 | /// Compresses eligible output 527 | public extension Routes where OutType: HTTPOutput { 528 | func compressed() -> Routes 529 | } 530 | ``` 531 | 532 | Compressed content takes HTTPOutput and then selectively compresses and sends the content to the client. If the source HTTPOutput object specifies a response Content-Length and that content length is less than 14k, the response will not be compressed. If the source HTTPoutput specifies a content-type and that type begins with "image/", "video/", or "audio/", the response will not be compressed. 533 | 534 | Example: 535 | 536 | ```swift 537 | class StreamOutput: HTTPOutput { 538 | var counter = 0 539 | override init() { 540 | super.init() 541 | kind = .stream 542 | } 543 | override func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) { 544 | if counter > 15 { 545 | promise.succeed(result: nil) 546 | } else { 547 | let toSend = String(repeating: "\(counter % 10)", count: 1024) 548 | counter += 1 549 | let ary = Array(toSend.utf8) 550 | var buf = allocator.buffer(capacity: ary.count) 551 | buf.write(bytes: ary) 552 | promise.succeed(result: .byteBuffer(buf)) 553 | } 554 | } 555 | } 556 | let route = root() { return StreamOutput() as HTTPOutput }.compressed() 557 | ``` 558 | 559 | This example streams text content to the client. The usage of `.compressed()` at the end of the route will turn on content compression. 560 | 561 | ### HTTPOutput 562 | 563 | Considering a complete set of routes as a function, it would look like: 564 | 565 | `(HTTPRequest) -> HTTPOutput` 566 | 567 | `HTTPOutput` is a base class which can optionally set the HTTP response status, headers and body data. Several concrete HTTPOutput implementations are provided for you, but you can add your own custom output by sub-classing and returning your object. 568 | 569 | Built-in HTTPOutput types include `HTTPOutputError`, which can be thrown, JSONOutput, TextOutput, CompressedOutput, FileOutput, MustacheOutput, and BytesOutput. 570 | 571 | ```swift 572 | /// The response output for the client 573 | open class HTTPOutput { 574 | /// Indicates how the `body` func data, and possibly content-length, should be handled 575 | var kind: HTTPOutputResponseHint 576 | /// Optional HTTP head 577 | open func head(request: HTTPRequestHead) -> HTTPHead? 578 | /// Produce body data 579 | /// Set nil on last chunk 580 | /// Call promise.fail upon failure 581 | open func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) 582 | /// Called when the request has completed either successfully or with a failure. 583 | /// Sub-classes can override to take special actions or perform cleanup operations. 584 | /// Inherited implimenation does nothing. 585 | open func closed() 586 | } 587 | ``` 588 | 589 | #### FileOutput 590 | 591 | File content can be returned from a route by using the `FileOutput` type. 592 | 593 | ```swift 594 | public class FileOutput: HTTPOutput { 595 | public init(localPath: String) throws 596 | } 597 | ``` 598 | 599 | Example: 600 | 601 | ```swift 602 | let route = root().test { 603 | try FileOutput(localPath: "/tmp/test.txt") as HTTPOutput 604 | }.ext("txt") 605 | ``` 606 | 607 | This example serves the route /test.txt and returns the content of a local file. If the file does not exist or is not readable then an Error will be thrown. 608 | 609 | #### MustacheOutput 610 | 611 | Content from mustache templates can be returned from a route by using the `MustacheOutput` type. 612 | 613 | ```swift 614 | public class MustacheOutput: HTTPOutput { 615 | public init(templatePath: String, 616 | inputs: [String:Any], 617 | contentType: String) throws 618 | } 619 | ``` 620 | 621 | Example: 622 | 623 | ```swift 624 | let route = root().test { 625 | try MustacheOutput(templatePath: tmpFilePath, 626 | inputs: ["key1":"value1", "key2":"value2"], 627 | contentType: "text/html") as HTTPOutput 628 | }.ext("html") 629 | ``` 630 | 631 | This example processes and serves a mustache template file as text/html. 632 | 633 | ### Caveats 634 | 635 | make notes on: 636 | using diseparate types in `dir` 637 | ordering of `wild` and `decode` wrt path variables 638 | doing blocking activities in a non-async func 639 | 640 | *TBD:* 641 | 642 | * Logging - use the new sss logging stuff 643 | 644 | ### Reference 645 | 646 | 647 | #### root() 648 | 649 | ```swift 650 | /// Create a root route accepting/returning the HTTPRequest. 651 | public func root() -> Routes 652 | /// Create a root route accepting the HTTPRequest and returning some new value. 653 | public func root(_ call: @escaping (HTTPRequest) throws -> NewOut) -> Routes 654 | /// Create a root route returning some new value. 655 | public func root(_ call: @escaping () throws -> NewOut) -> Routes 656 | /// Create a root route accepting and returning some new value. 657 | public func root(path: String = "/", _ type: NewOut.Type) -> Routes 658 | ``` 659 | 660 | 661 | #### Routes\ 662 | 663 | ```swift 664 | /// Main routes object. 665 | /// Created by calling `root()` or by chaining a function from an existing route. 666 | public struct Routes { 667 | // Routes can not be directly instantiated. 668 | // All functionality is provided through extensions. 669 | } 670 | ``` 671 | 672 | 673 | #### HTTPRequest 674 | 675 | ```swift 676 | public protocol HTTPRequest { 677 | var method: HTTPMethod { get } 678 | var uri: String { get } 679 | var headers: HTTPHeaders { get } 680 | var uriVariables: [String:String] { get set } 681 | var path: String { get } 682 | var searchArgs: QueryDecoder? { get } 683 | var contentType: String? { get } 684 | var contentLength: Int { get } 685 | var contentRead: Int { get } 686 | var contentConsumed: Int { get } 687 | var localAddress: SocketAddress? { get } 688 | var remoteAddress: SocketAddress? { get } 689 | /// Returns all the cookie name/value pairs parsed from the request. 690 | var cookies: [String:String] 691 | func readSomeContent() -> EventLoopFuture<[ByteBuffer]> 692 | func readContent() -> EventLoopFuture 693 | } 694 | ``` 695 | 696 | 697 | #### QueryDecoder 698 | 699 | ```swift 700 | public struct QueryDecoder { 701 | public init(_ c: [UInt8]) 702 | public subscript(_ key: String) -> [String] 703 | public func map(_ call: ((String,String)) throws -> T) rethrows -> [T] 704 | public func mapBytes(_ call: ((String,ArraySlice)) throws -> T) rethrows -> [T] 705 | public func get(_ key: String) -> [ArraySlice] 706 | } 707 | ``` 708 | 709 | 710 | #### dir 711 | 712 | ```swift 713 | /// These extensions append new route sets to an existing set. 714 | public extension Routes { 715 | /// Append new routes to the set given a new output type and a function which receives a route object and returns an array of new routes. 716 | /// This permits a sort of shorthand for adding new routes. 717 | /// At times, Swift's type inference can fail to discern what the programmer intends when calling functions like this. 718 | /// Calling the second version of this method, the one accepting a `type: NewOut.Type` as the first parameter, 719 | /// can often clarify your intentions to the compiler. If you experience a compilation error with this function, try the other. 720 | func dir(_ call: (Routes) throws -> [Routes]) throws -> Routes 721 | /// Append new routes to the set given a new output type and a function which receives a route object and returns an array of new routes. 722 | /// This permits a sort of shorthand for adding new routes. 723 | /// The first `type` argument to this function serves to help type inference. 724 | func dir(type: NewOut.Type, _ call: (Routes) -> [Routes]) throws -> Routes 725 | /// Append new routes to this set given an array. 726 | func dir(_ registries: [Routes]) throws -> Routes 727 | /// Append a new route set to this set. 728 | func dir(_ registry: Routes, _ registries: Routes...) throws -> Routes 729 | } 730 | ``` 731 | 732 | 733 | #### RouteError 734 | 735 | ```swift 736 | /// An error occurring during process of building a set of routes. 737 | public enum RouteError: Error, CustomStringConvertible { 738 | case duplicatedRoutes([String]) 739 | public var description: String 740 | } 741 | ``` 742 | 743 | 744 | #### HTTPOutput 745 | 746 | ```swift 747 | /// The response output for the client 748 | open class HTTPOutput { 749 | /// Indicates how the `body` func data, and possibly content-length, should be handled 750 | var kind: HTTPOutputResponseKind 751 | /// Optional HTTP head 752 | open func head(request: HTTPRequestHead) -> HTTPHead? 753 | /// Produce body data 754 | /// Set nil on last chunk 755 | /// Call promise.fail upon failure 756 | open func body(promise: EventLoopPromise, allocator: ByteBufferAllocator) 757 | /// Called when the request has completed either successfully or with a failure. 758 | /// Sub-classes can override to take special actions or perform cleanup operations. 759 | /// Inherited implimenation does nothing. 760 | open func closed() 761 | } 762 | ``` 763 | 764 | 765 | #### HTTPRequestContentType 766 | 767 | ```swift 768 | /// Client content which has been read and parsed (if needed). 769 | public enum HTTPRequestContentType { 770 | /// There was no content provided by the client. 771 | case none 772 | /// A multi-part form/file upload. 773 | case multiPartForm(MimeReader) 774 | /// A url-encoded form. 775 | case urlForm(QueryDecoder) 776 | /// Some other sort of content. 777 | case other([UInt8]) 778 | } 779 | ``` 780 | 781 | 782 | #### ListeningRoutes 783 | 784 | ```swift 785 | /// Routes which have been bound to a port and have started listening for connections. 786 | public protocol ListeningRoutes { 787 | /// Stop listening for requests 788 | @discardableResult 789 | func stop() -> ListeningRoutes 790 | /// Wait, perhaps forever, until the routes have stopped listening for requests. 791 | func wait() throws 792 | } 793 | ``` 794 | 795 | 796 | #### BoundRoutes 797 | 798 | ```swift 799 | /// Routes which have been bound to an address but are not yet listening for requests. 800 | public protocol BoundRoutes { 801 | /// The address the server is bound to. 802 | var address: SocketAddress { get } 803 | /// Start listening 804 | func listen() throws -> ListeningRoutes 805 | } 806 | ``` 807 | 808 | 809 | #### HTTP Methods 810 | 811 | ```swift 812 | public extension Routes { 813 | var GET: Routes 814 | var POST: Routes 815 | var PUT: Routes 816 | var DELETE: Routes 817 | var OPTIONS: Routes 818 | func method(_ method: HTTPMethod, _ methods: HTTPMethod...) -> Routes 819 | } 820 | ``` 821 | 822 | 823 | ### Package.swift Usage 824 | 825 | In your Package.swift: 826 | 827 | ```swift 828 | .package(url: "https://github.com/PerfectlySoft/Perfect-NIO.git", .branch("master")) 829 | ``` 830 | 831 | Your code may need to `import PerfectNIO`, `import NIO`, `import NIOHTTP1`, or `import NIOSSL`. 832 | --------------------------------------------------------------------------------