├── .gitignore ├── docker-compose.yml ├── Tests ├── LinuxMain.swift └── HTTPKitTests │ ├── HTTPHeaderTests.swift │ ├── HTTPServerTests.swift │ ├── HTTPCookieTests.swift │ ├── XCTestManifests.swift │ ├── HTTPClientTests.swift │ └── WebSocketTests.swift ├── circle.yml ├── Sources ├── HTTPKit │ ├── Utilities │ │ ├── String+IsIPAddress.swift │ │ ├── HTTPError.swift │ │ ├── HTTPCodingKey.swift │ │ ├── CaseInsensitiveString.swift │ │ ├── File.swift │ │ └── RFC1123.swift │ ├── Exports.swift │ ├── Client │ │ ├── HTTPClientProtocolUpgrader.swift │ │ ├── HTTPClientRequestEncoder.swift │ │ ├── HTTPClientUpgradeHandler.swift │ │ ├── HTTPClientProxyHandler.swift │ │ ├── HTTPClientResponseDecoder.swift │ │ ├── HTTPClientHandler.swift │ │ └── HTTPClient.swift │ ├── WebSocket │ │ ├── HTTPRequest+WebSocket.swift │ │ ├── HTTPResponse+WebSocket.swift │ │ ├── WebSocketFrameSequence.swift │ │ ├── WebSocket+Server.swift │ │ ├── WebSocket+Client.swift │ │ ├── WebSocketHandler.swift │ │ └── WebSocket.swift │ ├── Message │ │ ├── URL+HTTP.swift │ │ ├── HTTPMessage.swift │ │ ├── HTTPPeer.swift │ │ ├── MediaTypePreference.swift │ │ ├── HTTPResponse.swift │ │ ├── HTTPRequest.swift │ │ ├── HTTPCookies.swift │ │ └── HTTPBody.swift │ ├── Headers │ │ ├── HTTPHeaders+Bearer.swift │ │ ├── HTTPHeaders+Basic.swift │ │ ├── HTTPHeaderValue.swift │ │ └── HTTPHeaders+Name.swift │ └── Server │ │ ├── HTTPServerHandler.swift │ │ ├── HTTPRequestPartDecoder.swift │ │ ├── HTTPResponsePartEncoder.swift │ │ ├── HTTPServerUpgradeHandler.swift │ │ └── HTTPServer.swift └── HTTPKitExample │ └── main.swift ├── README.md ├── LICENSE ├── Package.swift └── certs ├── cert.pem └── key.pem /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | .DS_Store 4 | *.xcodeproj 5 | Package.pins 6 | DerivedData/ 7 | Package.resolved -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | tinyproxy: 2 | image: vimagick/tinyproxy 3 | ports: 4 | - 8888:8888 5 | restart: always 6 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import HTTPKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += HTTPKitTests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | linux: 5 | docker: 6 | - image: vapor/swift:5.0 7 | - image: vimagick/tinyproxy 8 | name: tinyproxy 9 | steps: 10 | - checkout 11 | - run: swift build -Xswiftc -DDOCKER 12 | - run: swift test -Xswiftc -DDOCKER 13 | 14 | workflows: 15 | version: 2 16 | tests: 17 | jobs: 18 | - linux 19 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Utilities/String+IsIPAddress.swift: -------------------------------------------------------------------------------- 1 | extension String { 2 | func isIPAddress() -> Bool { 3 | // We need some scratch space to let inet_pton write into. 4 | var ipv4Addr = in_addr() 5 | var ipv6Addr = in6_addr() 6 | 7 | return self.withCString { ptr in 8 | return inet_pton(AF_INET, ptr, &ipv4Addr) == 1 || 9 | inet_pton(AF_INET6, ptr, &ipv6Addr) == 1 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Utilities/HTTPError.swift: -------------------------------------------------------------------------------- 1 | /// Errors that can be thrown while working with HTTP. 2 | public struct HTTPError: Error { 3 | public enum Reason { 4 | case noContent 5 | case noContentType 6 | case unknownContentType 7 | case maxBodySize 8 | } 9 | 10 | /// See `Debuggable`. 11 | public let reason: Reason 12 | 13 | /// Creates a new `HTTPError`. 14 | public init( 15 | _ reason: Reason 16 | ) { 17 | self.reason = reason 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import NIO 2 | @_exported import NIOExtras 3 | @_exported import NIOHTTPCompression 4 | @_exported import NIOHTTP2 5 | @_exported import NIOHTTP1 6 | @_exported import NIOSSL 7 | @_exported import NIOWebSocket 8 | 9 | import protocol NIO.RemovableChannelHandler 10 | import class NIOHTTP1.HTTPRequestEncoder 11 | 12 | extension FixedWidthInteger { 13 | static var anyRandom: Self { 14 | return Self.random(in: Self.min.. HTTPHeaders 5 | 6 | /// Called if `isValidUpgradeResponse` returns `true`. This should return the `UpgradeResult` 7 | /// that will ultimately be returned by `HTTPClient.upgrade(...)`. 8 | func upgrade(context: ChannelHandlerContext, upgradeResponse: HTTPResponseHead) -> EventLoopFuture 9 | } 10 | -------------------------------------------------------------------------------- /Tests/HTTPKitTests/HTTPHeaderTests.swift: -------------------------------------------------------------------------------- 1 | import HTTPKit 2 | import XCTest 3 | 4 | final class HTTPHeaderTests: XCTestCase { 5 | func testAcceptHeader() throws { 6 | let httpReq = HTTPRequest(method: .GET, url: "/", headers: ["Accept": "text/html, application/json, application/xml;q=0.9, */*;q=0.8"]) 7 | XCTAssertTrue(httpReq.accept.mediaTypes.contains(.html)) 8 | XCTAssertEqual(httpReq.accept.comparePreference(for: .html, to: .xml), .orderedAscending) 9 | XCTAssertEqual(httpReq.accept.comparePreference(for: .plainText, to: .html), .orderedDescending) 10 | XCTAssertEqual(httpReq.accept.comparePreference(for: .html, to: .json), .orderedSame) 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Sources/HTTPKit/WebSocket/HTTPRequest+WebSocket.swift: -------------------------------------------------------------------------------- 1 | extension HTTPRequest { 2 | public func isRequestingUpgrade(to protocol: String) -> Bool { 3 | let connectionHeaders = Set(self.headers[canonicalForm: "connection"].map { $0.lowercased() }) 4 | let upgradeHeaders = Set(self.headers[canonicalForm: "upgrade"].map { $0.lowercased() }) 5 | 6 | return connectionHeaders.contains("upgrade") && upgradeHeaders.contains(`protocol`) 7 | } 8 | 9 | public mutating func webSocketUpgrade(onUpgrade: @escaping (WebSocket) -> ()) { 10 | self.upgrader = WebSocketClientUpgrader { channel, res in 11 | let ws = WebSocket(channel: channel, mode: .client) 12 | return channel.pipeline.add(webSocket: ws).map { 13 | onUpgrade(ws) 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Message/URL+HTTP.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Capable of converting `self` to a `URL`, returning `nil` if the conversion fails. 4 | public protocol URLRepresentable { 5 | /// Converts `Self` to a `URL`, returning `nil` if the conversion fails. 6 | func convertToURL() -> URL? 7 | } 8 | 9 | extension URL: URLRepresentable { 10 | /// See `URLRepresentable`. 11 | public func convertToURL() -> URL? { 12 | return self 13 | } 14 | 15 | /// Returns root URL for an HTTP request. 16 | public static var root: URL { 17 | return _rootURL 18 | } 19 | } 20 | 21 | extension String: URLRepresentable { 22 | /// See `URLRepresentable`. 23 | public func convertToURL() -> URL? { 24 | return URL(string: self) 25 | } 26 | } 27 | 28 | /// Pre-initialized default value. 29 | private let _rootURL = URL(string: "/")! 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | HTTP 3 |
4 |
5 | 6 | Documentation 7 | 8 | 9 | Team Chat 10 | 11 | 12 | MIT License 13 | 14 | 15 | Continuous Integration 16 | 17 | 18 | Swift 5 19 | 20 |

21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Qutheory, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Sources/HTTPKit/Headers/HTTPHeaders+Bearer.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | /// A bearer token. 4 | public struct BearerAuthorization { 5 | /// The plaintext token 6 | public let token: String 7 | 8 | /// Create a new `BearerAuthorization` 9 | public init(token: String) { 10 | self.token = token 11 | } 12 | } 13 | 14 | extension HTTPHeaders { 15 | /// Access or set the `Authorization: Bearer: ...` header. 16 | public var bearerAuthorization: BearerAuthorization? { 17 | get { 18 | guard let string = self[.authorization].first else { 19 | return nil 20 | } 21 | 22 | guard let range = string.range(of: "Bearer ") else { 23 | return nil 24 | } 25 | 26 | let token = string[range.upperBound...] 27 | return .init(token: String(token)) 28 | } 29 | set { 30 | if let bearer = newValue { 31 | replaceOrAdd(name: .authorization, value: "Bearer \(bearer.token)") 32 | } else { 33 | remove(name: .authorization) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/HTTPKit/WebSocket/HTTPResponse+WebSocket.swift: -------------------------------------------------------------------------------- 1 | extension HTTPRequest { 2 | public func makeWebSocketUpgradeResponse( 3 | extraHeaders: HTTPHeaders = [:], 4 | on channel: Channel, 5 | onUpgrade: @escaping (WebSocket) -> () 6 | ) -> EventLoopFuture { 7 | let upgrader = WebSocketUpgrader(shouldUpgrade: { channel, _ in 8 | return channel.eventLoop.makeSucceededFuture(extraHeaders) 9 | }, upgradePipelineHandler: { channel, req in 10 | let webSocket = WebSocket(channel: channel, mode: .server) 11 | onUpgrade(webSocket) 12 | return channel.pipeline.add(webSocket: webSocket) 13 | }) 14 | 15 | var head = HTTPRequestHead( 16 | version: self.version, 17 | method: self.method, 18 | uri: self.urlString 19 | ) 20 | head.headers = self.headers 21 | return upgrader.buildUpgradeResponse( 22 | channel: channel, 23 | upgradeRequest: head, 24 | initialResponseHeaders: headers 25 | ).map { headers in 26 | var res = HTTPResponse( 27 | status: .switchingProtocols, 28 | headers: headers 29 | ) 30 | res.upgrader = upgrader 31 | return res 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "http-kit", 6 | products: [ 7 | .library(name: "HTTPKit", targets: ["HTTPKit"]), 8 | ], 9 | dependencies: [ 10 | // Event-driven network application framework for high performance protocol servers & clients, non-blocking. 11 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"), 12 | 13 | // Bindings to OpenSSL-compatible libraries for TLS support in SwiftNIO 14 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"), 15 | 16 | // HTTP/2 support for SwiftNIO 17 | .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.0.0"), 18 | 19 | // Useful code around SwiftNIO. 20 | .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.0.0"), 21 | 22 | // Swift logging API 23 | .package(url: "https://github.com/apple/swift-log.git", .branch("master")), 24 | ], 25 | targets: [ 26 | .target(name: "HTTPKit", dependencies: [ 27 | "Logging", 28 | "NIO", 29 | "NIOExtras", 30 | "NIOFoundationCompat", 31 | "NIOHTTPCompression", 32 | "NIOHTTP1", 33 | "NIOHTTP2", 34 | "NIOSSL", 35 | "NIOWebSocket" 36 | ]), 37 | .target(name: "HTTPKitExample", dependencies: ["HTTPKit"]), 38 | .testTarget(name: "HTTPKitTests", dependencies: ["HTTPKit"]), 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Utilities/HTTPCodingKey.swift: -------------------------------------------------------------------------------- 1 | /// A basic `CodingKey` implementation. 2 | public struct HTTPCodingKey: CodingKey { 3 | /// See `CodingKey`. 4 | public var stringValue: String 5 | 6 | /// See `CodingKey`. 7 | public var intValue: Int? 8 | 9 | /// Creates a new `BasicKey` from a `String.` 10 | public init(_ string: String) { 11 | self.stringValue = string 12 | } 13 | 14 | /// Creates a new `BasicKey` from a `Int.` 15 | /// 16 | /// These are usually used to specify array indexes. 17 | public init(_ int: Int) { 18 | self.intValue = int 19 | self.stringValue = int.description 20 | } 21 | 22 | /// See `CodingKey`. 23 | public init?(stringValue: String) { 24 | self.stringValue = stringValue 25 | } 26 | 27 | /// See `CodingKey`. 28 | public init?(intValue: Int) { 29 | self.intValue = intValue 30 | self.stringValue = intValue.description 31 | } 32 | } 33 | 34 | /// Capable of being represented by a `BasicKey`. 35 | public protocol HTTPCodingKeyRepresentable { 36 | /// Converts this type to a `BasicKey`. 37 | func makeHTTPCodingKey() -> HTTPCodingKey 38 | } 39 | 40 | extension String: HTTPCodingKeyRepresentable { 41 | /// See `BasicKeyRepresentable` 42 | public func makeHTTPCodingKey() -> HTTPCodingKey { 43 | return HTTPCodingKey(self) 44 | } 45 | } 46 | 47 | extension Int: HTTPCodingKeyRepresentable { 48 | /// See `BasicKeyRepresentable` 49 | public func makeHTTPCodingKey() -> HTTPCodingKey { 50 | return HTTPCodingKey(self) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEpDCCAowCCQCp8zIL1sF/yjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls 3 | b2NhbGhvc3QwHhcNMTkwMTA4MjAxMTM2WhcNMjAwMTA4MjAxMTM2WjAUMRIwEAYD 4 | VQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ 5 | nNBddXW4LA+Edxw2m0KbtZurNUSoWpgrLq+EZ6p3pV+pjAqJc2rBM/84x4mWJizl 6 | ebcugXQ1f9wIbAMRZe/8/FyFo2R0PZW4sfjiYsiK/aC1r0oL1KDz/bWkbLBgbQk9 7 | VhzNS3iNl3H1WFBOGy9/HyhizckYTXWd8tPnkiul+YyxysNWuf/lbCPVsOpP3URu 8 | OFINc0ghT1dp/EAm0Fs+74cpe7TnON+AoAAcu79OqHdBWC+SffCEaM+ELVw6BF65 9 | /iF/tzigBK8W1+MEpuuI5d3MjCcn6l51hSGGD68U67q49Ez/n3CAw2V/COFXRMAr 10 | +6ExFx04I0LJkisLFzliOUoG+N0s6cXq4ny1TNx3uqlYevhaJS9pxOflQFF9hPR1 11 | ks35C40bawki9Dl7BjvDwUqcpCoYjondtWO4wt6L1w22KE2pdQFG864fz4I3hQdS 12 | 4VROpnz5Jr4dXSeKHYpZC5WilQJzM7MPxYxnDqvJqBh0aR43L9+f4+XE8jcg27dU 13 | I0zrOqOHwpcTABZV62NFQ36vEGLGqo/pssuktcj4wPvN3AnXTtUP2GjZKc7epmEk 14 | dLrV41gvJNFCUC0Xm660DwBdL6Z3UyRCSxvE6fz5cEUpL5i59w2+htIfNhlqLLLl 15 | 73wk7TJMruYP6KussYI1N1rp/3tpJruUHAS69x7ZEQIDAQABMA0GCSqGSIb3DQEB 16 | CwUAA4ICAQAyRPxPS2CppJWTJA8LlVxe+VOhDTZEog1l1b2eVuLYEf6XmjGpF/tQ 17 | zOPsTokxhQNlGPZLCnLUQcmOWfeGt9WlpwHXleYN/pib+HiDPMMLzLXy4gJGKNRX 18 | I5kPlgJ9NKfAYtB0MV80MbZKBBvBvkCF3mKVDUxXOK0Q4FuIf4oDeFiM/8iskFSf 19 | NzBdR3/mxGTlK5mQJtad5K8m4V3F3IJihGBTX32nF6qqw22ARJyeA9nRHA4HbyD4 20 | W0kPgFgle5/xgCSemVLh/Y6YBz8r3HKJ+FToTA4x2WULsRSt6k1NBdfBJ6vAyHDg 21 | ampcmnJBsEczRmAAZEG8fBrpu8nF9xukdowd3ncRBdpNcmuiOdzpe4bFXOPVvfbo 22 | WhCANZhb9A9sCu7D1M/QGT6eBw1h46kIXQIWQeJOdJFihUN8WvO+Dc5TvFMaPGbz 23 | SUpHdUuqF+eUvti39+/gu3qOMdGYFXRsQ9//uJBo2tKy6buKBy2BWR0dPdgnLygf 24 | z9TcfxOijaKCFy+FLsBy6ozqXgXS433yDhI0BKa61QyuvZaKsah1zwVba4qi4gm4 25 | amnnlpvGVGY9IP1YscHfGL3LNfXSFj5dgrE70aEj9I0k5x9cBiQWflemfqqV+6pm 26 | jcCuj/G0TqwnnRnPOx3fFjJo2Mlh8gya8Yj+aXbgAohRC+XkAiFQAA== 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Client/HTTPClientRequestEncoder.swift: -------------------------------------------------------------------------------- 1 | /// Private `ChannelOutboundHandler` that serializes `HTTPRequest` to `HTTPClientRequestPart`. 2 | internal final class HTTPClientRequestEncoder: ChannelOutboundHandler, RemovableChannelHandler { 3 | typealias OutboundIn = HTTPRequest 4 | typealias OutboundOut = HTTPClientRequestPart 5 | 6 | let hostHeaderValue: String 7 | 8 | /// Creates a new `HTTPClientRequestSerializer`. 9 | init(hostHeaderValue: String) { 10 | self.hostHeaderValue = hostHeaderValue 11 | } 12 | 13 | /// See `ChannelOutboundHandler`. 14 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 15 | let req = unwrapOutboundIn(data) 16 | var headers = req.headers 17 | headers.add(name: .host, value: self.hostHeaderValue) 18 | 19 | let path: String 20 | if let query = req.url.query { 21 | path = req.url.path + "?" + query 22 | } else { 23 | path = req.url.path 24 | } 25 | 26 | headers.replaceOrAdd(name: .userAgent, value: "Vapor/4.0 (Swift)") 27 | var httpHead = HTTPRequestHead( 28 | version: req.version, 29 | method: req.method, 30 | uri: path.hasPrefix("/") ? path : "/" + path 31 | ) 32 | httpHead.headers = headers 33 | context.write(wrapOutboundOut(.head(httpHead)), promise: nil) 34 | if let data = req.body.data { 35 | var buffer = ByteBufferAllocator().buffer(capacity: data.count) 36 | buffer.writeBytes(data) 37 | context.write(self.wrapOutboundOut(.body(.byteBuffer(buffer))), promise: nil) 38 | } 39 | context.write(self.wrapOutboundOut(.end(nil)), promise: promise) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Utilities/CaseInsensitiveString.swift: -------------------------------------------------------------------------------- 1 | /// A `CaseInsensitiveString` (for `Comparable`, `Equatable`, and `Hashable`). 2 | /// 3 | /// 4 | /// "HELLO".ci == "hello".ci // true 5 | /// 6 | public struct CaseInsensitiveString: ExpressibleByStringLiteral, Comparable, Equatable, Hashable, CustomStringConvertible { 7 | /// See `Equatable`. 8 | public static func == (lhs: CaseInsensitiveString, rhs: CaseInsensitiveString) -> Bool { 9 | return lhs.storage.lowercased() == rhs.storage.lowercased() 10 | } 11 | 12 | /// See `Comparable`. 13 | public static func < (lhs: CaseInsensitiveString, rhs: CaseInsensitiveString) -> Bool { 14 | return lhs.storage.lowercased() < rhs.storage.lowercased() 15 | } 16 | 17 | /// Internal `String` storage. 18 | private let storage: String 19 | 20 | /// See `Hashable`. 21 | public func hash(into hasher: inout Hasher) { 22 | self.storage.lowercased().hash(into: &hasher) 23 | } 24 | 25 | /// See `CustomStringConvertible`. 26 | public var description: String { 27 | return storage 28 | } 29 | 30 | /// Creates a new `CaseInsensitiveString`. 31 | /// 32 | /// let ciString = CaseInsensitiveString("HeLlO") 33 | /// 34 | /// - parameters: 35 | /// - string: A case-sensitive `String`. 36 | public init(_ string: String) { 37 | self.storage = string 38 | } 39 | 40 | /// See `ExpressibleByStringLiteral`. 41 | public init(stringLiteral value: String) { 42 | self.storage = value 43 | } 44 | } 45 | 46 | extension String { 47 | /// Creates a `CaseInsensitiveString` from this `String`. 48 | /// 49 | /// "HELLO".ci == "hello".ci // true 50 | /// 51 | public var ci: CaseInsensitiveString { 52 | return .init(self) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Utilities/File.swift: -------------------------------------------------------------------------------- 1 | /// Represents a single file. 2 | public struct HTTPFile: Codable { 3 | /// Name of the file, including extension. 4 | public var filename: String 5 | 6 | /// The file's data. 7 | public var data: ByteBuffer 8 | 9 | /// Associated `MediaType` for this file's extension, if it has one. 10 | public var contentType: HTTPMediaType? { 11 | return ext.flatMap { HTTPMediaType.fileExtension($0.lowercased()) } 12 | } 13 | 14 | /// The file extension, if it has one. 15 | public var ext: String? { 16 | let parts = filename.split(separator: ".") 17 | 18 | if parts.count > 1 { 19 | return parts.last.map(String.init) 20 | } else { 21 | return nil 22 | } 23 | } 24 | 25 | public init(from decoder: Decoder) throws { 26 | fatalError() 27 | } 28 | 29 | public func encode(to encoder: Encoder) throws { 30 | fatalError() 31 | } 32 | 33 | /// Creates a new `File`. 34 | /// 35 | /// let file = File(data: "hello", filename: "foo.txt") 36 | /// 37 | /// - parameters: 38 | /// - data: The file's contents. 39 | /// - filename: The name of the file, not including path. 40 | public init(data: String, filename: String) { 41 | var buffer = ByteBufferAllocator().buffer(capacity: data.utf8.count) 42 | buffer.writeString(data) 43 | self.init(data: buffer, filename: filename) 44 | } 45 | 46 | /// Creates a new `File`. 47 | /// 48 | /// let file = File(data: "hello", filename: "foo.txt") 49 | /// 50 | /// - parameters: 51 | /// - data: The file's contents. 52 | /// - filename: The name of the file, not including path. 53 | public init(data: ByteBuffer, filename: String) { 54 | self.data = data 55 | self.filename = filename 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Headers/HTTPHeaders+Basic.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOHTTP1 3 | 4 | /// A basic username and password. 5 | public struct BasicAuthorization { 6 | /// The username, sometimes an email address 7 | public let username: String 8 | 9 | /// The plaintext password 10 | public let password: String 11 | 12 | /// Create a new `BasicAuthorization`. 13 | public init(username: String, password: String) { 14 | self.username = username 15 | self.password = password 16 | } 17 | } 18 | 19 | extension HTTPHeaders { 20 | /// Access or set the `Authorization: Basic: ...` header. 21 | public var basicAuthorization: BasicAuthorization? { 22 | get { 23 | guard let string = self[.authorization].first else { 24 | return nil 25 | } 26 | 27 | guard let range = string.range(of: "Basic ") else { 28 | return nil 29 | } 30 | 31 | let token = string[range.upperBound...] 32 | guard let decodedToken = Data(base64Encoded: .init(token)) else { 33 | return nil 34 | } 35 | 36 | let parts = decodedToken.split(separator: 0x3A) // colon 37 | 38 | guard parts.count == 2 else { 39 | return nil 40 | } 41 | 42 | guard 43 | let username = String(data: parts[0], encoding: .utf8), 44 | let password = String(data: parts[1], encoding: .utf8) 45 | else { 46 | return nil 47 | } 48 | 49 | return .init(username: username, password: password) 50 | } 51 | set { 52 | if let basic = newValue { 53 | let credentials = "\(basic.username):\(basic.password)" 54 | let encoded = Data(credentials.utf8).base64EncodedString() 55 | replaceOrAdd(name: .authorization, value: "Basic \(encoded)") 56 | } else { 57 | remove(name: .authorization) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Client/HTTPClientUpgradeHandler.swift: -------------------------------------------------------------------------------- 1 | internal final class HTTPClientUpgradeHandler: ChannelDuplexHandler, RemovableChannelHandler { 2 | typealias InboundIn = HTTPResponse 3 | typealias OutboundIn = HTTPRequest 4 | typealias OutboundOut = HTTPRequest 5 | 6 | enum UpgradeState { 7 | case ready 8 | case pending(HTTPClientProtocolUpgrader) 9 | } 10 | 11 | var state: UpgradeState 12 | let httpHandlerNames: [String] 13 | 14 | init( 15 | httpHandlerNames: [String] 16 | ) { 17 | self.httpHandlerNames = httpHandlerNames 18 | self.state = .ready 19 | } 20 | 21 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 22 | context.fireChannelRead(data) 23 | switch self.state { 24 | case .pending(let upgrader): 25 | let res = self.unwrapInboundIn(data) 26 | if res.status == .switchingProtocols { 27 | let futures = [ 28 | context.pipeline.removeHandler(self) 29 | ] + self.httpHandlerNames.map { context.pipeline.removeHandler(name: $0) } 30 | EventLoopFuture.andAllSucceed(futures, on: context.eventLoop).flatMap { () -> EventLoopFuture in 31 | return upgrader.upgrade(context: context, upgradeResponse: .init( 32 | version: res.version, 33 | status: res.status, 34 | headers: res.headers 35 | )) 36 | }.whenFailure { error in 37 | self.errorCaught(context: context, error: error) 38 | } 39 | 40 | } 41 | case .ready: break 42 | } 43 | } 44 | 45 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 46 | var req = self.unwrapOutboundIn(data) 47 | if let upgrader = req.upgrader { 48 | for (name, value) in upgrader.buildUpgradeRequest() { 49 | req.headers.add(name: name, value: value) 50 | } 51 | self.state = .pending(upgrader) 52 | } 53 | context.write(self.wrapOutboundOut(req), promise: promise) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Server/HTTPServerHandler.swift: -------------------------------------------------------------------------------- 1 | final class HTTPServerHandler: ChannelInboundHandler, RemovableChannelHandler { 2 | typealias InboundIn = HTTPRequest 3 | typealias OutboundOut = HTTPResponse 4 | 5 | let delegate: HTTPServerDelegate 6 | let errorHandler: (Error) -> () 7 | 8 | init( 9 | delegate: HTTPServerDelegate, 10 | errorHandler: @escaping (Error) -> () 11 | ) { 12 | self.delegate = delegate 13 | self.errorHandler = errorHandler 14 | } 15 | 16 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 17 | var req = self.unwrapInboundIn(data) 18 | 19 | // change HEAD -> GET 20 | let originalMethod = req.method 21 | switch req.method { 22 | case .HEAD: req.method = .GET 23 | default: break 24 | } 25 | 26 | // query delegate for response 27 | self.delegate.respond(to: req, on: context.channel).whenComplete { res in 28 | switch res { 29 | case .failure(let error): 30 | self.errorHandler(error) 31 | context.close(promise: nil) 32 | case .success(var res): 33 | if originalMethod == .HEAD { 34 | res.body = .init() 35 | } 36 | self.serialize(res, for: req, context: context) 37 | } 38 | } 39 | } 40 | 41 | func serialize(_ res: HTTPResponse, for req: HTTPRequest, context: ChannelHandlerContext) { 42 | switch req.body.storage { 43 | case .stream(let stream): 44 | assert(stream.isClosed, "HTTPResponse sent while HTTPRequest had unconsumed chunked data.") 45 | default: break 46 | } 47 | 48 | switch req.version.major { 49 | case 2: 50 | context.write(self.wrapOutboundOut(res), promise: nil) 51 | default: 52 | var res = res 53 | res.headers.add(name: .connection, value: req.isKeepAlive ? "keep-alive" : "close") 54 | let done = context.write(self.wrapOutboundOut(res)) 55 | if !req.isKeepAlive { 56 | _ = done.flatMap { 57 | return context.close() 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Client/HTTPClientProxyHandler.swift: -------------------------------------------------------------------------------- 1 | final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChannelHandler { 2 | typealias InboundIn = HTTPClientResponsePart 3 | typealias OutboundIn = HTTPClientRequestPart 4 | typealias OutboundOut = HTTPClientRequestPart 5 | 6 | let hostname: String 7 | let port: Int 8 | var onConnect: (ChannelHandlerContext) -> () 9 | 10 | private var buffer: [HTTPClientRequestPart] 11 | 12 | 13 | init(hostname: String, port: Int, onConnect: @escaping (ChannelHandlerContext) -> ()) { 14 | self.hostname = hostname 15 | self.port = port 16 | self.onConnect = onConnect 17 | self.buffer = [] 18 | } 19 | 20 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 21 | let res = self.unwrapInboundIn(data) 22 | switch res { 23 | case .head(let head): 24 | assert(head.status == .ok) 25 | case .end: 26 | self.configureTLS(context: context) 27 | default: assertionFailure("invalid state: \(res)") 28 | } 29 | } 30 | 31 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 32 | let req = self.unwrapOutboundIn(data) 33 | self.buffer.append(req) 34 | promise?.succeed(()) 35 | } 36 | 37 | func channelActive(context: ChannelHandlerContext) { 38 | self.sendConnect(context: context) 39 | } 40 | 41 | // MARK: Private 42 | 43 | private func configureTLS(context: ChannelHandlerContext) { 44 | self.onConnect(context) 45 | self.buffer.forEach { context.write(self.wrapOutboundOut($0), promise: nil) } 46 | context.flush() 47 | _ = context.pipeline.removeHandler(self) 48 | } 49 | 50 | private func sendConnect(context: ChannelHandlerContext) { 51 | var head = HTTPRequestHead( 52 | version: .init(major: 1, minor: 1), 53 | method: .CONNECT, 54 | uri: "\(self.hostname):\(self.port)" 55 | ) 56 | head.headers.add(name: "proxy-connection", value: "keep-alive") 57 | context.write(self.wrapOutboundOut(.head(head)), promise: nil) 58 | context.write(self.wrapOutboundOut(.end(nil)), promise: nil) 59 | context.flush() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/HTTPKitTests/HTTPServerTests.swift: -------------------------------------------------------------------------------- 1 | import HTTPKit 2 | import XCTest 3 | 4 | class HTTPServerTests: XCTestCase { 5 | func testLargeResponseClose() throws { 6 | struct LargeResponder: HTTPServerDelegate { 7 | func respond(to request: HTTPRequest, on channel: Channel) -> EventLoopFuture { 8 | let res = HTTPResponse( 9 | status: .ok, 10 | body: .init(string: .init(repeating: "0", count: 2_000_000)) 11 | ) 12 | return channel.eventLoop.makeSucceededFuture(res) 13 | } 14 | } 15 | let server = HTTPServer( 16 | configuration: .init( 17 | hostname: "localhost", 18 | port: 8080, 19 | supportVersions: [.one], 20 | errorHandler: { error in 21 | XCTFail("\(error)") 22 | } 23 | ), 24 | on: self.eventLoopGroup 25 | ) 26 | try server.start(delegate: LargeResponder()).wait() 27 | 28 | var req = HTTPRequest(method: .GET, url: "http://localhost:8080/") 29 | req.headers.replaceOrAdd(name: .connection, value: "close") 30 | let res = try HTTPClient(on: self.eventLoopGroup) 31 | .send(req).wait() 32 | XCTAssertEqual(res.body.count, 2_000_000) 33 | try server.shutdown().wait() 34 | try server.onClose.wait() 35 | } 36 | 37 | func testRFC1123Flip() throws { 38 | var now: Date? 39 | var boundary = 0.0 40 | while boundary <= 0.01 { 41 | now = Date() 42 | boundary = 1.0 - now!.timeIntervalSince1970.truncatingRemainder(dividingBy: 1) 43 | } 44 | let nowStamp = now!.rfc1123 45 | Thread.sleep(forTimeInterval: boundary - 0.01) 46 | let beforeStamp = Date().rfc1123 47 | Thread.sleep(forTimeInterval: 0.02) 48 | let afterStamp = Date().rfc1123 49 | 50 | XCTAssertEqual(nowStamp, beforeStamp) 51 | XCTAssertNotEqual(beforeStamp, afterStamp) 52 | } 53 | 54 | var eventLoopGroup: EventLoopGroup! 55 | 56 | override func setUp() { 57 | self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 58 | } 59 | 60 | override func tearDown() { 61 | try! self.eventLoopGroup.syncShutdownGracefully() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/HTTPKitTests/HTTPCookieTests.swift: -------------------------------------------------------------------------------- 1 | import HTTPKit 2 | import XCTest 3 | 4 | final class HTTPCookieTests: XCTestCase { 5 | func testCookieParse() throws { 6 | /// from https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies 7 | guard let (name, value) = HTTPCookies.Value.parse("id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly") else { 8 | throw CookieError() 9 | } 10 | XCTAssertEqual(name, "id") 11 | XCTAssertEqual(value.string, "a3fWa") 12 | XCTAssertEqual(value.expires, Date(rfc1123: "Wed, 21 Oct 2015 07:28:00 GMT")) 13 | XCTAssertEqual(value.isSecure, true) 14 | XCTAssertEqual(value.isHTTPOnly, true) 15 | 16 | guard let cookie: (name: String, value: HTTPCookies.Value) = HTTPCookies.Value.parse("vapor=; Secure; HttpOnly") else { 17 | throw CookieError() 18 | } 19 | XCTAssertEqual(cookie.name, "vapor") 20 | XCTAssertEqual(cookie.value.string, "") 21 | XCTAssertEqual(cookie.value.isSecure, true) 22 | XCTAssertEqual(cookie.value.isHTTPOnly, true) 23 | } 24 | 25 | func testCookieIsSerializedCorrectly() throws { 26 | var httpReq = HTTPRequest(method: .GET, url: "/") 27 | 28 | guard let (name, value) = HTTPCookies.Value.parse("id=value; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly") else { 29 | throw CookieError() 30 | } 31 | 32 | httpReq.cookies = HTTPCookies(dictionaryLiteral: (name, value)) 33 | 34 | XCTAssertEqual(httpReq.headers.firstValue(name: .cookie), "id=value") 35 | } 36 | 37 | func testMultipleCookiesAreSerializedCorrectly() throws { 38 | var httpRes = HTTPResponse() 39 | httpRes.cookies["a"] = HTTPCookies.Value(string: "1") 40 | XCTAssertEqual(httpRes.headers[.setCookie].filter { $0.contains("a=1") }.count, 1) 41 | httpRes.cookies["b"] = HTTPCookies.Value(string: "2") 42 | XCTAssertEqual(httpRes.headers[.setCookie].filter { $0.contains("a=1") }.count, 1) 43 | XCTAssertEqual(httpRes.headers[.setCookie].filter { $0.contains("b=2") }.count, 1) 44 | httpRes.cookies["c"] = HTTPCookies.Value(string: "3") 45 | XCTAssertEqual(httpRes.headers[.setCookie].filter { $0.contains("a=1") }.count, 1) 46 | XCTAssertEqual(httpRes.headers[.setCookie].filter { $0.contains("b=2") }.count, 1) 47 | XCTAssertEqual(httpRes.headers[.setCookie].filter { $0.contains("c=3") }.count, 1) 48 | } 49 | } 50 | 51 | struct CookieError: Error { } 52 | -------------------------------------------------------------------------------- /Sources/HTTPKit/WebSocket/WebSocketFrameSequence.swift: -------------------------------------------------------------------------------- 1 | /// Collects WebSocket frame sequences. 2 | /// 3 | /// See https://tools.ietf.org/html/rfc6455#section-5 below. 4 | /// 5 | /// 5. Data Framing 6 | /// 5.1. Overview 7 | /// 8 | /// In the WebSocket Protocol, data is transmitted using a sequence of 9 | /// frames. To avoid confusing network intermediaries (such as 10 | /// intercepting proxies) and for security reasons that are further 11 | /// discussed in Section 10.3, a client MUST mask all frames that it 12 | /// sends to the server (see Section 5.3 for further details). (Note 13 | /// that masking is done whether or not the WebSocket Protocol is running 14 | /// over TLS.) The server MUST close the connection upon receiving a 15 | /// frame that is not masked. In this case, a server MAY send a Close 16 | /// frame with a status code of 1002 (protocol error) as defined in 17 | /// Section 7.4.1. A server MUST NOT mask any frames that it sends to 18 | /// the client. A client MUST close a connection if it detects a masked 19 | /// frame. In this case, it MAY use the status code 1002 (protocol 20 | /// error) as defined in Section 7.4.1. (These rules might be relaxed in 21 | /// a future specification.) 22 | /// 23 | /// The base framing protocol defines a frame type with an opcode, a 24 | /// payload length, and designated locations for "Extension data" and 25 | /// "Application data", which together define the "Payload data". 26 | /// Certain bits and opcodes are reserved for future expansion of the 27 | /// protocol. 28 | /// 29 | /// A data frame MAY be transmitted by either the client or the server at 30 | /// any time after opening handshake completion and before that endpoint 31 | /// has sent a Close frame (Section 5.5.1). 32 | struct WebSocketFrameSequence { 33 | var binaryBuffer: ByteBuffer? 34 | var textBuffer: String 35 | var type: WebSocketOpcode 36 | 37 | init(type: WebSocketOpcode) { 38 | self.binaryBuffer = nil 39 | self.textBuffer = .init() 40 | self.type = type 41 | } 42 | 43 | mutating func append(_ frame: WebSocketFrame) { 44 | var data = frame.unmaskedData 45 | switch type { 46 | case .binary: 47 | if var existing = self.binaryBuffer { 48 | existing.writeBuffer(&data) 49 | self.binaryBuffer = existing 50 | } else { 51 | self.binaryBuffer = data 52 | } 53 | case .text: textBuffer.append(data.readString(length: data.readableBytes) ?? "") 54 | default: break 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Client/HTTPClientResponseDecoder.swift: -------------------------------------------------------------------------------- 1 | /// Private `ChannelInboundHandler` that parses `HTTPClientResponsePart` to `HTTPResponse`. 2 | internal final class HTTPClientResponseDecoder: ChannelInboundHandler, RemovableChannelHandler { 3 | typealias InboundIn = HTTPClientResponsePart 4 | typealias OutboundOut = HTTPResponse 5 | 6 | /// Tracks `HTTPClientHandler`'s state. 7 | enum ResponseState { 8 | /// Waiting to parse the next response. 9 | case ready 10 | /// Currently parsing the response's body. 11 | case parsingBody(HTTPResponseHead, ByteBuffer?) 12 | } 13 | 14 | var state: ResponseState 15 | 16 | init() { 17 | self.state = .ready 18 | } 19 | 20 | /// See `ChannelInboundHandler`. 21 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 22 | let res = self.unwrapInboundIn(data) 23 | switch res { 24 | case .head(let head): 25 | switch self.state { 26 | case .ready: self.state = .parsingBody(head, nil) 27 | case .parsingBody: assert(false, "Unexpected HTTPClientResponsePart.head when body was being parsed.") 28 | } 29 | case .body(var body): 30 | switch self.state { 31 | case .ready: assert(false, "Unexpected HTTPClientResponsePart.body when awaiting request head.") 32 | case .parsingBody(let head, let existingData): 33 | let buffer: ByteBuffer 34 | if var existing = existingData { 35 | existing.writeBuffer(&body) 36 | buffer = existing 37 | } else { 38 | buffer = body 39 | } 40 | self.state = .parsingBody(head, buffer) 41 | } 42 | case .end(let tailHeaders): 43 | assert(tailHeaders == nil, "Unexpected tail headers") 44 | switch self.state { 45 | case .ready: assert(false, "Unexpected HTTPClientResponsePart.end when awaiting request head.") 46 | case .parsingBody(let head, let data): 47 | let body: HTTPBody = data.flatMap { HTTPBody(buffer: $0) } ?? .init() 48 | let res = HTTPResponse( 49 | status: head.status, 50 | version: head.version, 51 | headersNoUpdate: head.headers, 52 | body: body 53 | ) 54 | self.state = .ready 55 | context.fireChannelRead(wrapOutboundOut(res)) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/HTTPKitExample/main.swift: -------------------------------------------------------------------------------- 1 | import HTTPKit 2 | import Logging 3 | 4 | let hostname = "127.0.0.1" 5 | let elg = MultiThreadedEventLoopGroup(numberOfThreads: 8) 6 | 7 | let client = HTTPClient(on: elg) 8 | 9 | let res0 = client.send(HTTPRequest(method: .GET, url: "http://httpbin.org/status/200")) 10 | let res1 = client.send(HTTPRequest(method: .GET, url: "http://httpbin.org/status/201")) 11 | let res2 = client.send(HTTPRequest(method: .GET, url: "http://httpbin.org/status/202")) 12 | 13 | try print(res0.wait().status) 14 | try print(res1.wait().status) 15 | try print(res2.wait().status) 16 | 17 | let res = HTTPResponse(body: HTTPBody(staticString: "pong")) 18 | 19 | struct EchoResponder: HTTPServerDelegate { 20 | func respond(to req: HTTPRequest, on channel: Channel) -> EventLoopFuture { 21 | let promise = channel.eventLoop.makePromise(of: HTTPResponse.self) 22 | print("got: \(req)") 23 | if let stream = req.body.stream { 24 | stream.read { chunk, stream in 25 | switch chunk { 26 | case .chunk(var chunk): 27 | let string = chunk.readString(length: chunk.readableBytes) ?? "" 28 | print("streamed: \(string.debugDescription)") 29 | case .end: 30 | promise.succeed(res) 31 | case .error(let error): 32 | promise.fail(error) 33 | } 34 | } 35 | } else { 36 | promise.succeed(res) 37 | } 38 | return promise.futureResult 39 | } 40 | } 41 | let responder = EchoResponder() 42 | 43 | print("Plaintext server starting on http://\(hostname):8080") 44 | 45 | let plaintextServer = HTTPServer( 46 | configuration: .init( 47 | hostname: hostname, 48 | port: 8080, 49 | supportVersions: [.one] 50 | ), 51 | on: elg 52 | ) 53 | try plaintextServer.start(delegate: responder).wait() 54 | 55 | 56 | // Uncomment to start only plaintext server 57 | // plaintextServer.onClose.wait() 58 | 59 | print("TLS server starting on https://\(hostname):8443") 60 | 61 | let tlsServer = HTTPServer( 62 | configuration 63 | : .init( 64 | hostname: hostname, 65 | port: 8443, 66 | supportVersions: [.one, .two], 67 | tlsConfig: .forServer( 68 | certificateChain: [.file("/Users/tanner0101/dev/vapor/http-kit/certs/cert.pem")], 69 | privateKey: .file("/Users/tanner0101/dev/vapor/http-kit/certs/key.pem") 70 | ) 71 | ), 72 | on: elg 73 | ) 74 | try tlsServer.start(delegate: responder).wait() 75 | 76 | _ = try plaintextServer.onClose 77 | .and(tlsServer.onClose).wait() 78 | 79 | try elg.syncShutdownGracefully() 80 | -------------------------------------------------------------------------------- /Sources/HTTPKit/WebSocket/WebSocket+Server.swift: -------------------------------------------------------------------------------- 1 | ///// Allows `HTTPServer` to accept `WebSocket` connections. 2 | ///// 3 | ///// let ws = HTTPServer.webSocketUpgrader(shouldUpgrade: { req in 4 | ///// // return non-nil HTTPHeaders to allow upgrade 5 | ///// }, onUpgrade: { ws, req in 6 | ///// // setup callbacks or send data to connected WebSocket 7 | ///// }) 8 | ///// 9 | ///// HTTPServer.start(..., upgraders: [ws]) 10 | ///// 11 | //extension HTTPServer { 12 | // // MARK: Server Upgrade 13 | // 14 | // /// Creates an `HTTPProtocolUpgrader` that will accept incoming `WebSocket` upgrade requests. 15 | // /// 16 | // /// let ws = HTTPServer.webSocketUpgrader(shouldUpgrade: { req in 17 | // /// // return non-nil HTTPHeaders to allow upgrade 18 | // /// }, onUpgrade: { ws, req in 19 | // /// // setup callbacks or send data to connected WebSocket 20 | // /// }) 21 | // /// 22 | // /// HTTPServer.start(..., upgraders: [ws]) 23 | // /// 24 | // /// - parameters: 25 | // /// - maxFrameSize: Maximum WebSocket frame size this server will accept. 26 | // /// - shouldUpgrade: Called when an incoming HTTPRequest attempts to upgrade. 27 | // /// Return non-nil headers to accept the upgrade. 28 | // /// - onUpgrade: Called when a new WebSocket client has connected. 29 | // /// - returns: An `HTTPProtocolUpgrader` for use with `HTTPServer`. 30 | // public static func webSocketUpgrader( 31 | // maxFrameSize: Int = 1 << 14, 32 | // shouldUpgrade: @escaping (HTTPRequest) -> (HTTPHeaders?), 33 | // onUpgrade: @escaping (WebSocket, HTTPRequest) -> () 34 | // ) -> HTTPProtocolUpgrader { 35 | // return WebSocketUpgrader(maxFrameSize: maxFrameSize, shouldUpgrade: { head in 36 | // let req = HTTPRequest( 37 | // method: head.method, 38 | // url: head.uri, 39 | // version: head.version, 40 | // headers: head.headers 41 | // ) 42 | // return shouldUpgrade(req) 43 | // }, upgradePipelineHandler: { channel, head in 44 | // let req = HTTPRequest( 45 | // method: head.method, 46 | // url: head.uri, 47 | // version: head.version, 48 | // headers: head.headers 49 | // ) 50 | // #warning("TODO: pass channel if necessary") 51 | // // req.channel = channel 52 | // let webSocket = WebSocket(channel: channel, mode: .server) 53 | // return channel.pipeline.add(webSocket: webSocket).map { 54 | // onUpgrade(webSocket, req) 55 | // } 56 | // }) 57 | // } 58 | //} 59 | -------------------------------------------------------------------------------- /Tests/HTTPKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension HTTPClientTests { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__HTTPClientTests = [ 9 | ("testClientDefaultConfig", testClientDefaultConfig), 10 | ("testClientProxyPlaintext", testClientProxyPlaintext), 11 | ("testClientProxyTLS", testClientProxyTLS), 12 | ("testExampleCom", testExampleCom), 13 | ("testGoogleAPIsFCM", testGoogleAPIsFCM), 14 | ("testGoogleWithTLS", testGoogleWithTLS), 15 | ("testHTTPBin418", testHTTPBin418), 16 | ("testHTTPBinAnything", testHTTPBinAnything), 17 | ("testHTTPBinRobots", testHTTPBinRobots), 18 | ("testQuery", testQuery), 19 | ("testRemotePeer", testRemotePeer), 20 | ("testSNIWebsite", testSNIWebsite), 21 | ("testUncleanShutdown", testUncleanShutdown), 22 | ("testVaporWithTLS", testVaporWithTLS), 23 | ("testZombo", testZombo), 24 | ] 25 | } 26 | 27 | extension HTTPCookieTests { 28 | // DO NOT MODIFY: This is autogenerated, use: 29 | // `swift test --generate-linuxmain` 30 | // to regenerate. 31 | static let __allTests__HTTPCookieTests = [ 32 | ("testCookieIsSerializedCorrectly", testCookieIsSerializedCorrectly), 33 | ("testCookieParse", testCookieParse), 34 | ("testMultipleCookiesAreSerializedCorrectly", testMultipleCookiesAreSerializedCorrectly), 35 | ] 36 | } 37 | 38 | extension HTTPHeaderTests { 39 | // DO NOT MODIFY: This is autogenerated, use: 40 | // `swift test --generate-linuxmain` 41 | // to regenerate. 42 | static let __allTests__HTTPHeaderTests = [ 43 | ("testAcceptHeader", testAcceptHeader), 44 | ] 45 | } 46 | 47 | extension HTTPServerTests { 48 | // DO NOT MODIFY: This is autogenerated, use: 49 | // `swift test --generate-linuxmain` 50 | // to regenerate. 51 | static let __allTests__HTTPServerTests = [ 52 | ("testLargeResponseClose", testLargeResponseClose), 53 | ("testRFC1123Flip", testRFC1123Flip), 54 | ] 55 | } 56 | 57 | extension WebSocketTests { 58 | // DO NOT MODIFY: This is autogenerated, use: 59 | // `swift test --generate-linuxmain` 60 | // to regenerate. 61 | static let __allTests__WebSocketTests = [ 62 | ("testClient", testClient), 63 | ("testClientTLS", testClientTLS), 64 | ("testServer", testServer), 65 | ("testServerContinuation", testServerContinuation), 66 | ] 67 | } 68 | 69 | public func __allTests() -> [XCTestCaseEntry] { 70 | return [ 71 | testCase(HTTPClientTests.__allTests__HTTPClientTests), 72 | testCase(HTTPCookieTests.__allTests__HTTPCookieTests), 73 | testCase(HTTPHeaderTests.__allTests__HTTPHeaderTests), 74 | testCase(HTTPServerTests.__allTests__HTTPServerTests), 75 | testCase(WebSocketTests.__allTests__WebSocketTests), 76 | ] 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Message/HTTPMessage.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | import NIOHTTP1 3 | 4 | /// An HTTP message. 5 | /// This is the basis of `HTTPRequest` and `HTTPResponse`. It has the general structure of: 6 | /// 7 | /// HTTP/1.1 8 | /// Content-Length: 5 9 | /// Foo: Bar 10 | /// 11 | /// hello 12 | /// 13 | /// - note: The status line contains information that differentiates requests and responses. 14 | /// If the status line contains an HTTP method and URI it is a request. 15 | /// If the status line contains an HTTP status code it is a response. 16 | /// 17 | /// This protocol is useful for adding methods to both requests and responses, such as the ability to serialize 18 | /// content to both message types. 19 | public protocol HTTPMessage: CustomStringConvertible, CustomDebugStringConvertible { 20 | /// The HTTP version of this message. 21 | var version: HTTPVersion { get set } 22 | 23 | /// The HTTP headers. 24 | var headers: HTTPHeaders { get set } 25 | 26 | /// The optional HTTP body. 27 | var body: HTTPBody { get set } 28 | } 29 | 30 | extension HTTPMessage { 31 | /// `MediaType` specified by this message's `"Content-Type"` header. 32 | public var contentType: HTTPMediaType? { 33 | get { return headers.firstValue(name: .contentType).flatMap(HTTPMediaType.parse) } 34 | set { 35 | if let new = newValue?.serialize() { 36 | headers.replaceOrAdd(name: .contentType, value: new) 37 | } else { 38 | headers.remove(name: .contentType) 39 | } 40 | } 41 | } 42 | 43 | /// Returns a collection of `MediaTypePreference`s specified by this HTTP message's `"Accept"` header. 44 | /// 45 | /// You can returns all `MediaType`s in this collection to check membership. 46 | /// 47 | /// httpReq.accept.mediaTypes.contains(.html) 48 | /// 49 | /// Or you can compare preferences for two `MediaType`s. 50 | /// 51 | /// let pref = httpReq.accept.comparePreference(for: .json, to: .html) 52 | /// 53 | public var accept: [MediaTypePreference] { 54 | return headers.firstValue(name: .accept).flatMap([MediaTypePreference].parse) ?? [] 55 | } 56 | 57 | /// See `CustomDebugStringConvertible` 58 | public var debugDescription: String { 59 | return description 60 | } 61 | } 62 | 63 | extension HTTPHeaders { 64 | /// Updates transport headers for current body. 65 | /// This should be called automatically be `HTTPRequest` and `HTTPResponse` when their `body` property is set. 66 | internal mutating func updateTransportHeaders(for body: HTTPBody) { 67 | if let count = body.count?.description { 68 | self.remove(name: .transferEncoding) 69 | if count != self[.contentLength].first { 70 | self.replaceOrAdd(name: .contentLength, value: count) 71 | } 72 | } else { 73 | self.remove(name: .contentLength) 74 | if self[.transferEncoding].first != "chunked" { 75 | self.replaceOrAdd(name: .transferEncoding, value: "chunked") 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Client/HTTPClientHandler.swift: -------------------------------------------------------------------------------- 1 | internal final class HTTPClientHandler: ChannelDuplexHandler, RemovableChannelHandler { 2 | typealias InboundIn = HTTPResponse 3 | typealias OutboundIn = HTTPClientRequestContext 4 | typealias OutboundOut = HTTPRequest 5 | 6 | private var queue: [HTTPClientRequestContext] 7 | 8 | init() { 9 | self.queue = [] 10 | } 11 | 12 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 13 | let res = self.unwrapInboundIn(data) 14 | self.queue[0].promise.succeed(res) 15 | self.queue.removeFirst() 16 | } 17 | 18 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 19 | let req = self.unwrapOutboundIn(data) 20 | self.queue.append(req) 21 | context.write(self.wrapOutboundOut(req.request), promise: nil) 22 | context.flush() 23 | } 24 | 25 | func errorCaught(context: ChannelHandlerContext, error: Error) { 26 | switch self.queue.count { 27 | case 0: 28 | context.fireErrorCaught(error) 29 | default: 30 | self.queue.removeFirst().promise.fail(error) 31 | } 32 | } 33 | 34 | func close(context: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise?) { 35 | if let promise = promise { 36 | // we need to do some error mapping here, so create a new promise 37 | let p = context.eventLoop.makePromise(of: Void.self) 38 | 39 | // forward the close request with our new promise 40 | context.close(mode: mode, promise: p) 41 | 42 | // forward close future results based on whether 43 | // the close was successful 44 | p.futureResult.whenSuccess { promise.succeed(()) } 45 | p.futureResult.whenFailure { error in 46 | if 47 | let sslError = error as? NIOSSLError, 48 | case .uncleanShutdown = sslError, 49 | self.queue.isEmpty 50 | { 51 | // we can ignore unclear shutdown errors 52 | // since no requests are pending 53 | // 54 | // NOTE: this logic assumes that when self.queue is empty, 55 | // all HTTP responses have been completely recieved. 56 | // Special attention should be given to this if / when 57 | // streaming body support is added. 58 | promise.succeed(()) 59 | } else { 60 | promise.fail(error) 61 | } 62 | } 63 | } else { 64 | // no close promise anyway, just forward request 65 | context.close(mode: mode, promise: nil) 66 | } 67 | } 68 | } 69 | 70 | internal final class HTTPClientRequestContext { 71 | let request: HTTPRequest 72 | let promise: EventLoopPromise 73 | 74 | init(request: HTTPRequest, promise: EventLoopPromise) { 75 | self.request = request 76 | self.promise = promise 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Message/HTTPPeer.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | 3 | extension HTTPMessage { 4 | /// Represents the information we have about the remote peer of this message. 5 | /// 6 | /// See https://en.wikipedia.org/wiki/X-Forwarded-For 7 | public func remotePeer(on channel: Channel? = nil) -> HTTPPeer { 8 | return .init(self, channel: channel) 9 | } 10 | } 11 | 12 | /// Contain's information about the remote peer. 13 | public struct HTTPPeer: CustomStringConvertible { 14 | /// `HTTPMessage` that peer info will be extracted from. 15 | let message: HTTPMessage 16 | 17 | let channel: Channel? 18 | 19 | /// See `CustomStringConvertible`. 20 | public var description: String { 21 | var desc = "" 22 | if let scheme = scheme { 23 | desc += "\(scheme)://" 24 | } 25 | if let hostname = hostname { 26 | desc += "\(hostname)" 27 | } 28 | if let port = port { 29 | desc += ":\(port)" 30 | } 31 | return desc 32 | } 33 | 34 | /// Creates a new `HTTPPeer` wrapper around an `HTTPMessage`. 35 | init(_ message: HTTPMessage, channel: Channel?) { 36 | self.message = message 37 | self.channel = channel 38 | } 39 | 40 | /// The peer's scheme, like `http` or `https`. 41 | public var scheme: String? { 42 | return self.message.headers.firstValue(name: .forwarded).flatMap(Forwarded.parse)?.proto 43 | ?? self.message.headers.firstValue(name: .init("X-Forwarded-Proto")) 44 | ?? self.message.headers.firstValue(name: .init("X-Scheme")) 45 | } 46 | 47 | /// The peer's hostname. 48 | public var hostname: String? { 49 | return self.message.headers.firstValue(name: .forwarded).flatMap(Forwarded.parse)?.for 50 | ?? self.message.headers.firstValue(name: .init("X-Forwarded-For")) 51 | ?? self.channel?.remoteAddress?.hostname 52 | } 53 | 54 | /// The peer's port. 55 | public var port: Int? { 56 | return self.message.headers.firstValue(name: .init("X-Forwarded-Port")).flatMap(Int.init) 57 | ?? self.channel?.remoteAddress?.port.flatMap(Int.init) 58 | } 59 | } 60 | 61 | // MARK: Private 62 | 63 | private extension SocketAddress { 64 | /// Returns the hostname for this `SocketAddress` if one exists. 65 | var hostname: String? { 66 | switch self { 67 | case .unixDomainSocket: return nil 68 | case .v4(let v4): return v4.host 69 | case .v6(let v6): return v6.host 70 | } 71 | } 72 | } 73 | 74 | /// Parses the `Forwarded` header. 75 | private struct Forwarded { 76 | /// "for" section of the header 77 | var `for`: String? 78 | 79 | /// "proto" section of the header. 80 | var proto: String? 81 | 82 | /// "by" section of the header. 83 | var by: String? 84 | 85 | /// Creates a new `Forwaded` header object from the header value. 86 | static func parse(_ data: String) -> Forwarded? { 87 | guard let value = HTTPHeaderValue.parse(data) else { 88 | return nil 89 | } 90 | 91 | return .init(for: value.parameters["for"], proto: value.parameters["proto"], by: value.parameters["by"]) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Message/MediaTypePreference.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a `MediaType` and its associated preference, `q`. 4 | public struct MediaTypePreference { 5 | /// The `MediaType` in question. 6 | public var mediaType: HTTPMediaType 7 | 8 | /// Its associated preference. 9 | public var q: Double? 10 | } 11 | 12 | extension Array where Element == MediaTypePreference { 13 | /// Parses an array of `[MediaTypePreference]` from an accept header value 14 | /// 15 | /// - parameters: 16 | /// - data: The accept header value to parse. 17 | public static func parse(_ data: String) -> [MediaTypePreference] { 18 | return data.split(separator: ",").compactMap { token in 19 | let parts = token.split(separator: ";", maxSplits: 1) 20 | let value = String(parts[0]).trimmingCharacters(in: .whitespaces) 21 | guard let mediaType = HTTPMediaType.parse(value) else { 22 | return nil 23 | } 24 | switch parts.count { 25 | case 1: return .init(mediaType: mediaType, q: nil) 26 | case 2: 27 | let qparts = parts[1].split(separator: "=", maxSplits: 1) 28 | guard qparts.count == 2 else { 29 | return nil 30 | } 31 | guard let preference = Double(qparts[1]) else { 32 | return nil 33 | } 34 | return .init(mediaType: mediaType, q: preference) 35 | default: return nil 36 | } 37 | } 38 | } 39 | 40 | /// Returns all `MediaType`s in this array of `MediaTypePreference`. 41 | /// 42 | /// httpReq.accept.mediaTypes.contains(.html) 43 | /// 44 | public var mediaTypes: [HTTPMediaType] { 45 | return map { $0.mediaType } 46 | } 47 | 48 | /// Returns `ComparisonResult` comparing the supplied `MediaType`s against these preferences. 49 | /// 50 | /// let pref = httpReq.accept.comparePreference(for: .json, to: .html) 51 | /// 52 | public func comparePreference(for a: HTTPMediaType, to b: HTTPMediaType) -> ComparisonResult { 53 | var aq: Double? 54 | var bq: Double? 55 | for pref in self { 56 | if aq == nil, pref.mediaType == a { aq = pref.q ?? 1.0 } 57 | if bq == nil, pref.mediaType == b { bq = pref.q ?? 1.0 } 58 | } 59 | switch (aq, bq) { 60 | case (.some(let aq), .some(let bq)): 61 | /// there is a value for both media types, compare the preference 62 | if aq == bq { 63 | return .orderedSame 64 | } else if aq > bq { 65 | return .orderedAscending 66 | } else { 67 | return .orderedDescending 68 | } 69 | case (.none, .some): 70 | /// there is not a value for a, no way it can be preferred 71 | return .orderedDescending 72 | case (.some, .none): 73 | /// there is not a value for b, a is preferred by default 74 | return .orderedAscending 75 | case (.none, .none): 76 | /// there is no value for either, neither is preferred 77 | return .orderedSame 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDJnNBddXW4LA+E 3 | dxw2m0KbtZurNUSoWpgrLq+EZ6p3pV+pjAqJc2rBM/84x4mWJizlebcugXQ1f9wI 4 | bAMRZe/8/FyFo2R0PZW4sfjiYsiK/aC1r0oL1KDz/bWkbLBgbQk9VhzNS3iNl3H1 5 | WFBOGy9/HyhizckYTXWd8tPnkiul+YyxysNWuf/lbCPVsOpP3URuOFINc0ghT1dp 6 | /EAm0Fs+74cpe7TnON+AoAAcu79OqHdBWC+SffCEaM+ELVw6BF65/iF/tzigBK8W 7 | 1+MEpuuI5d3MjCcn6l51hSGGD68U67q49Ez/n3CAw2V/COFXRMAr+6ExFx04I0LJ 8 | kisLFzliOUoG+N0s6cXq4ny1TNx3uqlYevhaJS9pxOflQFF9hPR1ks35C40bawki 9 | 9Dl7BjvDwUqcpCoYjondtWO4wt6L1w22KE2pdQFG864fz4I3hQdS4VROpnz5Jr4d 10 | XSeKHYpZC5WilQJzM7MPxYxnDqvJqBh0aR43L9+f4+XE8jcg27dUI0zrOqOHwpcT 11 | ABZV62NFQ36vEGLGqo/pssuktcj4wPvN3AnXTtUP2GjZKc7epmEkdLrV41gvJNFC 12 | UC0Xm660DwBdL6Z3UyRCSxvE6fz5cEUpL5i59w2+htIfNhlqLLLl73wk7TJMruYP 13 | 6KussYI1N1rp/3tpJruUHAS69x7ZEQIDAQABAoICADfdrB85nRUboHEkUD0RaLOc 14 | 7zXR3OSJtgDBPwGBeEsPGCLzGzhbMwm2TtJ8+DMTAP3VbF0GohzxAReUVoVLqeSA 15 | SWL+k6diKUq0hjK1DLz26sFtgScmFDOH6f1V4MnXbOgCJJj+gLk7beh+QEJkZhTj 16 | nV1DiKWjUSQjVXSlnFKxg7sAZIBzqeT4wjVEgKkAtyWTlakEeMhCJAbKTzxyH9ag 17 | qONl1Ar+R3Bs5ZsQq0IwjFsfBI+jNjJ6mIYXy6aH73e6Y63wJ1Cv+5Ro3g878ahN 18 | idhUPcVhvCr0aQWs8ubpKyR1DaEHnTUQ797zpv13n4iQgWoI7Z/QpYEVb7fzB4Mi 19 | Ra/pgGTfNS19rYurZzGvS2OBOzG6D15b2Bim8x16CzppLWerQIKA1lJkZS6dKPfT 20 | 5c9fMuYDJ9UIzBHI3C39byk/mvbXWuJg4BqgE/MkeenITIUd31HDkQ5r1jBeGxYh 21 | GHPllINugjfzK1rWi0uYBjwn/5WlmU2NRRJtXJX6WSQPSfIp4iQ3hyqROZHrEdl0 22 | 0Dql665goH5ji2XeylfDYLJROm1U1iMHYaldrLcEesaD35KddA3KMPs9McYfU9ns 23 | KFfJMsi0/c60If7jKNYFppdZW6lZCnsZTq5XmymwmrIS8yluAtToChez6LD3smmF 24 | 4cD+WMgNyeL8ow56alnBAoIBAQDqTHT4Apd+9Jwsgp6Ic9Ptfj/8DJqIxN4uwfE7 25 | ifKyOw3VHjLE6+5bPMH0LZOP6ftffpl4zOpm1s6r0Y75nTWm4GiAgEHuRRSjnf2X 26 | nkj5uk+ZZySiwEsAGsJlTY5a62qhHQLYOJujc1M1o8LybmNm93m65tctKBOxG9Cv 27 | VX4ff23mw1bbBCmYMk7G211GR1e8zQ6kQPFgEP8QrXjvf5JsNwkEulz5XHC5Zwq0 28 | EBLKU1/NAF0R5O9ayPEdsj3reSsrnrHPOqQACfJDNRzfEfoaqul5/tK44PUGKBYK 29 | GHFnCExBOxUwHOC+q0XCsAwj+6yBoLI3xyDzvn5Dxj1ANMFvAoIBAQDcSVMWDSFD 30 | p9pJVNqlBE+UXu7RCif3hfGaV7yL8ahpCzei9iJncdePHC2mK6oUUtcWRVAC9VqA 31 | 0oU3kVZgLWRDKdNsphFVULJ8PGevc8mpHXI4gAi2T4TVoCtruDRQGuyqUm9/UfKW 32 | QlolZvo+b+SEF3J5C+CAXQ6zFxjEIsXSyS0qhX/LYAPPHFXPnURdVLPqs6oIzPcs 33 | 9uxheO9oriDkxrf1IQjka+E+ESImCkomr47+QQjC68KiIJmf6gAEMuxbHfLavwbd 34 | uX/R/AqJHhFmV013+8EO+xuMtMEh8hEw93mviJyw9Oa10323AaX16GfjWvBHdCKq 35 | Rw4VgjmiQ81/AoIBAQDKtoflJGjqwLx2jjSXno9XxYmai1UQzDPnhIkwmDPRHOLk 36 | 5xHJ6s75RWIR33TyaODCSIiESdbsTyHU6qSnNut6jGId+9oPStmGjmUgTySYXCgO 37 | c3zxCypY1s4mU6m85D4HU1BovuF504fEuwEqO3PpXGiH2r2FJk2TSJuPd4p9ugTe 38 | SVfWeo16Ttx1lMnoykUQ5uGqHz43fb0RMuWVYWzQD/XPj2ZtoRsubgaGr/lx8BzN 39 | A2mC1+53P6/Nn6+PEfDV9qGkx/moxvJCT6vBHI6lkAYdNOogZxHX18rpxSgomrdP 40 | IM0+KV4R2aGrxiFa6J7iMKTBOZo7CS+5d8T6ziolAoIBAQCFxkWds5WK14lZVR1+ 41 | btLBALG44pkI2R+yoitPNQ8X04cTALQ968WDFBKowgWEWBZkYO1JDqTKbI4YA7vX 42 | plkn7GOHrIYEMIBjr+8rfn5tpAM0SpR/2mSJsr4LqY6jQuaMPImXYTWXiG3kHthD 43 | SIIWsYKD/fF1Io4FtjYFDjTiX+d81huYbxOBF+0IqrHUl9a25iqRM5+7f6xnOSDy 44 | iiRRqlgWldwnk1fy8EazoTYMrjqBLw9aCABnorKIiXs/EYm1f8d2jeFmjvsPkyb2 45 | w/q/Eq9p+MnyBg5h8XcD3VQRT4hnT+gQEnl03B2RIIAfiOZmDFi0m9NdYs8rwqyr 46 | XM+TAoIBADEssO6R8QaOjH+pZDZh2LphuY4xUtrOuCoczNnaMZKxc2yCf6LFZYXi 47 | G2sawoqXKhqAPjis5knZfmDfWhLM5pVW8EtV/sLIkFWlmhMmplMoJq3bpTa0QpK1 48 | FLTo/ZvQJnGkMj9Hf6VXtFgsjfhhl8+0n0NUYUhD57BpNtFHejwgVynsiZf+FdTs 49 | ifWdxgsBgHTdanmjOOrOt4+cXB9f7IiSWVGW+ElaEdCuDQ10SIUAMRsn70C/UHke 50 | H89xOvOlx3Vh5Rrd0TML0FBqMUsYHkPnY/0NmkhZoeesJRaOFFwQBjjhLKZqm3OP 51 | ZR95Sa/Pke6ZJKOQhoyR+fK7v+vE+cs= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Message/HTTPResponse.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | import NIOHTTP1 3 | 4 | /// An HTTP response from a server back to the client. 5 | /// 6 | /// let httpRes = HTTPResponse(status: .ok) 7 | /// 8 | /// See `HTTPClient` and `HTTPServer`. 9 | public struct HTTPResponse: HTTPMessage { 10 | /// The HTTP version that corresponds to this response. 11 | public var version: HTTPVersion 12 | 13 | /// The HTTP response status. 14 | public var status: HTTPResponseStatus 15 | 16 | /// The header fields for this HTTP response. 17 | /// The `"Content-Length"` and `"Transfer-Encoding"` headers will be set automatically 18 | /// when the `body` property is mutated. 19 | public var headers: HTTPHeaders 20 | 21 | /// The `HTTPBody`. Updating this property will also update the associated transport headers. 22 | /// 23 | /// httpRes.body = HTTPBody(string: "Hello, world!") 24 | /// 25 | /// Also be sure to set this message's `contentType` property to a `MediaType` that correctly 26 | /// represents the `HTTPBody`. 27 | public var body: HTTPBody { 28 | didSet { self.headers.updateTransportHeaders(for: self.body) } 29 | } 30 | 31 | public var upgrader: HTTPServerProtocolUpgrader? 32 | 33 | /// Get and set `HTTPCookies` for this `HTTPResponse` 34 | /// This accesses the `"Set-Cookie"` header. 35 | public var cookies: HTTPCookies { 36 | get { return HTTPCookies.parse(setCookieHeaders: self.headers[.setCookie]) ?? [:] } 37 | set { newValue.serialize(into: &self) } 38 | } 39 | 40 | /// See `CustomStringConvertible` 41 | public var description: String { 42 | var desc: [String] = [] 43 | desc.append("HTTP/\(self.version.major).\(self.version.minor) \(self.status.code) \(self.status.reasonPhrase)") 44 | desc.append(self.headers.debugDescription) 45 | desc.append(self.body.description) 46 | return desc.joined(separator: "\n") 47 | } 48 | 49 | // MARK: Init 50 | 51 | /// Creates a new `HTTPResponse`. 52 | /// 53 | /// let httpRes = HTTPResponse(status: .ok) 54 | /// 55 | /// - parameters: 56 | /// - status: `HTTPResponseStatus` to use. This defaults to `HTTPResponseStatus.ok` 57 | /// - version: `HTTPVersion` of this response, should usually be (and defaults to) 1.1. 58 | /// - headers: `HTTPHeaders` to include with this response. 59 | /// Defaults to empty headers. 60 | /// The `"Content-Length"` and `"Transfer-Encoding"` headers will be set automatically. 61 | /// - body: `HTTPBody` for this response, defaults to an empty body. 62 | /// See `LosslessHTTPBodyRepresentable` for more information. 63 | public init( 64 | status: HTTPResponseStatus = .ok, 65 | version: HTTPVersion = .init(major: 1, minor: 1), 66 | headers: HTTPHeaders = .init(), 67 | body: HTTPBody = .empty 68 | ) { 69 | self.init( 70 | status: status, 71 | version: version, 72 | headersNoUpdate: headers, 73 | body: body.convertToHTTPBody() 74 | ) 75 | self.headers.updateTransportHeaders(for: self.body) 76 | } 77 | 78 | 79 | /// Internal init that creates a new `HTTPResponse` without sanitizing headers. 80 | public init( 81 | status: HTTPResponseStatus, 82 | version: HTTPVersion, 83 | headersNoUpdate headers: HTTPHeaders, 84 | body: HTTPBody 85 | ) { 86 | self.status = status 87 | self.version = version 88 | self.headers = headers 89 | self.body = body 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Server/HTTPRequestPartDecoder.swift: -------------------------------------------------------------------------------- 1 | import Logging 2 | 3 | final class HTTPRequestPartDecoder: ChannelInboundHandler, RemovableChannelHandler { 4 | typealias InboundIn = HTTPServerRequestPart 5 | typealias InboundOut = HTTPRequest 6 | 7 | /// Tracks current HTTP server state 8 | enum RequestState { 9 | /// Waiting for request headers 10 | case ready 11 | /// Waiting for the body 12 | /// This allows for performance optimization incase 13 | /// a body never comes 14 | case awaitingBody(HTTPRequestHead) 15 | // first chunk 16 | case awaitingEnd(HTTPRequestHead, ByteBuffer) 17 | /// Collecting streaming body 18 | case streamingBody(HTTPBody.Stream) 19 | } 20 | 21 | /// Current HTTP state. 22 | var requestState: RequestState 23 | 24 | /// Maximum body size allowed per request. 25 | private let maxBodySize: Int 26 | 27 | private let logger: Logger 28 | 29 | init(maxBodySize: Int) { 30 | self.maxBodySize = maxBodySize 31 | self.requestState = .ready 32 | self.logger = Logger(label: "http-kit.server-decoder") 33 | } 34 | 35 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 36 | assert(context.channel.eventLoop.inEventLoop) 37 | let part = self.unwrapInboundIn(data) 38 | self.logger.debug("got \(part)") 39 | switch part { 40 | case .head(let head): 41 | switch self.requestState { 42 | case .ready: self.requestState = .awaitingBody(head) 43 | default: assertionFailure("Unexpected state: \(self.requestState)") 44 | } 45 | case .body(let chunk): 46 | switch self.requestState { 47 | case .ready: assertionFailure("Unexpected state: \(self.requestState)") 48 | case .awaitingBody(let head): 49 | self.requestState = .awaitingEnd(head, chunk) 50 | case .awaitingEnd(let head, let bodyStart): 51 | let stream = HTTPBody.Stream(on: context.channel.eventLoop) 52 | self.requestState = .streamingBody(stream) 53 | self.fireRequestRead(head: head, body: .init(stream: stream), context: context) 54 | stream.write(.chunk(bodyStart)) 55 | stream.write(.chunk(chunk)) 56 | case .streamingBody(let stream): 57 | stream.write(.chunk(chunk)) 58 | } 59 | case .end(let tailHeaders): 60 | assert(tailHeaders == nil, "Tail headers are not supported.") 61 | switch self.requestState { 62 | case .ready: assertionFailure("Unexpected state: \(self.requestState)") 63 | case .awaitingBody(let head): 64 | self.fireRequestRead(head: head, body: .empty, context: context) 65 | case .awaitingEnd(let head, let chunk): 66 | self.fireRequestRead(head: head, body: .init(buffer: chunk), context: context) 67 | case .streamingBody(let stream): stream.write(.end) 68 | } 69 | self.requestState = .ready 70 | } 71 | } 72 | 73 | private func fireRequestRead(head: HTTPRequestHead, body: HTTPBody, context: ChannelHandlerContext) { 74 | var req = HTTPRequest( 75 | method: head.method, 76 | urlString: head.uri, 77 | version: head.version, 78 | headersNoUpdate: head.headers, 79 | body: body 80 | ) 81 | #warning("TODO: https://github.com/apple/swift-nio/issues/849") 82 | switch head.version.major { 83 | case 2: 84 | req.isKeepAlive = true 85 | default: 86 | req.isKeepAlive = head.isKeepAlive 87 | } 88 | context.fireChannelRead(self.wrapInboundOut(req)) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Server/HTTPResponsePartEncoder.swift: -------------------------------------------------------------------------------- 1 | final class HTTPResponsePartEncoder: ChannelOutboundHandler, RemovableChannelHandler { 2 | typealias OutboundIn = HTTPResponse 3 | typealias OutboundOut = HTTPServerResponsePart 4 | 5 | /// Optional server header. 6 | private let serverHeader: String? 7 | private let dateCache: RFC1123DateCache 8 | 9 | init(serverHeader: String?, dateCache: RFC1123DateCache) { 10 | self.serverHeader = serverHeader 11 | self.dateCache = dateCache 12 | } 13 | 14 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 15 | var res = self.unwrapOutboundIn(data) 16 | // add a RFC1123 timestamp to the Date header to make this 17 | // a valid request 18 | res.headers.add(name: "date", value: self.dateCache.currentTimestamp()) 19 | 20 | if let server = self.serverHeader { 21 | res.headers.add(name: "server", value: server) 22 | } 23 | 24 | // begin serializing 25 | context.write(wrapOutboundOut(.head(.init( 26 | version: res.version, 27 | status: res.status, 28 | headers: res.headers 29 | ))), promise: nil) 30 | 31 | if res.status == .noContent { 32 | // don't send bodies for 204 (no content) requests 33 | context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: promise) 34 | } else { 35 | switch res.body.storage { 36 | case .none: 37 | context.writeAndFlush(wrapOutboundOut(.end(nil)), promise: promise) 38 | case .buffer(let buffer): 39 | self.writeAndflush(buffer: buffer, context: context, promise: promise) 40 | case .string(let string): 41 | var buffer = context.channel.allocator.buffer(capacity: string.count) 42 | buffer.writeString(string) 43 | self.writeAndflush(buffer: buffer, context: context, promise: promise) 44 | case .staticString(let string): 45 | var buffer = context.channel.allocator.buffer(capacity: string.utf8CodeUnitCount) 46 | buffer.writeStaticString(string) 47 | self.writeAndflush(buffer: buffer, context: context, promise: promise) 48 | case .data(let data): 49 | var buffer = context.channel.allocator.buffer(capacity: data.count) 50 | #warning("TODO: use nio foundation compat") 51 | buffer.writeBytes(data) 52 | self.writeAndflush(buffer: buffer, context: context, promise: promise) 53 | case .dispatchData(let data): 54 | var buffer = context.channel.allocator.buffer(capacity: data.count) 55 | buffer.writeDispatchData(data) 56 | self.writeAndflush(buffer: buffer, context: context, promise: promise) 57 | case .stream(let stream): 58 | stream.read { result, stream in 59 | switch result { 60 | case .chunk(let buffer): 61 | context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(buffer))), promise: nil) 62 | case .end: 63 | promise?.succeed(()) 64 | context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) 65 | case .error(let error): 66 | promise?.fail(error) 67 | context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | /// Writes a `ByteBuffer` to the context. 75 | private func writeAndflush(buffer: ByteBuffer, context: ChannelHandlerContext, promise: EventLoopPromise?) { 76 | if buffer.readableBytes > 0 { 77 | _ = context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))) 78 | } 79 | context.writeAndFlush(wrapOutboundOut(.end(nil)), promise: promise) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Server/HTTPServerUpgradeHandler.swift: -------------------------------------------------------------------------------- 1 | final class HTTPServerUpgradeHandler: ChannelDuplexHandler, RemovableChannelHandler { 2 | typealias InboundIn = HTTPRequest 3 | typealias OutboundIn = HTTPResponse 4 | typealias OutboundOut = HTTPResponse 5 | 6 | 7 | private enum UpgradeState { 8 | case ready 9 | case pending(HTTPRequest, UpgradeBufferHandler) 10 | case upgraded 11 | } 12 | 13 | 14 | private var upgradeState: UpgradeState 15 | let httpRequestDecoder: ByteToMessageHandler 16 | let otherHTTPHandlers: [RemovableChannelHandler] 17 | 18 | init( 19 | httpRequestDecoder: ByteToMessageHandler, 20 | otherHTTPHandlers: [RemovableChannelHandler] 21 | ) { 22 | self.upgradeState = .ready 23 | self.httpRequestDecoder = httpRequestDecoder 24 | self.otherHTTPHandlers = otherHTTPHandlers 25 | } 26 | 27 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 28 | let req = self.unwrapInboundIn(data) 29 | 30 | // check if request is upgrade 31 | let connectionHeaders = Set(req.headers[canonicalForm: "connection"].map { $0.lowercased() }) 32 | if connectionHeaders.contains("upgrade") { 33 | // remove http decoder 34 | let buffer = UpgradeBufferHandler() 35 | _ = context.channel.pipeline.addHandler(buffer, position: .after(self.httpRequestDecoder)).flatMap { 36 | return context.channel.pipeline.removeHandler(self.httpRequestDecoder) 37 | } 38 | self.upgradeState = .pending(req, buffer) 39 | } 40 | 41 | context.fireChannelRead(data) 42 | } 43 | 44 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 45 | let res = self.unwrapOutboundIn(data) 46 | 47 | context.write(self.wrapOutboundOut(res), promise: promise) 48 | 49 | // check upgrade 50 | switch self.upgradeState { 51 | case .pending(let req, let buffer): 52 | if res.status == .switchingProtocols, let upgrader = res.upgrader { 53 | // do upgrade 54 | let handlers: [RemovableChannelHandler] = [self] + self.otherHTTPHandlers 55 | _ = EventLoopFuture.andAllComplete(handlers.map { handler in 56 | return context.pipeline.removeHandler(handler) 57 | }, on: context.eventLoop).flatMap { _ in 58 | return upgrader.upgrade( 59 | context: context, 60 | upgradeRequest: .init( 61 | version: req.version, 62 | method: req.method, 63 | uri: req.urlString 64 | ) 65 | ) 66 | }.flatMap { 67 | return context.pipeline.removeHandler(buffer) 68 | } 69 | self.upgradeState = .upgraded 70 | } else { 71 | // reset handlers 72 | self.upgradeState = .ready 73 | _ = context.channel.pipeline.addHandler(self.httpRequestDecoder, position: .after(buffer)).flatMap { 74 | return context.channel.pipeline.removeHandler(buffer) 75 | } 76 | } 77 | case .ready: break 78 | case .upgraded: break 79 | } 80 | } 81 | } 82 | 83 | private final class UpgradeBufferHandler: ChannelInboundHandler, RemovableChannelHandler { 84 | typealias InboundIn = ByteBuffer 85 | 86 | var buffer: [ByteBuffer] 87 | 88 | init() { 89 | self.buffer = [] 90 | } 91 | 92 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 93 | let data = self.unwrapInboundIn(data) 94 | self.buffer.append(data) 95 | } 96 | 97 | func handlerRemoved(context: ChannelHandlerContext) { 98 | for data in self.buffer { 99 | context.fireChannelRead(NIOAny(data)) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Message/HTTPRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NIOHTTP1 4 | 5 | /// An HTTP request from a client to a server. 6 | /// 7 | /// let httpReq = HTTPRequest(method: .GET, url: "/hello") 8 | /// 9 | /// See `HTTPClient` and `HTTPServer`. 10 | public struct HTTPRequest: HTTPMessage { 11 | // MARK: Properties 12 | 13 | /// The HTTP method for this request. 14 | /// 15 | /// httpReq.method = .GET 16 | /// 17 | public var method: HTTPMethod 18 | 19 | /// The URL used on this request. 20 | public var url: URL { 21 | get { return URL(string: self.urlString) ?? .root } 22 | set { self.urlString = newValue.absoluteString } 23 | } 24 | 25 | /// The unparsed URL string. This is usually set through the `url` property. 26 | /// 27 | /// httpReq.urlString = "/welcome" 28 | /// 29 | public var urlString: String 30 | 31 | /// The version for this HTTP request. 32 | public var version: HTTPVersion 33 | 34 | /// The header fields for this HTTP request. 35 | /// The `"Content-Length"` and `"Transfer-Encoding"` headers will be set automatically 36 | /// when the `body` property is mutated. 37 | public var headers: HTTPHeaders 38 | 39 | /// The `HTTPBody`. Updating this property will also update the associated transport headers. 40 | /// 41 | /// httpReq.body = HTTPBody(string: "Hello, world!") 42 | /// 43 | /// Also be sure to set this message's `contentType` property to a `MediaType` that correctly 44 | /// represents the `HTTPBody`. 45 | public var body: HTTPBody { 46 | didSet { self.headers.updateTransportHeaders(for: self.body) } 47 | } 48 | 49 | public var isKeepAlive: Bool 50 | 51 | public var upgrader: HTTPClientProtocolUpgrader? 52 | 53 | /// Get and set `HTTPCookies` for this `HTTPRequest` 54 | /// This accesses the `"Cookie"` header. 55 | public var cookies: HTTPCookies { 56 | get { return headers.firstValue(name: .cookie).flatMap(HTTPCookies.parse) ?? [:] } 57 | set { newValue.serialize(into: &self) } 58 | } 59 | 60 | /// See `CustomStringConvertible` 61 | public var description: String { 62 | var desc: [String] = [] 63 | desc.append("\(self.method) \(self.url) HTTP/\(self.version.major).\(self.version.minor)") 64 | desc.append(self.headers.debugDescription) 65 | desc.append(self.body.description) 66 | return desc.joined(separator: "\n") 67 | } 68 | 69 | // MARK: Init 70 | 71 | /// Creates a new `HTTPRequest`. 72 | /// 73 | /// let httpReq = HTTPRequest(method: .GET, url: "/hello") 74 | /// 75 | /// - parameters: 76 | /// - method: `HTTPMethod` to use. This defaults to `HTTPMethod.GET`. 77 | /// - url: A `URLRepresentable` item that represents the request's URL. 78 | /// This defaults to `"/"`. 79 | /// - version: `HTTPVersion` of this request, should usually be (and defaults to) 1.1. 80 | /// - headers: `HTTPHeaders` to include with this request. 81 | /// Defaults to empty headers. 82 | /// The `"Content-Length"` and `"Transfer-Encoding"` headers will be set automatically. 83 | /// - body: `HTTPBody` for this request, defaults to an empty body. 84 | /// See `LosslessHTTPBodyRepresentable` for more information. 85 | public init( 86 | method: HTTPMethod = .GET, 87 | url: URLRepresentable = URL.root, 88 | version: HTTPVersion = .init(major: 1, minor: 1), 89 | headers: HTTPHeaders = .init(), 90 | body: HTTPBody = .empty 91 | ) { 92 | self.init( 93 | method: method, 94 | urlString: url.convertToURL()?.absoluteString ?? "/", 95 | version: version, 96 | headersNoUpdate: headers, 97 | body: body.convertToHTTPBody() 98 | ) 99 | self.headers.updateTransportHeaders(for: self.body) 100 | } 101 | 102 | /// Internal init that creates a new `HTTPRequest` without sanitizing headers. 103 | public init( 104 | method: HTTPMethod, 105 | urlString: String, 106 | version: HTTPVersion = .init(major: 1, minor: 1), 107 | headersNoUpdate headers: HTTPHeaders = .init(), 108 | body: HTTPBody = .init() 109 | ) { 110 | self.method = method 111 | self.urlString = urlString 112 | self.version = version 113 | self.headers = headers 114 | self.body = body 115 | self.isKeepAlive = true 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Tests/HTTPKitTests/HTTPClientTests.swift: -------------------------------------------------------------------------------- 1 | import HTTPKit 2 | import XCTest 3 | 4 | class HTTPClientTests: XCTestCase { 5 | func testHTTPBin418() throws { 6 | try testURL("http://httpbin.org/status/418", contains: "[ teapot ]") 7 | } 8 | 9 | func testHTTPBinRobots() throws { 10 | try testURL("http://httpbin.org/robots.txt", contains: "Disallow: /deny") 11 | } 12 | 13 | func testHTTPBinAnything() throws { 14 | try testURL("http://httpbin.org/anything", contains: "://httpbin.org/anything") 15 | } 16 | 17 | func testGoogleAPIsFCM() throws { 18 | try testURL("http://fcm.googleapis.com/fcm/send", contains: "Moved Temporarily") 19 | } 20 | 21 | func testExampleCom() throws { 22 | try testURL("http://example.com", contains: "Example Domain") 23 | } 24 | 25 | func testZombo() throws { 26 | try testURL("http://zombo.com", contains: "ZOMBO") 27 | } 28 | 29 | func testVaporWithTLS() throws { 30 | try testURL("https://vapor.codes", contains: "Server-side Swift") 31 | } 32 | 33 | func testGoogleWithTLS() throws { 34 | try testURL("https://www.google.com/search?q=vapor+swift", contains: "web framework") 35 | } 36 | 37 | func testSNIWebsite() throws { 38 | try testURL("https://chrismeller.com", contains: "Chris") 39 | } 40 | 41 | func testQuery() throws { 42 | try testURL("http://httpbin.org/get?foo=bar", contains: "bar") 43 | } 44 | 45 | func testClientDefaultConfig() throws { 46 | let client = HTTPClient(on: self.eventLoopGroup) 47 | let res = try client.send(HTTPRequest(method: .GET, url: "https://vapor.codes")).wait() 48 | XCTAssertEqual(res.status, .ok) 49 | } 50 | 51 | func testRemotePeer() throws { 52 | let client = HTTPClient(on: self.eventLoopGroup) 53 | let httpReq = HTTPRequest(method: .GET, url: "http://vapor.codes/") 54 | let httpRes = try client.send(httpReq).wait() 55 | // TODO: how to get access to channel? 56 | // XCTAssertEqual(httpRes.remotePeer(on: client.channel).port, 80) 57 | } 58 | 59 | func testUncleanShutdown() throws { 60 | let res = try HTTPClient( 61 | configuration: .init( 62 | tlsConfig: .forClient(certificateVerification: .none) 63 | ), 64 | on: self.eventLoopGroup 65 | ).send(HTTPRequest(method: .GET, url: "https://www.google.com/search?q=vapor")).wait() 66 | XCTAssertEqual(res.status, .ok) 67 | } 68 | 69 | func testClientProxyPlaintext() throws { 70 | let res = try HTTPClient( 71 | configuration: .init( 72 | proxy: .server(hostname: proxyHostname, port: 8888) 73 | ), 74 | on: self.eventLoopGroup 75 | ).send(HTTPRequest(method: .GET, url: "http://httpbin.org/anything")).wait() 76 | XCTAssertEqual(res.status, .ok) 77 | } 78 | 79 | func testClientProxyTLS() throws { 80 | let res = try HTTPClient( 81 | configuration: .init( 82 | tlsConfig: .forClient(certificateVerification: .none), 83 | proxy: .server(hostname: proxyHostname, port: 8888) 84 | ), 85 | on: self.eventLoopGroup 86 | ).send(HTTPRequest(method: .GET, url: "https://vapor.codes/")).wait() 87 | XCTAssertEqual(res.status, .ok) 88 | } 89 | 90 | var eventLoopGroup: EventLoopGroup! 91 | 92 | override func setUp() { 93 | self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 94 | } 95 | 96 | override func tearDown() { 97 | try! self.eventLoopGroup.syncShutdownGracefully() 98 | } 99 | } 100 | 101 | // MARK: Private 102 | 103 | #if DOCKER 104 | private let proxyHostname = "tinyproxy" 105 | #else 106 | private let proxyHostname = "127.0.0.1" 107 | #endif 108 | 109 | private func testURL(_ string: String, times: Int = 3, contains: String) throws { 110 | try testURL(string, times: times) { res in 111 | let string = String(data: res.body.data ?? Data(), encoding: .ascii) ?? "" 112 | if string.contains(contains) != true { 113 | throw TestError(string) 114 | } 115 | } 116 | } 117 | 118 | struct TestError: Error { 119 | let string: String 120 | init(_ string: String) { 121 | self.string = string 122 | } 123 | } 124 | 125 | private func testURL( 126 | _ string: String, 127 | times: Int = 3, 128 | check: (HTTPResponse) throws -> () 129 | ) throws { 130 | let worker = MultiThreadedEventLoopGroup(numberOfThreads: 1) 131 | for _ in 0.. () 12 | ) throws -> HTTPRequest { 13 | var req = HTTPRequest(method: method, url: url, version: version, headers: headers, body: body) 14 | let upgrader = WebSocketClientUpgrader(upgradePipelineHandler: { channel, res in 15 | let webSocket = WebSocket(channel: channel, mode: .client) 16 | onUpgrade(webSocket) 17 | return channel.pipeline.add(webSocket: webSocket) 18 | }) 19 | for (name, value) in upgrader.buildUpgradeRequest() { 20 | req.headers.add(name: name, value: value) 21 | } 22 | req.upgrader = upgrader 23 | return req 24 | } 25 | } 26 | 27 | /// Allows `HTTPClient` to be used to create `WebSocket` connections. 28 | /// 29 | /// let ws = try HTTPClient.webSocket(hostname: "echo.websocket.org", on: ...).wait() 30 | /// ws.onText { ws, text in 31 | /// print("server said: \(text)") 32 | /// } 33 | /// ws.send("Hello, world!") 34 | /// try ws.onClose.wait() 35 | /// 36 | //extension HTTPClient { 37 | // // MARK: Client Upgrade 38 | // 39 | // /// Performs an HTTP protocol upgrade to` WebSocket` protocol `HTTPClient`. 40 | // /// 41 | // /// let ws = try HTTPClient.webSocket(hostname: "echo.websocket.org", on: ...).wait() 42 | // /// ws.onText { ws, text in 43 | // /// print("server said: \(text)") 44 | // /// } 45 | // /// ws.send("Hello, world!") 46 | // /// try ws.onClose.wait() 47 | // /// 48 | // /// - parameters: 49 | // /// - scheme: Transport layer security to use, either tls or plainText. 50 | // /// - hostname: Remote server's hostname. 51 | // /// - port: Remote server's port, defaults to 80 for TCP and 443 for TLS. 52 | // /// - path: Path on remote server to connect to. 53 | // /// - headers: Additional HTTP headers are used to establish a connection. 54 | // /// - maxFrameSize: Maximum WebSocket frame size this client will accept. 55 | // /// - worker: `Worker` to perform async work on. 56 | // /// - returns: A `Future` containing the connected `WebSocket`. 57 | // public static func webSocket( 58 | // scheme: HTTPScheme = .ws, 59 | // hostname: String, 60 | // port: Int? = nil, 61 | // path: String = "/", 62 | // headers: HTTPHeaders = .init(), 63 | // maxFrameSize: Int = 1 << 14, 64 | // on eventLoop: EventLoop 65 | // ) -> EventLoopFuture { 66 | // let upgrader = WebSocketClientUpgrader(hostname: hostname, path: path, headers: headers, maxFrameSize: maxFrameSize) 67 | // return HTTPClient.upgrade(scheme: scheme, hostname: hostname, port: port, upgrader: upgrader, on: eventLoop) 68 | // } 69 | //} 70 | 71 | // MARK: Private 72 | 73 | /// Private `HTTPClientProtocolUpgrader` for use with `HTTPClient.upgrade(...)`. 74 | public final class WebSocketClientUpgrader: HTTPClientProtocolUpgrader { 75 | /// Maximum frame size for decoder. 76 | private let maxFrameSize: Int 77 | 78 | private let upgradePipelineHandler: (Channel, HTTPResponseHead) -> EventLoopFuture 79 | 80 | /// Creates a new `WebSocketClientUpgrader`. 81 | public init( 82 | maxFrameSize: Int = 1 << 14, 83 | upgradePipelineHandler: @escaping (Channel, HTTPResponseHead) -> EventLoopFuture 84 | ) { 85 | self.maxFrameSize = maxFrameSize 86 | self.upgradePipelineHandler = upgradePipelineHandler 87 | } 88 | 89 | /// See `HTTPClientProtocolUpgrader`. 90 | public func buildUpgradeRequest() -> HTTPHeaders { 91 | var headers = HTTPHeaders() 92 | headers.add(name: .connection, value: "Upgrade") 93 | headers.add(name: .upgrade, value: "websocket") 94 | headers.add(name: .origin, value: "vapor/websocket") 95 | headers.add(name: .secWebSocketVersion, value: "13") 96 | let bytes: [UInt8] = [ 97 | .anyRandom, .anyRandom, .anyRandom, .anyRandom, 98 | .anyRandom, .anyRandom, .anyRandom, .anyRandom, 99 | .anyRandom, .anyRandom, .anyRandom, .anyRandom, 100 | .anyRandom, .anyRandom, .anyRandom, .anyRandom 101 | ] 102 | headers.add(name: .secWebSocketKey, value: Data(bytes).base64EncodedString()) 103 | return headers 104 | } 105 | 106 | /// See `HTTPClientProtocolUpgrader`. 107 | public func upgrade(context: ChannelHandlerContext, upgradeResponse: HTTPResponseHead) -> EventLoopFuture { 108 | return context.channel.pipeline.addHandlers([ 109 | WebSocketFrameEncoder(), 110 | ByteToMessageHandler(WebSocketFrameDecoder(maxFrameSize: maxFrameSize)) 111 | ], position: .first).flatMap { 112 | self.upgradePipelineHandler(context.channel, upgradeResponse) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Tests/HTTPKitTests/WebSocketTests.swift: -------------------------------------------------------------------------------- 1 | import HTTPKit 2 | import XCTest 3 | 4 | class WebSocketTests: XCTestCase { 5 | func testClient() throws { 6 | // ws://echo.websocket.org 7 | let client = HTTPClient(on: self.eventLoopGroup) 8 | 9 | let message = "Hello, world!" 10 | let promise = self.eventLoopGroup.next().makePromise(of: String.self) 11 | 12 | var req = HTTPRequest(url: "ws://echo.websocket.org/") 13 | req.webSocketUpgrade { ws in 14 | ws.onText { ws, text in 15 | promise.succeed(text) 16 | ws.close(code: .normalClosure) 17 | } 18 | ws.send(text: message) 19 | } 20 | do { 21 | let res = try client.send(req).wait() 22 | XCTAssertEqual(res.status, .switchingProtocols) 23 | } catch { 24 | promise.fail(error) 25 | } 26 | try XCTAssertEqual(promise.futureResult.wait(), message) 27 | } 28 | 29 | func testClientTLS() throws { 30 | // wss://echo.websocket.org 31 | let client = HTTPClient( 32 | configuration: .init( 33 | tlsConfig: .forClient(certificateVerification: .none) 34 | ), 35 | on: self.eventLoopGroup 36 | ) 37 | 38 | let message = "Hello, world!" 39 | let promise = self.eventLoopGroup.next().makePromise(of: String.self) 40 | 41 | var req = HTTPRequest(url: "wss://echo.websocket.org/") 42 | req.webSocketUpgrade { ws in 43 | ws.onText { ws, text in 44 | promise.succeed(text) 45 | ws.close(code: .normalClosure) 46 | } 47 | ws.send(text: message) 48 | } 49 | do { 50 | let res = try client.send(req).wait() 51 | XCTAssertEqual(res.status, .switchingProtocols) 52 | } catch { 53 | promise.fail(error) 54 | } 55 | try XCTAssertEqual(promise.futureResult.wait(), message) 56 | } 57 | 58 | func testServer() throws { 59 | let delegate = WebSocketServerDelegate { ws, req in 60 | ws.send(text: req.url.path) 61 | ws.onText { ws, string in 62 | ws.send(text: string.reversed()) 63 | if string == "close" { 64 | ws.close() 65 | } 66 | } 67 | ws.onBinary { ws, data in 68 | print("data: \(data)") 69 | } 70 | ws.onCloseCode { code in 71 | print("code: \(code)") 72 | } 73 | ws.onClose.whenSuccess { 74 | print("closed") 75 | } 76 | } 77 | let server = HTTPServer( 78 | configuration: .init( 79 | hostname: "127.0.0.1", 80 | port: 8001 81 | ), 82 | on: self.eventLoopGroup 83 | ) 84 | try server.start(delegate: delegate).wait() 85 | try server.shutdown().wait() 86 | // uncomment to test websocket server 87 | // try server.onClose.wait() 88 | } 89 | 90 | 91 | func testServerContinuation() throws { 92 | let promise = self.eventLoopGroup.next().makePromise(of: String.self) 93 | let delegate = WebSocketServerDelegate { ws, req in 94 | ws.onText { ws, text in 95 | promise.succeed(text) 96 | } 97 | } 98 | let server = HTTPServer( 99 | configuration: .init( 100 | hostname: "127.0.0.1", 101 | port: 8002, 102 | supportVersions: [.one] 103 | ), 104 | on: self.eventLoopGroup 105 | ) 106 | try server.start(delegate: delegate).wait() 107 | let client = HTTPClient(on: self.eventLoopGroup) 108 | var req = HTTPRequest(url: "ws://127.0.0.1:8002/") 109 | req.webSocketUpgrade { ws in 110 | ws.send(raw: Array("Hello, ".utf8), opcode: .text, fin: false) 111 | ws.send(raw: Array("world".utf8), opcode: .continuation, fin: false) 112 | ws.send(raw: Array("!".utf8), opcode: .continuation) 113 | ws.close() 114 | } 115 | do { 116 | let res = try client.send(req).wait() 117 | XCTAssertEqual(res.status, .switchingProtocols) 118 | } catch { 119 | promise.fail(error) 120 | } 121 | try XCTAssertEqual(promise.futureResult.wait(), "Hello, world!") 122 | try server.shutdown().wait() 123 | } 124 | 125 | 126 | var eventLoopGroup: EventLoopGroup! 127 | 128 | override func setUp() { 129 | self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 130 | } 131 | 132 | override func tearDown() { 133 | try! self.eventLoopGroup.syncShutdownGracefully() 134 | } 135 | } 136 | 137 | // MARK: Private 138 | 139 | private struct WebSocketServerDelegate: HTTPServerDelegate { 140 | let onUpgrade: (WebSocket, HTTPRequest) -> () 141 | init(onUpgrade: @escaping (WebSocket, HTTPRequest) -> ()) { 142 | self.onUpgrade = onUpgrade 143 | } 144 | 145 | func respond(to req: HTTPRequest, on channel: Channel) -> EventLoopFuture { 146 | guard req.isRequestingUpgrade(to: "websocket") else { 147 | return channel.eventLoop.makeFailedFuture(CookieError()) 148 | } 149 | 150 | return req.makeWebSocketUpgradeResponse(on: channel) { ws in 151 | self.onUpgrade(ws, req) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Sources/HTTPKit/WebSocket/WebSocketHandler.swift: -------------------------------------------------------------------------------- 1 | extension ChannelPipeline { 2 | /// Adds the supplied `WebSocket` to this `ChannelPipeline`. 3 | public func add(webSocket: WebSocket) -> EventLoopFuture { 4 | let handler = WebSocketHandler(webSocket: webSocket) 5 | return self.addHandler(handler) 6 | } 7 | } 8 | 9 | // MARK: Private 10 | 11 | /// Decodes `WebSocketFrame`s, forwarding to a `WebSocket`. 12 | private final class WebSocketHandler: ChannelInboundHandler { 13 | /// See `ChannelInboundHandler`. 14 | typealias InboundIn = WebSocketFrame 15 | 16 | /// See `ChannelInboundHandler`. 17 | typealias OutboundOut = WebSocketFrame 18 | 19 | /// `WebSocket` to handle the incoming events. 20 | private var webSocket: WebSocket 21 | 22 | /// Current frame sequence. 23 | private var frameSequence: WebSocketFrameSequence? 24 | 25 | /// Creates a new `WebSocketEventDecoder` 26 | init(webSocket: WebSocket) { 27 | self.webSocket = webSocket 28 | } 29 | 30 | /// See `ChannelInboundHandler`. 31 | func channelActive(context: ChannelHandlerContext) { 32 | // connected 33 | } 34 | 35 | /// See `ChannelInboundHandler`. 36 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 37 | var frame = self.unwrapInboundIn(data) 38 | switch frame.opcode { 39 | case .connectionClose: self.receivedClose(context: context, frame: frame) 40 | case .ping: 41 | if !frame.fin { 42 | closeOnError(context: context) // control frames can't be fragmented it should be final 43 | } else { 44 | pong(context: context, frame: frame) 45 | } 46 | case .text, .binary: 47 | // create a new frame sequence or use existing 48 | var frameSequence: WebSocketFrameSequence 49 | if let existing = self.frameSequence { 50 | frameSequence = existing 51 | } else { 52 | frameSequence = WebSocketFrameSequence(type: frame.opcode) 53 | } 54 | // append this frame and update the sequence 55 | frameSequence.append(frame) 56 | self.frameSequence = frameSequence 57 | case .continuation: 58 | // we must have an existing sequence 59 | if var frameSequence = self.frameSequence { 60 | // append this frame and update 61 | frameSequence.append(frame) 62 | self.frameSequence = frameSequence 63 | } else { 64 | self.closeOnError(context: context) 65 | } 66 | default: 67 | // We ignore all other frames. 68 | break 69 | } 70 | 71 | // if this frame was final and we have a non-nil frame sequence, 72 | // output it to the websocket and clear storage 73 | if var frameSequence = self.frameSequence, frame.fin { 74 | switch frameSequence.type { 75 | case .binary: 76 | #warning("TODO: pass buffered results") 77 | // webSocket.onBinaryCallback(webSocket, frameSequence.binaryBuffer?.readBytes(length: frameSequence.binaryBuffer?.readableBytes ?? 0) ?? []) 78 | case .text: webSocket.onTextCallback(webSocket, frameSequence.textBuffer) 79 | default: break 80 | } 81 | self.frameSequence = nil 82 | } 83 | } 84 | 85 | /// See `ChannelInboundHandler`. 86 | func errorCaught(context: ChannelHandlerContext, error: Error) { 87 | webSocket.onErrorCallback(webSocket, error) 88 | } 89 | 90 | /// Closes gracefully. 91 | private func receivedClose(context: ChannelHandlerContext, frame: WebSocketFrame) { 92 | /// Parse the close frame. 93 | var data = frame.unmaskedData 94 | if let closeCode = data.readInteger(as: UInt16.self) 95 | .map(Int.init) 96 | .flatMap(WebSocketErrorCode.init(codeNumber:)) 97 | { 98 | webSocket.onCloseCodeCallback(closeCode) 99 | } 100 | 101 | // Handle a received close frame. In websockets, we're just going to send the close 102 | // frame and then close, unless we already sent our own close frame. 103 | if webSocket.isClosed { 104 | // Cool, we started the close and were waiting for the user. We're done. 105 | context.close(promise: nil) 106 | } else { 107 | // This is an unsolicited close. We're going to send a response frame and 108 | // then, when we've sent it, close up shop. We should send back the close code the remote 109 | // peer sent us, unless they didn't send one at all. 110 | let closeFrame = WebSocketFrame(fin: true, opcode: .connectionClose, data: data) 111 | _ = context.writeAndFlush(wrapOutboundOut(closeFrame)).whenComplete { _ in 112 | _ = context.close(promise: nil) 113 | } 114 | } 115 | } 116 | 117 | /// Sends a pong frame in response to ping. 118 | private func pong(context: ChannelHandlerContext, frame: WebSocketFrame) { 119 | let pongFrame = WebSocketFrame( 120 | fin: true, 121 | opcode: .pong, 122 | maskKey: webSocket.mode.makeMaskKey(), 123 | data: frame.data 124 | ) 125 | context.writeAndFlush(self.wrapOutboundOut(pongFrame), promise: nil) 126 | } 127 | 128 | /// Closes the connection with error frame. 129 | private func closeOnError(context: ChannelHandlerContext) { 130 | // We have hit an error, we want to close. We do that by sending a close frame and then 131 | // shutting down the write side of the connection. 132 | var data = context.channel.allocator.buffer(capacity: 2) 133 | let error = WebSocketErrorCode.protocolError 134 | data.write(webSocketErrorCode: error) 135 | let frame = WebSocketFrame( 136 | fin: true, 137 | opcode: .connectionClose, 138 | maskKey: webSocket.mode.makeMaskKey(), 139 | data: data 140 | ) 141 | 142 | _ = context.writeAndFlush(self.wrapOutboundOut(frame)).flatMap { 143 | context.close(mode: .output) 144 | } 145 | self.webSocket.isClosed = true 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Utilities/RFC1123.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | import Foundation 7 | import NIO 8 | 9 | /// An internal helper that formats cookie dates as RFC1123 10 | private final class RFC1123 { 11 | /// Thread-specific RFC1123 12 | private static let thread: ThreadSpecificVariable = .init() 13 | 14 | /// A static RFC1123 helper instance 15 | static var shared: RFC1123 { 16 | if let existing = thread.currentValue { 17 | return existing 18 | } else { 19 | let new = RFC1123() 20 | thread.currentValue = new 21 | return new 22 | } 23 | } 24 | 25 | /// The RFC1123 formatter 26 | private let formatter: DateFormatter 27 | 28 | /// Creates a new RFC1123 helper 29 | private init() { 30 | let formatter = DateFormatter() 31 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 32 | formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss z" 33 | self.formatter = formatter 34 | } 35 | 36 | func string(from date: Date) -> String { 37 | return formatter.string(from: date) 38 | } 39 | 40 | func date(from string: String) -> Date? { 41 | return formatter.date(from: string) 42 | } 43 | } 44 | 45 | extension Date { 46 | /// Formats a `Date` as RFC1123 47 | public var rfc1123: String { 48 | return RFC1123.shared.string(from: self) 49 | } 50 | 51 | /// Creates a `Date` from an RFC1123 string 52 | public init?(rfc1123: String) { 53 | guard let date = RFC1123.shared.date(from: rfc1123) else { 54 | return nil 55 | } 56 | 57 | self = date 58 | } 59 | } 60 | 61 | /// Performant method for generating RFC1123 date headers. 62 | internal final class RFC1123DateCache { 63 | static func eventLoop(_ eventLoop: EventLoop) -> RFC1123DateCache { 64 | assert(eventLoop.inEventLoop) 65 | 66 | if let existing = thread.currentValue { 67 | return existing 68 | } else { 69 | let new = RFC1123DateCache() 70 | let fracSeconds = 1.0 - Date().timeIntervalSince1970.truncatingRemainder(dividingBy: 1) 71 | let msToNextSecond = Int64(fracSeconds * 1000) + 1 72 | eventLoop.scheduleRepeatedTask(initialDelay: .milliseconds(msToNextSecond), delay: .seconds(1)) { task in 73 | new.updateTimestamp() 74 | } 75 | thread.currentValue = new 76 | return new 77 | } 78 | } 79 | 80 | /// Thread-specific RFC1123 81 | private static let thread: ThreadSpecificVariable = .init() 82 | 83 | /// Currently cached time components. 84 | private var cachedTimeComponents: (key: time_t, components: tm)? 85 | 86 | /// Currently cached timestamp. 87 | private var timestamp: String 88 | 89 | /// Creates a new `RFC1123DateCache`. 90 | private init() { 91 | self.timestamp = "" 92 | self.updateTimestamp() 93 | } 94 | 95 | func currentTimestamp() -> String { 96 | return self.timestamp 97 | } 98 | 99 | /// Updates the current RFC 1123 date string. 100 | func updateTimestamp() { 101 | // get the current time 102 | var date = time(nil) 103 | 104 | // generate a key used for caching 105 | // this key is a unique id for each day 106 | let key = date / secondsInDay 107 | 108 | // get time components 109 | let dateComponents: tm 110 | if let cached = self.cachedTimeComponents, cached.key == key { 111 | dateComponents = cached.components 112 | } else { 113 | var tc = tm.init() 114 | gmtime_r(&date, &tc) 115 | dateComponents = tc 116 | self.cachedTimeComponents = (key: key, components: tc) 117 | } 118 | 119 | // parse components 120 | let year: Int = numericCast(dateComponents.tm_year) + 1900 // years since 1900 121 | let month: Int = numericCast(dateComponents.tm_mon) // months since January [0-11] 122 | let monthDay: Int = numericCast(dateComponents.tm_mday) // day of the month [1-31] 123 | let weekDay: Int = numericCast(dateComponents.tm_wday) // days since Sunday [0-6] 124 | 125 | // get basic time info 126 | let t: Int = date % secondsInDay 127 | let hours: Int = numericCast(t / 3600) 128 | let minutes: Int = numericCast((t / 60) % 60) 129 | let seconds: Int = numericCast(t % 60) 130 | 131 | // generate the RFC 1123 formatted string 132 | var rfc1123 = "" 133 | rfc1123.reserveCapacity(30) 134 | rfc1123.append(dayNames[weekDay]) 135 | rfc1123.append(", ") 136 | rfc1123.append(stringNumbers[monthDay]) 137 | rfc1123.append(" ") 138 | rfc1123.append(monthNames[month]) 139 | rfc1123.append(" ") 140 | rfc1123.append(stringNumbers[year / 100]) 141 | rfc1123.append(stringNumbers[year % 100]) 142 | rfc1123.append(" ") 143 | rfc1123.append(stringNumbers[hours]) 144 | rfc1123.append(":") 145 | rfc1123.append(stringNumbers[minutes]) 146 | rfc1123.append(":") 147 | rfc1123.append(stringNumbers[seconds]) 148 | rfc1123.append(" GMT") 149 | 150 | // cache the new timestamp 151 | self.timestamp = rfc1123 152 | } 153 | } 154 | 155 | // MARK: Private 156 | 157 | private let dayNames = [ 158 | "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" 159 | ] 160 | 161 | private let monthNames = [ 162 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 163 | ] 164 | 165 | private let stringNumbers = [ 166 | "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", 167 | "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", 168 | "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", 169 | "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", 170 | "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", 171 | "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", 172 | "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", 173 | "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", 174 | "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", 175 | "90", "91", "92", "93", "94", "95", "96", "97", "98", "99" 176 | ] 177 | 178 | private let secondsInDay = 60 * 60 * 24 179 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Client/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | import NIOSSL 3 | import NIOHTTP1 4 | 5 | public final class HTTPClient { 6 | /// Configuration options for `HTTPClient`. 7 | public struct Configuration { 8 | public var tlsConfig: TLSConfiguration? 9 | 10 | /// The timeout that will apply to the connection attempt. 11 | public var connectTimeout: TimeAmount 12 | 13 | public var proxy: Proxy 14 | 15 | /// Optional closure, which fires when a networking error is caught. 16 | public var errorHandler: (Error) -> () 17 | 18 | /// Creates a new `HTTPClientConfig`. 19 | /// 20 | public init( 21 | tlsConfig: TLSConfiguration? = nil, 22 | connectTimeout: TimeAmount = TimeAmount.seconds(10), 23 | proxy: Proxy = .none, 24 | errorHandler: @escaping (Error) -> () = { _ in } 25 | ) { 26 | self.tlsConfig = tlsConfig 27 | self.connectTimeout = connectTimeout 28 | self.proxy = proxy 29 | self.errorHandler = errorHandler 30 | } 31 | } 32 | 33 | public struct Proxy { 34 | enum Storage { 35 | case none 36 | case server(hostname: String, port: Int) 37 | } 38 | 39 | public static var none: Proxy { 40 | return .init(storage: .none) 41 | } 42 | 43 | public static func server(url: URLRepresentable) -> Proxy? { 44 | guard let url = url.convertToURL() else { 45 | return nil 46 | } 47 | guard let hostname = url.host else { 48 | return nil 49 | } 50 | return .server(hostname: hostname, port: url.port ?? 80) 51 | } 52 | 53 | public static func server(hostname: String, port: Int) -> Proxy { 54 | return .init(storage: .server(hostname: hostname, port: port)) 55 | } 56 | 57 | var storage: Storage 58 | } 59 | 60 | 61 | public let configuration: Configuration 62 | 63 | public let eventLoopGroup: EventLoopGroup 64 | 65 | public init(configuration: Configuration = .init(), on eventLoopGroup: EventLoopGroup) { 66 | self.configuration = configuration 67 | self.eventLoopGroup = eventLoopGroup 68 | } 69 | 70 | public func send(_ request: HTTPRequest) -> EventLoopFuture { 71 | let hostname = request.url.host ?? "" 72 | let defaultSchemePort = request.url.scheme == "https" ? 443 : 80 73 | let port = request.url.port ?? defaultSchemePort 74 | let tlsConfig: TLSConfiguration? 75 | switch request.url.scheme { 76 | case "https": 77 | tlsConfig = self.configuration.tlsConfig ?? .forClient() 78 | default: 79 | tlsConfig = nil 80 | } 81 | let eventLoop = self.eventLoopGroup.next() 82 | 83 | let bootstrap = ClientBootstrap(group: eventLoop) 84 | .connectTimeout(self.configuration.connectTimeout) 85 | .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) 86 | .channelInitializer 87 | { channel in 88 | var handlers: [(String, ChannelHandler)] = [] 89 | var httpHandlerNames: [String] = [] 90 | 91 | switch self.configuration.proxy.storage { 92 | case .none: 93 | if let tlsConfig = tlsConfig { 94 | let sslContext = try! NIOSSLContext(configuration: tlsConfig) 95 | let tlsHandler = try! NIOSSLClientHandler( 96 | context: sslContext, 97 | serverHostname: hostname.isIPAddress() ? nil : hostname 98 | ) 99 | handlers.append(("tls", tlsHandler)) 100 | } 101 | case .server: 102 | // tls will be set up after connect 103 | break 104 | } 105 | 106 | let httpReqEncoder = HTTPRequestEncoder() 107 | handlers.append(("http-encoder", httpReqEncoder)) 108 | httpHandlerNames.append("http-encoder") 109 | 110 | let httpResDecoder = ByteToMessageHandler(HTTPResponseDecoder()) 111 | handlers.append(("http-decoder", httpResDecoder)) 112 | httpHandlerNames.append("http-decoder") 113 | 114 | switch self.configuration.proxy.storage { 115 | case .none: break 116 | case .server: 117 | let proxy = HTTPClientProxyHandler(hostname: hostname, port: port) { context in 118 | // re-add HTTPDecoder since it may consider the connection to be closed 119 | _ = context.pipeline.removeHandler(name: "http-decoder") 120 | _ = context.pipeline.addHandler( 121 | ByteToMessageHandler(HTTPResponseDecoder()), 122 | name: "http-decoder", 123 | position: .after(httpReqEncoder) 124 | ) 125 | 126 | // if necessary, add TLS handlers 127 | if let tlsConfig = tlsConfig { 128 | let sslContext = try! NIOSSLContext(configuration: tlsConfig) 129 | let tlsHandler = try! NIOSSLClientHandler( 130 | context: sslContext, 131 | serverHostname: hostname.isIPAddress() ? nil : hostname 132 | ) 133 | _ = context.pipeline.addHandler(tlsHandler, position: .first) 134 | } 135 | } 136 | handlers.append(("http-proxy", proxy)) 137 | } 138 | 139 | let clientResDecoder = HTTPClientResponseDecoder() 140 | handlers.append(("client-decoder", clientResDecoder)) 141 | httpHandlerNames.append("client-decoder") 142 | 143 | // When port is different from the default port for the scheme 144 | // it must be explicitly specified in the `Host` HTTP header. 145 | // See https://tools.ietf.org/html/rfc2616#section-14.23 146 | let clientReqEncoder: HTTPClientRequestEncoder 147 | if port == defaultSchemePort { 148 | clientReqEncoder = HTTPClientRequestEncoder(hostHeaderValue: hostname) 149 | } else { 150 | clientReqEncoder = HTTPClientRequestEncoder(hostHeaderValue: "\(hostname):\(port)") 151 | } 152 | 153 | handlers.append(("client-encoder", clientReqEncoder)) 154 | httpHandlerNames.append("client-encoder") 155 | 156 | let handler = HTTPClientHandler() 157 | httpHandlerNames.append("client") 158 | 159 | let upgrader = HTTPClientUpgradeHandler(httpHandlerNames: httpHandlerNames) 160 | handlers.append(("upgrader", upgrader)) 161 | handlers.append(("client", handler)) 162 | return .andAllSucceed( 163 | handlers.map { channel.pipeline.addHandler($1, name: $0, position: .last) }, 164 | on: channel.eventLoop 165 | ) 166 | } 167 | let connectHostname: String 168 | let connectPort: Int 169 | switch self.configuration.proxy.storage { 170 | case .none: 171 | connectHostname = hostname 172 | connectPort = port 173 | case .server(let hostname, let port): 174 | connectHostname = hostname 175 | connectPort = port 176 | } 177 | 178 | return bootstrap.connect( 179 | host: connectHostname, 180 | port: connectPort 181 | ).flatMap { channel in 182 | let promise = channel.eventLoop.makePromise(of: HTTPResponse.self) 183 | let context = HTTPClientRequestContext(request: request, promise: promise) 184 | channel.write(context, promise: nil) 185 | return promise.futureResult.flatMap { response in 186 | if request.upgrader != nil { 187 | // upgrader is responsible for closing 188 | return channel.eventLoop.makeSucceededFuture(response) 189 | } else { 190 | return channel.close().map { response } 191 | } 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Sources/HTTPKit/WebSocket/WebSocket.swift: -------------------------------------------------------------------------------- 1 | /// Represents a client connected via WebSocket protocol. 2 | /// Use this to receive text/data frames and send responses. 3 | /// 4 | /// ws.onText { ws, string in 5 | /// ws.send(string.reversed()) 6 | /// } 7 | /// 8 | public final class WebSocket { 9 | /// Available WebSocket modes. Either `Client` or `Server`. 10 | public enum Mode { 11 | /// Uses socket in `Client` mode 12 | case client 13 | 14 | /// Uses socket in `Server` mode 15 | case server 16 | 17 | /// RFC 6455 Section 5.1 18 | /// To avoid confusing network intermediaries (such as intercepting proxies) and 19 | /// for security reasons that are further, a client MUST mask all frames that it 20 | /// sends to the server. 21 | /// The server MUST close the connection upon receiving a frame that is not masked. 22 | /// A server MUST NOT mask any frames that it sends to the client. 23 | /// A client MUST close a connection if it detects a masked frame. 24 | /// 25 | /// RFC 6455 Section 5.3 26 | /// The masking key is a 32-bit value chosen at random by the client. 27 | /// When preparing a masked frame, the client MUST pick a fresh masking 28 | /// key from the set of allowed 32-bit values. 29 | internal func makeMaskKey() -> WebSocketMaskingKey? { 30 | switch self { 31 | case .client: 32 | return WebSocketMaskingKey([.anyRandom, .anyRandom, .anyRandom, .anyRandom]) 33 | case .server: 34 | return nil 35 | } 36 | } 37 | } 38 | 39 | /// See `BasicWorker`. 40 | public var eventLoop: EventLoop { 41 | return channel.eventLoop 42 | } 43 | 44 | /// Outbound `WebSocketEventHandler`. 45 | private let channel: Channel 46 | 47 | /// `WebSocket` processing mode. 48 | internal let mode: Mode 49 | 50 | /// See `onText(...)`. 51 | var onTextCallback: (WebSocket, String) -> () 52 | 53 | /// See `onBinary(...)`. 54 | var onBinaryCallback: (WebSocket, [UInt8]) -> () 55 | 56 | /// See `onError(...)`. 57 | var onErrorCallback: (WebSocket, Error) -> () 58 | 59 | /// See `onCloseCode(...)`. 60 | var onCloseCodeCallback: (WebSocketErrorCode) -> () 61 | 62 | /// Creates a new `WebSocket` using the supplied `Channel` and `Mode`. 63 | /// Use `httpProtocolUpgrader(...)` to create a protocol upgrader that can create `WebSocket`s. 64 | public init(channel: Channel, mode: Mode) { 65 | self.channel = channel 66 | self.mode = mode 67 | self.isClosed = false 68 | self.onTextCallback = { _, _ in } 69 | self.onBinaryCallback = { _, _ in } 70 | self.onErrorCallback = { _, _ in } 71 | self.onCloseCodeCallback = { _ in } 72 | } 73 | 74 | // MARK: Receive 75 | 76 | /// Adds a callback to this `WebSocket` to receive text-formatted messages. 77 | /// 78 | /// ws.onText { ws, string in 79 | /// ws.send(string.reversed()) 80 | /// } 81 | /// 82 | /// Use `onBinary(_:)` to handle binary-formatted messages. 83 | /// 84 | /// - parameters: 85 | /// - callback: Closure to accept incoming text-formatted data. 86 | /// This will be called every time the connected client sends text. 87 | public func onText(_ callback: @escaping (WebSocket, String) -> ()) { 88 | onTextCallback = callback 89 | } 90 | 91 | /// Adds a callback to this `WebSocket` to receive binary-formatted messages. 92 | /// 93 | /// ws.onBinary { ws, data in 94 | /// print(data) 95 | /// } 96 | /// 97 | /// Use `onText(_:)` to handle text-formatted messages. 98 | /// 99 | /// - parameters: 100 | /// - callback: Closure to accept incoming binary-formatted data. 101 | /// This will be called every time the connected client sends binary-data. 102 | public func onBinary(_ callback: @escaping (WebSocket, [UInt8]) -> ()) { 103 | onBinaryCallback = callback 104 | } 105 | 106 | /// Adds a callback to this `WebSocket` to handle errors. 107 | /// 108 | /// ws.onError { ws, error in 109 | /// print(error) 110 | /// } 111 | /// 112 | /// - parameters: 113 | /// - callback: Closure to handle error's caught during this connection. 114 | public func onError(_ callback: @escaping (WebSocket, Error) -> ()) { 115 | onErrorCallback = callback 116 | } 117 | 118 | /// Adds a callback to this `WebSocket` to handle incoming close codes. 119 | /// 120 | /// ws.onCloseCode { closeCode in 121 | /// print(closeCode) 122 | /// } 123 | /// 124 | /// - parameters: 125 | /// - callback: Closure to handle received close codes. 126 | public func onCloseCode(_ callback: @escaping (WebSocketErrorCode) -> ()) { 127 | onCloseCodeCallback = callback 128 | } 129 | 130 | // MARK: Send 131 | 132 | /// Sends text-formatted data to the connected client. 133 | /// 134 | /// ws.onText { ws, string in 135 | /// ws.send(string.reversed()) 136 | /// } 137 | /// 138 | /// - parameters: 139 | /// - text: `String` to send as text-formatted data to the client. 140 | /// - promise: Optional `Promise` to complete when the send is finished. 141 | public func send(text: S, promise: EventLoopPromise? = nil) where S: Collection, S.Element == Character { 142 | let string = String(text) 143 | var buffer = channel.allocator.buffer(capacity: text.count) 144 | buffer.writeString(string) 145 | self.send(buffer, opcode: .text, fin: true, promise: promise) 146 | 147 | } 148 | 149 | /// Sends binary-formatted data to the connected client. 150 | /// 151 | /// ws.onText { ws, string in 152 | /// ws.send([0x68, 0x69]) 153 | /// } 154 | /// 155 | /// - parameters: 156 | /// - text: `Data` to send as binary-formatted data to the client. 157 | /// - promise: Optional `Promise` to complete when the send is finished. 158 | public func send(binary: [UInt8], promise: EventLoopPromise? = nil) { 159 | self.send(raw: binary, opcode: .binary, promise: promise) 160 | } 161 | 162 | /// Sends raw-data to the connected client using the supplied WebSocket opcode. 163 | /// 164 | /// // client will receive "Hello, world!" as one message 165 | /// ws.send(raw: "Hello, ", opcode: .text, fin: false) 166 | /// ws.send(raw: "world", opcode: .continuation, fin: false) 167 | /// ws.send(raw: "!", opcode: .continuation) 168 | /// 169 | /// - parameters: 170 | /// - data: `LosslessDataConvertible` to send to the client. 171 | /// - opcode: `WebSocketOpcode` indicating data format. Usually `.text` or `.binary`. 172 | /// - fin: If `false`, additional `.continuation` frames are expected. 173 | /// - promise: Optional `Promise` to complete when the send is finished. 174 | public func send(raw data: [UInt8], opcode: WebSocketOpcode, fin: Bool = true, promise: EventLoopPromise? = nil) { 175 | var buffer = channel.allocator.buffer(capacity: data.count) 176 | buffer.writeBytes(data) 177 | self.send(buffer, opcode: opcode, fin: fin, promise: promise) 178 | } 179 | 180 | // MARK: Close 181 | 182 | /// `true` if the `WebSocket` has been closed. 183 | public internal(set) var isClosed: Bool 184 | 185 | /// A `Future` that will be completed when the `WebSocket` closes. 186 | public var onClose: EventLoopFuture { 187 | return channel.closeFuture 188 | } 189 | 190 | /// Closes the `WebSocket`'s connection, disconnecting the client. 191 | /// 192 | /// - parameters: 193 | /// - code: Optional `WebSocketCloseCode` to send before closing the connection. 194 | /// If a code is provided, the WebSocket will wait until an acknowledgment is 195 | /// received from the server before actually closing the connection. 196 | public func close(code: WebSocketErrorCode? = nil) { 197 | guard !isClosed else { 198 | return 199 | } 200 | self.isClosed = true 201 | if let code = code { 202 | sendClose(code: code) 203 | } else { 204 | channel.close(promise: nil) 205 | } 206 | } 207 | 208 | // MARK: Private 209 | 210 | /// Private just send close code. 211 | private func sendClose(code: WebSocketErrorCode) { 212 | var buffer = channel.allocator.buffer(capacity: 2) 213 | buffer.write(webSocketErrorCode: code) 214 | send(buffer, opcode: .connectionClose, fin: true, promise: nil) 215 | } 216 | 217 | /// Private send that accepts a raw `WebSocketFrame`. 218 | private func send(_ buffer: ByteBuffer, opcode: WebSocketOpcode, fin: Bool, promise: EventLoopPromise?) { 219 | guard !self.isClosed else { return } 220 | let frame = WebSocketFrame( 221 | fin: fin, 222 | opcode: opcode, 223 | maskKey: mode.makeMaskKey(), 224 | data: buffer 225 | ) 226 | channel.writeAndFlush(frame, promise: promise) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Headers/HTTPHeaderValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a header value with optional parameter metadata. 4 | /// 5 | /// Parses a header string like `application/json; charset="utf8"`, into: 6 | /// 7 | /// - value: `"application/json"` 8 | /// - parameters: ["charset": "utf8"] 9 | /// 10 | /// Simplified format: 11 | /// 12 | /// headervalue := value *(";" parameter) 13 | /// ; Matching of media type and subtype 14 | /// ; is ALWAYS case-insensitive. 15 | /// 16 | /// value := token 17 | /// 18 | /// parameter := attribute "=" value 19 | /// 20 | /// attribute := token 21 | /// ; Matching of attributes 22 | /// ; is ALWAYS case-insensitive. 23 | /// 24 | /// token := 1* 26 | /// 27 | /// value := token 28 | /// ; token MAY be quoted 29 | /// 30 | /// tspecials := "(" / ")" / "<" / ">" / "@" / 31 | /// "," / ";" / ":" / "\" / <"> 32 | /// "/" / "[" / "]" / "?" / "=" 33 | /// ; Must be in quoted-string, 34 | /// ; to use within parameter values 35 | public struct HTTPHeaderValue: Codable { 36 | /// The `HeaderValue`'s main value. 37 | /// 38 | /// In the `HeaderValue` `"application/json; charset=utf8"`: 39 | /// 40 | /// - value: `"application/json"` 41 | public var value: String 42 | 43 | /// The `HeaderValue`'s metadata. Zero or more key/value pairs. 44 | /// 45 | /// In the `HeaderValue` `"application/json; charset=utf8"`: 46 | /// 47 | /// - parameters: ["charset": "utf8"] 48 | public var parameters: [CaseInsensitiveString: String] 49 | 50 | /// Creates a new `HeaderValue`. 51 | public init(_ value: String, parameters: [CaseInsensitiveString: String] = [:]) { 52 | self.value = value 53 | self.parameters = parameters 54 | } 55 | 56 | /// Initialize a `HTTPHeaderValue` from a Decoder. 57 | /// 58 | /// This will decode a `String` from the decoder and parse it to a `HTTPHeaderValue`. 59 | public init(from decoder: Decoder) throws { 60 | let container = try decoder.singleValueContainer() 61 | let string = try container.decode(String.self) 62 | guard let tempValue = HTTPHeaderValue.parse(string) else { 63 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid header value string") 64 | } 65 | self.parameters = tempValue.parameters 66 | self.value = tempValue.value 67 | } 68 | 69 | /// Encode a `HTTPHeaderValue` into an Encoder. 70 | /// 71 | /// This will encode the `HTTPHeaderValue` as a `String`. 72 | public func encode(to encoder: Encoder) throws { 73 | var container = encoder.singleValueContainer() 74 | try container.encode(self.serialize()) 75 | } 76 | 77 | /// Serializes this `HeaderValue` to a `String`. 78 | public func serialize() -> String { 79 | var string = "\(value)" 80 | for (key, val) in parameters { 81 | string += "; \(key)=\"\(val)\"" 82 | } 83 | return string 84 | } 85 | 86 | /// Parse a `HeaderValue` from a `String`. 87 | /// 88 | /// guard let headerValue = HTTPHeaderValue.parse("application/json; charset=utf8") else { ... } 89 | /// 90 | public static func parse(_ data: String) -> HTTPHeaderValue? { 91 | let data = data 92 | 93 | /// separate the zero or more parameters 94 | let parts = data.split(separator: ";", maxSplits: 1) 95 | 96 | /// there must be at least one part, the value 97 | guard let value = parts.first else { 98 | /// should never hit this 99 | return nil 100 | } 101 | 102 | /// get the remaining parameters string 103 | var remaining: Substring 104 | 105 | switch parts.count { 106 | case 1: 107 | /// no parameters, early exit 108 | return HTTPHeaderValue(String(value), parameters: [:]) 109 | case 2: remaining = parts[1] 110 | default: return nil 111 | } 112 | 113 | /// collect all of the parameters 114 | var parameters: [CaseInsensitiveString: String] = [:] 115 | 116 | /// loop over all parts after the value 117 | parse: while remaining.count > 0 { 118 | let semicolon = remaining.firstIndex(of: ";") 119 | let equals = remaining.firstIndex(of: "=") 120 | 121 | let key: Substring 122 | let val: Substring 123 | 124 | if equals == nil || (equals != nil && semicolon != nil && semicolon! < equals!) { 125 | /// parsing a single flag, without = 126 | key = remaining[remaining.startIndex..<(semicolon ?? remaining.endIndex)] 127 | val = .init() 128 | if let s = semicolon { 129 | remaining = remaining[remaining.index(after: s)...] 130 | } else { 131 | remaining = .init() 132 | } 133 | } else { 134 | /// parsing a normal key=value pair. 135 | /// parse the parameters by splitting on the `=` 136 | let parameterParts = remaining.split(separator: "=", maxSplits: 1) 137 | 138 | key = parameterParts[0] 139 | 140 | switch parameterParts.count { 141 | case 1: 142 | val = .init() 143 | remaining = .init() 144 | case 2: 145 | let trailing = parameterParts[1] 146 | 147 | if trailing.first == "\"" { 148 | /// find first unescaped quote 149 | var quoteIndex: String.Index? 150 | var escapedIndexes: [String.Index] = [] 151 | findQuote: for i in 1.. 0 { 172 | /// go reverse so that we can correctly remove multiple 173 | for escapeLoc in escapedIndexes.reversed() { 174 | valpart.remove(at: escapeLoc) 175 | } 176 | } 177 | 178 | val = valpart 179 | 180 | let rest = trailing[trailing.index(after: trailing.startIndex)...] 181 | if let nextSemicolon = rest.firstIndex(of: ";") { 182 | remaining = rest[rest.index(after: nextSemicolon)...] 183 | } else { 184 | remaining = .init() 185 | } 186 | } else { 187 | /// find first semicolon 188 | var semicolonOffset: String.Index? 189 | findSemicolon: for i in 0.. (String, Value)? { 30 | /// Parse `HeaderValue` or return nil. 31 | guard let header = HTTPHeaderValue.parse(data) else { 32 | return nil 33 | } 34 | 35 | /// Fetch name and value. 36 | var name: String 37 | var string: String 38 | 39 | let parts = header.value.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) 40 | switch parts.count { 41 | case 2: 42 | name = String(parts[0]).trimmingCharacters(in: .whitespaces) 43 | string = String(parts[1]).trimmingCharacters(in: .whitespaces) 44 | default: return nil 45 | } 46 | 47 | /// Fetch params. 48 | var expires: Date? 49 | var maxAge: Int? 50 | var domain: String? 51 | var path: String? 52 | var secure = false 53 | var httpOnly = false 54 | var sameSite: SameSitePolicy? 55 | 56 | for (key, val) in header.parameters { 57 | switch key { 58 | case "domain": domain = val 59 | case "path": path = val 60 | case "expires": expires = Date(rfc1123: val) 61 | case "httponly": httpOnly = true 62 | case "secure": secure = true 63 | case "max-age": maxAge = Int(val) ?? 0 64 | case "samesite": sameSite = SameSitePolicy(rawValue: val) 65 | default: break 66 | } 67 | } 68 | 69 | let value = Value( 70 | string: string, 71 | expires: expires, 72 | maxAge: maxAge, 73 | domain: domain, 74 | path: path, 75 | isSecure: secure, 76 | isHTTPOnly: httpOnly, 77 | sameSite: sameSite 78 | ) 79 | return (name, value) 80 | } 81 | 82 | // MARK: Properties 83 | 84 | /// The cookie's value. 85 | public var string: String 86 | 87 | /// The cookie's expiration date 88 | public var expires: Date? 89 | 90 | /// The maximum cookie age in seconds. 91 | public var maxAge: Int? 92 | 93 | /// The affected domain at which the cookie is active. 94 | public var domain: String? 95 | 96 | /// The path at which the cookie is active. 97 | public var path: String? 98 | 99 | /// Limits the cookie to secure connections. 100 | public var isSecure: Bool 101 | 102 | /// Does not expose the cookie over non-HTTP channels. 103 | public var isHTTPOnly: Bool 104 | 105 | /// A cookie which can only be sent in requests originating from the same origin as the target domain. 106 | /// 107 | /// This restriction mitigates attacks such as cross-site request forgery (XSRF). 108 | public var sameSite: SameSitePolicy? 109 | 110 | // MARK: Init 111 | 112 | /// Creates a new `HTTPCookieValue`. 113 | /// 114 | /// let cookie = HTTPCookieValue(string: "123") 115 | /// 116 | /// - parameters: 117 | /// - value: Value for this cookie. 118 | /// - expires: The cookie's expiration date. Defaults to `nil`. 119 | /// - maxAge: The maximum cookie age in seconds. Defaults to `nil`. 120 | /// - domain: The affected domain at which the cookie is active. Defaults to `nil`. 121 | /// - path: The path at which the cookie is active. Defaults to `"/"`. 122 | /// - isSecure: Limits the cookie to secure connections. Defaults to `false`. 123 | /// - isHTTPOnly: Does not expose the cookie over non-HTTP channels. Defaults to `false`. 124 | /// - sameSite: See `HTTPSameSitePolicy`. Defaults to `nil`. 125 | public init( 126 | string: String, 127 | expires: Date? = nil, 128 | maxAge: Int? = nil, 129 | domain: String? = nil, 130 | path: String? = "/", 131 | isSecure: Bool = false, 132 | isHTTPOnly: Bool = false, 133 | sameSite: SameSitePolicy? = nil 134 | ) { 135 | self.string = string 136 | self.expires = expires 137 | self.maxAge = maxAge 138 | self.domain = domain 139 | self.path = path 140 | self.isSecure = isSecure 141 | self.isHTTPOnly = isHTTPOnly 142 | self.sameSite = sameSite 143 | } 144 | 145 | /// See `ExpressibleByStringLiteral`. 146 | public init(stringLiteral value: String) { 147 | self.init(string: value) 148 | } 149 | 150 | // MARK: Methods 151 | 152 | /// Seriaizes an `HTTPCookie` to a `String`. 153 | public func serialize(name: String) -> String { 154 | var serialized = "\(name)=\(string)" 155 | 156 | if let expires = self.expires { 157 | serialized += "; Expires=\(expires.rfc1123)" 158 | } 159 | 160 | if let maxAge = self.maxAge { 161 | serialized += "; Max-Age=\(maxAge)" 162 | } 163 | 164 | if let domain = self.domain { 165 | serialized += "; Domain=\(domain)" 166 | } 167 | 168 | if let path = self.path { 169 | serialized += "; Path=\(path)" 170 | } 171 | 172 | if isSecure { 173 | serialized += "; Secure" 174 | } 175 | 176 | if isHTTPOnly { 177 | serialized += "; HttpOnly" 178 | } 179 | 180 | if let sameSite = self.sameSite { 181 | serialized += "; SameSite" 182 | switch sameSite { 183 | case .lax: 184 | serialized += "=Lax" 185 | case .strict: 186 | serialized += "=Strict" 187 | } 188 | } 189 | 190 | return serialized 191 | } 192 | } 193 | 194 | /// Internal storage. 195 | private var cookies: [String: Value] 196 | 197 | /// Creates an empty `HTTPCookies` 198 | public init() { 199 | self.cookies = [:] 200 | } 201 | 202 | // MARK: Parse 203 | 204 | /// Parses a `Request` cookie 205 | public static func parse(cookieHeader: String) -> HTTPCookies? { 206 | var cookies: HTTPCookies = [:] 207 | 208 | // cookies are sent separated by semicolons 209 | let tokens = cookieHeader.components(separatedBy: ";") 210 | 211 | for token in tokens { 212 | // If a single deserialization fails, the cookies are malformed 213 | guard let (name, value) = Value.parse(token) else { 214 | return nil 215 | } 216 | 217 | cookies[name] = value 218 | } 219 | 220 | return cookies 221 | } 222 | 223 | /// Parses a `Response` cookie 224 | public static func parse(setCookieHeaders: [String]) -> HTTPCookies? { 225 | var cookies: HTTPCookies = [:] 226 | 227 | for token in setCookieHeaders { 228 | // If a single deserialization fails, the cookies are malformed 229 | guard let (name, value) = Value.parse(token) else { 230 | return nil 231 | } 232 | 233 | cookies[name] = value 234 | } 235 | 236 | return cookies 237 | } 238 | 239 | /// See `ExpressibleByDictionaryLiteral`. 240 | public init(dictionaryLiteral elements: (String, Value)...) { 241 | var cookies: [String: Value] = [:] 242 | for (name, value) in elements { 243 | cookies[name] = value 244 | } 245 | self.cookies = cookies 246 | } 247 | 248 | // MARK: Serialize 249 | 250 | /// Seriaizes the `Cookies` for a `Request` 251 | public func serialize(into request: inout HTTPRequest) { 252 | guard !cookies.isEmpty else { 253 | request.headers.remove(name: .cookie) 254 | return 255 | } 256 | 257 | let cookie: String = cookies.map { (name, value) in 258 | return "\(name)=\(value.string)" 259 | }.joined(separator: "; ") 260 | 261 | request.headers.replaceOrAdd(name: .cookie, value: cookie) 262 | } 263 | 264 | /// Seriaizes the `Cookies` for a `Response` 265 | public func serialize(into response: inout HTTPResponse) { 266 | response.headers.remove(name: .setCookie) 267 | for (name, value) in cookies { 268 | response.headers.add(name: .setCookie, value: value.serialize(name: name)) 269 | } 270 | } 271 | 272 | // MARK: Access 273 | 274 | /// All cookies. 275 | public var all: [String: Value] { 276 | get { return cookies } 277 | set { cookies = newValue } 278 | } 279 | 280 | /// Access `HTTPCookies` by name 281 | public subscript(name: String) -> Value? { 282 | get { return cookies[name] } 283 | set { cookies[name] = newValue } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Message/HTTPBody.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Data 2 | import struct Foundation.DispatchData 3 | import NIOFoundationCompat 4 | 5 | /// Represents an `HTTPMessage`'s body. 6 | /// 7 | /// let body = HTTPBody(string: "Hello, world!") 8 | /// 9 | /// This can contain any data (streaming or static) and should match the message's `"Content-Type"` header. 10 | public struct HTTPBody: CustomStringConvertible, CustomDebugStringConvertible, ExpressibleByStringLiteral { 11 | public final class Stream { 12 | /// Supported types that can be sent and recieved from a `HTTPChunkedStream`. 13 | public enum Result { 14 | /// A normal data chunk. 15 | /// There will be 0 or more of these. 16 | case chunk(ByteBuffer) 17 | /// Indicates an error. 18 | /// There will be 0 or 1 of these. 0 if the stream closes cleanly. 19 | case error(Error) 20 | /// Indicates the stream has completed. 21 | /// There will be 0 or 1 of these. 0 if the stream errors. 22 | case end 23 | } 24 | 25 | /// Handles an incoming `HTTPChunkedStreamResult`. 26 | public typealias Handler = (Result, Stream) -> () 27 | 28 | /// See `BasicWorker`. 29 | public var eventLoop: EventLoop 30 | 31 | /// If `true`, this `HTTPChunkedStream` has already sent an `end` chunk. 32 | public private(set) var isClosed: Bool 33 | 34 | /// This stream's `HTTPChunkedHandler`, if one is set. 35 | private var handler: Handler? 36 | 37 | /// If a `handler` has not been set when `write(_:)` is called, this property 38 | /// is used to store the waiting data. 39 | private var buffer: [Result] 40 | 41 | /// Creates a new `HTTPChunkedStream`. 42 | /// 43 | /// - parameters: 44 | /// - worker: `Worker` to complete futures on. 45 | public init(on eventLoop: EventLoop) { 46 | self.eventLoop = eventLoop 47 | self.isClosed = false 48 | self.buffer = [] 49 | } 50 | 51 | /// Sets a handler for reading `HTTPChunkedStreamResult`s from the stream. 52 | /// 53 | /// chunkedStream.read { res, stream in 54 | /// print(res) // prints the chunk 55 | /// return .done(on: stream) // you can do async work or just return done 56 | /// } 57 | /// 58 | /// - parameters: 59 | /// - handler: `HTTPChunkedHandler` to use for receiving chunks from this stream. 60 | public func read(_ handler: @escaping Handler) { 61 | self.handler = handler 62 | for item in self.buffer { 63 | handler(item, self) 64 | } 65 | self.buffer = [] 66 | } 67 | 68 | /// Writes a `HTTPChunkedStreamResult` to the stream. 69 | /// 70 | /// try chunkedStream.write(.end).wait() 71 | /// 72 | /// You must wait for the returned `Future` to complete before writing additional data. 73 | /// 74 | /// - parameters: 75 | /// - chunk: A `HTTPChunkedStreamResult` to write to the stream. 76 | /// - returns: A `Future` that will be completed when the write was successful. 77 | /// You must wait for this future to complete before calling `write(_:)` again. 78 | public func write(_ chunk: Result) { 79 | if case .end = chunk { 80 | self.isClosed = true 81 | } 82 | 83 | if let handler = handler { 84 | handler(chunk, self) 85 | } else { 86 | self.buffer.append(chunk) 87 | } 88 | } 89 | 90 | /// Reads all `HTTPChunkedStreamResult`s from this stream until `end` is received. 91 | /// The output is combined into a single `Data`. 92 | /// 93 | /// let data = try stream.drain(max: ...).wait() 94 | /// print(data) // Data 95 | /// 96 | /// - parameters: 97 | /// - max: The maximum number of bytes to allow before throwing an error. 98 | /// Use this to prevent using excessive memory on your server. 99 | /// - returns: `Future` containing the collected `Data`. 100 | public func consume(max: Int) -> EventLoopFuture { 101 | let promise = eventLoop.makePromise(of: ByteBuffer.self) 102 | var data = ByteBufferAllocator().buffer(capacity: 0) 103 | self.read { chunk, stream in 104 | switch chunk { 105 | case .chunk(var buffer): 106 | if data.readableBytes + buffer.readableBytes >= max { 107 | let error = HTTPError(.maxBodySize) 108 | promise.fail(error) 109 | } else { 110 | data.writeBuffer(&buffer) 111 | } 112 | case .error(let error): promise.fail(error) 113 | case .end: promise.succeed(data) 114 | } 115 | } 116 | return promise.futureResult 117 | } 118 | 119 | /// See `HTTPBodyRepresentable`. 120 | public func convertToHTTPBody() -> HTTPBody { 121 | return .init(stream: self) 122 | } 123 | } 124 | 125 | /// The internal HTTP body storage enum. This is an implementation detail. 126 | internal enum Storage { 127 | /// Cases 128 | case none 129 | case buffer(ByteBuffer) 130 | case data(Data) 131 | case dispatchData(DispatchData) 132 | case staticString(StaticString) 133 | case stream(Stream) 134 | case string(String) 135 | } 136 | 137 | /// An empty `HTTPBody`. 138 | public static let empty: HTTPBody = .init() 139 | 140 | public var string: String? { 141 | switch self.storage { 142 | case .buffer(var buffer): return buffer.readString(length: buffer.readableBytes) 143 | case .data(let data): return String(decoding: data, as: UTF8.self) 144 | case .dispatchData(let dispatchData): return String(decoding: dispatchData, as: UTF8.self) 145 | case .staticString(let staticString): return staticString.description 146 | case .string(let string): return string 147 | default: return nil 148 | } 149 | } 150 | 151 | public var stream: Stream? { 152 | switch self.storage { 153 | case .stream(let stream): return stream 154 | default: return nil 155 | } 156 | } 157 | 158 | /// The size of the HTTP body's data. 159 | /// `nil` is a stream. 160 | public var count: Int? { 161 | switch self.storage { 162 | case .data(let data): return data.count 163 | case .dispatchData(let data): return data.count 164 | case .staticString(let staticString): return staticString.utf8CodeUnitCount 165 | case .string(let string): return string.utf8.count 166 | case .buffer(let buffer): return buffer.readableBytes 167 | case .stream: return nil 168 | case .none: return 0 169 | } 170 | } 171 | 172 | /// Returns static data if not streaming. 173 | public var data: Data? { 174 | switch self.storage { 175 | case .buffer(var buffer): return buffer.readData(length: buffer.readableBytes) 176 | case .data(let data): return data 177 | case .dispatchData(let dispatchData): return Data(dispatchData) 178 | case .staticString(let staticString): return Data(bytes: staticString.utf8Start, count: staticString.utf8CodeUnitCount) 179 | case .string(let string): return Data(string.utf8) 180 | case .stream: return nil 181 | case .none: return nil 182 | } 183 | } 184 | 185 | public var buffer: ByteBuffer? { 186 | switch self.storage { 187 | case .buffer(let buffer): return buffer 188 | case .data(let data): 189 | var buffer = ByteBufferAllocator().buffer(capacity: data.count) 190 | buffer.writeBytes(data) 191 | return buffer 192 | case .dispatchData(let dispatchData): 193 | var buffer = ByteBufferAllocator().buffer(capacity: dispatchData.count) 194 | buffer.writeDispatchData(dispatchData) 195 | return buffer 196 | case .staticString(let staticString): 197 | var buffer = ByteBufferAllocator().buffer(capacity: staticString.utf8CodeUnitCount) 198 | buffer.writeStaticString(staticString) 199 | return buffer 200 | case .string(let string): 201 | var buffer = ByteBufferAllocator().buffer(capacity: string.count) 202 | buffer.writeString(string) 203 | return buffer 204 | case .stream: return nil 205 | case .none: return nil 206 | } 207 | } 208 | 209 | /// See `CustomStringConvertible`. 210 | public var description: String { 211 | switch storage { 212 | case .data, .buffer, .dispatchData, .staticString, .string, .none: return debugDescription 213 | case .stream(let stream): 214 | guard !stream.isClosed else { 215 | return debugDescription 216 | } 217 | return "" 218 | } 219 | } 220 | 221 | /// See `CustomDebugStringConvertible`. 222 | public var debugDescription: String { 223 | switch storage { 224 | case .none: return "" 225 | case .buffer(let buffer): return buffer.getString(at: 0, length: buffer.readableBytes) ?? "n/a" 226 | case .data(let data): return String(data: data, encoding: .ascii) ?? "n/a" 227 | case .dispatchData(let data): return String(data: Data(data), encoding: .ascii) ?? "n/a" 228 | case .staticString(let string): return string.description 229 | case .string(let string): return string 230 | case .stream(let stream): 231 | guard !stream.isClosed else { 232 | return "" 233 | } 234 | do { 235 | var data = try stream.consume(max: maxDebugStreamingBodySize).wait() 236 | return data.readString(length: data.readableBytes) ?? "" 237 | } catch { 238 | return "" 239 | } 240 | } 241 | } 242 | 243 | internal var storage: Storage 244 | 245 | /// Creates an empty body. Useful for `GET` requests where HTTP bodies are forbidden. 246 | public init() { 247 | self.storage = .none 248 | } 249 | 250 | /// Create a new body wrapping `Data`. 251 | public init(data: Data) { 252 | storage = .data(data) 253 | } 254 | 255 | /// Create a new body wrapping `DispatchData`. 256 | public init(dispatchData: DispatchData) { 257 | storage = .dispatchData(dispatchData) 258 | } 259 | 260 | /// Create a new body from the UTF8 representation of a `StaticString`. 261 | public init(staticString: StaticString) { 262 | storage = .staticString(staticString) 263 | } 264 | 265 | /// Create a new body from the UTF8 representation of a `String`. 266 | public init(string: String) { 267 | self.storage = .string(string) 268 | } 269 | 270 | /// Create a new body from an `HTTPBodyStream`. 271 | public init(stream: Stream) { 272 | self.storage = .stream(stream) 273 | } 274 | 275 | /// Create a new body from a Swift NIO `ByteBuffer`. 276 | public init(buffer: ByteBuffer) { 277 | self.storage = .buffer(buffer) 278 | } 279 | 280 | /// `ExpressibleByStringLiteral` conformance. 281 | public init(stringLiteral value: String) { 282 | self.storage = .string(value) 283 | } 284 | 285 | /// Internal init. 286 | internal init(storage: Storage) { 287 | self.storage = storage 288 | } 289 | 290 | /// Consumes the body if it is a stream. Otherwise, returns the same value as the `data` property. 291 | /// 292 | /// let data = try httpRes.body.consumeData(max: 1_000_000, on: ...).wait() 293 | /// 294 | /// - parameters: 295 | /// - max: The maximum streaming body size to allow. 296 | /// This only applies to streaming bodies, like chunked streams. 297 | /// Defaults to 1MB. 298 | /// - eventLoop: The event loop to perform this async work on. 299 | public func consume(max: Int = 1_000_000, on eventLoop: EventLoop) -> EventLoopFuture { 300 | if let buffer = self.buffer { 301 | return eventLoop.makeSucceededFuture(buffer) 302 | } else if let stream = self.stream { 303 | return stream.consume(max: max) 304 | } else { 305 | let empty = ByteBufferAllocator().buffer(capacity: 0) 306 | return eventLoop.makeSucceededFuture(empty) 307 | } 308 | } 309 | 310 | /// See `LosslessHTTPBodyRepresentable`. 311 | public func convertToHTTPBody() -> HTTPBody { 312 | return self 313 | } 314 | } 315 | 316 | /// Maximum streaming body size to use for `debugPrint(_:)`. 317 | private let maxDebugStreamingBodySize: Int = 1_000_000 318 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Server/HTTPServer.swift: -------------------------------------------------------------------------------- 1 | import NIOTLS 2 | import NIO 3 | import NIOHTTP1 4 | import NIOHTTP2 5 | 6 | public enum HTTPVersionMajor: Equatable, Hashable { 7 | case one 8 | case two 9 | } 10 | 11 | /// Capable of responding to incoming `HTTPRequest`s. 12 | public protocol HTTPServerDelegate { 13 | /// Responds to an incoming `HTTPRequest`. 14 | /// 15 | /// - parameters: 16 | /// - req: Incoming `HTTPRequest` to respond to. 17 | /// - returns: Future `HTTPResponse` to send back. 18 | func respond(to req: HTTPRequest, on channel: Channel) -> EventLoopFuture 19 | } 20 | 21 | public final class HTTPServer { 22 | /// Engine server config struct. 23 | /// 24 | /// let serverConfig = HTTPServerConfig.default(port: 8123) 25 | /// services.register(serverConfig) 26 | /// 27 | public struct Configuration { 28 | /// Host name the server will bind to. 29 | public var hostname: String 30 | 31 | /// Port the server will bind to. 32 | public var port: Int 33 | 34 | /// Listen backlog. 35 | public var backlog: Int 36 | 37 | /// Requests containing bodies larger than this maximum will be rejected, closing the connection. 38 | public var maxBodySize: Int 39 | 40 | /// When `true`, can prevent errors re-binding to a socket after successive server restarts. 41 | public var reuseAddress: Bool 42 | 43 | /// When `true`, OS will attempt to minimize TCP packet delay. 44 | public var tcpNoDelay: Bool 45 | 46 | /// Number of webSocket maxFrameSize. 47 | public var webSocketMaxFrameSize: Int 48 | 49 | /// When `true`, HTTP server will support gzip and deflate compression. 50 | public var supportCompression: Bool 51 | 52 | /// When `true`, HTTP server will support pipelined requests. 53 | public var supportPipelining: Bool 54 | 55 | public var supportVersions: Set 56 | 57 | public var tlsConfig: TLSConfiguration? 58 | 59 | /// If set, this name will be serialized as the `Server` header in outgoing responses. 60 | public var serverName: String? 61 | 62 | /// Any uncaught server or responder errors will go here. 63 | public var errorHandler: (Error) -> () 64 | 65 | /// Creates a new `HTTPServerConfig`. 66 | /// 67 | /// - parameters: 68 | /// - hostname: Socket hostname to bind to. Usually `localhost` or `::1`. 69 | /// - port: Socket port to bind to. Usually `8080` for development and `80` for production. 70 | /// - backlog: OS socket backlog size. 71 | /// - workerCount: Number of `Worker`s to use for responding to incoming requests. 72 | /// This should be (and is by default) equal to the number of logical cores. 73 | /// - maxBodySize: Requests with bodies larger than this maximum will be rejected. 74 | /// Streaming bodies, like chunked bodies, ignore this maximum. 75 | /// - reuseAddress: When `true`, can prevent errors re-binding to a socket after successive server restarts. 76 | /// - tcpNoDelay: When `true`, OS will attempt to minimize TCP packet delay. 77 | /// - webSocketMaxFrameSize: Number of webSocket maxFrameSize. 78 | /// - supportCompression: When `true`, HTTP server will support gzip and deflate compression. 79 | /// - supportPipelining: When `true`, HTTP server will support pipelined requests. 80 | /// - serverName: If set, this name will be serialized as the `Server` header in outgoing responses. 81 | /// - upgraders: An array of `HTTPProtocolUpgrader` to check for with each request. 82 | /// - errorHandler: Any uncaught server or responder errors will go here. 83 | public init( 84 | hostname: String = "127.0.0.1", 85 | port: Int = 8080, 86 | backlog: Int = 256, 87 | maxBodySize: Int = 1_000_000, 88 | reuseAddress: Bool = true, 89 | tcpNoDelay: Bool = true, 90 | webSocketMaxFrameSize: Int = 1 << 14, 91 | supportCompression: Bool = false, 92 | supportPipelining: Bool = false, 93 | supportVersions: Set = [.one, .two], 94 | tlsConfig: TLSConfiguration? = nil, 95 | serverName: String? = nil, 96 | errorHandler: @escaping (Error) -> () = { _ in } 97 | ) { 98 | self.hostname = hostname 99 | self.port = port 100 | self.backlog = backlog 101 | self.maxBodySize = maxBodySize 102 | self.reuseAddress = reuseAddress 103 | self.tcpNoDelay = tcpNoDelay 104 | self.webSocketMaxFrameSize = webSocketMaxFrameSize 105 | self.supportCompression = supportCompression 106 | self.supportPipelining = supportPipelining 107 | self.supportVersions = supportVersions 108 | self.tlsConfig = tlsConfig 109 | self.serverName = serverName 110 | self.errorHandler = errorHandler 111 | } 112 | } 113 | 114 | public let configuration: Configuration 115 | public let eventLoopGroup: EventLoopGroup 116 | 117 | private var channel: Channel? 118 | private var quiesce: ServerQuiescingHelper? 119 | 120 | public init(configuration: Configuration = .init(), on eventLoopGroup: EventLoopGroup) { 121 | self.configuration = configuration 122 | self.eventLoopGroup = eventLoopGroup 123 | } 124 | 125 | public func start(delegate: HTTPServerDelegate) -> EventLoopFuture { 126 | let quiesce = ServerQuiescingHelper(group: eventLoopGroup) 127 | let bootstrap = ServerBootstrap(group: eventLoopGroup) 128 | // Specify backlog and enable SO_REUSEADDR for the server itself 129 | .serverChannelOption(ChannelOptions.backlog, value: Int32(self.configuration.backlog)) 130 | .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: self.configuration.reuseAddress ? SocketOptionValue(1) : SocketOptionValue(0)) 131 | 132 | // Set handlers that are applied to the Server's channel 133 | .serverChannelInitializer { channel in 134 | channel.pipeline.addHandler(quiesce.makeServerChannelHandler(channel: channel)) 135 | } 136 | 137 | // Set the handlers that are applied to the accepted Channels 138 | .childChannelInitializer { [weak self] channel in 139 | guard let self = self else { 140 | fatalError("HTTP server has deinitialized") 141 | } 142 | // add TLS handlers if configured 143 | if var tlsConfig = self.configuration.tlsConfig { 144 | // prioritize http/2 145 | if self.configuration.supportVersions.contains(.two) { 146 | tlsConfig.applicationProtocols.append("h2") 147 | } 148 | if self.configuration.supportVersions.contains(.one) { 149 | tlsConfig.applicationProtocols.append("http/1.1") 150 | } 151 | let sslContext: NIOSSLContext 152 | let tlsHandler: NIOSSLServerHandler 153 | do { 154 | sslContext = try NIOSSLContext(configuration: tlsConfig) 155 | tlsHandler = try NIOSSLServerHandler(context: sslContext) 156 | } catch { 157 | print("Could not configure TLS: \(error)") 158 | return channel.close(mode: .all) 159 | } 160 | return channel.pipeline.addHandler(tlsHandler).flatMap { 161 | return channel.pipeline.configureHTTP2SecureUpgrade(h2PipelineConfigurator: { pipeline in 162 | return channel.configureHTTP2Pipeline(mode: .server, inboundStreamStateInitializer: { channel, streamID in 163 | return channel.pipeline.addHandlers(self.http2Handlers(delegate: delegate, channel: channel, streamID: streamID)) 164 | }).flatMap { _ in 165 | return channel.pipeline.addHandler(HTTPServerErrorHandler()) 166 | } 167 | }, http1PipelineConfigurator: { pipeline in 168 | return pipeline.addHandlers(self.http1Handlers(delegate: delegate, channel: channel)) 169 | }) 170 | } 171 | } else { 172 | guard !self.configuration.supportVersions.contains(.two) else { 173 | fatalError("Plaintext HTTP/2 (h2c) not yet supported.") 174 | } 175 | let handlers = self.http1Handlers(delegate: delegate, channel: channel) 176 | return channel.pipeline.addHandlers(handlers, position: .last) 177 | } 178 | } 179 | 180 | // Enable TCP_NODELAY and SO_REUSEADDR for the accepted Channels 181 | .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: self.configuration.tcpNoDelay ? SocketOptionValue(1) : SocketOptionValue(0)) 182 | .childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: self.configuration.reuseAddress ? SocketOptionValue(1) : SocketOptionValue(0)) 183 | .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1) 184 | // .childChannelOption(ChannelOptions.allowRemoteHalfClosure, value: 1) 185 | 186 | return bootstrap.bind(host: self.configuration.hostname, port: self.configuration.port).map { channel in 187 | self.channel = channel 188 | self.quiesce = quiesce 189 | } 190 | } 191 | 192 | public func shutdown() -> EventLoopFuture { 193 | #warning("TODO: create shutdown timeout") 194 | guard let channel = self.channel, let quiesce = self.quiesce else { 195 | fatalError("Called shutdown() before start()") 196 | } 197 | let promise = channel.eventLoop.makePromise(of: Void.self) 198 | quiesce.initiateShutdown(promise: promise) 199 | return promise.futureResult 200 | } 201 | 202 | public var onClose: EventLoopFuture { 203 | guard let channel = self.channel else { 204 | fatalError("Called onClose before start()") 205 | } 206 | return channel.closeFuture 207 | } 208 | 209 | private func http2Handlers(delegate: HTTPServerDelegate, channel: Channel, streamID: HTTP2StreamID) -> [ChannelHandler] { 210 | // create server pipeline array 211 | var handlers: [ChannelHandler] = [] 212 | 213 | let http2 = HTTP2ToHTTP1ServerCodec(streamID: streamID) 214 | handlers.append(http2) 215 | 216 | // add NIO -> HTTP request decoder 217 | let serverReqDecoder = HTTPRequestPartDecoder( 218 | maxBodySize: self.configuration.maxBodySize 219 | ) 220 | handlers.append(serverReqDecoder) 221 | 222 | // add NIO -> HTTP response encoder 223 | let serverResEncoder = HTTPResponsePartEncoder( 224 | serverHeader: self.configuration.serverName, 225 | dateCache: .eventLoop(channel.eventLoop) 226 | ) 227 | handlers.append(serverResEncoder) 228 | 229 | // add server request -> response delegate 230 | let handler = HTTPServerHandler( 231 | delegate: delegate, 232 | errorHandler: self.configuration.errorHandler 233 | ) 234 | handlers.append(handler) 235 | 236 | return handlers 237 | } 238 | 239 | private func http1Handlers(delegate: HTTPServerDelegate, channel: Channel) -> [ChannelHandler] { 240 | // create server pipeline array 241 | var handlers: [ChannelHandler] = [] 242 | var otherHTTPHandlers: [RemovableChannelHandler] = [] 243 | 244 | // configure HTTP/1 245 | // add http parsing and serializing 246 | let httpResEncoder = HTTPResponseEncoder() 247 | let httpReqDecoder = ByteToMessageHandler(HTTPRequestDecoder( 248 | leftOverBytesStrategy: .forwardBytes 249 | )) 250 | handlers += [httpResEncoder, httpReqDecoder] 251 | otherHTTPHandlers += [httpResEncoder] 252 | 253 | // add pipelining support if configured 254 | if self.configuration.supportPipelining { 255 | let pipelineHandler = HTTPServerPipelineHandler() 256 | handlers.append(pipelineHandler) 257 | otherHTTPHandlers.append(pipelineHandler) 258 | } 259 | 260 | // add response compressor if configured 261 | if self.configuration.supportCompression { 262 | let compressionHandler = HTTPResponseCompressor() 263 | handlers.append(compressionHandler) 264 | otherHTTPHandlers.append(compressionHandler) 265 | } 266 | 267 | // add NIO -> HTTP request decoder 268 | let serverReqDecoder = HTTPRequestPartDecoder( 269 | maxBodySize: self.configuration.maxBodySize 270 | ) 271 | handlers.append(serverReqDecoder) 272 | otherHTTPHandlers.append(serverReqDecoder) 273 | 274 | // add NIO -> HTTP response encoder 275 | let serverResEncoder = HTTPResponsePartEncoder( 276 | serverHeader: self.configuration.serverName, 277 | dateCache: .eventLoop(channel.eventLoop) 278 | ) 279 | handlers.append(serverResEncoder) 280 | otherHTTPHandlers.append(serverResEncoder) 281 | 282 | // add server request -> response delegate 283 | let handler = HTTPServerHandler( 284 | delegate: delegate, 285 | errorHandler: self.configuration.errorHandler 286 | ) 287 | otherHTTPHandlers.append(handler) 288 | 289 | // add HTTP upgrade handler 290 | let upgrader = HTTPServerUpgradeHandler( 291 | httpRequestDecoder: httpReqDecoder, 292 | otherHTTPHandlers: otherHTTPHandlers 293 | ) 294 | handlers.append(upgrader) 295 | 296 | // wait to add delegate as final step 297 | handlers.append(handler) 298 | return handlers 299 | } 300 | 301 | deinit { 302 | assert(!channel!.isActive, "HTTPServer deinitialized without calling shutdown()") 303 | } 304 | } 305 | 306 | final class HTTPServerErrorHandler: ChannelInboundHandler { 307 | typealias InboundIn = Never 308 | 309 | func errorCaught(context: ChannelHandlerContext, error: Error) { 310 | print("HTTP Server received error: \(error)") 311 | context.close(promise: nil) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /Sources/HTTPKit/Headers/HTTPHeaders+Name.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | extension HTTPHeaders { 4 | /// Type used for the name of a HTTP header in the `HTTPHeaders` storage. 5 | public struct Name: Codable, Hashable, Equatable, CustomStringConvertible { 6 | /// See `Hashable` 7 | public func hash(into hasher: inout Hasher) { 8 | self.lowercased.hash(into: &hasher) 9 | } 10 | 11 | /// Lowercased-ASCII version of the header. 12 | internal let lowercased: String 13 | 14 | /// Create a HTTP header name with the provided String. 15 | public init(_ name: String) { 16 | self.lowercased = name.lowercased() 17 | } 18 | 19 | /// `ExpressibleByStringLiteral` conformance. 20 | public init(stringLiteral: String) { 21 | self.init(stringLiteral) 22 | } 23 | 24 | /// See `CustomStringConvertible.description` 25 | public var description: String { 26 | return lowercased 27 | } 28 | 29 | // https://www.iana.org/assignments/message-headers/message-headers.xhtml 30 | // Permanent Message Header Field Names 31 | 32 | /// A-IM header. 33 | public static let aIM = Name("A-IM") 34 | /// Accept header. 35 | public static let accept = Name("Accept") 36 | /// Accept-Additions header. 37 | public static let acceptAdditions = Name("Accept-Additions") 38 | /// Accept-Charset header. 39 | public static let acceptCharset = Name("Accept-Charset") 40 | /// Accept-Datetime header. 41 | public static let acceptDatetime = Name("Accept-Datetime") 42 | /// Accept-Encoding header. 43 | public static let acceptEncoding = Name("Accept-Encoding") 44 | /// Accept-Features header. 45 | public static let acceptFeatures = Name("Accept-Features") 46 | /// Accept-Language header. 47 | public static let acceptLanguage = Name("Accept-Language") 48 | /// Accept-Patch header. 49 | public static let acceptPatch = Name("Accept-Patch") 50 | /// Accept-Post header. 51 | public static let acceptPost = Name("Accept-Post") 52 | /// Accept-Ranges header. 53 | public static let acceptRanges = Name("Accept-Ranges") 54 | /// Accept-Age header. 55 | public static let age = Name("Age") 56 | /// Accept-Allow header. 57 | public static let allow = Name("Allow") 58 | /// ALPN header. 59 | public static let alpn = Name("ALPN") 60 | /// Alt-Svc header. 61 | public static let altSvc = Name("Alt-Svc") 62 | /// Alt-Used header. 63 | public static let altUsed = Name("Alt-Used") 64 | /// Alternates header. 65 | public static let alternates = Name("Alternates") 66 | /// Apply-To-Redirect-Ref header. 67 | public static let applyToRedirectRef = Name("Apply-To-Redirect-Ref") 68 | /// Authentication-Control header. 69 | public static let authenticationControl = Name("Authentication-Control") 70 | /// Authentication-Info header. 71 | public static let authenticationInfo = Name("Authentication-Info") 72 | /// Authorization header. 73 | public static let authorization = Name("Authorization") 74 | /// C-Ext header. 75 | public static let cExt = Name("C-Ext") 76 | /// C-Man header. 77 | public static let cMan = Name("C-Man") 78 | /// C-Opt header. 79 | public static let cOpt = Name("C-Opt") 80 | /// C-PEP header. 81 | public static let cPEP = Name("C-PEP") 82 | /// C-PEP-Info header. 83 | public static let cPEPInfo = Name("C-PEP-Info") 84 | /// Cache-Control header. 85 | public static let cacheControl = Name("Cache-Control") 86 | /// CalDav-Timezones header. 87 | public static let calDAVTimezones = Name("CalDAV-Timezones") 88 | /// Close header. 89 | public static let close = Name("Close") 90 | /// Connection header. 91 | public static let connection = Name("Connection") 92 | /// Content-Base header. 93 | public static let contentBase = Name("Content-Base") 94 | /// Content-Disposition header. 95 | public static let contentDisposition = Name("Content-Disposition") 96 | /// Content-Encoding header. 97 | public static let contentEncoding = Name("Content-Encoding") 98 | /// Content-ID header. 99 | public static let contentID = Name("Content-ID") 100 | /// Content-Language header. 101 | public static let contentLanguage = Name("Content-Language") 102 | /// Content-Length header. 103 | public static let contentLength = Name("Content-Length") 104 | /// Content-Location header. 105 | public static let contentLocation = Name("Content-Location") 106 | /// Content-MD5 header. 107 | public static let contentMD5 = Name("Content-MD5") 108 | /// Content-Range header. 109 | public static let contentRange = Name("Content-Range") 110 | /// Content-Script-Type header. 111 | public static let contentScriptType = Name("Content-Script-Type") 112 | /// Content-Security-Policy 113 | public static let contentSecurityPolicy = Name("Content-Security-Policy") 114 | /// Content-Style-Type header. 115 | public static let contentStyleType = Name("Content-Style-Type") 116 | /// Content-Type header. 117 | public static let contentType = Name("Content-Type") 118 | /// Content-Version header. 119 | public static let contentVersion = Name("Content-Version") 120 | /// Cookie header. 121 | public static let cookie = Name("Cookie") 122 | /// Cookie2 header. 123 | public static let cookie2 = Name("Cookie2") 124 | /// DASL header. 125 | public static let dasl = Name("DASL") 126 | /// DASV header. 127 | public static let dav = Name("DAV") 128 | /// Date header. 129 | public static let date = Name("Date") 130 | /// Default-Style header. 131 | public static let defaultStyle = Name("Default-Style") 132 | /// Delta-Base header. 133 | public static let deltaBase = Name("Delta-Base") 134 | /// Depth header. 135 | public static let depth = Name("Depth") 136 | /// Derived-From header. 137 | public static let derivedFrom = Name("Derived-From") 138 | /// Destination header. 139 | public static let destination = Name("Destination") 140 | /// Differential-ID header. 141 | public static let differentialID = Name("Differential-ID") 142 | /// Digest header. 143 | public static let digest = Name("Digest") 144 | /// ETag header. 145 | public static let eTag = Name("ETag") 146 | /// Expect header. 147 | public static let expect = Name("Expect") 148 | /// Expires header. 149 | public static let expires = Name("Expires") 150 | /// Ext header. 151 | public static let ext = Name("Ext") 152 | /// Forwarded header. 153 | public static let forwarded = Name("Forwarded") 154 | /// From header. 155 | public static let from = Name("From") 156 | /// GetProfile header. 157 | public static let getProfile = Name("GetProfile") 158 | /// Hobareg header. 159 | public static let hobareg = Name("Hobareg") 160 | /// Host header. 161 | public static let host = Name("Host") 162 | /// HTTP2-Settings header. 163 | public static let http2Settings = Name("HTTP2-Settings") 164 | /// IM header. 165 | public static let im = Name("IM") 166 | /// If header. 167 | public static let `if` = Name("If") 168 | /// If-Match header. 169 | public static let ifMatch = Name("If-Match") 170 | /// If-Modified-Since header. 171 | public static let ifModifiedSince = Name("If-Modified-Since") 172 | /// If-None-Match header. 173 | public static let ifNoneMatch = Name("If-None-Match") 174 | /// If-Range header. 175 | public static let ifRange = Name("If-Range") 176 | /// If-Schedule-Tag-Match header. 177 | public static let ifScheduleTagMatch = Name("If-Schedule-Tag-Match") 178 | /// If-Unmodified-Since header. 179 | public static let ifUnmodifiedSince = Name("If-Unmodified-Since") 180 | /// Keep-Alive header. 181 | public static let keepAlive = Name("Keep-Alive") 182 | /// Label header. 183 | public static let label = Name("Label") 184 | /// Last-Modified header. 185 | public static let lastModified = Name("Last-Modified") 186 | /// Link header. 187 | public static let link = Name("Link") 188 | /// Location header. 189 | public static let location = Name("Location") 190 | /// Lock-Token header. 191 | public static let lockToken = Name("Lock-Token") 192 | /// Man header. 193 | public static let man = Name("Man") 194 | /// Max-Forwards header. 195 | public static let maxForwards = Name("Max-Forwards") 196 | /// Memento-Datetime header. 197 | public static let mementoDatetime = Name("Memento-Datetime") 198 | /// Meter header. 199 | public static let meter = Name("Meter") 200 | /// MIME-Version header. 201 | public static let mimeVersion = Name("MIME-Version") 202 | /// Negotiate header. 203 | public static let negotiate = Name("Negotiate") 204 | /// Opt header. 205 | public static let opt = Name("Opt") 206 | /// Optional-WWW-Authenticate header. 207 | public static let optionalWWWAuthenticate = Name("Optional-WWW-Authenticate") 208 | /// Ordering-Type header. 209 | public static let orderingType = Name("Ordering-Type") 210 | /// Origin header. 211 | public static let origin = Name("Origin") 212 | /// Overwrite header. 213 | public static let overwrite = Name("Overwrite") 214 | /// P3P header. 215 | public static let p3p = Name("P3P") 216 | /// PEP header. 217 | public static let pep = Name("PEP") 218 | /// PICS-Label header. 219 | public static let picsLabel = Name("PICS-Label") 220 | /// Pep-Info header. 221 | public static let pepInfo = Name("Pep-Info") 222 | /// Position header. 223 | public static let position = Name("Position") 224 | /// Pragma header. 225 | public static let pragma = Name("Pragma") 226 | /// Prefer header. 227 | public static let prefer = Name("Prefer") 228 | /// Preference-Applied header. 229 | public static let preferenceApplied = Name("Preference-Applied") 230 | /// ProfileObject header. 231 | public static let profileObject = Name("ProfileObject") 232 | /// Protocol header. 233 | public static let `protocol` = Name("Protocol") 234 | /// Protocol-Info header. 235 | public static let protocolInfo = Name("Protocol-Info") 236 | /// Protocol-Query header. 237 | public static let protocolQuery = Name("Protocol-Query") 238 | /// Protocol-Request header. 239 | public static let protocolRequest = Name("Protocol-Request") 240 | /// Proxy-Authenticate header. 241 | public static let proxyAuthenticate = Name("Proxy-Authenticate") 242 | /// Proxy-Authentication-Info header. 243 | public static let proxyAuthenticationInfo = Name("Proxy-Authentication-Info") 244 | /// Proxy-Authorization header. 245 | public static let proxyAuthorization = Name("Proxy-Authorization") 246 | /// Proxy-Features header. 247 | public static let proxyFeatures = Name("Proxy-Features") 248 | /// Proxy-Instruction header. 249 | public static let proxyInstruction = Name("Proxy-Instruction") 250 | /// Public header. 251 | public static let `public` = Name("Public") 252 | /// Public-Key-Pins header. 253 | public static let publicKeyPins = Name("Public-Key-Pins") 254 | /// Public-Key-Pins-Report-Only header. 255 | public static let publicKeyPinsReportOnly = Name("Public-Key-Pins-Report-Only") 256 | /// Range header. 257 | public static let range = Name("Range") 258 | /// Redirect-Ref header. 259 | public static let redirectRef = Name("Redirect-Ref") 260 | /// Referer header. 261 | public static let referer = Name("Referer") 262 | /// Retry-After header. 263 | public static let retryAfter = Name("Retry-After") 264 | /// Safe header. 265 | public static let safe = Name("Safe") 266 | /// Schedule-Reply header. 267 | public static let scheduleReply = Name("Schedule-Reply") 268 | /// Schedule-Tag header. 269 | public static let scheduleTag = Name("Schedule-Tag") 270 | /// Sec-WebSocket-Accept header. 271 | public static let secWebSocketAccept = Name("Sec-WebSocket-Accept") 272 | /// Sec-WebSocket-Extensions header. 273 | public static let secWebSocketExtensions = Name("Sec-WebSocket-Extensions") 274 | /// Sec-WebSocket-Key header. 275 | public static let secWebSocketKey = Name("Sec-WebSocket-Key") 276 | /// Sec-WebSocket-Protocol header. 277 | public static let secWebSocketProtocol = Name("Sec-WebSocket-Protocol") 278 | /// Sec-WebSocket-Version header. 279 | public static let secWebSocketVersion = Name("Sec-WebSocket-Version") 280 | /// Security-Scheme header. 281 | public static let securityScheme = Name("Security-Scheme") 282 | /// Server header. 283 | public static let server = Name("Server") 284 | /// Set-Cookie header. 285 | public static let setCookie = Name("Set-Cookie") 286 | /// Set-Cookie2 header. 287 | public static let setCookie2 = Name("Set-Cookie2") 288 | /// SetProfile header. 289 | public static let setProfile = Name("SetProfile") 290 | /// SLUG header. 291 | public static let slug = Name("SLUG") 292 | /// SoapAction header. 293 | public static let soapAction = Name("SoapAction") 294 | /// Status-URI header. 295 | public static let statusURI = Name("Status-URI") 296 | /// Strict-Transport-Security header. 297 | public static let strictTransportSecurity = Name("Strict-Transport-Security") 298 | /// Surrogate-Capability header. 299 | public static let surrogateCapability = Name("Surrogate-Capability") 300 | /// Surrogate-Control header. 301 | public static let surrogateControl = Name("Surrogate-Control") 302 | /// TCN header. 303 | public static let tcn = Name("TCN") 304 | /// TE header. 305 | public static let te = Name("TE") 306 | /// Timeout header. 307 | public static let timeout = Name("Timeout") 308 | /// Topic header. 309 | public static let topic = Name("Topic") 310 | /// Trailer header. 311 | public static let trailer = Name("Trailer") 312 | /// Transfer-Encoding header. 313 | public static let transferEncoding = Name("Transfer-Encoding") 314 | /// TTL header. 315 | public static let ttl = Name("TTL") 316 | /// Urgency header. 317 | public static let urgency = Name("Urgency") 318 | /// URI header. 319 | public static let uri = Name("URI") 320 | /// Upgrade header. 321 | public static let upgrade = Name("Upgrade") 322 | /// User-Agent header. 323 | public static let userAgent = Name("User-Agent") 324 | /// Variant-Vary header. 325 | public static let variantVary = Name("Variant-Vary") 326 | /// Vary header. 327 | public static let vary = Name("Vary") 328 | /// Via header. 329 | public static let via = Name("Via") 330 | /// WWW-Authenticate header. 331 | public static let wwwAuthenticate = Name("WWW-Authenticate") 332 | /// Want-Digest header. 333 | public static let wantDigest = Name("Want-Digest") 334 | /// Warning header. 335 | public static let warning = Name("Warning") 336 | /// X-Content-Type-Options 337 | public static let xContentTypeOptions = Name("X-Content-Type-Options") 338 | /// X-Frame-Options header. 339 | public static let xFrameOptions = Name("X-Frame-Options") 340 | /// X-XSS-Protection header 341 | public static let xssProtection = Name("X-XSS-Protection") 342 | 343 | 344 | // https://www.iana.org/assignments/message-headers/message-headers.xhtml 345 | // Provisional Message Header Field Names 346 | /// Access-Control header. 347 | public static let accessControl = Name("Access-Control") 348 | /// Access-Control-Allow-Credentials header. 349 | public static let accessControlAllowCredentials = Name("Access-Control-Allow-Credentials") 350 | /// Access-Control-Allow-Headers header. 351 | public static let accessControlAllowHeaders = Name("Access-Control-Allow-Headers") 352 | /// Access-Control-Allow-Methods header. 353 | public static let accessControlAllowMethods = Name("Access-Control-Allow-Methods") 354 | /// Access-Control-Allow-Origin header. 355 | public static let accessControlAllowOrigin = Name("Access-Control-Allow-Origin") 356 | /// Access-Control-Expose-Headers header. 357 | public static let accessControlExpose = Name("Access-Control-Expose-Headers") 358 | /// Access-Control-Max-Age header. 359 | public static let accessControlMaxAge = Name("Access-Control-Max-Age") 360 | /// Access-Control-Request-Method header. 361 | public static let accessControlRequestMethod = Name("Access-Control-Request-Method") 362 | /// Access-Control-Request-Headers header. 363 | public static let accessControlRequestHeaders = Name("Access-Control-Request-Headers") 364 | /// Compliance header. 365 | public static let compliance = Name("Compliance") 366 | /// Content-Transfer-Encoding header. 367 | public static let contentTransferEncoding = Name("Content-Transfer-Encoding") 368 | /// Cost header. 369 | public static let cost = Name("Cost") 370 | /// EDIINT-Features header. 371 | public static let ediintFeatures = Name("EDIINT-Features") 372 | /// Message-ID header. 373 | public static let messageID = Name("Message-ID") 374 | /// Method-Check header. 375 | public static let methodCheck = Name("Method-Check") 376 | /// Method-Check-Expires header. 377 | public static let methodCheckExpires = Name("Method-Check-Expires") 378 | /// Non-Compliance header. 379 | public static let nonCompliance = Name("Non-Compliance") 380 | /// Optional header. 381 | public static let optional = Name("Optional") 382 | /// Referer-Root header. 383 | public static let refererRoot = Name("Referer-Root") 384 | /// Resolution-Hint header. 385 | public static let resolutionHint = Name("Resolution-Hint") 386 | /// Resolver-Location header. 387 | public static let resolverLocation = Name("Resolver-Location") 388 | /// SubOK header. 389 | public static let subOK = Name("SubOK") 390 | /// Subst header. 391 | public static let subst = Name("Subst") 392 | /// Title header. 393 | public static let title = Name("Title") 394 | /// UA-Color header. 395 | public static let uaColor = Name("UA-Color") 396 | /// UA-Media header. 397 | public static let uaMedia = Name("UA-Media") 398 | /// UA-Pixels header. 399 | public static let uaPixels = Name("UA-Pixels") 400 | /// UA-Resolution header. 401 | public static let uaResolution = Name("UA-Resolution") 402 | /// UA-Windowpixels header. 403 | public static let uaWindowpixels = Name("UA-Windowpixels") 404 | /// Version header. 405 | public static let version = Name("Version") 406 | /// X-Device-Accept header. 407 | public static let xDeviceAccept = Name("X-Device-Accept") 408 | /// X-Device-Accept-Charset header. 409 | public static let xDeviceAcceptCharset = Name("X-Device-Accept-Charset") 410 | /// X-Device-Accept-Encoding header. 411 | public static let xDeviceAcceptEncoding = Name("X-Device-Accept-Encoding") 412 | /// X-Device-Accept-Language header. 413 | public static let xDeviceAcceptLanguage = Name("X-Device-Accept-Language") 414 | /// X-Device-User-Agent header. 415 | public static let xDeviceUserAgent = Name("X-Device-User-Agent") 416 | /// X-Requested-With header. 417 | public static let xRequestedWith = Name("X-Requested-With") 418 | } 419 | 420 | /// Add a header name/value pair to the block. 421 | /// 422 | /// This method is strictly additive: if there are other values for the given header name 423 | /// already in the block, this will add a new entry. `add` performs case-insensitive 424 | /// comparisons on the header field name. 425 | /// 426 | /// - Parameter name: The header field name. For maximum compatibility this should be an 427 | /// ASCII string. For future-proofing with HTTP/2 lowercase header names are strongly 428 | // recommended. 429 | /// - Parameter value: The header field value to add for the given name. 430 | public mutating func add(name: Name, value: String) { 431 | add(name: name.lowercased, value: value) 432 | } 433 | 434 | /// Add a header name/value pair to the block, replacing any previous values for the 435 | /// same header name that are already in the block. 436 | /// 437 | /// This is a supplemental method to `add` that essentially combines `remove` and `add` 438 | /// in a single function. It can be used to ensure that a header block is in a 439 | /// well-defined form without having to check whether the value was previously there. 440 | /// Like `add`, this method performs case-insensitive comparisons of the header field 441 | /// names. 442 | /// 443 | /// - Parameter name: The header field name. For maximum compatibility this should be an 444 | /// ASCII string. For future-proofing with HTTP/2 lowercase header names are strongly 445 | // recommended. 446 | /// - Parameter value: The header field value to add for the given name. 447 | public mutating func replaceOrAdd(name: Name, value: String) { 448 | replaceOrAdd(name: name.lowercased, value: value) 449 | } 450 | 451 | /// Remove all values for a given header name from the block. 452 | /// 453 | /// This method uses case-insensitive comparisons for the header field name. 454 | /// 455 | /// - Parameter name: The name of the header field to remove from the block. 456 | public mutating func remove(name: Name) { 457 | remove(name: name.lowercased) 458 | } 459 | 460 | /// Retrieve all of the values for a given header field name from the block. 461 | /// 462 | /// This method uses case-insensitive comparisons for the header field name. It 463 | /// does not return a maximally-decomposed list of the header fields, but instead 464 | /// returns them in their original representation: that means that a comma-separated 465 | /// header field list may contain more than one entry, some of which contain commas 466 | /// and some do not. If you want a representation of the header fields suitable for 467 | /// performing computation on, consider `getCanonicalForm`. 468 | /// 469 | /// - Parameter name: The header field name whose values are to be retrieved. 470 | /// - Returns: A list of the values for that header field name. 471 | public subscript(name: Name) -> [String] { 472 | return self[name.lowercased] 473 | } 474 | 475 | /// Returns `true` if the `HTTPHeaders` contains a value for the supplied name. 476 | /// - Parameter name: The header field name to check. 477 | public func contains(name: Name) -> Bool { 478 | return self.contains(name: name.lowercased) 479 | } 480 | 481 | /// Returns the first header value with the supplied name. 482 | /// - Parameter name: The header field name whose values are to be retrieved. 483 | public func firstValue(name: Name) -> String? { 484 | // fixme: optimize 485 | return self[name.lowercased].first 486 | } 487 | } 488 | 489 | extension HTTPHeaders: CustomDebugStringConvertible { 490 | /// See `CustomDebugStringConvertible.debugDescription` 491 | public var debugDescription: String { 492 | var desc: [String] = [] 493 | for (key, val) in self { 494 | desc.append("\(key): \(val)") 495 | } 496 | return desc.joined(separator: "\n") 497 | } 498 | } 499 | 500 | --------------------------------------------------------------------------------