├── .codecov.yml ├── .gitignore ├── .swift-version ├── .travis.yml ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── HTTP │ ├── Content │ ├── Request+Content.swift │ └── Response+Content.swift │ ├── Error │ └── Error.swift │ ├── HTTP.swift │ ├── Message │ ├── AttributedCookie.swift │ ├── Body.swift │ ├── Cookie.swift │ ├── Headers.swift │ ├── Message.swift │ ├── Method.swift │ ├── Request.swift │ ├── Response.swift │ ├── Status.swift │ └── Version.swift │ ├── Middleware │ ├── BasicAuthMiddleware │ │ └── BasicAuthMiddleware.swift │ ├── ContentNegotiationMiddleware │ │ ├── ContentMapperMiddleware.swift │ │ └── ContentNegotiatonMiddleware.swift │ ├── LogMiddleware │ │ └── LogMiddleware.swift │ ├── Middleware.swift │ ├── RecoveryMiddleware │ │ └── RecoveryMiddleware.swift │ ├── RedirectMiddleware │ │ └── RedirectMiddleware.swift │ └── SessionMiddleware │ │ ├── Session.swift │ │ ├── SessionMiddleware.swift │ │ └── SessionStorage.swift │ ├── Parser │ └── MessageParser.swift │ ├── Responder │ └── Responder.swift │ └── Serializer │ ├── BodyStream.swift │ ├── RequestSerializer.swift │ └── ResponseSerializer.swift └── Tests ├── HTTPTests ├── Content │ ├── RequestContentTests.swift │ └── ResponseContentTests.swift ├── Error │ └── ErrorTests.swift ├── Message │ ├── AttributedCookieTests.swift │ ├── BodyTests.swift │ ├── CookieTests.swift │ ├── MessageTests.swift │ ├── RequestMethodTests.swift │ ├── RequestTests.swift │ ├── ResponseStatusTests.swift │ └── ResponseTests.swift ├── Middleware │ ├── BasicAuthMiddlewareTests.swift │ ├── BufferClientContentNegotiationMiddlewareTests.swift │ ├── BufferServerContentNegotiationMiddlewareTests.swift │ ├── LogMiddlewareTests.swift │ ├── RecoveryMiddlewareTests.swift │ ├── RedirectMiddlewareTests.swift │ ├── SessionMiddlewareTests.swift │ ├── StreamClientContentNegotiationMiddlewareTests.swift │ └── StreamServerContentNegotiationMiddlewareTests.swift ├── Parser │ ├── RequestParserTests.swift │ └── ResponseParserTests.swift └── Serializer │ └── HTTPSerializerTests.swift └── LinuxMain.swift /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: master 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | ignore: 10 | - "Tests/*" 11 | 12 | notify: 13 | slack: 14 | default: 15 | url: "secret:NnVfkTAF7NSxRk1AAGU13WKLgty/kCIY3mpKlUy5NpqlYdEiYxz/VbEelmj5aIm2I6roJJZY4KbICMO9GlbkwG+i19hJBKF1oo9Z3ZR3M0ICwJ9hUrPVZ4cg1pCKKUjZ4mi/nELOwxdYazhNCgNGx1huu0QTHB5059rYmEr5xWw=" 16 | attachments: "sunburst, diff" 17 | 18 | fixes: 19 | - "Sources/::" 20 | 21 | comment: 22 | layout: "header, sunburst, diff, changes, uncovered, suggestions" 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | slack: zewo:VjyVCCQvTOw9yrbzQysZezD1 3 | os: 4 | - linux 5 | - osx 6 | language: generic 7 | sudo: required 8 | dist: trusty 9 | osx_image: xcode8 10 | install: 11 | - eval "$(curl -sL https://raw.githubusercontent.com/Zewo/Zewo/master/Scripts/Travis/install.sh)" 12 | script: 13 | - bash <(curl -s https://raw.githubusercontent.com/Zewo/Zewo/master/Scripts/Travis/build-test.sh) HTTP 14 | after_success: 15 | - bash <(curl -s https://raw.githubusercontent.com/Zewo/Zewo/master/Scripts/Travis/report-coverage.sh) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Zewo 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. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "HTTP", 5 | dependencies: [ 6 | .Package(url: "https://github.com/Zewo/Axis.git", majorVersion: 0, minor: 14), 7 | .Package(url: "https://github.com/Zewo/CHTTPParser.git", majorVersion: 0, minor: 14), 8 | ] 9 | ) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP 2 | 3 | [![Swift][swift-badge]][swift-url] 4 | [![License][mit-badge]][mit-url] 5 | [![Slack][slack-badge]][slack-url] 6 | [![Travis][travis-badge]][travis-url] 7 | [![Codecov][codecov-badge]][codecov-url] 8 | [![Codebeat][codebeat-badge]][codebeat-url] 9 | 10 | ## Installation 11 | 12 | ```swift 13 | import PackageDescription 14 | 15 | let package = Package( 16 | dependencies: [ 17 | .Package(url: "https://github.com/Zewo/HTTP.git", majorVersion: 0, minor: 14), 18 | ] 19 | ) 20 | ``` 21 | 22 | ## Support 23 | 24 | If you need any help you can join our [Slack](http://slack.zewo.io) and go to the **#help** channel. Or you can create a Github [issue](https://github.com/Zewo/Zewo/issues/new) in our main repository. When stating your issue be sure to add enough details, specify what module is causing the problem and reproduction steps. 25 | 26 | ## Community 27 | 28 | [![Slack][slack-image]][slack-url] 29 | 30 | The entire Zewo code base is licensed under MIT. By contributing to Zewo you are contributing to an open and engaged community of brilliant Swift programmers. Join us on [Slack](http://slack.zewo.io) to get to know us! 31 | 32 | ## License 33 | 34 | This project is released under the MIT license. See [LICENSE](LICENSE) for details. 35 | 36 | [swift-badge]: https://img.shields.io/badge/Swift-3.0-orange.svg?style=flat 37 | [swift-url]: https://swift.org 38 | [mit-badge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat 39 | [mit-url]: https://tldrlegal.com/license/mit-license 40 | [slack-image]: http://s13.postimg.org/ybwy92ktf/Slack.png 41 | [slack-badge]: https://zewo-slackin.herokuapp.com/badge.svg 42 | [slack-url]: http://slack.zewo.io 43 | [travis-badge]: https://travis-ci.org/Zewo/HTTP.svg?branch=master 44 | [travis-url]: https://travis-ci.org/Zewo/HTTP 45 | [codecov-badge]: https://codecov.io/gh/Zewo/HTTP/branch/master/graph/badge.svg 46 | [codecov-url]: https://codecov.io/gh/Zewo/HTTP 47 | [codebeat-badge]: https://codebeat.co/badges/14a285db-4f0c-4a80-ab0c-e90fb08d0cfa 48 | [codebeat-url]: https://codebeat.co/projects/github-com-zewo-http 49 | -------------------------------------------------------------------------------- /Sources/HTTP/Content/Request+Content.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Request { 4 | public var content: Map? { 5 | get { 6 | return storage["content"] as? Map 7 | } 8 | 9 | set(content) { 10 | storage["content"] = content 11 | } 12 | } 13 | } 14 | 15 | extension Request { 16 | public init(method: Method = .get, url: URL = URL(string: "/")!, headers: Headers = [:], content: T, contentType: MediaType? = nil) { 17 | self.init( 18 | method: method, 19 | url: url, 20 | headers: headers, 21 | body: Buffer() 22 | ) 23 | 24 | self.content = content.map 25 | 26 | if let contentType = contentType { 27 | self.contentType = contentType 28 | } 29 | } 30 | 31 | public init(method: Method = .get, url: URL = URL(string: "/")!, headers: Headers = [:], content: T?, contentType: MediaType? = nil) { 32 | self.init( 33 | method: method, 34 | url: url, 35 | headers: headers, 36 | body: Buffer() 37 | ) 38 | 39 | self.content = content.map 40 | 41 | if let contentType = contentType { 42 | self.contentType = contentType 43 | } 44 | } 45 | 46 | public init(method: Method = .get, url: URL = URL(string: "/")!, headers: Headers = [:], content: [T], contentType: MediaType? = nil) { 47 | self.init( 48 | method: method, 49 | url: url, 50 | headers: headers, 51 | body: Buffer() 52 | ) 53 | 54 | self.content = content.map 55 | 56 | if let contentType = contentType { 57 | self.contentType = contentType 58 | } 59 | } 60 | 61 | public init(method: Method = .get, url: URL = URL(string: "/")!, headers: Headers = [:], content: [String: T], contentType: MediaType? = nil) { 62 | self.init( 63 | method: method, 64 | url: url, 65 | headers: headers, 66 | body: Buffer() 67 | ) 68 | 69 | self.content = content.map 70 | 71 | if let contentType = contentType { 72 | self.contentType = contentType 73 | } 74 | } 75 | 76 | public init(method: Method = .get, url: URL = URL(string: "/")!, headers: Headers = [:], content: T, contentType: MediaType? = nil) throws { 77 | self.init( 78 | method: method, 79 | url: url, 80 | headers: headers, 81 | body: Buffer() 82 | ) 83 | 84 | self.content = try content.asMap() 85 | 86 | if let contentType = contentType { 87 | self.contentType = contentType 88 | } 89 | } 90 | } 91 | 92 | extension Request { 93 | public init?(method: Method = .get, url: String, headers: Headers = [:], content: T, contentType: MediaType? = nil) { 94 | guard let url = URL(string: url) else { 95 | return nil 96 | } 97 | 98 | self.init( 99 | method: method, 100 | url: url, 101 | headers: headers, 102 | body: Buffer() 103 | ) 104 | 105 | self.content = content.map 106 | 107 | if let contentType = contentType { 108 | self.contentType = contentType 109 | } 110 | } 111 | 112 | public init?(method: Method = .get, url: String, headers: Headers = [:], content: T?, contentType: MediaType? = nil) { 113 | guard let url = URL(string: url) else { 114 | return nil 115 | } 116 | 117 | self.init( 118 | method: method, 119 | url: url, 120 | headers: headers, 121 | body: Buffer() 122 | ) 123 | 124 | self.content = content.map 125 | 126 | if let contentType = contentType { 127 | self.contentType = contentType 128 | } 129 | } 130 | 131 | public init?(method: Method = .get, url: String, headers: Headers = [:], content: [T], contentType: MediaType? = nil) { 132 | guard let url = URL(string: url) else { 133 | return nil 134 | } 135 | 136 | self.init( 137 | method: method, 138 | url: url, 139 | headers: headers, 140 | body: Buffer() 141 | ) 142 | 143 | self.content = content.map 144 | 145 | if let contentType = contentType { 146 | self.contentType = contentType 147 | } 148 | } 149 | 150 | public init?(method: Method = .get, url: String, headers: Headers = [:], content: [String: T], contentType: MediaType? = nil) { 151 | guard let url = URL(string: url) else { 152 | return nil 153 | } 154 | 155 | self.init( 156 | method: method, 157 | url: url, 158 | headers: headers, 159 | body: Buffer() 160 | ) 161 | 162 | self.content = content.map 163 | 164 | if let contentType = contentType { 165 | self.contentType = contentType 166 | } 167 | } 168 | 169 | public init(method: Method = .get, url: String, headers: Headers = [:], content: T, contentType: MediaType? = nil) throws { 170 | guard let url = URL(string: url) else { 171 | throw URLError.invalidURL 172 | } 173 | 174 | self.init( 175 | method: method, 176 | url: url, 177 | headers: headers, 178 | body: Buffer() 179 | ) 180 | 181 | self.content = try content.asMap() 182 | 183 | if let contentType = contentType { 184 | self.contentType = contentType 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Sources/HTTP/Content/Response+Content.swift: -------------------------------------------------------------------------------- 1 | import Axis 2 | 3 | extension Response { 4 | public var content: Map? { 5 | get { 6 | return storage["content"] as? Map 7 | } 8 | 9 | set(content) { 10 | storage["content"] = content 11 | } 12 | } 13 | } 14 | 15 | extension Response { 16 | public init(status: Status = .ok, headers: Headers = [:], content: T, contentType: MediaType? = nil) { 17 | self.init( 18 | status: status, 19 | headers: headers, 20 | body: Buffer() 21 | ) 22 | 23 | self.content = content.map 24 | 25 | if let contentType = contentType { 26 | self.contentType = contentType 27 | } 28 | } 29 | 30 | public init(status: Status = .ok, headers: Headers = [:], content: T?, contentType: MediaType? = nil) { 31 | self.init( 32 | status: status, 33 | headers: headers, 34 | body: Buffer() 35 | ) 36 | 37 | self.content = content.map 38 | 39 | if let contentType = contentType { 40 | self.contentType = contentType 41 | } 42 | } 43 | 44 | public init(status: Status = .ok, headers: Headers = [:], content: [T], contentType: MediaType? = nil) { 45 | self.init( 46 | status: status, 47 | headers: headers, 48 | body: Buffer() 49 | ) 50 | 51 | self.content = content.map 52 | 53 | if let contentType = contentType { 54 | self.contentType = contentType 55 | } 56 | } 57 | 58 | public init(status: Status = .ok, headers: Headers = [:], content: [String: T], contentType: MediaType? = nil) { 59 | self.init( 60 | status: status, 61 | headers: headers, 62 | body: Buffer() 63 | ) 64 | 65 | self.content = content.map 66 | 67 | if let contentType = contentType { 68 | self.contentType = contentType 69 | } 70 | } 71 | 72 | public init(status: Status = .ok, headers: Headers = [:], content: T, contentType: MediaType? = nil) throws { 73 | self.init( 74 | status: status, 75 | headers: headers, 76 | body: Buffer() 77 | ) 78 | 79 | self.content = try content.asMap() 80 | 81 | if let contentType = contentType { 82 | self.contentType = contentType 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/HTTP/HTTP.swift: -------------------------------------------------------------------------------- 1 | @_exported import Axis 2 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/AttributedCookie.swift: -------------------------------------------------------------------------------- 1 | public struct AttributedCookie { 2 | public enum Expiration { 3 | case maxAge(Int) 4 | case expires(String) 5 | } 6 | 7 | public var name: String 8 | public var value: String 9 | 10 | public var expiration: Expiration? 11 | public var domain: String? 12 | public var path: String? 13 | public var secure: Bool 14 | public var httpOnly: Bool 15 | 16 | public init(name: String, value: String, expiration: Expiration? = nil, domain: String? = nil, path: String? = nil, secure: Bool = false, httpOnly: Bool = false) { 17 | self.name = name 18 | self.value = value 19 | self.expiration = expiration 20 | self.domain = domain 21 | self.path = path 22 | self.secure = secure 23 | self.httpOnly = httpOnly 24 | } 25 | 26 | public init?(_ string: String) { 27 | let cookieStringTokens = string.split(separator: ";") 28 | 29 | guard let cookieTokens = cookieStringTokens.first?.split(separator: "="), cookieTokens.count == 2 else { 30 | return nil 31 | } 32 | 33 | let name = cookieTokens[0] 34 | let value = cookieTokens[1] 35 | 36 | var attributes: [CaseInsensitiveString: String] = [:] 37 | 38 | for i in 1 ..< cookieStringTokens.count { 39 | let attributeTokens = cookieStringTokens[i].split(separator: "=") 40 | 41 | switch attributeTokens.count { 42 | case 1: 43 | attributes[CaseInsensitiveString(attributeTokens[0].trim())] = "" 44 | case 2: 45 | attributes[CaseInsensitiveString(attributeTokens[0].trim())] = attributeTokens[1].trim() 46 | default: 47 | return nil 48 | } 49 | } 50 | 51 | var expiration: Expiration? 52 | 53 | if let maxAge = attributes["Max-Age"].flatMap({Int($0)}) { 54 | expiration = .maxAge(maxAge) 55 | } 56 | 57 | if let expires = attributes["Expires"] { 58 | expiration = .expires(expires) 59 | } 60 | 61 | let domain = attributes["Domain"] 62 | let path = attributes["Path"] 63 | let secure = attributes["Secure"] != nil 64 | let httpOnly = attributes["HttpOnly"] != nil 65 | 66 | self.init( 67 | name: name, 68 | value: value, 69 | expiration: expiration, 70 | domain: domain, 71 | path: path, 72 | secure: secure, 73 | httpOnly: httpOnly 74 | ) 75 | } 76 | } 77 | 78 | extension AttributedCookie : Hashable { 79 | public var hashValue: Int { 80 | return name.hashValue 81 | } 82 | } 83 | 84 | public func == (lhs: AttributedCookie, rhs: AttributedCookie) -> Bool { 85 | return lhs.hashValue == rhs.hashValue 86 | } 87 | 88 | extension AttributedCookie : CustomStringConvertible { 89 | public var description: String { 90 | var string = "\(name)=\(value)" 91 | 92 | if let expiration = expiration { 93 | switch expiration { 94 | case .expires(let expires): 95 | string += "; Expires=\(expires)" 96 | case .maxAge(let maxAge): 97 | string += "; Max-Age=\(maxAge)" 98 | } 99 | } 100 | 101 | if let domain = domain { 102 | string += "; Domain=\(domain)" 103 | } 104 | 105 | if let path = path { 106 | string += "; Path=\(path)" 107 | } 108 | 109 | if secure { 110 | string += "; Secure" 111 | } 112 | 113 | if httpOnly { 114 | string += "; HttpOnly" 115 | } 116 | 117 | return string 118 | } 119 | } 120 | 121 | extension AttributedCookie.Expiration : Equatable {} 122 | 123 | public func == (lhs: AttributedCookie.Expiration, rhs: AttributedCookie.Expiration) -> Bool { 124 | switch (lhs, rhs) { 125 | case let (.maxAge(l), .maxAge(r)): return l == r 126 | case let (.expires(l), .expires(r)): return l == r 127 | default: return false 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/Body.swift: -------------------------------------------------------------------------------- 1 | public enum Body { 2 | case buffer(Buffer) 3 | case reader(InputStream) 4 | case writer((OutputStream) throws -> Void) 5 | } 6 | 7 | extension Body { 8 | public static var empty: Body { 9 | return .buffer(.empty) 10 | } 11 | 12 | public var isEmpty: Bool { 13 | switch self { 14 | case .buffer(let buffer): return buffer.isEmpty 15 | default: return false 16 | } 17 | } 18 | } 19 | 20 | extension Body { 21 | public var isBuffer: Bool { 22 | switch self { 23 | case .buffer: return true 24 | default: return false 25 | } 26 | } 27 | 28 | public var isReader: Bool { 29 | switch self { 30 | case .reader: return true 31 | default: return false 32 | } 33 | } 34 | 35 | public var isWriter: Bool { 36 | switch self { 37 | case .writer: return true 38 | default: return false 39 | } 40 | } 41 | } 42 | 43 | extension Body { 44 | public mutating func becomeBuffer(deadline: Double) throws -> Buffer { 45 | switch self { 46 | case .buffer(let buffer): 47 | return buffer 48 | case .reader(let reader): 49 | let buffer = try reader.drain(deadline: deadline) 50 | self = .buffer(buffer) 51 | return buffer 52 | case .writer(let writer): 53 | let bufferStream = BufferStream() 54 | try writer(bufferStream) 55 | let buffer = bufferStream.buffer 56 | self = .buffer(buffer) 57 | return buffer 58 | } 59 | } 60 | 61 | public mutating func becomeReader() throws -> InputStream { 62 | switch self { 63 | case .reader(let reader): 64 | return reader 65 | case .buffer(let buffer): 66 | let bufferStream = BufferStream(buffer: buffer) 67 | self = .reader(bufferStream) 68 | return bufferStream 69 | case .writer(let writer): 70 | let bufferStream = BufferStream() 71 | try writer(bufferStream) 72 | self = .reader(bufferStream) 73 | return bufferStream 74 | } 75 | } 76 | 77 | public mutating func becomeWriter(deadline: Double) throws -> ((OutputStream) throws -> Void) { 78 | switch self { 79 | case .buffer(let buffer): 80 | let writer: ((OutputStream) throws -> Void) = { writer in 81 | try writer.write(buffer, deadline: deadline) 82 | try writer.flush(deadline: deadline) 83 | } 84 | self = .writer(writer) 85 | return writer 86 | case .reader(let reader): 87 | let writer: ((OutputStream) throws -> Void) = { writer in 88 | let buffer = try reader.drain(deadline: deadline) 89 | try writer.write(buffer, deadline: deadline) 90 | try writer.flush(deadline: deadline) 91 | } 92 | self = .writer(writer) 93 | return writer 94 | case .writer(let writer): 95 | return writer 96 | } 97 | } 98 | } 99 | 100 | extension Body : Equatable {} 101 | 102 | public func == (lhs: Body, rhs: Body) -> Bool { 103 | switch (lhs, rhs) { 104 | case let (.buffer(l), .buffer(r)) where l == r: return true 105 | default: return false 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/Cookie.swift: -------------------------------------------------------------------------------- 1 | public struct Cookie : CookieProtocol { 2 | public var name: String 3 | public var value: String 4 | 5 | public init(name: String, value: String) { 6 | self.name = name 7 | self.value = value 8 | } 9 | } 10 | 11 | extension Cookie : Hashable { 12 | public var hashValue: Int { 13 | return name.hashValue 14 | } 15 | } 16 | 17 | extension Cookie : Equatable {} 18 | 19 | public func == (lhs: Cookie, rhs: Cookie) -> Bool { 20 | return lhs.hashValue == rhs.hashValue 21 | } 22 | 23 | extension Cookie : CustomStringConvertible { 24 | public var description: String { 25 | return "\(name)=\(value)" 26 | } 27 | } 28 | 29 | public protocol CookieProtocol { 30 | init(name: String, value: String) 31 | } 32 | 33 | extension Set where Element : CookieProtocol { 34 | public init?(cookieHeader: String) { 35 | var cookies = Set() 36 | let tokens = cookieHeader.split(separator: ";") 37 | 38 | for token in tokens { 39 | let cookieTokens = token.split(separator: "=", maxSplits: 1) 40 | 41 | guard cookieTokens.count == 2 else { 42 | return nil 43 | } 44 | 45 | cookies.insert(Element(name: cookieTokens[0].trim(), value: cookieTokens[1].trim())) 46 | } 47 | 48 | self = cookies 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/Headers.swift: -------------------------------------------------------------------------------- 1 | public struct Headers { 2 | public var headers: [CaseInsensitiveString: String] 3 | 4 | public init(_ headers: [CaseInsensitiveString: String]) { 5 | self.headers = headers 6 | } 7 | } 8 | 9 | extension Headers { 10 | public static var empty: Headers { 11 | return Headers() 12 | } 13 | } 14 | 15 | extension Headers : ExpressibleByDictionaryLiteral { 16 | public init(dictionaryLiteral elements: (CaseInsensitiveString, String)...) { 17 | var headers: [CaseInsensitiveString: String] = [:] 18 | 19 | for (key, value) in elements { 20 | headers[key] = value 21 | } 22 | 23 | self.headers = headers 24 | } 25 | } 26 | 27 | extension Headers : Sequence { 28 | public func makeIterator() -> DictionaryIterator { 29 | return headers.makeIterator() 30 | } 31 | 32 | public var count: Int { 33 | return headers.count 34 | } 35 | 36 | public var isEmpty: Bool { 37 | return headers.isEmpty 38 | } 39 | 40 | public subscript(field: CaseInsensitiveString) -> String? { 41 | get { 42 | return headers[field] 43 | } 44 | 45 | set(header) { 46 | headers[field] = header 47 | 48 | if field == "Content-Length" && header != nil && headers["Transfer-Encoding"] == "chunked" { 49 | headers["Transfer-Encoding"] = nil 50 | } else if field == "Transfer-Encoding" && header == "chunked" { 51 | headers["Content-Length"] = nil 52 | } 53 | } 54 | } 55 | 56 | public subscript(field: CaseInsensitiveStringRepresentable) -> String? { 57 | get { 58 | return self[field.caseInsensitiveString] 59 | } 60 | 61 | set(header) { 62 | self[field.caseInsensitiveString] = header 63 | } 64 | } 65 | } 66 | 67 | extension Headers : CustomStringConvertible { 68 | public var description: String { 69 | var string = "" 70 | 71 | for (header, value) in headers { 72 | string += "\(header): \(value)\n" 73 | } 74 | 75 | return string 76 | } 77 | } 78 | 79 | extension Headers : Equatable {} 80 | 81 | public func == (lhs: Headers, rhs: Headers) -> Bool { 82 | return lhs.headers == rhs.headers 83 | } 84 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/Message.swift: -------------------------------------------------------------------------------- 1 | public protocol Message { 2 | var version: Version { get set } 3 | var headers: Headers { get set } 4 | var body: Body { get set } 5 | var storage: [String: Any] { get set } 6 | } 7 | 8 | extension Message { 9 | public var contentType: MediaType? { 10 | get { 11 | return headers["Content-Type"].flatMap({try? MediaType(string: $0)}) 12 | } 13 | 14 | set(contentType) { 15 | headers["Content-Type"] = contentType?.description 16 | } 17 | } 18 | 19 | public var contentLength: Int? { 20 | get { 21 | return headers["Content-Length"].flatMap({Int($0)}) 22 | } 23 | 24 | set(contentLength) { 25 | headers["Content-Length"] = contentLength?.description 26 | } 27 | } 28 | 29 | public var transferEncoding: String? { 30 | get { 31 | return headers["Transfer-Encoding"] 32 | } 33 | 34 | set(transferEncoding) { 35 | headers["Transfer-Encoding"] = transferEncoding 36 | } 37 | } 38 | 39 | public var isChunkEncoded: Bool { 40 | return transferEncoding == "chunked" 41 | } 42 | 43 | public var connection: String? { 44 | get { 45 | return headers["Connection"] 46 | } 47 | 48 | set(connection) { 49 | headers["Connection"] = connection 50 | } 51 | } 52 | 53 | public var isKeepAlive: Bool { 54 | if version.minor == 0 { 55 | return connection?.lowercased() == "keep-alive" 56 | } 57 | 58 | return connection?.lowercased() != "close" 59 | } 60 | 61 | public var isUpgrade: Bool { 62 | return connection?.lowercased() == "upgrade" 63 | } 64 | 65 | public var upgrade: String? { 66 | get { 67 | return headers["Upgrade"] 68 | } 69 | 70 | set(upgrade) { 71 | headers["Upgrade"] = upgrade 72 | } 73 | } 74 | } 75 | 76 | extension Message { 77 | public var storageDescription: String { 78 | var string = "Storage:\n" 79 | 80 | if storage.isEmpty { 81 | string += "-" 82 | } 83 | 84 | for (key, value) in storage { 85 | string += "\(key): \(value)\n" 86 | } 87 | 88 | return string 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/Method.swift: -------------------------------------------------------------------------------- 1 | extension Request.Method { 2 | init(_ rawValue: String) { 3 | let method = rawValue.uppercased() 4 | switch method { 5 | case "DELETE": self = .delete 6 | case "GET": self = .get 7 | case "HEAD": self = .head 8 | case "POST": self = .post 9 | case "PUT": self = .put 10 | case "CONNECT": self = .connect 11 | case "OPTIONS": self = .options 12 | case "TRACE": self = .trace 13 | case "PATCH": self = .patch 14 | default: self = .other(method: method) 15 | } 16 | } 17 | } 18 | 19 | extension Request.Method : CustomStringConvertible { 20 | public var description: String { 21 | switch self { 22 | case .delete: return "DELETE" 23 | case .get: return "GET" 24 | case .head: return "HEAD" 25 | case .post: return "POST" 26 | case .put: return "PUT" 27 | case .connect: return "CONNECT" 28 | case .options: return "OPTIONS" 29 | case .trace: return "TRACE" 30 | case .patch: return "PATCH" 31 | case .other(let method): return method.uppercased() 32 | } 33 | } 34 | } 35 | 36 | extension Request.Method : Hashable { 37 | public var hashValue: Int { 38 | switch self { 39 | case .delete: return 0 40 | case .get: return 1 41 | case .head: return 2 42 | case .post: return 3 43 | case .put: return 4 44 | case .connect: return 5 45 | case .options: return 6 46 | case .trace: return 7 47 | case .patch: return 8 48 | case .other(let method): return 9 + method.hashValue 49 | } 50 | } 51 | } 52 | 53 | public func ==(lhs: Request.Method, rhs: Request.Method) -> Bool { 54 | return lhs.description == rhs.description 55 | } 56 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/Request.swift: -------------------------------------------------------------------------------- 1 | import Axis 2 | 3 | public struct Request : Message { 4 | public enum Method { 5 | case delete 6 | case get 7 | case head 8 | case post 9 | case put 10 | case connect 11 | case options 12 | case trace 13 | case patch 14 | case other(method: String) 15 | } 16 | 17 | public var method: Method 18 | public var url: URL 19 | public var version: Version 20 | public var headers: Headers 21 | public var body: Body 22 | public var storage: [String: Any] 23 | 24 | public init(method: Method, url: URL, version: Version, headers: Headers, body: Body) { 25 | self.method = method 26 | self.url = url 27 | self.version = version 28 | self.headers = headers 29 | self.body = body 30 | self.storage = [:] 31 | } 32 | } 33 | 34 | public protocol RequestInitializable { 35 | init(request: Request) 36 | } 37 | 38 | public protocol RequestRepresentable { 39 | var request: Request { get } 40 | } 41 | 42 | public protocol RequestConvertible : RequestInitializable, RequestRepresentable {} 43 | 44 | extension Request : RequestConvertible { 45 | public init(request: Request) { 46 | self = request 47 | } 48 | 49 | public var request: Request { 50 | return self 51 | } 52 | } 53 | 54 | extension Request { 55 | public init(method: Method = .get, url: URL = URL(string: "/")!, headers: Headers = [:], body: Body) { 56 | self.init( 57 | method: method, 58 | url: url, 59 | version: Version(major: 1, minor: 1), 60 | headers: headers, 61 | body: body 62 | ) 63 | 64 | switch body { 65 | case let .buffer(body): 66 | self.headers["Content-Length"] = body.count.description 67 | default: 68 | self.headers["Transfer-Encoding"] = "chunked" 69 | } 70 | } 71 | 72 | public init(method: Method = .get, url: URL = URL(string: "/")!, headers: Headers = [:], body: BufferRepresentable = Buffer()) { 73 | self.init( 74 | method: method, 75 | url: url, 76 | headers: headers, 77 | body: .buffer(body.buffer) 78 | ) 79 | } 80 | 81 | public init(method: Method = .get, url: URL = URL(string: "/")!, headers: Headers = [:], body: InputStream) { 82 | self.init( 83 | method: method, 84 | url: url, 85 | headers: headers, 86 | body: .reader(body) 87 | ) 88 | } 89 | 90 | public init(method: Method = .get, url: URL = URL(string: "/")!, headers: Headers = [:], body: @escaping (OutputStream) throws -> Void) { 91 | self.init( 92 | method: method, 93 | url: url, 94 | headers: headers, 95 | body: .writer(body) 96 | ) 97 | } 98 | } 99 | 100 | extension Request { 101 | public init?(method: Method = .get, url: String, headers: Headers = [:], body: BufferRepresentable = Buffer()) { 102 | guard let url = URL(string: url) else { 103 | return nil 104 | } 105 | 106 | self.init( 107 | method: method, 108 | url: url, 109 | headers: headers, 110 | body: body 111 | ) 112 | } 113 | 114 | public init?(method: Method = .get, url: String, headers: Headers = [:], body: InputStream) { 115 | guard let url = URL(string: url) else { 116 | return nil 117 | } 118 | 119 | self.init( 120 | method: method, 121 | url: url, 122 | headers: headers, 123 | body: body 124 | ) 125 | } 126 | 127 | public init?(method: Method = .get, url: String, headers: Headers = [:], body: @escaping (OutputStream) throws -> Void) { 128 | guard let url = URL(string: url) else { 129 | return nil 130 | } 131 | 132 | self.init( 133 | method: method, 134 | url: url, 135 | headers: headers, 136 | body: body 137 | ) 138 | } 139 | } 140 | 141 | extension Request { 142 | public var path: String? { 143 | return url.path 144 | } 145 | 146 | public var queryItems: [URLQueryItem] { 147 | return url.queryItems 148 | } 149 | } 150 | 151 | extension Request { 152 | public var accept: [MediaType] { 153 | get { 154 | var acceptedMediaTypes: [MediaType] = [] 155 | 156 | if let acceptString = headers["Accept"] { 157 | let acceptedTypesString = acceptString.split(separator: ",") 158 | 159 | for acceptedTypeString in acceptedTypesString { 160 | let acceptedTypeTokens = acceptedTypeString.split(separator: ";") 161 | 162 | if acceptedTypeTokens.count >= 1 { 163 | let mediaTypeString = acceptedTypeTokens[0].trim() 164 | if let acceptedMediaType = try? MediaType(string: mediaTypeString) { 165 | acceptedMediaTypes.append(acceptedMediaType) 166 | } 167 | } 168 | } 169 | } 170 | 171 | return acceptedMediaTypes 172 | } 173 | 174 | set(accept) { 175 | headers["Accept"] = accept.map({$0.type + "/" + $0.subtype}).joined(separator: ", ") 176 | } 177 | } 178 | 179 | public var cookies: Set { 180 | get { 181 | return headers["Cookie"].flatMap({Set(cookieHeader: $0)}) ?? [] 182 | } 183 | 184 | set(cookies) { 185 | headers["Cookie"] = cookies.map({$0.description}).joined(separator: ", ") 186 | } 187 | } 188 | 189 | public var authorization: String? { 190 | get { 191 | return headers["Authorization"] 192 | } 193 | 194 | set(authorization) { 195 | headers["Authorization"] = authorization 196 | } 197 | } 198 | 199 | public var host: String? { 200 | get { 201 | return headers["Host"] 202 | } 203 | 204 | set(host) { 205 | headers["Host"] = host 206 | } 207 | } 208 | 209 | public var userAgent: String? { 210 | get { 211 | return headers["User-Agent"] 212 | } 213 | 214 | set(userAgent) { 215 | headers["User-Agent"] = userAgent 216 | } 217 | } 218 | } 219 | 220 | extension Request { 221 | public typealias UpgradeConnection = (Response, Stream) throws -> Void 222 | 223 | public var upgradeConnection: UpgradeConnection? { 224 | return storage["request-connection-upgrade"] as? UpgradeConnection 225 | } 226 | 227 | public mutating func upgradeConnection(_ upgrade: @escaping UpgradeConnection) { 228 | storage["request-connection-upgrade"] = upgrade 229 | } 230 | } 231 | 232 | extension Request { 233 | public var pathParameters: [String: String] { 234 | get { 235 | return storage["pathParameters"] as? [String: String] ?? [:] 236 | } 237 | 238 | set(pathParameters) { 239 | storage["pathParameters"] = pathParameters 240 | } 241 | } 242 | } 243 | 244 | extension Request : CustomStringConvertible { 245 | public var requestLineDescription: String { 246 | return String(describing: method) + " " + url.absoluteString + " HTTP/" + String(describing: version.major) + "." + String(describing: version.minor) + "\n" 247 | } 248 | 249 | public var description: String { 250 | return requestLineDescription + 251 | headers.description 252 | } 253 | } 254 | 255 | extension Request : CustomDebugStringConvertible { 256 | public var debugDescription: String { 257 | return description + "\n" + storageDescription 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/Response.swift: -------------------------------------------------------------------------------- 1 | public struct Response : Message { 2 | public enum Status { 3 | case `continue` 4 | case switchingProtocols 5 | case processing 6 | 7 | case ok 8 | case created 9 | case accepted 10 | case nonAuthoritativeInformation 11 | case noContent 12 | case resetContent 13 | case partialContent 14 | 15 | case multipleChoices 16 | case movedPermanently 17 | case found 18 | case seeOther 19 | case notModified 20 | case useProxy 21 | case switchProxy 22 | case temporaryRedirect 23 | case permanentRedirect 24 | 25 | case badRequest 26 | case unauthorized 27 | case paymentRequired 28 | case forbidden 29 | case notFound 30 | case methodNotAllowed 31 | case notAcceptable 32 | case proxyAuthenticationRequired 33 | case requestTimeout 34 | case conflict 35 | case gone 36 | case lengthRequired 37 | case preconditionFailed 38 | case requestEntityTooLarge 39 | case requestURITooLong 40 | case unsupportedMediaType 41 | case requestedRangeNotSatisfiable 42 | case expectationFailed 43 | case imATeapot 44 | case authenticationTimeout 45 | case enhanceYourCalm 46 | case unprocessableEntity 47 | case locked 48 | case failedDependency 49 | case preconditionRequired 50 | case tooManyRequests 51 | case requestHeaderFieldsTooLarge 52 | 53 | case internalServerError 54 | case notImplemented 55 | case badGateway 56 | case serviceUnavailable 57 | case gatewayTimeout 58 | case httpVersionNotSupported 59 | case variantAlsoNegotiates 60 | case insufficientStorage 61 | case loopDetected 62 | case notExtended 63 | case networkAuthenticationRequired 64 | 65 | case other(statusCode: Int, reasonPhrase: String) 66 | } 67 | 68 | public var version: Version 69 | public var status: Status 70 | public var headers: Headers 71 | public var cookieHeaders: Set 72 | public var body: Body 73 | public var storage: [String: Any] = [:] 74 | 75 | public init(version: Version, status: Status, headers: Headers, cookieHeaders: Set, body: Body) { 76 | self.version = version 77 | self.status = status 78 | self.headers = headers 79 | self.cookieHeaders = cookieHeaders 80 | self.body = body 81 | } 82 | } 83 | 84 | public protocol ResponseInitializable { 85 | init(response: Response) 86 | } 87 | 88 | public protocol ResponseRepresentable { 89 | var response: Response { get } 90 | } 91 | 92 | public protocol ResponseConvertible : ResponseInitializable, ResponseRepresentable {} 93 | 94 | extension Response : ResponseConvertible { 95 | public init(response: Response) { 96 | self = response 97 | } 98 | 99 | public var response: Response { 100 | return self 101 | } 102 | } 103 | 104 | extension Response { 105 | public init(status: Status = .ok, headers: Headers = [:], body: Body) { 106 | self.init( 107 | version: Version(major: 1, minor: 1), 108 | status: status, 109 | headers: headers, 110 | cookieHeaders: [], 111 | body: body 112 | ) 113 | 114 | switch body { 115 | case let .buffer(body): 116 | self.headers["Content-Length"] = body.count.description 117 | default: 118 | self.headers["Transfer-Encoding"] = "chunked" 119 | } 120 | } 121 | 122 | public init(status: Status = .ok, headers: Headers = [:], body: BufferRepresentable = Buffer()) { 123 | self.init( 124 | status: status, 125 | headers: headers, 126 | body: .buffer(body.buffer) 127 | ) 128 | } 129 | 130 | public init(status: Status = .ok, headers: Headers = [:], body: InputStream) { 131 | self.init( 132 | status: status, 133 | headers: headers, 134 | body: .reader(body) 135 | ) 136 | } 137 | 138 | public init(status: Status = .ok, headers: Headers = [:], body: @escaping (OutputStream) throws -> Void) { 139 | self.init( 140 | status: status, 141 | headers: headers, 142 | body: .writer(body) 143 | ) 144 | } 145 | } 146 | 147 | extension Response { 148 | public var statusCode: Int { 149 | return status.statusCode 150 | } 151 | 152 | public var isInformational: Bool { 153 | return status.isInformational 154 | } 155 | 156 | public var isSuccessfull: Bool { 157 | return status.isSuccessful 158 | } 159 | 160 | public var isRedirection: Bool { 161 | return status.isRedirection 162 | } 163 | 164 | public var isError: Bool { 165 | return status.isError 166 | } 167 | 168 | public var isClientError: Bool { 169 | return status.isClientError 170 | } 171 | 172 | public var isServerError: Bool { 173 | return status.isServerError 174 | } 175 | 176 | public var reasonPhrase: String { 177 | return status.reasonPhrase 178 | } 179 | } 180 | 181 | extension Response { 182 | public var cookies: Set { 183 | get { 184 | var cookies = Set() 185 | 186 | for header in cookieHeaders { 187 | if let cookie = AttributedCookie(header) { 188 | cookies.insert(cookie) 189 | } 190 | } 191 | 192 | return cookies 193 | } 194 | 195 | set(cookies) { 196 | var headers = Set() 197 | 198 | for cookie in cookies { 199 | let header = String(describing: cookie) 200 | headers.insert(header) 201 | } 202 | 203 | cookieHeaders = headers 204 | } 205 | } 206 | } 207 | 208 | extension Response { 209 | public typealias UpgradeConnection = (Request, Stream) throws -> Void 210 | 211 | public var upgradeConnection: UpgradeConnection? { 212 | return storage["response-connection-upgrade"] as? UpgradeConnection 213 | } 214 | 215 | public mutating func upgradeConnection(_ upgrade: @escaping UpgradeConnection) { 216 | storage["response-connection-upgrade"] = upgrade 217 | } 218 | } 219 | 220 | extension Response : CustomStringConvertible { 221 | public var statusLineDescription: String { 222 | return "HTTP/" + String(version.major) + "." + String(version.minor) + " " + String(statusCode) + " " + reasonPhrase + "\n" 223 | } 224 | 225 | public var description: String { 226 | return statusLineDescription + 227 | headers.description 228 | } 229 | } 230 | 231 | extension Response : CustomDebugStringConvertible { 232 | public var debugDescription: String { 233 | return description + "\n" + storageDescription 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/Status.swift: -------------------------------------------------------------------------------- 1 | extension Response.Status { 2 | public init(statusCode: Int, reasonPhrase: String? = nil) { 3 | if let reasonPhrase = reasonPhrase { 4 | self = .other(statusCode: statusCode, reasonPhrase: reasonPhrase) 5 | } else { 6 | switch statusCode { 7 | case Response.Status.`continue`.statusCode: self = .`continue` 8 | case Response.Status.switchingProtocols.statusCode: self = .switchingProtocols 9 | case Response.Status.processing.statusCode: self = .processing 10 | 11 | case Response.Status.ok.statusCode: self = .ok 12 | case Response.Status.created.statusCode: self = .created 13 | case Response.Status.accepted.statusCode: self = .accepted 14 | case Response.Status.nonAuthoritativeInformation.statusCode: self = .nonAuthoritativeInformation 15 | case Response.Status.noContent.statusCode: self = .noContent 16 | case Response.Status.resetContent.statusCode: self = .resetContent 17 | case Response.Status.partialContent.statusCode: self = .partialContent 18 | 19 | case Response.Status.multipleChoices.statusCode: self = .multipleChoices 20 | case Response.Status.movedPermanently.statusCode: self = .movedPermanently 21 | case Response.Status.found.statusCode: self = .found 22 | case Response.Status.seeOther.statusCode: self = .seeOther 23 | case Response.Status.notModified.statusCode: self = .notModified 24 | case Response.Status.useProxy.statusCode: self = .useProxy 25 | case Response.Status.switchProxy.statusCode: self = .switchProxy 26 | case Response.Status.temporaryRedirect.statusCode: self = .temporaryRedirect 27 | case Response.Status.permanentRedirect.statusCode: self = .permanentRedirect 28 | 29 | case Response.Status.badRequest.statusCode: self = .badRequest 30 | case Response.Status.unauthorized.statusCode: self = .unauthorized 31 | case Response.Status.paymentRequired.statusCode: self = .paymentRequired 32 | case Response.Status.forbidden.statusCode: self = .forbidden 33 | case Response.Status.notFound.statusCode: self = .notFound 34 | case Response.Status.methodNotAllowed.statusCode: self = .methodNotAllowed 35 | case Response.Status.notAcceptable.statusCode: self = .notAcceptable 36 | case Response.Status.proxyAuthenticationRequired.statusCode: self = .proxyAuthenticationRequired 37 | case Response.Status.requestTimeout.statusCode: self = .requestTimeout 38 | case Response.Status.conflict.statusCode: self = .conflict 39 | case Response.Status.gone.statusCode: self = .gone 40 | case Response.Status.lengthRequired.statusCode: self = .lengthRequired 41 | case Response.Status.preconditionFailed.statusCode: self = .preconditionFailed 42 | case Response.Status.requestEntityTooLarge.statusCode: self = .requestEntityTooLarge 43 | case Response.Status.requestURITooLong.statusCode: self = .requestURITooLong 44 | case Response.Status.unsupportedMediaType.statusCode: self = .unsupportedMediaType 45 | case Response.Status.requestedRangeNotSatisfiable.statusCode: self = .requestedRangeNotSatisfiable 46 | case Response.Status.expectationFailed.statusCode: self = .expectationFailed 47 | case Response.Status.imATeapot.statusCode: self = .imATeapot 48 | case Response.Status.authenticationTimeout.statusCode: self = .authenticationTimeout 49 | case Response.Status.enhanceYourCalm.statusCode: self = .enhanceYourCalm 50 | case Response.Status.unprocessableEntity.statusCode: self = .unprocessableEntity 51 | case Response.Status.locked.statusCode: self = .locked 52 | case Response.Status.failedDependency.statusCode: self = .failedDependency 53 | case Response.Status.preconditionRequired.statusCode: self = .preconditionRequired 54 | case Response.Status.tooManyRequests.statusCode: self = .tooManyRequests 55 | case Response.Status.requestHeaderFieldsTooLarge.statusCode: self = .requestHeaderFieldsTooLarge 56 | 57 | case Response.Status.internalServerError.statusCode: self = .internalServerError 58 | case Response.Status.notImplemented.statusCode: self = .notImplemented 59 | case Response.Status.badGateway.statusCode: self = .badGateway 60 | case Response.Status.serviceUnavailable.statusCode: self = .serviceUnavailable 61 | case Response.Status.gatewayTimeout.statusCode: self = .gatewayTimeout 62 | case Response.Status.httpVersionNotSupported.statusCode: self = .httpVersionNotSupported 63 | case Response.Status.variantAlsoNegotiates.statusCode: self = .variantAlsoNegotiates 64 | case Response.Status.insufficientStorage.statusCode: self = .insufficientStorage 65 | case Response.Status.loopDetected.statusCode: self = .loopDetected 66 | case Response.Status.notExtended.statusCode: self = .notExtended 67 | case Response.Status.networkAuthenticationRequired.statusCode: self = .networkAuthenticationRequired 68 | 69 | default: self = .other(statusCode: statusCode, reasonPhrase: "CUSTOM") 70 | } 71 | } 72 | } 73 | } 74 | 75 | extension Response.Status { 76 | public var statusCode: Int { 77 | switch self { 78 | case .`continue`: return 100 79 | case .switchingProtocols: return 101 80 | case .processing: return 102 81 | 82 | case .ok: return 200 83 | case .created: return 201 84 | case .accepted: return 202 85 | case .nonAuthoritativeInformation: return 203 86 | case .noContent: return 204 87 | case .resetContent: return 205 88 | case .partialContent: return 206 89 | 90 | case .multipleChoices: return 300 91 | case .movedPermanently: return 301 92 | case .found: return 302 93 | case .seeOther: return 303 94 | case .notModified: return 304 95 | case .useProxy: return 305 96 | case .switchProxy: return 306 97 | case .temporaryRedirect: return 307 98 | case .permanentRedirect: return 308 99 | 100 | 101 | case .badRequest: return 400 102 | case .unauthorized: return 401 103 | case .paymentRequired: return 402 104 | case .forbidden: return 403 105 | case .notFound: return 404 106 | case .methodNotAllowed: return 405 107 | case .notAcceptable: return 406 108 | case .proxyAuthenticationRequired: return 407 109 | case .requestTimeout: return 408 110 | case .conflict: return 409 111 | case .gone: return 410 112 | case .lengthRequired: return 411 113 | case .preconditionFailed: return 412 114 | case .requestEntityTooLarge: return 413 115 | case .requestURITooLong: return 414 116 | case .unsupportedMediaType: return 415 117 | case .requestedRangeNotSatisfiable: return 416 118 | case .expectationFailed: return 417 119 | case .imATeapot: return 418 120 | case .authenticationTimeout: return 419 121 | case .enhanceYourCalm: return 420 122 | case .unprocessableEntity: return 422 123 | case .locked: return 423 124 | case .failedDependency: return 424 125 | case .preconditionRequired: return 428 126 | case .tooManyRequests: return 429 127 | case .requestHeaderFieldsTooLarge: return 431 128 | 129 | case .internalServerError: return 500 130 | case .notImplemented: return 501 131 | case .badGateway: return 502 132 | case .serviceUnavailable: return 503 133 | case .gatewayTimeout: return 504 134 | case .httpVersionNotSupported: return 505 135 | case .variantAlsoNegotiates: return 506 136 | case .insufficientStorage: return 507 137 | case .loopDetected: return 508 138 | case .notExtended: return 510 139 | case .networkAuthenticationRequired: return 511 140 | 141 | case .other(let statusCode, _): return statusCode 142 | } 143 | } 144 | } 145 | 146 | extension Response.Status { 147 | public var reasonPhrase: String { 148 | switch self { 149 | case .`continue`: return "Continue" 150 | case .switchingProtocols: return "Switching Protocols" 151 | case .processing: return "Processing" 152 | 153 | case .ok: return "OK" 154 | case .created: return "Created" 155 | case .accepted: return "Accepted" 156 | case .nonAuthoritativeInformation: return "Non Authoritative Information" 157 | case .noContent: return "No Content" 158 | case .resetContent: return "Reset Content" 159 | case .partialContent: return "Partial Content" 160 | 161 | case .multipleChoices: return "Multiple Choices" 162 | case .movedPermanently: return "Moved Permanently" 163 | case .found: return "Found" 164 | case .seeOther: return "See Other" 165 | case .notModified: return "Not Modified" 166 | case .useProxy: return "Use Proxy" 167 | case .switchProxy: return "Switch Proxy" 168 | case .temporaryRedirect: return "Temporary Redirect" 169 | case .permanentRedirect: return "Permanent Redirect" 170 | 171 | case .badRequest: return "Bad Request" 172 | case .unauthorized: return "Unauthorized" 173 | case .paymentRequired: return "Payment Required" 174 | case .forbidden: return "Forbidden" 175 | case .notFound: return "Not Found" 176 | case .methodNotAllowed: return "Method Not Allowed" 177 | case .notAcceptable: return "Not Acceptable" 178 | case .proxyAuthenticationRequired: return "Proxy Authentication Required" 179 | case .requestTimeout: return "Request Timeout" 180 | case .conflict: return "Conflict" 181 | case .gone: return "Gone" 182 | case .lengthRequired: return "Length Required" 183 | case .preconditionFailed: return "Precondition Failed" 184 | case .requestEntityTooLarge: return "Request Entity Too Large" 185 | case .requestURITooLong: return "Request URI Too Long" 186 | case .unsupportedMediaType: return "Unsupported Media Type" 187 | case .requestedRangeNotSatisfiable: return "Requested Range Not Satisfiable" 188 | case .expectationFailed: return "Expectation Failed" 189 | case .imATeapot: return "I'm A Teapot" 190 | case .authenticationTimeout: return "Authentication Timeout" 191 | case .enhanceYourCalm: return "Enhance Your Calm" 192 | case .unprocessableEntity: return "Unprocessable Entity" 193 | case .locked: return "Locked" 194 | case .failedDependency: return "Failed Dependency" 195 | case .preconditionRequired: return "Precondition Required" 196 | case .tooManyRequests: return "Too Many Requests" 197 | case .requestHeaderFieldsTooLarge: return "Request Header Fields Too Large" 198 | 199 | case .internalServerError: return "Internal Server Error" 200 | case .notImplemented: return "Not Implemented" 201 | case .badGateway: return "Bad Gateway" 202 | case .serviceUnavailable: return "Service Unavailable" 203 | case .gatewayTimeout: return "Gateway Timeout" 204 | case .httpVersionNotSupported: return "HTTP Version Not Supported" 205 | case .variantAlsoNegotiates: return "Variant Also Negotiates" 206 | case .insufficientStorage: return "Insufficient Storage" 207 | case .loopDetected: return "Loop Detected" 208 | case .notExtended: return "Not Extended" 209 | case .networkAuthenticationRequired: return "Network Authentication Required" 210 | 211 | case .other(_, let reasonPhrase): return reasonPhrase 212 | } 213 | } 214 | } 215 | 216 | extension Response.Status { 217 | public var isInformational: Bool { 218 | return 100 ..< 200 ~= statusCode 219 | } 220 | 221 | public var isSuccessful: Bool { 222 | return 200 ..< 300 ~= statusCode 223 | } 224 | 225 | public var isRedirection: Bool { 226 | return 300 ..< 400 ~= statusCode 227 | } 228 | 229 | public var isError: Bool { 230 | return 400 ..< 600 ~= statusCode 231 | } 232 | 233 | public var isClientError: Bool { 234 | return 400 ..< 500 ~= statusCode 235 | } 236 | 237 | public var isServerError: Bool { 238 | return 500 ..< 600 ~= statusCode 239 | } 240 | } 241 | 242 | extension Response.Status : Hashable { 243 | public var hashValue: Int { 244 | return statusCode 245 | } 246 | } 247 | 248 | public func ==(lhs: Response.Status, rhs: Response.Status) -> Bool { 249 | return lhs.hashValue == rhs.hashValue 250 | } 251 | -------------------------------------------------------------------------------- /Sources/HTTP/Message/Version.swift: -------------------------------------------------------------------------------- 1 | public struct Version { 2 | public var major: Int 3 | public var minor: Int 4 | 5 | public init(major: Int, minor: Int) { 6 | self.major = major 7 | self.minor = minor 8 | } 9 | } 10 | 11 | extension Version : Hashable { 12 | public var hashValue: Int { 13 | return major ^ minor 14 | } 15 | } 16 | 17 | extension Version : Equatable {} 18 | 19 | public func == (lhs: Version, rhs: Version) -> Bool { 20 | return lhs.hashValue == rhs.hashValue 21 | } 22 | -------------------------------------------------------------------------------- /Sources/HTTP/Middleware/BasicAuthMiddleware/BasicAuthMiddleware.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Data 2 | 3 | public enum AuthenticationResult { 4 | case accessDenied 5 | case authenticated 6 | case payload(key: String, value: Any) 7 | } 8 | 9 | enum AuthenticationType { 10 | case server(realm: String?, authenticate: (_ username: String, _ password: String) throws -> AuthenticationResult) 11 | case client(username: String, password: String) 12 | } 13 | 14 | public struct BasicAuthMiddleware : Middleware { 15 | let type: AuthenticationType 16 | 17 | public init(realm: String? = nil, authenticate: @escaping (_ username: String, _ password: String) throws -> AuthenticationResult) { 18 | type = .server(realm: realm, authenticate: authenticate) 19 | } 20 | 21 | public init(username: String, password: String) { 22 | type = .client(username: username, password: password) 23 | } 24 | 25 | public func respond(to request: Request, chainingTo chain: Responder) throws -> Response { 26 | switch type { 27 | case .server(let realm, let authenticate): 28 | return try serverRespond(request, chain: chain, realm: realm, authenticate: authenticate) 29 | case .client(let username, let password): 30 | return try clientRespond(request, chain: chain, username: username, password: password) 31 | } 32 | } 33 | 34 | public func serverRespond(_ request: Request, chain: Responder, realm: String? = nil, authenticate: (_ username: String, _ password: String) throws -> AuthenticationResult) throws -> Response { 35 | var deniedResponse : Response 36 | if let realm = realm { 37 | deniedResponse = Response(status: .unauthorized, headers: ["WWW-Authenticate": "Basic realm=\"\(realm)\""]) 38 | } else { 39 | deniedResponse = Response(status: .unauthorized) 40 | } 41 | 42 | guard let authorization = request.authorization else { 43 | return deniedResponse 44 | } 45 | 46 | let tokens = authorization.split(separator: " ") 47 | 48 | guard tokens.count == 2 || tokens.first == "Basic" else { 49 | return deniedResponse 50 | } 51 | 52 | guard 53 | let decodedData = Data(base64Encoded: tokens[1]), 54 | let decodedCredentials = String(data: decodedData, encoding: .utf8) 55 | else { 56 | return deniedResponse 57 | } 58 | let credentials = decodedCredentials.split(separator: ":") 59 | 60 | guard credentials.count == 2 else { 61 | return deniedResponse 62 | } 63 | 64 | let username = credentials[0] 65 | let password = credentials[1] 66 | 67 | switch try authenticate(username, password) { 68 | case .accessDenied: 69 | return deniedResponse 70 | case .authenticated: 71 | return try chain.respond(to: request) 72 | case .payload(let key, let value): 73 | var request = request 74 | request.storage[key] = value 75 | return try chain.respond(to: request) 76 | } 77 | } 78 | 79 | public func clientRespond(_ request: Request, chain: Responder, username: String, password: String) throws -> Response { 80 | var request = request 81 | let credentials = Data("\(username):\(password)".utf8).base64EncodedString() 82 | request.authorization = "Basic " + credentials 83 | return try chain.respond(to: request) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/HTTP/Middleware/ContentNegotiationMiddleware/ContentMapperMiddleware.swift: -------------------------------------------------------------------------------- 1 | extension MapInitializable { 2 | public static var contentMapperKey: String { 3 | return String(reflecting: type(of: self)) 4 | } 5 | } 6 | 7 | public struct ContentMapperMiddleware : Middleware { 8 | let type: MapInitializable.Type 9 | public let mode: Mode 10 | 11 | public enum Mode { 12 | case server 13 | case client 14 | } 15 | 16 | public init(mappingTo type: MapInitializable.Type, mode: Mode = .server) { 17 | self.type = type 18 | self.mode = mode 19 | } 20 | 21 | public func respond(to request: Request, chainingTo next: Responder) throws -> Response { 22 | switch mode { 23 | case .server: 24 | return try serverRespond(to: request, chainingTo: next) 25 | case .client: 26 | return try clientRespond(to: request, chainingTo: next) 27 | } 28 | } 29 | 30 | public func serverRespond(to request: Request, chainingTo next: Responder) throws -> Response { 31 | guard let content = request.content else { 32 | return try next.respond(to: request) 33 | } 34 | 35 | var request = request 36 | 37 | do { 38 | let target = try type.init(map: content) 39 | request.storage[type.contentMapperKey] = target 40 | } catch MapError.incompatibleType { 41 | // TODO: Use custom error but make it ResponseConvertible 42 | throw HTTPError.badRequest 43 | } 44 | 45 | return try next.respond(to: request) 46 | } 47 | 48 | public func clientRespond(to request: Request, chainingTo next: Responder) throws -> Response { 49 | var response = try next.respond(to: request) 50 | 51 | guard let content = response.content else { 52 | return response 53 | } 54 | 55 | do { 56 | let target = try type.init(map: content) 57 | response.storage[type.contentMapperKey] = target 58 | } catch MapError.incompatibleType { 59 | // TODO: Use custom error but make it ResponseConvertible 60 | throw HTTPError.badRequest 61 | } 62 | 63 | return response 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/HTTP/Middleware/ContentNegotiationMiddleware/ContentNegotiatonMiddleware.swift: -------------------------------------------------------------------------------- 1 | public enum ContentNegotiationMiddlewareError : Error { 2 | case noSuitableParser 3 | case noSuitableSerializer 4 | case writerBodyNotSupported 5 | } 6 | 7 | public struct ContentNegotiationMiddleware : Middleware { 8 | public enum Mode { 9 | case server 10 | case client 11 | } 12 | 13 | public enum SerializationMode { 14 | case buffer 15 | case stream 16 | } 17 | 18 | private let converters: [MediaTypeConverter] 19 | private let mediaTypes: [MediaType] 20 | private let mode: Mode 21 | private let serializationMode: SerializationMode 22 | 23 | public init(mediaTypes: [MediaTypeConverter], mode: Mode = .server, serializationMode: SerializationMode = .stream) { 24 | self.converters = mediaTypes 25 | self.mediaTypes = converters.map({$0.mediaType}) 26 | self.mode = mode 27 | self.serializationMode = serializationMode 28 | } 29 | 30 | public func respond(to request: Request, chainingTo chain: Responder) throws -> Response { 31 | switch mode { 32 | case .server: 33 | return try serverRespond(to: request, chainingTo: chain) 34 | case .client: 35 | return try clientRespond(to: request, chainingTo: chain) 36 | } 37 | } 38 | 39 | private func serverRespond(to request: Request, chainingTo chain: Responder) throws -> Response { 40 | var request = request 41 | 42 | if let contentType = request.contentType, !request.body.isEmpty { 43 | do { 44 | let content: Map 45 | 46 | switch request.body { 47 | case .buffer(let buffer): 48 | content = try parse(buffer: buffer, mediaType: contentType) 49 | case .reader(let stream): 50 | content = try parse(stream: stream, deadline: .never, mediaType: contentType) 51 | case .writer: 52 | // TODO: Deal with writer bodies 53 | throw ContentNegotiationMiddlewareError.writerBodyNotSupported 54 | } 55 | request.content = content 56 | } catch ContentNegotiationMiddlewareError.noSuitableParser { 57 | throw HTTPError.unsupportedMediaType 58 | } 59 | } 60 | 61 | var response = try chain.respond(to: request) 62 | 63 | if let content = response.content { 64 | let mediaTypes: [MediaType] 65 | 66 | if let contentType = response.contentType { 67 | mediaTypes = [contentType] 68 | } else { 69 | mediaTypes = request.accept.isEmpty ? self.mediaTypes : request.accept 70 | } 71 | 72 | response.content = nil 73 | 74 | switch serializationMode { 75 | case .buffer: 76 | let (mediaType, buffer) = try serializeToBuffer(from: content, mediaTypes: mediaTypes) 77 | response.contentType = mediaType 78 | // TODO: Maybe add `willSet` to `body` and configure the headers there 79 | response.transferEncoding = nil 80 | response.contentLength = buffer.count 81 | response.body = .buffer(buffer) 82 | case .stream: 83 | let (mediaType, writer) = try serializeToStream(from: content, deadline: .never, mediaTypes: mediaTypes) 84 | response.contentType = mediaType 85 | // TODO: Maybe add `willSet` to `body` and configure the headers there 86 | response.contentLength = nil 87 | response.transferEncoding = "chunked" 88 | response.body = .writer(writer) 89 | } 90 | } 91 | 92 | return response 93 | } 94 | 95 | private func clientRespond(to request: Request, chainingTo chain: Responder) throws -> Response { 96 | var request = request 97 | 98 | request.accept = mediaTypes 99 | 100 | if let content = request.content { 101 | let mediaTypes: [MediaType] 102 | 103 | if let contentType = request.contentType { 104 | mediaTypes = [contentType] 105 | } else { 106 | mediaTypes = self.mediaTypes 107 | } 108 | 109 | request.content = nil 110 | 111 | switch serializationMode { 112 | case .buffer: 113 | let (mediaType, buffer) = try serializeToBuffer(from: content, mediaTypes: mediaTypes) 114 | request.contentType = mediaType 115 | // TODO: Maybe add `willSet` to `body` and configure the headers there 116 | request.transferEncoding = nil 117 | request.contentLength = buffer.count 118 | request.body = .buffer(buffer) 119 | case .stream: 120 | let (mediaType, writer) = try serializeToStream(from: content, deadline: .never, mediaTypes: mediaTypes) 121 | request.contentType = mediaType 122 | // TODO: Maybe add `willSet` to `body` and configure the headers there 123 | request.contentLength = nil 124 | request.transferEncoding = "chunked" 125 | request.body = .writer(writer) 126 | } 127 | } 128 | 129 | var response = try chain.respond(to: request) 130 | 131 | if let contentType = response.contentType { 132 | let content: Map 133 | 134 | switch response.body { 135 | case .buffer(let buffer): 136 | content = try parse(buffer: buffer, mediaType: contentType) 137 | case .reader(let stream): 138 | content = try parse(stream: stream, deadline: .never, mediaType: contentType) 139 | case .writer: 140 | // TODO: Deal with writer bodies 141 | throw ContentNegotiationMiddlewareError.writerBodyNotSupported 142 | } 143 | 144 | response.content = content 145 | } 146 | 147 | return response 148 | } 149 | 150 | private func parserTypes(for mediaType: MediaType) -> [MapParser.Type] { 151 | var parsers: [MapParser.Type] = [] 152 | 153 | for converter in converters where converter.mediaType.matches(other: mediaType) { 154 | parsers.append(converter.parser) 155 | } 156 | 157 | return parsers 158 | } 159 | 160 | private func firstParserType(for mediaType: MediaType) throws -> MapParser.Type { 161 | guard let first = parserTypes(for: mediaType).first else { 162 | throw ContentNegotiationMiddlewareError.noSuitableParser 163 | } 164 | 165 | return first 166 | } 167 | 168 | private func serializerTypes(for mediaType: MediaType) -> [(MediaType, MapSerializer.Type)] { 169 | var serializers: [(MediaType, MapSerializer.Type)] = [] 170 | 171 | for converter in converters where converter.mediaType.matches(other: mediaType) { 172 | serializers.append(converter.mediaType, converter.serializer) 173 | } 174 | 175 | return serializers 176 | } 177 | 178 | private func firstSerializerType(for mediaType: MediaType) throws -> (MediaType, MapSerializer.Type) { 179 | guard let first = serializerTypes(for: mediaType).first else { 180 | throw ContentNegotiationMiddlewareError.noSuitableSerializer 181 | } 182 | 183 | return first 184 | } 185 | 186 | private func parse(stream: InputStream, deadline: Double, mediaType: MediaType) throws -> Map { 187 | let parserType = try firstParserType(for: mediaType) 188 | 189 | do { 190 | return try parserType.parse(stream, deadline: deadline) 191 | } catch { 192 | throw ContentNegotiationMiddlewareError.noSuitableParser 193 | } 194 | } 195 | 196 | private func parse(buffer: Buffer, mediaType: MediaType) throws -> Map { 197 | var lastError: Error? 198 | 199 | for parserType in parserTypes(for: mediaType) { 200 | do { 201 | return try parserType.parse(buffer) 202 | } catch { 203 | lastError = error 204 | continue 205 | } 206 | } 207 | 208 | if let lastError = lastError { 209 | throw lastError 210 | } else { 211 | throw ContentNegotiationMiddlewareError.noSuitableParser 212 | } 213 | } 214 | 215 | private func serializeToStream(from content: Map, deadline: Double, mediaTypes: [MediaType]) throws -> (MediaType, (OutputStream) throws -> Void) { 216 | for acceptedType in mediaTypes { 217 | for (mediaType, serializerType) in serializerTypes(for: acceptedType) { 218 | return (mediaType, { stream in 219 | try serializerType.serialize(content, stream: stream, deadline: deadline) 220 | }) 221 | } 222 | } 223 | 224 | throw ContentNegotiationMiddlewareError.noSuitableSerializer 225 | } 226 | 227 | private func serializeToBuffer(from content: Map, mediaTypes: [MediaType]) throws -> (MediaType, Buffer) { 228 | var lastError: Error? 229 | 230 | for acceptedType in mediaTypes { 231 | for (mediaType, serializerType) in serializerTypes(for: acceptedType) { 232 | do { 233 | let buffer = try serializerType.serialize(content) 234 | return (mediaType, buffer) 235 | } catch { 236 | lastError = error 237 | continue 238 | } 239 | } 240 | } 241 | 242 | if let lastError = lastError { 243 | throw lastError 244 | } else { 245 | throw ContentNegotiationMiddlewareError.noSuitableSerializer 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /Sources/HTTP/Middleware/LogMiddleware/LogMiddleware.swift: -------------------------------------------------------------------------------- 1 | import Axis 2 | 3 | public struct LogMiddleware : Middleware { 4 | private let debug: Bool 5 | private let stream: OutputStream? 6 | private let timeout: Double 7 | 8 | public init(debug: Bool = false, stream: OutputStream? = nil, timeout: Double = 30.seconds) { 9 | self.debug = debug 10 | self.stream = stream 11 | self.timeout = timeout 12 | } 13 | 14 | public func respond(to request: Request, chainingTo next: Responder) throws -> Response { 15 | let response = try next.respond(to: request) 16 | var message: String = "" 17 | message = "================================================================================\n" 18 | message += "Request:\n\n" 19 | message += (debug ? String(describing: request.debugDescription) : String(describing: request)) + "\n" 20 | message += "--------------------------------------------------------------------------------\n" 21 | message += "Response:\n\n" 22 | message += (debug ? String(describing: response.debugDescription) : String(describing: response)) + "\n" 23 | message += "================================================================================\n" 24 | 25 | if let stream = stream { 26 | try stream.write(message, deadline: timeout.fromNow()) 27 | } else { 28 | print(message) 29 | } 30 | 31 | return response 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/HTTP/Middleware/Middleware.swift: -------------------------------------------------------------------------------- 1 | public protocol Middleware { 2 | func respond(to request: Request, chainingTo next: Responder) throws -> Response 3 | } 4 | 5 | extension Middleware { 6 | public func chain(to responder: Responder) -> Responder { 7 | return BasicResponder { (request: Request) throws -> Response in 8 | return try self.respond(to: request, chainingTo: responder) 9 | } 10 | } 11 | } 12 | 13 | extension Collection where Self.Iterator.Element == Middleware { 14 | public func chain(to responder: Responder) -> Responder { 15 | var responder = responder 16 | 17 | for middleware in self.reversed() { 18 | responder = middleware.chain(to: responder) 19 | } 20 | 21 | return responder 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/HTTP/Middleware/RecoveryMiddleware/RecoveryMiddleware.swift: -------------------------------------------------------------------------------- 1 | public typealias Recover = (Error) throws -> Response 2 | 3 | public struct RecoveryMiddleware : Middleware { 4 | let recover: Recover 5 | 6 | public init(_ recover: @escaping Recover = RecoveryMiddleware.recover) { 7 | self.recover = recover 8 | } 9 | 10 | public func respond(to request: Request, chainingTo chain: Responder) throws -> Response { 11 | do { 12 | return try chain.respond(to: request) 13 | } catch { 14 | return try recover(error) 15 | } 16 | } 17 | 18 | public static func recover(error: Error) throws -> Response { 19 | switch error { 20 | case let error as ClientError: 21 | return Response(status: error.status, body: "\(error.status.statusCode) \(error.status.reasonPhrase)") 22 | case let error as ServerError: 23 | return Response(status: error.status, body: "\(error.status.statusCode) \(error.status.reasonPhrase)") 24 | case let error as ResponseRepresentable: 25 | return error.response 26 | default: 27 | throw error 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/HTTP/Middleware/RedirectMiddleware/RedirectMiddleware.swift: -------------------------------------------------------------------------------- 1 | public struct RedirectMiddleware : Middleware { 2 | let location: String 3 | let shouldRedirect: (Request) -> Bool 4 | 5 | public init(redirectTo location: String, if shouldRedirect: @escaping (Request) -> Bool) { 6 | self.location = location 7 | self.shouldRedirect = shouldRedirect 8 | } 9 | 10 | public func respond(to request: Request, chainingTo chain: Responder) throws -> Response { 11 | if shouldRedirect(request) { 12 | return Response(redirectTo: location) 13 | } 14 | 15 | return try chain.respond(to: request) 16 | } 17 | } 18 | 19 | extension Response { 20 | public init(redirectTo location: String, headers: Headers = [:]) { 21 | var headers = headers 22 | headers["location"] = location 23 | self.init(status: .found, headers: headers) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/HTTP/Middleware/SessionMiddleware/Session.swift: -------------------------------------------------------------------------------- 1 | public final class Session { 2 | public let token: String 3 | public var info: [String: Any] = [:] 4 | 5 | init(token: String) { 6 | self.token = token 7 | } 8 | 9 | public subscript(key: String) -> Any? { 10 | get { 11 | return info[key] 12 | } 13 | set { 14 | info[key] = newValue 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/HTTP/Middleware/SessionMiddleware/SessionMiddleware.swift: -------------------------------------------------------------------------------- 1 | private var uuidCount = 0 2 | 3 | private func uuid() -> String { 4 | uuidCount += 1 5 | return String(uuidCount) 6 | } 7 | 8 | public struct SessionMiddleware: Middleware { 9 | public static let cookieName = "zewo-session" 10 | public let storage: SessionStorage 11 | 12 | public init(storage: SessionStorage = SessionInMemoryStorage()) { 13 | self.storage = storage 14 | } 15 | 16 | public func respond(to request: Request, chainingTo chain: Responder) throws -> Response { 17 | var request = request 18 | let (cookie, createdCookie) = getOrCreateCookie(request: request) 19 | 20 | // ensure that we have a session and add it to the request 21 | let session = getExistingSession(cookie: cookie) ?? createNewSession(cookie: cookie) 22 | add(session: session, toRequest: &request) 23 | 24 | // at this point, we have a cookie and a session. call the rest of the chain! 25 | var response = try chain.respond(to: request) 26 | 27 | // if no cookie was originally in the request, we should put it in the response 28 | if createdCookie { 29 | let cookie = AttributedCookie(name: cookie.name, value: cookie.value) 30 | response.cookies.insert(cookie) 31 | } 32 | 33 | // done! response have the session cookie and request has the session 34 | return response 35 | } 36 | 37 | private func getOrCreateCookie(request: Request) -> (Cookie, Bool) { 38 | // if request contains a session cookie, return that cookie 39 | if let requestCookie = request.cookies.filter({ $0.name == SessionMiddleware.cookieName }).first { 40 | return (requestCookie, false) 41 | } 42 | 43 | // otherwise, create a new cookie 44 | let cookie = Cookie(name: SessionMiddleware.cookieName, value: uuid()) 45 | return (cookie, true) 46 | } 47 | 48 | private func getExistingSession(cookie: Cookie) -> Session? { 49 | return storage.fetchSession(key: cookie.extendedHash) 50 | } 51 | 52 | private func createNewSession(cookie: Cookie) -> Session { 53 | // where cookie.value is the cookie uuid 54 | let session = Session(token: cookie.value) 55 | storage.save(key: cookie.extendedHash, session: session) 56 | return session 57 | } 58 | 59 | private func add(session: Session, toRequest request: inout Request) { 60 | request.storage[SessionMiddleware.cookieName] = session 61 | } 62 | } 63 | 64 | extension Request { 65 | // TODO: Add a Quark compiler flag and then make different versions Session/Session? 66 | public var session: Session { 67 | guard let session = storage[SessionMiddleware.cookieName] as? Session else { 68 | fatalError("SessionMiddleware should be applied to the chain. Quark guarantees it, so this error should never happen within Quark.") 69 | } 70 | return session 71 | } 72 | } 73 | 74 | extension Cookie { 75 | public typealias Hash = Int 76 | public var extendedHash: Hash { 77 | return "\(name)+\(value)".hashValue 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/HTTP/Middleware/SessionMiddleware/SessionStorage.swift: -------------------------------------------------------------------------------- 1 | public protocol SessionStorage : class { 2 | func fetchSession(key: Cookie.Hash) -> Session? 3 | func save(key: Cookie.Hash, session: Session) 4 | } 5 | 6 | public final class SessionInMemoryStorage : SessionStorage { 7 | private var sessions: [Cookie.Hash: Session] = [:] 8 | 9 | public func fetchSession(key: Cookie.Hash) -> Session? { 10 | return sessions[key] 11 | } 12 | 13 | public func save(key: Cookie.Hash, session: Session) { 14 | sessions[key] = session 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/HTTP/Parser/MessageParser.swift: -------------------------------------------------------------------------------- 1 | import CHTTPParser 2 | import Axis 3 | 4 | public typealias MessageParserError = http_errno 5 | 6 | public final class MessageParser { 7 | public typealias Result = Message 8 | 9 | public enum Mode { 10 | case request 11 | case response 12 | } 13 | 14 | fileprivate enum State: Int { 15 | case ready = 1 16 | case messageBegin = 2 17 | case url = 3 18 | case status = 4 19 | case headerField = 5 20 | case headerValue = 6 21 | case headersComplete = 7 22 | case body = 8 23 | case messageComplete = 9 24 | } 25 | 26 | fileprivate class Context { 27 | var method: Request.Method? = nil 28 | var status: Response.Status? = nil 29 | var version: Version? = nil 30 | var url: URL? = nil 31 | var headers: [CaseInsensitiveString: String] = [:] 32 | var body = Buffer() 33 | 34 | var currentHeaderField: CaseInsensitiveString? = nil 35 | 36 | func addValueForCurrentHeaderField(_ value: String) { 37 | let key = currentHeaderField! 38 | 39 | if let existing = headers[key] { 40 | headers[key] = existing + ", " + value 41 | } else { 42 | headers[key] = value 43 | } 44 | } 45 | } 46 | 47 | public var parser: http_parser 48 | public var parserSettings: http_parser_settings 49 | public let mode: Mode 50 | 51 | private var state: State = .ready 52 | private var context = Context() 53 | private var buffer: [UInt8] = [] 54 | 55 | private var messages: [Message] = [] 56 | 57 | public init(mode: Mode) { 58 | var parser = http_parser() 59 | 60 | switch mode { 61 | case .request: 62 | http_parser_init(&parser, HTTP_REQUEST) 63 | case .response: 64 | http_parser_init(&parser, HTTP_RESPONSE) 65 | } 66 | 67 | var parserSettings = http_parser_settings() 68 | http_parser_settings_init(&parserSettings) 69 | 70 | parserSettings.on_message_begin = http_parser_on_message_begin 71 | parserSettings.on_url = http_parser_on_url 72 | parserSettings.on_status = http_parser_on_status 73 | parserSettings.on_header_field = http_parser_on_header_field 74 | parserSettings.on_header_value = http_parser_on_header_value 75 | parserSettings.on_headers_complete = http_parser_on_headers_complete 76 | parserSettings.on_body = http_parser_on_body 77 | parserSettings.on_message_complete = http_parser_on_message_complete 78 | 79 | self.parser = parser 80 | self.parserSettings = parserSettings 81 | self.mode = mode 82 | 83 | self.parser.data = Unmanaged.passUnretained(self).toOpaque() 84 | } 85 | 86 | public func parse(_ from: BufferRepresentable) throws -> [Message] { 87 | let buffer = from.buffer 88 | 89 | return try buffer.withUnsafeBytes { 90 | try self.parse(UnsafeBufferPointer(start: $0, count: buffer.count)) 91 | } 92 | } 93 | 94 | public func parse(_ bytes: UnsafeBufferPointer) throws -> [Message] { 95 | let final = bytes.isEmpty 96 | let needsMessage: Bool 97 | switch state { 98 | case .ready, .messageComplete: 99 | needsMessage = false 100 | default: 101 | needsMessage = final 102 | } 103 | 104 | let processedCount: Int 105 | if final { 106 | processedCount = http_parser_execute(&parser, &parserSettings, nil, 0) 107 | } else { 108 | processedCount = bytes.baseAddress!.withMemoryRebound(to: Int8.self, capacity: bytes.count) { 109 | return http_parser_execute(&self.parser, &self.parserSettings, $0, bytes.count) 110 | } 111 | } 112 | 113 | guard processedCount == bytes.count else { 114 | throw MessageParserError(parser.http_errno) 115 | } 116 | 117 | let parsed = messages 118 | messages = [] 119 | 120 | guard !parsed.isEmpty || !needsMessage else { 121 | throw MessageParserError(HPE_INVALID_EOF_STATE.rawValue) 122 | } 123 | 124 | return parsed 125 | } 126 | 127 | public func finish() throws -> [Message] { 128 | return try parse(UnsafeBufferPointer()) 129 | } 130 | 131 | fileprivate func processOnMessageBegin() -> Int32 { 132 | return process(state: .messageBegin) 133 | } 134 | 135 | fileprivate func processOnURL(data: UnsafePointer, length: Int) -> Int32 { 136 | return process(state: .url, data: UnsafeBufferPointer(start: data, count: length)) 137 | } 138 | 139 | fileprivate func processOnStatus(data: UnsafePointer, length: Int) -> Int32 { 140 | return process(state: .status, data: UnsafeBufferPointer(start: data, count: length)) 141 | } 142 | 143 | fileprivate func processOnHeaderField(data: UnsafePointer, length: Int) -> Int32 { 144 | return process(state: .headerField, data: UnsafeBufferPointer(start: data, count: length)) 145 | } 146 | 147 | fileprivate func processOnHeaderValue(data: UnsafePointer, length: Int) -> Int32 { 148 | return process(state: .headerValue, data: UnsafeBufferPointer(start: data, count: length)) 149 | } 150 | 151 | fileprivate func processOnHeadersComplete() -> Int32 { 152 | return process(state: .headersComplete) 153 | } 154 | 155 | fileprivate func processOnBody(data: UnsafePointer, length: Int) -> Int32 { 156 | return process(state: .body, data: UnsafeBufferPointer(start: data, count: length)) 157 | } 158 | 159 | fileprivate func processOnMessageComplete() -> Int32 { 160 | return process(state: .messageComplete) 161 | } 162 | 163 | fileprivate func process(state newState: State, data: UnsafeBufferPointer? = nil) -> Int32 { 164 | if state != newState { 165 | switch state { 166 | case .ready, .messageBegin, .messageComplete: 167 | break 168 | case .url: 169 | buffer.append(0) 170 | 171 | let string = buffer.withUnsafeBufferPointer { (ptr: UnsafeBufferPointer) -> String in 172 | return String(cString: ptr.baseAddress!) 173 | } 174 | 175 | context.url = URL(string: string)! 176 | case .status: 177 | buffer.append(0) 178 | 179 | let string = buffer.withUnsafeBufferPointer { (ptr: UnsafeBufferPointer) -> String in 180 | return String(cString: ptr.baseAddress!) 181 | } 182 | 183 | context.status = Response.Status( 184 | statusCode: Int(parser.status_code), 185 | reasonPhrase: string 186 | ) 187 | case .headerField: 188 | buffer.append(0) 189 | 190 | let string = buffer.withUnsafeBufferPointer { (ptr: UnsafeBufferPointer) -> String in 191 | return String(cString: ptr.baseAddress!) 192 | } 193 | 194 | context.currentHeaderField = CaseInsensitiveString(string) 195 | case .headerValue: 196 | buffer.append(0) 197 | 198 | let string = buffer.withUnsafeBufferPointer { (ptr: UnsafeBufferPointer) -> String in 199 | return String(cString: ptr.baseAddress!) 200 | } 201 | 202 | context.addValueForCurrentHeaderField(string) 203 | case .headersComplete: 204 | context.currentHeaderField = nil 205 | context.method = Request.Method(code: http_method(rawValue: parser.method)) 206 | context.version = Version(major: Int(parser.http_major), minor: Int(parser.http_minor)) 207 | case .body: 208 | context.body = Buffer(buffer) 209 | } 210 | 211 | buffer = [] 212 | state = newState 213 | 214 | if state == .messageComplete { 215 | let message: Message 216 | switch mode { 217 | case .request: 218 | var request = Request( 219 | method: context.method!, 220 | url: context.url!, 221 | headers: Headers(), 222 | body: .buffer(context.body) 223 | ) 224 | 225 | request.headers = Headers(context.headers) 226 | message = request 227 | case .response: 228 | let cookieHeaders = 229 | self.context.headers 230 | .filter { $0.key == "Set-Cookie" } 231 | .map { $0.value } 232 | .reduce(Set()) { initial, value in 233 | return initial.union(Set(value.components(separatedBy: ", "))) 234 | } 235 | 236 | let response = Response( 237 | version: context.version!, 238 | status: context.status!, 239 | headers: Headers(context.headers), 240 | cookieHeaders: cookieHeaders, 241 | body: .buffer(context.body) 242 | ) 243 | 244 | message = response 245 | } 246 | 247 | messages.append(message) 248 | context = Context() 249 | } 250 | } 251 | 252 | guard let data = data, data.count > 0 else { 253 | return 0 254 | } 255 | 256 | data.baseAddress!.withMemoryRebound(to: UInt8.self, capacity: data.count) { ptr in 257 | for i in 0..?) -> Int32 { 314 | let ref = Unmanaged.fromOpaque(ctx!.pointee.data).takeUnretainedValue() 315 | return ref.processOnMessageBegin() 316 | } 317 | 318 | private func http_parser_on_url(ctx: UnsafeMutablePointer?, data: UnsafePointer?, length: Int) -> Int32 { 319 | let ref = Unmanaged.fromOpaque(ctx!.pointee.data).takeUnretainedValue() 320 | return ref.processOnURL(data: data!, length: length) 321 | } 322 | 323 | private func http_parser_on_status(ctx: UnsafeMutablePointer?, data: UnsafePointer?, length: Int) -> Int32 { 324 | let ref = Unmanaged.fromOpaque(ctx!.pointee.data).takeUnretainedValue() 325 | return ref.processOnStatus(data: data!, length: length) 326 | } 327 | 328 | private func http_parser_on_header_field(ctx: UnsafeMutablePointer?, data: UnsafePointer?, length: Int) -> Int32 { 329 | let ref = Unmanaged.fromOpaque(ctx!.pointee.data).takeUnretainedValue() 330 | return ref.processOnHeaderField(data: data!, length: length) 331 | } 332 | 333 | private func http_parser_on_header_value(ctx: UnsafeMutablePointer?, data: UnsafePointer?, length: Int) -> Int32 { 334 | let ref = Unmanaged.fromOpaque(ctx!.pointee.data).takeUnretainedValue() 335 | return ref.processOnHeaderValue(data: data!, length: length) 336 | } 337 | 338 | private func http_parser_on_headers_complete(ctx: UnsafeMutablePointer?) -> Int32 { 339 | let ref = Unmanaged.fromOpaque(ctx!.pointee.data).takeUnretainedValue() 340 | return ref.processOnHeadersComplete() 341 | } 342 | 343 | private func http_parser_on_body(ctx: UnsafeMutablePointer?, data: UnsafePointer?, length: Int) -> Int32 { 344 | let ref = Unmanaged.fromOpaque(ctx!.pointee.data).takeUnretainedValue() 345 | return ref.processOnBody(data: data!, length: length) 346 | } 347 | 348 | private func http_parser_on_message_complete(ctx: UnsafeMutablePointer?) -> Int32 { 349 | let ref = Unmanaged.fromOpaque(ctx!.pointee.data).takeUnretainedValue() 350 | return ref.processOnMessageComplete() 351 | } 352 | -------------------------------------------------------------------------------- /Sources/HTTP/Responder/Responder.swift: -------------------------------------------------------------------------------- 1 | public protocol Responder : ResponderRepresentable { 2 | func respond(to request: Request) throws -> Response 3 | } 4 | 5 | extension Responder { 6 | public var responder: Responder { 7 | return self 8 | } 9 | } 10 | 11 | public protocol ResponderRepresentable { 12 | var responder: Responder { get } 13 | } 14 | 15 | public typealias Respond = (_ to: Request) throws -> Response 16 | 17 | public struct BasicResponder : Responder { 18 | let respond: Respond 19 | 20 | public init(_ respond: @escaping Respond) { 21 | self.respond = respond 22 | } 23 | 24 | public func respond(to request: Request) throws -> Response { 25 | return try self.respond(request) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/HTTP/Serializer/BodyStream.swift: -------------------------------------------------------------------------------- 1 | public enum BodyStreamError : Error { 2 | case receiveUnsupported 3 | } 4 | 5 | final class BodyStream : Stream { 6 | var closed = false 7 | let transport: Stream 8 | 9 | init(_ transport: Stream) { 10 | self.transport = transport 11 | } 12 | 13 | public func open(deadline: Double) throws { 14 | closed = false 15 | } 16 | 17 | func close() { 18 | closed = true 19 | } 20 | 21 | func read(into readBuffer: UnsafeMutableBufferPointer, deadline: Double) throws -> UnsafeBufferPointer { 22 | throw BodyStreamError.receiveUnsupported 23 | } 24 | 25 | func write(_ buffer: UnsafeBufferPointer, deadline: Double) throws { 26 | guard !buffer.isEmpty else { 27 | return 28 | } 29 | 30 | if closed { 31 | throw StreamError.closedStream 32 | } 33 | 34 | try transport.write(String(buffer.count, radix: 16), deadline: deadline) 35 | try transport.write("\r\n", deadline: deadline) 36 | try transport.write(buffer, deadline: deadline) 37 | try transport.write("\r\n", deadline: deadline) 38 | } 39 | 40 | func flush(deadline: Double) throws { 41 | try transport.flush(deadline: deadline) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/HTTP/Serializer/RequestSerializer.swift: -------------------------------------------------------------------------------- 1 | public class RequestSerializer { 2 | let stream: Stream 3 | let bufferSize: Int 4 | 5 | public init(stream: Stream, bufferSize: Int = 2048) { 6 | self.stream = stream 7 | self.bufferSize = bufferSize 8 | } 9 | 10 | public func serialize(_ request: Request, deadline: Double) throws { 11 | let newLine: [UInt8] = [13, 10] 12 | 13 | try stream.write("\(request.method) \(request.url.absoluteString) HTTP/\(request.version.major).\(request.version.minor)", deadline: deadline) 14 | try stream.write(newLine, deadline: deadline) 15 | 16 | for (name, value) in request.headers.headers { 17 | try stream.write("\(name): \(value)", deadline: deadline) 18 | try stream.write(newLine, deadline: deadline) 19 | } 20 | 21 | try stream.write(newLine, deadline: deadline) 22 | 23 | switch request.body { 24 | case .buffer(let buffer): 25 | try stream.write(buffer, deadline: deadline) 26 | case .reader(let reader): 27 | while !reader.closed { 28 | let buffer = try reader.read(upTo: bufferSize, deadline: deadline) 29 | guard !buffer.isEmpty else { 30 | break 31 | } 32 | 33 | try stream.write(String(buffer.count, radix: 16), deadline: deadline) 34 | try stream.write(newLine, deadline: deadline) 35 | try stream.write(buffer, deadline: deadline) 36 | try stream.write(newLine, deadline: deadline) 37 | } 38 | 39 | try stream.write("0", deadline: deadline) 40 | try stream.write(newLine, deadline: deadline) 41 | try stream.write(newLine, deadline: deadline) 42 | case .writer(let writer): 43 | let body = BodyStream(stream) 44 | try writer(body) 45 | 46 | try stream.write("0", deadline: deadline) 47 | try stream.write(newLine, deadline: deadline) 48 | try stream.write(newLine, deadline: deadline) 49 | } 50 | 51 | try stream.flush(deadline: deadline) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/HTTP/Serializer/ResponseSerializer.swift: -------------------------------------------------------------------------------- 1 | public class ResponseSerializer { 2 | let stream: Stream 3 | let bufferSize: Int 4 | 5 | public init(stream: Stream, bufferSize: Int = 2048) { 6 | self.stream = stream 7 | self.bufferSize = bufferSize 8 | } 9 | 10 | public func serialize(_ response: Response, deadline: Double) throws { 11 | var header = "HTTP/" 12 | header += response.version.major.description 13 | header += "." 14 | header += response.version.minor.description 15 | header += " " 16 | header += response.status.statusCode.description 17 | header += " " 18 | header += response.reasonPhrase 19 | header += "\r\n" 20 | 21 | for (name, value) in response.headers.headers { 22 | header += name.string 23 | header += ": " 24 | header += value 25 | header += "\r\n" 26 | } 27 | 28 | for cookie in response.cookieHeaders { 29 | header += "Set-Cookie: " 30 | header += cookie 31 | header += "\r\n" 32 | } 33 | 34 | header += "\r\n" 35 | 36 | try stream.write(header, deadline: deadline) 37 | 38 | switch response.body { 39 | case .buffer(let buffer): 40 | try stream.write(buffer, deadline: deadline) 41 | case .reader(let reader): 42 | while !reader.closed { 43 | let buffer = try reader.read(upTo: bufferSize, deadline: deadline) 44 | 45 | guard !buffer.isEmpty else { 46 | break 47 | } 48 | 49 | try stream.write(String(buffer.count, radix: 16), deadline: deadline) 50 | try stream.write("\r\n", deadline: deadline) 51 | try stream.write(buffer, deadline: deadline) 52 | try stream.write("\r\n", deadline: deadline) 53 | } 54 | 55 | try stream.write("0\r\n\r\n", deadline: deadline) 56 | case .writer(let writer): 57 | let body = BodyStream(stream) 58 | try writer(body) 59 | try stream.write("0\r\n\r\n", deadline: deadline) 60 | } 61 | 62 | try stream.flush(deadline: deadline) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Content/RequestContentTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | struct Foo : MapFallibleRepresentable { 5 | let content: Map 6 | func asMap() throws -> Map { 7 | return content 8 | } 9 | } 10 | 11 | public class RequestContentTests : XCTestCase { 12 | func testContent() throws { 13 | let content = 1969 14 | let request = Request(content: content) 15 | XCTAssertEqual(request.method, .get) 16 | XCTAssertEqual(request.url.path, "/") 17 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 18 | XCTAssertEqual(request.body, .empty) 19 | XCTAssertEqual(request.content, Map(content)) 20 | } 21 | 22 | func testOptionalContent() throws { 23 | let content: Int? = 1969 24 | let request = Request(content: content) 25 | XCTAssertEqual(request.method, .get) 26 | XCTAssertEqual(request.url.path, "/") 27 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 28 | XCTAssertEqual(request.body, .empty) 29 | XCTAssertEqual(request.content, Map(content)) 30 | } 31 | 32 | func testArrayContent() throws { 33 | let content = [1969] 34 | let request = Request(content: content) 35 | XCTAssertEqual(request.method, .get) 36 | XCTAssertEqual(request.url.path, "/") 37 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 38 | XCTAssertEqual(request.body, .empty) 39 | XCTAssertEqual(request.content, Map(content)) 40 | } 41 | 42 | func testDictionaryContent() throws { 43 | let content = ["Woodstock": 1969] 44 | let request = Request(content: content) 45 | XCTAssertEqual(request.method, .get) 46 | XCTAssertEqual(request.url.path, "/") 47 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 48 | XCTAssertEqual(request.body, .empty) 49 | XCTAssertEqual(request.content, Map(content)) 50 | } 51 | 52 | func testFallibleContent() throws { 53 | let content = 1969 54 | let foo = Foo(content: 1969) 55 | let request = try Request(content: foo) 56 | XCTAssertEqual(request.method, .get) 57 | XCTAssertEqual(request.url.path, "/") 58 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 59 | XCTAssertEqual(request.body, .empty) 60 | XCTAssertEqual(request.content, Map(content)) 61 | } 62 | 63 | func testContentStringURL() throws { 64 | let content = 1969 65 | guard let request = Request(url: "/", content: content) else { 66 | return XCTFail() 67 | } 68 | XCTAssertEqual(request.method, .get) 69 | XCTAssertEqual(request.url.path, "/") 70 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 71 | XCTAssertEqual(request.body, .empty) 72 | XCTAssertEqual(request.content, Map(content)) 73 | } 74 | 75 | func testOptionalContentStringURL() throws { 76 | let content: Int? = 1969 77 | guard let request = Request(url: "/", content: content) else { 78 | return XCTFail() 79 | } 80 | XCTAssertEqual(request.method, .get) 81 | XCTAssertEqual(request.url.path, "/") 82 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 83 | XCTAssertEqual(request.body, .empty) 84 | XCTAssertEqual(request.content, Map(content)) 85 | } 86 | 87 | func testArrayContentStringURL() throws { 88 | let content = [1969] 89 | guard let request = Request(url: "/", content: content) else { 90 | return XCTFail() 91 | } 92 | XCTAssertEqual(request.method, .get) 93 | XCTAssertEqual(request.url.path, "/") 94 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 95 | XCTAssertEqual(request.body, .empty) 96 | XCTAssertEqual(request.content, Map(content)) 97 | } 98 | 99 | func testDictionaryContentStringURL() throws { 100 | let content = ["Woodstock": 1969] 101 | guard let request = Request(url: "/", content: content) else { 102 | return XCTFail() 103 | } 104 | XCTAssertEqual(request.method, .get) 105 | XCTAssertEqual(request.url.path, "/") 106 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 107 | XCTAssertEqual(request.body, .empty) 108 | XCTAssertEqual(request.content, Map(content)) 109 | } 110 | 111 | func testFallibleContentStringURL() throws { 112 | let content = 1969 113 | let foo = Foo(content: 1969) 114 | let request = try Request(url: "/", content: foo) 115 | XCTAssertEqual(request.method, .get) 116 | XCTAssertEqual(request.url.path, "/") 117 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 118 | XCTAssertEqual(request.body, .empty) 119 | XCTAssertEqual(request.content, Map(content)) 120 | } 121 | } 122 | 123 | extension RequestContentTests { 124 | public static var allTests: [(String, (RequestContentTests) -> () throws -> Void)] { 125 | return [ 126 | ("testContent", testContent), 127 | ("testOptionalContent", testOptionalContent), 128 | ("testArrayContent", testArrayContent), 129 | ("testDictionaryContent", testDictionaryContent), 130 | ("testFallibleContent", testFallibleContent), 131 | ("testContentStringURL", testContentStringURL), 132 | ("testOptionalContentStringURL", testOptionalContentStringURL), 133 | ("testArrayContentStringURL", testArrayContentStringURL), 134 | ("testDictionaryContentStringURL", testDictionaryContentStringURL), 135 | ("testFallibleContentStringURL", testFallibleContentStringURL), 136 | ] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Content/ResponseContentTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | struct Fuu : MapFallibleRepresentable { 5 | let content: Map 6 | func asMap() throws -> Map { 7 | return content 8 | } 9 | } 10 | 11 | public class ResponseContentTests : XCTestCase { 12 | func testContent() throws { 13 | let content = 1969 14 | let response = Response(content: content) 15 | XCTAssertEqual(response.status, .ok) 16 | XCTAssertEqual(response.headers, ["Content-Length": "0"]) 17 | XCTAssertEqual(response.body, .empty) 18 | XCTAssertEqual(response.content, Map(content)) 19 | } 20 | 21 | func testOptionalContent() throws { 22 | let content: Int? = 1969 23 | let response = Response(content: content) 24 | XCTAssertEqual(response.status, .ok) 25 | XCTAssertEqual(response.headers, ["Content-Length": "0"]) 26 | XCTAssertEqual(response.body, .empty) 27 | XCTAssertEqual(response.content, Map(content)) 28 | } 29 | 30 | func testArrayContent() throws { 31 | let content = [1969] 32 | let response = Response(content: content) 33 | XCTAssertEqual(response.status, .ok) 34 | XCTAssertEqual(response.headers, ["Content-Length": "0"]) 35 | XCTAssertEqual(response.body, .empty) 36 | XCTAssertEqual(response.content, Map(content)) 37 | } 38 | 39 | func testDictionaryContent() throws { 40 | let content = ["Woodstock": 1969] 41 | let response = Response(content: content) 42 | XCTAssertEqual(response.status, .ok) 43 | XCTAssertEqual(response.headers, ["Content-Length": "0"]) 44 | XCTAssertEqual(response.body, .empty) 45 | XCTAssertEqual(response.content, Map(content)) 46 | } 47 | 48 | func testFallibleContent() throws { 49 | let content = 1969 50 | let fuu = Fuu(content: 1969) 51 | let response = try Response(content: fuu) 52 | XCTAssertEqual(response.status, .ok) 53 | XCTAssertEqual(response.headers, ["Content-Length": "0"]) 54 | XCTAssertEqual(response.body, .empty) 55 | XCTAssertEqual(response.content, Map(content)) 56 | } 57 | } 58 | 59 | extension ResponseContentTests { 60 | public static var allTests: [(String, (ResponseContentTests) -> () throws -> Void)] { 61 | return [ 62 | ("testContent", testContent), 63 | ("testOptionalContent", testOptionalContent), 64 | ("testArrayContent", testArrayContent), 65 | ("testDictionaryContent", testDictionaryContent), 66 | ("testFallibleContent", testFallibleContent), 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Error/ErrorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | let clientErrors: [(ClientError, Response)] = [ 5 | (.badRequest(headers: .empty, body: .empty), Response(status: .badRequest)), 6 | (.unauthorized(headers: .empty, body: .empty), Response(status: .unauthorized)), 7 | (.paymentRequired(headers: .empty, body: .empty), Response(status: .paymentRequired)), 8 | (.forbidden(headers: .empty, body: .empty), Response(status: .forbidden)), 9 | (.notFound(headers: .empty, body: .empty), Response(status: .notFound)), 10 | (.methodNotAllowed(headers: .empty, body: .empty), Response(status: .methodNotAllowed)), 11 | (.notAcceptable(headers: .empty, body: .empty), Response(status: .notAcceptable)), 12 | (.proxyAuthenticationRequired(headers: .empty, body: .empty), Response(status: .proxyAuthenticationRequired)), 13 | (.requestTimeout(headers: .empty, body: .empty), Response(status: .requestTimeout)), 14 | (.conflict(headers: .empty, body: .empty), Response(status: .conflict)), 15 | (.gone(headers: .empty, body: .empty), Response(status: .gone)), 16 | (.lengthRequired(headers: .empty, body: .empty), Response(status: .lengthRequired)), 17 | (.preconditionFailed(headers: .empty, body: .empty), Response(status: .preconditionFailed)), 18 | (.requestEntityTooLarge(headers: .empty, body: .empty), Response(status: .requestEntityTooLarge)), 19 | (.requestURITooLong(headers: .empty, body: .empty), Response(status: .requestURITooLong)), 20 | (.unsupportedMediaType(headers: .empty, body: .empty), Response(status: .unsupportedMediaType)), 21 | (.requestedRangeNotSatisfiable(headers: .empty, body: .empty), Response(status: .requestedRangeNotSatisfiable)), 22 | (.expectationFailed(headers: .empty, body: .empty), Response(status: .expectationFailed)), 23 | (.imATeapot(headers: .empty, body: .empty), Response(status: .imATeapot)), 24 | (.authenticationTimeout(headers: .empty, body: .empty), Response(status: .authenticationTimeout)), 25 | (.enhanceYourCalm(headers: .empty, body: .empty), Response(status: .enhanceYourCalm)), 26 | (.unprocessableEntity(headers: .empty, body: .empty), Response(status: .unprocessableEntity)), 27 | (.locked(headers: .empty, body: .empty), Response(status: .locked)), 28 | (.failedDependency(headers: .empty, body: .empty), Response(status: .failedDependency)), 29 | (.preconditionRequired(headers: .empty, body: .empty), Response(status: .preconditionRequired)), 30 | (.tooManyRequests(headers: .empty, body: .empty), Response(status: .tooManyRequests)), 31 | (.requestHeaderFieldsTooLarge(headers: .empty, body: .empty), Response(status: .requestHeaderFieldsTooLarge)), 32 | ] 33 | 34 | let serverErrors: [(ServerError, Response)] = [ 35 | (.internalServerError(headers: .empty, body: .empty), Response(status: .internalServerError)), 36 | (.notImplemented(headers: .empty, body: .empty), Response(status: .notImplemented)), 37 | (.badGateway(headers: .empty, body: .empty), Response(status: .badGateway)), 38 | (.serviceUnavailable(headers: .empty, body: .empty), Response(status: .serviceUnavailable)), 39 | (.gatewayTimeout(headers: .empty, body: .empty), Response(status: .gatewayTimeout)), 40 | (.httpVersionNotSupported(headers: .empty, body: .empty), Response(status: .httpVersionNotSupported)), 41 | (.variantAlsoNegotiates(headers: .empty, body: .empty), Response(status: .variantAlsoNegotiates)), 42 | (.insufficientStorage(headers: .empty, body: .empty), Response(status: .insufficientStorage)), 43 | (.loopDetected(headers: .empty, body: .empty), Response(status: .loopDetected)), 44 | (.notExtended(headers: .empty, body: .empty), Response(status: .notExtended)), 45 | (.networkAuthenticationRequired(headers: .empty, body: .empty), Response(status: .networkAuthenticationRequired)), 46 | ] 47 | 48 | public class ErrorTests : XCTestCase { 49 | func testError() throws { 50 | for (error, response) in clientErrors { 51 | XCTAssertEqual(error.response.statusCode, response.statusCode) 52 | } 53 | 54 | XCTAssertEqual(HTTPError.badRequest, .badRequest(headers: .empty, body: .empty)) 55 | XCTAssertEqual(HTTPError.unauthorized, .unauthorized(headers: .empty, body: .empty)) 56 | XCTAssertEqual(HTTPError.paymentRequired, .paymentRequired(headers: .empty, body: .empty)) 57 | XCTAssertEqual(HTTPError.forbidden, .forbidden(headers: .empty, body: .empty)) 58 | XCTAssertEqual(HTTPError.notFound, .notFound(headers: .empty, body: .empty)) 59 | XCTAssertEqual(HTTPError.methodNotAllowed, .methodNotAllowed(headers: .empty, body: .empty)) 60 | XCTAssertEqual(HTTPError.notAcceptable, .notAcceptable(headers: .empty, body: .empty)) 61 | XCTAssertEqual(HTTPError.proxyAuthenticationRequired, .proxyAuthenticationRequired(headers: .empty, body: .empty)) 62 | XCTAssertEqual(HTTPError.requestTimeout, .requestTimeout(headers: .empty, body: .empty)) 63 | XCTAssertEqual(HTTPError.conflict, .conflict(headers: .empty, body: .empty)) 64 | XCTAssertEqual(HTTPError.gone, .gone(headers: .empty, body: .empty)) 65 | XCTAssertEqual(HTTPError.lengthRequired, .lengthRequired(headers: .empty, body: .empty)) 66 | XCTAssertEqual(HTTPError.preconditionFailed, .preconditionFailed(headers: .empty, body: .empty)) 67 | XCTAssertEqual(HTTPError.requestEntityTooLarge, .requestEntityTooLarge(headers: .empty, body: .empty)) 68 | XCTAssertEqual(HTTPError.requestURITooLong, .requestURITooLong(headers: .empty, body: .empty)) 69 | XCTAssertEqual(HTTPError.unsupportedMediaType, .unsupportedMediaType(headers: .empty, body: .empty)) 70 | XCTAssertEqual(HTTPError.requestedRangeNotSatisfiable, .requestedRangeNotSatisfiable(headers: .empty, body: .empty)) 71 | XCTAssertEqual(HTTPError.expectationFailed, .expectationFailed(headers: .empty, body: .empty)) 72 | XCTAssertEqual(HTTPError.imATeapot, .imATeapot(headers: .empty, body: .empty)) 73 | XCTAssertEqual(HTTPError.authenticationTimeout, .authenticationTimeout(headers: .empty, body: .empty)) 74 | XCTAssertEqual(HTTPError.enhanceYourCalm, .enhanceYourCalm(headers: .empty, body: .empty)) 75 | XCTAssertEqual(HTTPError.unprocessableEntity, .unprocessableEntity(headers: .empty, body: .empty)) 76 | XCTAssertEqual(HTTPError.locked, .locked(headers: .empty, body: .empty)) 77 | XCTAssertEqual(HTTPError.failedDependency, .failedDependency(headers: .empty, body: .empty)) 78 | XCTAssertEqual(HTTPError.preconditionRequired, .preconditionRequired(headers: .empty, body: .empty)) 79 | XCTAssertEqual(HTTPError.tooManyRequests, .tooManyRequests(headers: .empty, body: .empty)) 80 | XCTAssertEqual(HTTPError.requestHeaderFieldsTooLarge, .requestHeaderFieldsTooLarge(headers: .empty, body: .empty)) 81 | 82 | XCTAssertEqual(HTTPError.badRequest(body: "Hello!"), .badRequest(headers: .empty, body: .buffer(Buffer("Hello!")))) 83 | XCTAssertEqual(HTTPError.unauthorized(body: "Hello!"), .unauthorized(headers: .empty, body: .buffer(Buffer("Hello!")))) 84 | XCTAssertEqual(HTTPError.paymentRequired(body: "Hello!"), .paymentRequired(headers: .empty, body: .buffer(Buffer("Hello!")))) 85 | XCTAssertEqual(HTTPError.forbidden(body: "Hello!"), .forbidden(headers: .empty, body: .buffer(Buffer("Hello!")))) 86 | XCTAssertEqual(HTTPError.notFound(body: "Hello!"), .notFound(headers: .empty, body: .buffer(Buffer("Hello!")))) 87 | XCTAssertEqual(HTTPError.methodNotAllowed(body: "Hello!"), .methodNotAllowed(headers: .empty, body: .buffer(Buffer("Hello!")))) 88 | XCTAssertEqual(HTTPError.notAcceptable(body: "Hello!"), .notAcceptable(headers: .empty, body: .buffer(Buffer("Hello!")))) 89 | XCTAssertEqual(HTTPError.proxyAuthenticationRequired(body: "Hello!"), .proxyAuthenticationRequired(headers: .empty, body: .buffer(Buffer("Hello!")))) 90 | XCTAssertEqual(HTTPError.requestTimeout(body: "Hello!"), .requestTimeout(headers: .empty, body: .buffer(Buffer("Hello!")))) 91 | XCTAssertEqual(HTTPError.conflict(body: "Hello!"), .conflict(headers: .empty, body: .buffer(Buffer("Hello!")))) 92 | XCTAssertEqual(HTTPError.gone(body: "Hello!"), .gone(headers: .empty, body: .buffer(Buffer("Hello!")))) 93 | XCTAssertEqual(HTTPError.lengthRequired(body: "Hello!"), .lengthRequired(headers: .empty, body: .buffer(Buffer("Hello!")))) 94 | XCTAssertEqual(HTTPError.preconditionFailed(body: "Hello!"), .preconditionFailed(headers: .empty, body: .buffer(Buffer("Hello!")))) 95 | XCTAssertEqual(HTTPError.requestEntityTooLarge(body: "Hello!"), .requestEntityTooLarge(headers: .empty, body: .buffer(Buffer("Hello!")))) 96 | XCTAssertEqual(HTTPError.requestURITooLong(body: "Hello!"), .requestURITooLong(headers: .empty, body: .buffer(Buffer("Hello!")))) 97 | XCTAssertEqual(HTTPError.unsupportedMediaType(body: "Hello!"), .unsupportedMediaType(headers: .empty, body: .buffer(Buffer("Hello!")))) 98 | XCTAssertEqual(HTTPError.requestedRangeNotSatisfiable(body: "Hello!"), .requestedRangeNotSatisfiable(headers: .empty, body: .buffer(Buffer("Hello!")))) 99 | XCTAssertEqual(HTTPError.expectationFailed(body: "Hello!"), .expectationFailed(headers: .empty, body: .buffer(Buffer("Hello!")))) 100 | XCTAssertEqual(HTTPError.imATeapot(body: "Hello!"), .imATeapot(headers: .empty, body: .buffer(Buffer("Hello!")))) 101 | XCTAssertEqual(HTTPError.authenticationTimeout(body: "Hello!"), .authenticationTimeout(headers: .empty, body: .buffer(Buffer("Hello!")))) 102 | XCTAssertEqual(HTTPError.enhanceYourCalm(body: "Hello!"), .enhanceYourCalm(headers: .empty, body: .buffer(Buffer("Hello!")))) 103 | XCTAssertEqual(HTTPError.unprocessableEntity(body: "Hello!"), .unprocessableEntity(headers: .empty, body: .buffer(Buffer("Hello!")))) 104 | XCTAssertEqual(HTTPError.locked(body: "Hello!"), .locked(headers: .empty, body: .buffer(Buffer("Hello!")))) 105 | XCTAssertEqual(HTTPError.failedDependency(body: "Hello!"), .failedDependency(headers: .empty, body: .buffer(Buffer("Hello!")))) 106 | XCTAssertEqual(HTTPError.preconditionRequired(body: "Hello!"), .preconditionRequired(headers: .empty, body: .buffer(Buffer("Hello!")))) 107 | XCTAssertEqual(HTTPError.tooManyRequests(body: "Hello!"), .tooManyRequests(headers: .empty, body: .buffer(Buffer("Hello!")))) 108 | XCTAssertEqual(HTTPError.requestHeaderFieldsTooLarge(body: "Hello!"), .requestHeaderFieldsTooLarge(headers: .empty, body: .buffer(Buffer("Hello!")))) 109 | 110 | for (error, response) in serverErrors { 111 | XCTAssertEqual(error.response.statusCode, response.statusCode) 112 | } 113 | 114 | XCTAssertEqual(HTTPError.internalServerError, .internalServerError(headers: .empty, body: .empty)) 115 | XCTAssertEqual(HTTPError.notImplemented, .notImplemented(headers: .empty, body: .empty)) 116 | XCTAssertEqual(HTTPError.badGateway, .badGateway(headers: .empty, body: .empty)) 117 | XCTAssertEqual(HTTPError.serviceUnavailable, .serviceUnavailable(headers: .empty, body: .empty)) 118 | XCTAssertEqual(HTTPError.gatewayTimeout, .gatewayTimeout(headers: .empty, body: .empty)) 119 | XCTAssertEqual(HTTPError.httpVersionNotSupported, .httpVersionNotSupported(headers: .empty, body: .empty)) 120 | XCTAssertEqual(HTTPError.variantAlsoNegotiates, .variantAlsoNegotiates(headers: .empty, body: .empty)) 121 | XCTAssertEqual(HTTPError.insufficientStorage, .insufficientStorage(headers: .empty, body: .empty)) 122 | XCTAssertEqual(HTTPError.loopDetected, .loopDetected(headers: .empty, body: .empty)) 123 | XCTAssertEqual(HTTPError.notExtended, .notExtended(headers: .empty, body: .empty)) 124 | XCTAssertEqual(HTTPError.networkAuthenticationRequired, .networkAuthenticationRequired(headers: .empty, body: .empty)) 125 | 126 | XCTAssertEqual(HTTPError.internalServerError(body: "Hello!"), .internalServerError(headers: .empty, body: .buffer(Buffer("Hello!")))) 127 | XCTAssertEqual(HTTPError.notImplemented(body: "Hello!"), .notImplemented(headers: .empty, body: .buffer(Buffer("Hello!")))) 128 | XCTAssertEqual(HTTPError.badGateway(body: "Hello!"), .badGateway(headers: .empty, body: .buffer(Buffer("Hello!")))) 129 | XCTAssertEqual(HTTPError.serviceUnavailable(body: "Hello!"), .serviceUnavailable(headers: .empty, body: .buffer(Buffer("Hello!")))) 130 | XCTAssertEqual(HTTPError.gatewayTimeout(body: "Hello!"), .gatewayTimeout(headers: .empty, body: .buffer(Buffer("Hello!")))) 131 | XCTAssertEqual(HTTPError.httpVersionNotSupported(body: "Hello!"), .httpVersionNotSupported(headers: .empty, body: .buffer(Buffer("Hello!")))) 132 | XCTAssertEqual(HTTPError.variantAlsoNegotiates(body: "Hello!"), .variantAlsoNegotiates(headers: .empty, body: .buffer(Buffer("Hello!")))) 133 | XCTAssertEqual(HTTPError.insufficientStorage(body: "Hello!"), .insufficientStorage(headers: .empty, body: .buffer(Buffer("Hello!")))) 134 | XCTAssertEqual(HTTPError.loopDetected(body: "Hello!"), .loopDetected(headers: .empty, body: .buffer(Buffer("Hello!")))) 135 | XCTAssertEqual(HTTPError.notExtended(body: "Hello!"), .notExtended(headers: .empty, body: .buffer(Buffer("Hello!")))) 136 | XCTAssertEqual(HTTPError.networkAuthenticationRequired(body: "Hello!"), .networkAuthenticationRequired(headers: .empty, body: .buffer(Buffer("Hello!")))) 137 | } 138 | } 139 | 140 | extension ErrorTests { 141 | public static var allTests: [(String, (ErrorTests) -> () throws -> Void)] { 142 | return [ 143 | ("testError", testError), 144 | ] 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Message/AttributedCookieTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class AttributedCookieTests : XCTestCase { 5 | func testConstruction() throws { 6 | let cookieString = "foo=bar; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Domain=zewo.io; Path=/libs; Secure; HttpOnly" 7 | let cookie = AttributedCookie( 8 | name: "foo", 9 | value: "bar", 10 | expiration: .expires("Thu, 01 Jan 1970 00:00:01 GMT"), 11 | domain: "zewo.io", 12 | path: "/libs", 13 | secure: true, 14 | httpOnly: true 15 | ) 16 | XCTAssertEqual(cookie, cookie) 17 | XCTAssertEqual(String(describing: cookie), cookieString) 18 | XCTAssertEqual(cookie.name, "foo") 19 | XCTAssertEqual(cookie.value, "bar") 20 | XCTAssertEqual(cookie.expiration, .expires("Thu, 01 Jan 1970 00:00:01 GMT")) 21 | XCTAssertEqual(cookie.domain, "zewo.io") 22 | XCTAssertEqual(cookie.path, "/libs") 23 | XCTAssertTrue(cookie.secure) 24 | XCTAssertTrue(cookie.httpOnly) 25 | } 26 | 27 | func testParsing() throws { 28 | var cookieString = "foo=bar; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Domain=zewo.io; Path=/libs; Secure; HttpOnly" 29 | var parsedCookie = AttributedCookie(cookieString) 30 | XCTAssertNotNil(parsedCookie) 31 | XCTAssertEqual(parsedCookie?.name, "foo") 32 | XCTAssertEqual(parsedCookie?.value, "bar") 33 | XCTAssertEqual(parsedCookie?.expiration, .expires("Thu, 01 Jan 1970 00:00:01 GMT")) 34 | XCTAssertEqual(parsedCookie?.domain, "zewo.io") 35 | XCTAssertEqual(parsedCookie?.path, "/libs") 36 | XCTAssertTrue(parsedCookie?.secure ?? false) 37 | XCTAssertTrue(parsedCookie?.httpOnly ?? false) 38 | 39 | cookieString = "foo=bar; Max-Age=60; Domain=zewo.io; Path=/libs; Secure; HttpOnly" 40 | parsedCookie = AttributedCookie(cookieString) 41 | XCTAssertNotNil(parsedCookie) 42 | XCTAssertEqual(parsedCookie?.name, "foo") 43 | XCTAssertEqual(parsedCookie?.value, "bar") 44 | XCTAssertEqual(parsedCookie?.expiration, .maxAge(60)) 45 | XCTAssertEqual(parsedCookie?.domain, "zewo.io") 46 | XCTAssertEqual(parsedCookie?.path, "/libs") 47 | XCTAssertTrue(parsedCookie?.secure ?? false) 48 | XCTAssertTrue(parsedCookie?.httpOnly ?? false) 49 | 50 | cookieString = "foo" 51 | parsedCookie = AttributedCookie(cookieString) 52 | XCTAssertNil(parsedCookie) 53 | 54 | cookieString = "foo=bar; Max-Age=60=60" 55 | parsedCookie = AttributedCookie(cookieString) 56 | XCTAssertNil(parsedCookie) 57 | } 58 | 59 | func testExpirationEquality() { 60 | var expirationA = AttributedCookie.Expiration.expires("Thu, 01 Jan 1970 00:00:01 GMT") 61 | var expirationB = AttributedCookie.Expiration.expires("Thu, 01 Jan 1970 00:00:01 GMT") 62 | XCTAssertEqual(expirationA, expirationB) 63 | 64 | expirationA = AttributedCookie.Expiration.maxAge(60) 65 | expirationB = AttributedCookie.Expiration.maxAge(60) 66 | XCTAssertEqual(expirationA, expirationB) 67 | 68 | expirationA = AttributedCookie.Expiration.expires("Thu, 01 Jan 1970 00:00:01 GMT") 69 | expirationB = AttributedCookie.Expiration.maxAge(60) 70 | XCTAssertNotEqual(expirationA, expirationB) 71 | } 72 | } 73 | 74 | extension AttributedCookieTests { 75 | public static var allTests: [(String, (AttributedCookieTests) -> () throws -> Void)] { 76 | return [ 77 | ("testConstruction", testConstruction), 78 | ("testParsing", testParsing), 79 | ("testExpirationEquality", testExpirationEquality), 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Message/BodyTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class BodyTests : XCTestCase { 5 | let testData = Buffer([0x00, 0x01, 0x02, 0x03]) 6 | 7 | func testBufferBecomeBuffer() throws { 8 | var body: Body = .buffer(testData) 9 | let buffer = try body.becomeBuffer(deadline: 1.second.fromNow()) 10 | XCTAssertTrue(body.isBuffer) 11 | XCTAssertFalse(body.isReader) 12 | XCTAssertFalse(body.isWriter) 13 | XCTAssertEqual(buffer, testData) 14 | switch body { 15 | case .buffer(let data): 16 | XCTAssertEqual(data, self.testData) 17 | default: 18 | XCTFail() 19 | } 20 | } 21 | 22 | func testBufferBecomeReader() throws { 23 | var body: Body = .buffer(testData) 24 | let reader = try body.becomeReader() 25 | XCTAssertFalse(body.isBuffer) 26 | XCTAssertTrue(body.isReader) 27 | XCTAssertFalse(body.isWriter) 28 | XCTAssertFalse(reader.closed) 29 | let buffer = try reader.read(upTo: testData.count, deadline: 1.second.fromNow()) 30 | XCTAssertFalse(reader.closed) 31 | XCTAssertEqual(buffer.count, testData.count) 32 | XCTAssertEqual(buffer, testData) 33 | } 34 | 35 | func testBufferBecomeWriter() throws { 36 | var body: Body = .buffer(testData) 37 | let writer = try body.becomeWriter(deadline: 1.second.fromNow()) 38 | let writerStream = BufferStream() 39 | try writer(writerStream) 40 | XCTAssertFalse(body.isBuffer) 41 | XCTAssertFalse(body.isReader) 42 | XCTAssertTrue(body.isWriter) 43 | XCTAssertFalse(writerStream.closed) 44 | let buffer = try writerStream.read(upTo: testData.count, deadline: 1.second.fromNow()) 45 | XCTAssertFalse(writerStream.closed) 46 | XCTAssertEqual(buffer.count, testData.count) 47 | XCTAssertEqual(buffer, testData) 48 | } 49 | 50 | func testReaderBecomeBuffer() throws { 51 | let readerSteram = BufferStream(buffer: testData) 52 | var body: Body = .reader(readerSteram) 53 | let buffer = try body.becomeBuffer(deadline: 1.second.fromNow()) 54 | XCTAssertTrue(body.isBuffer) 55 | XCTAssertFalse(body.isReader) 56 | XCTAssertFalse(body.isWriter) 57 | XCTAssertEqual(buffer, testData) 58 | switch body { 59 | case .buffer(let data): 60 | XCTAssertEqual(data, self.testData) 61 | default: 62 | XCTFail() 63 | } 64 | } 65 | 66 | func testReaderBecomeReader() throws { 67 | let readerSteram = BufferStream(buffer: testData) 68 | var body: Body = .reader(readerSteram) 69 | let reader = try body.becomeReader() 70 | XCTAssertFalse(body.isBuffer) 71 | XCTAssertTrue(body.isReader) 72 | XCTAssertFalse(body.isWriter) 73 | XCTAssertFalse(reader.closed) 74 | let buffer = try reader.read(upTo: testData.count, deadline: 1.second.fromNow()) 75 | XCTAssertFalse(reader.closed) 76 | XCTAssertEqual(buffer.count, testData.count) 77 | XCTAssertEqual(buffer, testData) 78 | } 79 | 80 | func testReaderBecomeWriter() throws { 81 | let readerSteram = BufferStream(buffer: testData) 82 | var body: Body = .reader(readerSteram) 83 | let writer = try body.becomeWriter(deadline: 1.second.fromNow()) 84 | let writerStream = BufferStream() 85 | try writer(writerStream) 86 | XCTAssertFalse(body.isBuffer) 87 | XCTAssertFalse(body.isReader) 88 | XCTAssertTrue(body.isWriter) 89 | XCTAssertFalse(writerStream.closed) 90 | let buffer = try writerStream.read(upTo: testData.count, deadline: 1.second.fromNow()) 91 | XCTAssertFalse(writerStream.closed) 92 | XCTAssertEqual(buffer.count, testData.count) 93 | XCTAssertEqual(buffer, testData) 94 | } 95 | 96 | func testWriterBecomeBuffer() throws { 97 | var body: Body = .writer { writerStream in 98 | try writerStream.write(self.testData, deadline: 1.second.fromNow()) 99 | try writerStream.flush(deadline: 1.second.fromNow()) 100 | } 101 | let buffer = try body.becomeBuffer(deadline: 1.second.fromNow()) 102 | XCTAssertTrue(body.isBuffer) 103 | XCTAssertFalse(body.isReader) 104 | XCTAssertFalse(body.isWriter) 105 | XCTAssertEqual(buffer, testData) 106 | switch body { 107 | case .buffer(let data): 108 | XCTAssertEqual(data, self.testData) 109 | default: 110 | XCTFail() 111 | } 112 | } 113 | 114 | func testWriterBecomeReader() throws { 115 | var body: Body = .writer { writerStream in 116 | try writerStream.write(self.testData, deadline: 1.second.fromNow()) 117 | try writerStream.flush(deadline: 1.second.fromNow()) 118 | } 119 | let reader = try body.becomeReader() 120 | XCTAssertFalse(body.isBuffer) 121 | XCTAssertTrue(body.isReader) 122 | XCTAssertFalse(body.isWriter) 123 | XCTAssertFalse(reader.closed) 124 | let buffer = try reader.read(upTo: testData.count, deadline: 1.second.fromNow()) 125 | XCTAssertFalse(reader.closed) 126 | XCTAssertEqual(buffer.count, testData.count) 127 | XCTAssertEqual(buffer, testData) 128 | } 129 | 130 | func testWriterBecomeWriter() throws { 131 | var body: Body = .writer { writerStream in 132 | try writerStream.write(self.testData, deadline: 1.second.fromNow()) 133 | try writerStream.flush(deadline: 1.second.fromNow()) 134 | } 135 | let writer = try body.becomeWriter(deadline: 1.second.fromNow()) 136 | let writerStream = BufferStream() 137 | try writer(writerStream) 138 | XCTAssertFalse(body.isBuffer) 139 | XCTAssertFalse(body.isReader) 140 | XCTAssertTrue(body.isWriter) 141 | XCTAssertFalse(writerStream.closed) 142 | let buffer = try writerStream.read(upTo: testData.count, deadline: 1.second.fromNow()) 143 | XCTAssertFalse(writerStream.closed) 144 | XCTAssertEqual(buffer.count, testData.count) 145 | XCTAssertEqual(buffer, testData) 146 | } 147 | 148 | func testBodyEquality() { 149 | let buffer = Body.buffer(testData) 150 | 151 | let bufferStream = BufferStream(buffer: testData) 152 | let reader = Body.reader(bufferStream) 153 | 154 | let writer = Body.writer { stream in 155 | try stream.write(self.testData, deadline: 1.second.fromNow()) 156 | try stream.flush(deadline: 1.second.fromNow()) 157 | } 158 | 159 | XCTAssertEqual(buffer, buffer) 160 | XCTAssertNotEqual(buffer, reader) 161 | XCTAssertNotEqual(buffer, writer) 162 | XCTAssertNotEqual(reader, writer) 163 | } 164 | } 165 | 166 | extension BodyTests { 167 | public static var allTests: [(String, (BodyTests) -> () throws -> Void)] { 168 | return [ 169 | ("testBufferBecomeBuffer", testBufferBecomeBuffer), 170 | ("testBodyEquality", testBodyEquality), 171 | ] 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Message/CookieTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class CookieTests : XCTestCase { 5 | func testConstruction() throws { 6 | let cookieString = "foo=bar" 7 | let cookie = Cookie( 8 | name: "foo", 9 | value: "bar" 10 | ) 11 | XCTAssertEqual(cookie, cookie) 12 | XCTAssertEqual(String(describing: cookie), cookieString) 13 | XCTAssertEqual(cookie.name, "foo") 14 | XCTAssertEqual(cookie.value, "bar") 15 | } 16 | 17 | func testParsing() throws { 18 | var cookieString = "foo=bar; fuu=baz" 19 | var cookies = Set(cookieHeader: cookieString) 20 | XCTAssertNotNil(cookies) 21 | XCTAssertTrue(cookies?.contains(Cookie(name: "foo", value: "bar")) ?? false) 22 | XCTAssertTrue(cookies?.contains(Cookie(name: "fuu", value: "baz")) ?? false) 23 | 24 | cookieString = "foo; fuu" 25 | cookies = Set(cookieHeader: cookieString) 26 | XCTAssertNil(cookies) 27 | } 28 | } 29 | 30 | extension CookieTests { 31 | public static var allTests: [(String, (CookieTests) -> () throws -> Void)] { 32 | return [ 33 | ("testConstruction", testConstruction), 34 | ("testParsing", testParsing), 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Message/MessageTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class MessageTests : XCTestCase { 5 | func testHeadersCaseInsensitivity() { 6 | let headers: Headers = [ 7 | "Content-Type": "application/json", 8 | ] 9 | XCTAssertEqual(headers["content-TYPE"], "application/json") 10 | } 11 | 12 | func testHeadersDescription() { 13 | let headers: Headers = [ 14 | "Content-Type": "application/json", 15 | ] 16 | XCTAssertEqual(String(describing: headers), "Content-Type: application/json\n") 17 | } 18 | 19 | func testHeadersEquality() { 20 | let headers: Headers = [ 21 | "Content-Type": "application/json", 22 | ] 23 | XCTAssertEqual(headers, headers) 24 | } 25 | 26 | func testContentTypeHeader() { 27 | let mediaType = MediaType(type: "application", subtype: "json") 28 | var request = Request(headers: ["Content-Type": "application/json"]) 29 | XCTAssertEqual(request.contentType, mediaType) 30 | request.contentType = mediaType 31 | XCTAssertEqual(request.headers["Content-Type"], "application/json") 32 | } 33 | 34 | func testContentLengthHeader() { 35 | var request = Request() 36 | XCTAssertEqual(request.contentLength, 0) 37 | request.contentLength = 420 38 | XCTAssertEqual(request.headers["Content-Length"], "420") 39 | } 40 | 41 | func testTransferEncodingHeader() { 42 | var request = Request(headers: ["Transfer-Encoding": "foo"]) 43 | XCTAssertEqual(request.transferEncoding, "foo") 44 | request.transferEncoding = "chunked" 45 | XCTAssertTrue(request.isChunkEncoded) 46 | } 47 | 48 | func testConnectionHeader() { 49 | var request = Request(headers: ["Connection": "foo"]) 50 | XCTAssertEqual(request.connection, "foo") 51 | request.connection = "bar" 52 | XCTAssertEqual(request.headers["Connection"], "bar") 53 | XCTAssertEqual(request.connection, "bar") 54 | } 55 | 56 | func testIsKeepAlive() { 57 | var request = Request() 58 | XCTAssertTrue(request.isKeepAlive) 59 | request.connection = "close" 60 | XCTAssertFalse(request.isKeepAlive) 61 | request.version.minor = 0 62 | request.connection = nil 63 | XCTAssertFalse(request.isKeepAlive) 64 | request.connection = "keep-alive" 65 | XCTAssertTrue(request.isKeepAlive) 66 | } 67 | 68 | func testIsUpgrade() { 69 | var request = Request(headers: ["Connection": "foo"]) 70 | XCTAssertFalse(request.isUpgrade) 71 | request.connection = "upgrade" 72 | XCTAssertTrue(request.isUpgrade) 73 | } 74 | 75 | func testUpgradeHeader() { 76 | var request = Request(headers: ["Upgrade": "foo"]) 77 | XCTAssertEqual(request.upgrade, "foo") 78 | request.upgrade = "bar" 79 | XCTAssertEqual(request.headers["Upgrade"], "bar") 80 | XCTAssertEqual(request.upgrade, "bar") 81 | } 82 | 83 | func testStorageDescription() { 84 | var request = Request() 85 | XCTAssertEqual(request.storageDescription, "Storage:\n-") 86 | request.storage["foo"] = "bar" 87 | XCTAssertEqual(request.storageDescription, "Storage:\nfoo: bar\n") 88 | } 89 | } 90 | 91 | extension MessageTests { 92 | public static var allTests: [(String, (MessageTests) -> () throws -> Void)] { 93 | return [ 94 | ("testHeadersCaseInsensitivity", testHeadersCaseInsensitivity), 95 | ("testContentTypeHeader", testHeadersDescription), 96 | ("testContentTypeHeader", testHeadersEquality), 97 | ("testContentTypeHeader", testContentTypeHeader), 98 | ("testContentTypeHeader", testContentLengthHeader), 99 | ("testContentTypeHeader", testTransferEncodingHeader), 100 | ("testContentTypeHeader", testConnectionHeader), 101 | ("testContentTypeHeader", testIsKeepAlive), 102 | ("testContentTypeHeader", testIsUpgrade), 103 | ("testContentTypeHeader", testUpgradeHeader), 104 | ("testContentTypeHeader", testStorageDescription), 105 | ] 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Message/RequestMethodTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | let requestMethod: [Request.Method: String] = [ 5 | .delete: "DELETE", 6 | .get: "GET", 7 | .head: "HEAD", 8 | .post: "POST", 9 | .put: "PUT", 10 | .connect: "CONNECT", 11 | .options: "OPTIONS", 12 | .trace: "TRACE", 13 | .patch: "PATCH", 14 | .other(method: "open"): ("OPEN"), 15 | ] 16 | 17 | public class RequestMethodTests : XCTestCase { 18 | func testMethod() throws { 19 | for (method, rawMethod) in requestMethod { 20 | XCTAssertEqual(method, method) 21 | XCTAssertEqual(method.description, rawMethod) 22 | let newMethod = Request.Method(rawMethod) 23 | XCTAssertEqual(newMethod, method) 24 | } 25 | } 26 | } 27 | 28 | extension RequestMethodTests { 29 | public static var allTests: [(String, (RequestMethodTests) -> () throws -> Void)] { 30 | return [ 31 | ("testMethod", testMethod), 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Message/RequestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class RequestTests : XCTestCase { 5 | func testCreation() throws { 6 | var request = Request() 7 | XCTAssertEqual(request.method, .get) 8 | XCTAssertEqual(request.url, URL(string: "/")) 9 | XCTAssertEqual(request.version, Version(major: 1, minor: 1)) 10 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 11 | XCTAssertEqual(request.body, .empty) 12 | 13 | request = Request(body: BufferStream(buffer: "foo")) 14 | XCTAssertEqual(request.method, .get) 15 | XCTAssertEqual(request.url, URL(string: "/")) 16 | XCTAssertEqual(request.version, Version(major: 1, minor: 1)) 17 | XCTAssertEqual(request.headers, ["Transfer-Encoding": "chunked"]) 18 | XCTAssertTrue(request.body.isReader) 19 | 20 | request = Request { stream in 21 | try stream.write("foo", deadline: 1.second.fromNow()) 22 | try stream.flush(deadline: 1.second.fromNow()) 23 | } 24 | XCTAssertEqual(request.method, .get) 25 | XCTAssertEqual(request.url, URL(string: "/")) 26 | XCTAssertEqual(request.version, Version(major: 1, minor: 1)) 27 | XCTAssertEqual(request.headers, ["Transfer-Encoding": "chunked"]) 28 | XCTAssertTrue(request.body.isWriter) 29 | 30 | let body = "" 31 | request = Request(body: body) 32 | XCTAssertEqual(request.method, .get) 33 | XCTAssertEqual(request.url, URL(string: "/")) 34 | XCTAssertEqual(request.version, Version(major: 1, minor: 1)) 35 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 36 | XCTAssertEqual(request.body, .empty) 37 | 38 | request = Request(url: "/")! 39 | XCTAssertEqual(request.method, .get) 40 | XCTAssertEqual(request.url, URL(string: "/")) 41 | XCTAssertEqual(request.version, Version(major: 1, minor: 1)) 42 | XCTAssertEqual(request.headers, ["Content-Length": "0"]) 43 | XCTAssertEqual(request.body, .empty) 44 | 45 | request = Request(url: "/", body: BufferStream(buffer: "foo"))! 46 | XCTAssertEqual(request.method, .get) 47 | XCTAssertEqual(request.url, URL(string: "/")) 48 | XCTAssertEqual(request.version, Version(major: 1, minor: 1)) 49 | XCTAssertEqual(request.headers, ["Transfer-Encoding": "chunked"]) 50 | XCTAssertTrue(request.body.isReader) 51 | 52 | request = Request(url: "/") { stream in 53 | try stream.write("foo", deadline: 1.second.fromNow()) 54 | try stream.flush(deadline: 1.second.fromNow()) 55 | }! 56 | XCTAssertEqual(request.method, .get) 57 | XCTAssertEqual(request.url, URL(string: "/")) 58 | XCTAssertEqual(request.version, Version(major: 1, minor: 1)) 59 | XCTAssertEqual(request.headers, ["Transfer-Encoding": "chunked"]) 60 | XCTAssertTrue(request.body.isWriter) 61 | } 62 | 63 | func testURLAccessors() throws { 64 | #if os(macOS) 65 | guard let request = Request(url: "/foo?bar=baz") else { 66 | return XCTFail() 67 | } 68 | XCTAssertEqual(request.path, "/foo") 69 | XCTAssertEqual(request.queryItems, [URLQueryItem(name: "bar", value: "baz")]) 70 | #endif 71 | } 72 | 73 | func testAcceptHeader() throws { 74 | var request = Request(headers: ["Accept": "application/json"]) 75 | XCTAssertEqual(request.accept, [MediaType(type: "application", subtype: "json")]) 76 | request.accept = [MediaType(type: "text", subtype: "html")] 77 | XCTAssertEqual(request.headers["Accept"], "text/html") 78 | } 79 | 80 | func testCookieHeader() throws { 81 | var request = Request(headers: ["Cookie": "foo=bar"]) 82 | XCTAssertEqual(request.cookies, [Cookie(name: "foo", value: "bar")]) 83 | request.cookies = [Cookie(name: "fuu", value: "baz")] 84 | XCTAssertEqual(request.headers["Cookie"], "fuu=baz") 85 | request.headers["Cookie"] = "foo" 86 | XCTAssertEqual(request.cookies, []) 87 | } 88 | 89 | func testHostHeader() { 90 | var request = Request(headers: ["Host": "foo"]) 91 | XCTAssertEqual(request.host, "foo") 92 | request.host = "bar" 93 | XCTAssertEqual(request.headers["Host"], "bar") 94 | XCTAssertEqual(request.host, "bar") 95 | } 96 | 97 | func testUserAgentHeader() { 98 | var request = Request(headers: ["User-Agent": "foo"]) 99 | XCTAssertEqual(request.userAgent, "foo") 100 | request.userAgent = "bar" 101 | XCTAssertEqual(request.headers["User-Agent"], "bar") 102 | XCTAssertEqual(request.userAgent, "bar") 103 | } 104 | 105 | func testAuthorizationHeader() { 106 | var request = Request(headers: ["Authorization": "foo"]) 107 | XCTAssertEqual(request.authorization, "foo") 108 | request.authorization = "bar" 109 | XCTAssertEqual(request.headers["Authorization"], "bar") 110 | XCTAssertEqual(request.authorization, "bar") 111 | } 112 | 113 | func testUpgradeConnection() throws { 114 | var called = false 115 | var request = Request() 116 | request.upgradeConnection { (response, stream) in 117 | called = true 118 | } 119 | try request.upgradeConnection?(Response(), BufferStream()) 120 | XCTAssert(called) 121 | } 122 | 123 | func testPathParameters() throws { 124 | var request = Request() 125 | request.pathParameters = ["foo": "bar"] 126 | XCTAssertEqual(request.pathParameters, ["foo": "bar"]) 127 | } 128 | 129 | func testDescription() throws { 130 | let request = Request() 131 | XCTAssertEqual(request.requestLineDescription, "GET / HTTP/1.1\n") 132 | XCTAssertEqual(String(describing: request), "GET / HTTP/1.1\nContent-Length: 0\n") 133 | XCTAssertEqual(request.debugDescription, "GET / HTTP/1.1\nContent-Length: 0\n\nStorage:\n-") 134 | } 135 | } 136 | 137 | extension RequestTests { 138 | public static var allTests: [(String, (RequestTests) -> () throws -> Void)] { 139 | return [ 140 | ("testCreation", testCreation), 141 | ("testURLAccessors", testURLAccessors), 142 | ("testAcceptHeader", testAcceptHeader), 143 | ("testCookieHeader", testCookieHeader), 144 | ("testHostHeader", testHostHeader), 145 | ("testUserAgentHeader", testUserAgentHeader), 146 | ("testAuthorizationHeader", testAuthorizationHeader), 147 | ("testUpgradeConnection", testUpgradeConnection), 148 | ("testPathParameters", testPathParameters), 149 | ("testDescription", testDescription), 150 | ] 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Message/ResponseStatusTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | let responseStatus: [Response.Status: (Int, String)] = [ 5 | .`continue`: (100, "Continue"), 6 | .switchingProtocols: (101, "Switching Protocols"), 7 | .processing: (102, "Processing"), 8 | 9 | .ok: (200, "OK"), 10 | .created: (201, "Created"), 11 | .accepted: (202, "Accepted"), 12 | .nonAuthoritativeInformation: (203, "Non Authoritative Information"), 13 | .noContent: (204, "No Content"), 14 | .resetContent: (205, "Reset Content"), 15 | .partialContent: (206, "Partial Content"), 16 | 17 | .multipleChoices: (300, "Multiple Choices"), 18 | .movedPermanently: (301, "Moved Permanently"), 19 | .found: (302, "Found"), 20 | .seeOther: (303, "See Other"), 21 | .notModified: (304, "Not Modified"), 22 | .useProxy: (305, "Use Proxy"), 23 | .switchProxy: (306, "Switch Proxy"), 24 | .temporaryRedirect: (307, "Temporary Redirect"), 25 | .permanentRedirect: (308, "Permanent Redirect"), 26 | 27 | .badRequest: (400, "Bad Request"), 28 | .unauthorized: (401, "Unauthorized"), 29 | .paymentRequired: (402, "Payment Required"), 30 | .forbidden: (403, "Forbidden"), 31 | .notFound: (404, "Not Found"), 32 | .methodNotAllowed: (405, "Method Not Allowed"), 33 | .notAcceptable: (406, "Not Acceptable"), 34 | .proxyAuthenticationRequired: (407, "Proxy Authentication Required"), 35 | .requestTimeout: (408, "Request Timeout"), 36 | .conflict: (409, "Conflict"), 37 | .gone: (410, "Gone"), 38 | .lengthRequired: (411, "Length Required"), 39 | .preconditionFailed: (412, "Precondition Failed"), 40 | .requestEntityTooLarge: (413, "Request Entity Too Large"), 41 | .requestURITooLong: (414, "Request URI Too Long"), 42 | .unsupportedMediaType: (415, "Unsupported Media Type"), 43 | .requestedRangeNotSatisfiable: (416, "Requested Range Not Satisfiable"), 44 | .expectationFailed: (417, "Expectation Failed"), 45 | .imATeapot: (418, "I'm A Teapot"), 46 | .authenticationTimeout: (419, "Authentication Timeout"), 47 | .enhanceYourCalm: (420, "Enhance Your Calm"), 48 | .unprocessableEntity: (422, "Unprocessable Entity"), 49 | .locked: (423, "Locked"), 50 | .failedDependency: (424, "Failed Dependency"), 51 | .preconditionRequired: (428, "Precondition Required"), 52 | .tooManyRequests: (429, "Too Many Requests"), 53 | .requestHeaderFieldsTooLarge: (431, "Request Header Fields Too Large"), 54 | 55 | .internalServerError: (500, "Internal Server Error"), 56 | .notImplemented: (501, "Not Implemented"), 57 | .badGateway: (502, "Bad Gateway"), 58 | .serviceUnavailable: (503, "Service Unavailable"), 59 | .gatewayTimeout: (504, "Gateway Timeout"), 60 | .httpVersionNotSupported: (505, "HTTP Version Not Supported"), 61 | .variantAlsoNegotiates: (506, "Variant Also Negotiates"), 62 | .insufficientStorage: (507, "Insufficient Storage"), 63 | .loopDetected: (508, "Loop Detected"), 64 | .notExtended: (510, "Not Extended"), 65 | .networkAuthenticationRequired: (511, "Network Authentication Required"), 66 | 67 | .other(statusCode: 499, reasonPhrase: "OH NOES"): (499, "OH NOES"), 68 | ] 69 | 70 | public class ResponseStatusTests : XCTestCase { 71 | func testStatus() throws { 72 | for (status, (statusCode, reasonPhrase)) in responseStatus { 73 | XCTAssertEqual(status, status) 74 | XCTAssertEqual(status.statusCode, statusCode) 75 | XCTAssertEqual(status.reasonPhrase, reasonPhrase) 76 | let newStatus = Response.Status(statusCode: statusCode) 77 | XCTAssertEqual(newStatus, status) 78 | } 79 | let customReasonPhrase = Response.Status(statusCode: 200, reasonPhrase: "OH YEAHS") 80 | XCTAssertEqual(customReasonPhrase, .other(statusCode: 200, reasonPhrase: "OH YEAHS")) 81 | 82 | XCTAssertTrue(Response.Status.continue.isInformational) 83 | XCTAssertTrue(Response.Status.ok.isSuccessful) 84 | XCTAssertTrue(Response.Status.multipleChoices.isRedirection) 85 | XCTAssertTrue(Response.Status.badRequest.isError) 86 | XCTAssertTrue(Response.Status.badRequest.isClientError) 87 | XCTAssertTrue(Response.Status.internalServerError.isError) 88 | XCTAssertTrue(Response.Status.internalServerError.isServerError) 89 | } 90 | } 91 | 92 | extension ResponseStatusTests { 93 | public static var allTests: [(String, (ResponseStatusTests) -> () throws -> Void)] { 94 | return [ 95 | ("testStatus", testStatus), 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Message/ResponseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class ResponseTests : XCTestCase { 5 | func testCreation() throws { 6 | var response = Response() 7 | XCTAssertEqual(response.status, .ok) 8 | XCTAssertEqual(response.version, Version(major: 1, minor: 1)) 9 | XCTAssertEqual(response.headers, ["Content-Length": "0"]) 10 | XCTAssertEqual(response.body, .empty) 11 | 12 | response = Response(body: BufferStream(buffer: "foo")) 13 | XCTAssertEqual(response.status, .ok) 14 | XCTAssertEqual(response.version, Version(major: 1, minor: 1)) 15 | XCTAssertEqual(response.headers, ["Transfer-Encoding": "chunked"]) 16 | XCTAssertTrue(response.body.isReader) 17 | 18 | response = Response { stream in 19 | try stream.write("foo", deadline: 1.second.fromNow()) 20 | try stream.flush(deadline: 1.second.fromNow()) 21 | } 22 | XCTAssertEqual(response.status, .ok) 23 | XCTAssertEqual(response.version, Version(major: 1, minor: 1)) 24 | XCTAssertEqual(response.headers, ["Transfer-Encoding": "chunked"]) 25 | XCTAssertTrue(response.body.isWriter) 26 | 27 | let body = "" 28 | response = Response(body: body) 29 | XCTAssertEqual(response.status, .ok) 30 | XCTAssertEqual(response.version, Version(major: 1, minor: 1)) 31 | XCTAssertEqual(response.headers, ["Content-Length": "0"]) 32 | XCTAssertEqual(response.body, .empty) 33 | } 34 | 35 | func testStatusAccessors() throws { 36 | let response = Response(status: .ok) 37 | XCTAssertEqual(response.statusCode, 200) 38 | XCTAssertEqual(response.reasonPhrase, "OK") 39 | } 40 | 41 | func testCookieHeader() throws { 42 | var response = Response() 43 | response.cookieHeaders = ["foo=bar"] 44 | XCTAssertEqual(response.cookies, [AttributedCookie(name: "foo", value: "bar")]) 45 | response.cookies = [AttributedCookie(name: "fuu", value: "baz")] 46 | XCTAssertEqual(response.cookieHeaders, ["fuu=baz"]) 47 | response.cookieHeaders = ["foo"] 48 | XCTAssertEqual(response.cookies, []) 49 | } 50 | 51 | func testUpgradeConnection() throws { 52 | var called = false 53 | var response = Response() 54 | response.upgradeConnection { (request, stream) in 55 | called = true 56 | } 57 | try response.upgradeConnection?(Request(), BufferStream()) 58 | XCTAssert(called) 59 | } 60 | 61 | func testDescription() throws { 62 | let response = Response() 63 | XCTAssertEqual(response.statusLineDescription, "HTTP/1.1 200 OK\n") 64 | XCTAssertEqual(String(describing: response), "HTTP/1.1 200 OK\nContent-Length: 0\n") 65 | XCTAssertEqual(response.debugDescription, "HTTP/1.1 200 OK\nContent-Length: 0\n\nStorage:\n-") 66 | } 67 | } 68 | 69 | extension ResponseTests { 70 | public static var allTests: [(String, (ResponseTests) -> () throws -> Void)] { 71 | return [ 72 | ("testCreation", testCreation), 73 | ("testStatusAccessors", testStatusAccessors), 74 | ("testCookieHeader", testCookieHeader), 75 | ("testUpgradeConnection", testUpgradeConnection), 76 | ("testDescription", testDescription), 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Middleware/BasicAuthMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class BasicAuthMiddlewareTests : XCTestCase { 5 | func testBasicAuthMiddleware() throws { 6 | var serverBasicAuth = BasicAuthMiddleware(realm: "Zewo") { username, password in 7 | if username == "foo" && password == "bar" { 8 | return .authenticated 9 | } 10 | 11 | if username == "fuu" && password == "baz" { 12 | return .payload(key: "user", value: "bonzo") 13 | } 14 | 15 | return .accessDenied 16 | } 17 | 18 | var called = false 19 | var responder = BasicResponder { _ in 20 | called = true 21 | return Response() 22 | } 23 | 24 | let request = Request() 25 | var clientBasicAuth = BasicAuthMiddleware(username: "foo", password: "bar") 26 | var response = try [clientBasicAuth, serverBasicAuth].chain(to: responder).respond(to: request) 27 | 28 | XCTAssert(called) 29 | XCTAssertEqual(response.status, .ok) 30 | 31 | called = false 32 | responder = BasicResponder { request in 33 | called = true 34 | guard let value = request.storage["user"] as? String else { 35 | XCTFail("Should've set payload") 36 | return Response(status: .internalServerError) 37 | } 38 | XCTAssertEqual(value, "bonzo") 39 | return Response() 40 | } 41 | 42 | clientBasicAuth = BasicAuthMiddleware(username: "fuu", password: "baz") 43 | response = try [clientBasicAuth, serverBasicAuth].chain(to: responder).respond(to: request) 44 | 45 | XCTAssert(called) 46 | XCTAssertEqual(response.status, .ok) 47 | 48 | responder = BasicResponder { _ in 49 | XCTFail("Should've been bypassed") 50 | return Response() 51 | } 52 | 53 | clientBasicAuth = BasicAuthMiddleware(username: "fou", password: "boy") 54 | response = try [clientBasicAuth, serverBasicAuth].chain(to: responder).respond(to: request) 55 | 56 | XCTAssertEqual(response.status, .unauthorized) 57 | XCTAssertEqual(response.headers["WWW-Authenticate"], "Basic realm=\"Zewo\"") 58 | 59 | serverBasicAuth = BasicAuthMiddleware { username, password in 60 | return .accessDenied 61 | } 62 | 63 | clientBasicAuth = BasicAuthMiddleware(username: "fou", password: "boy") 64 | response = try [clientBasicAuth, serverBasicAuth].chain(to: responder).respond(to: request) 65 | 66 | XCTAssertEqual(response.status, .unauthorized) 67 | } 68 | 69 | func testInvalidRequests() throws { 70 | let basicAuth = BasicAuthMiddleware(realm: "Zewo") { username, password in 71 | return .accessDenied 72 | } 73 | 74 | let responder = BasicResponder { _ in 75 | XCTFail("Should've been bypassed") 76 | return Response() 77 | } 78 | 79 | var request = Request() 80 | var response = try basicAuth.respond(to: request, chainingTo: responder) 81 | XCTAssertEqual(response.status, .unauthorized) 82 | 83 | request.authorization = "" 84 | response = try basicAuth.respond(to: request, chainingTo: responder) 85 | XCTAssertEqual(response.status, .unauthorized) 86 | 87 | request.authorization = "Basic foo" 88 | response = try basicAuth.respond(to: request, chainingTo: responder) 89 | XCTAssertEqual(response.status, .unauthorized) 90 | 91 | request.authorization = "Basic Zm9v" 92 | response = try basicAuth.respond(to: request, chainingTo: responder) 93 | XCTAssertEqual(response.status, .unauthorized) 94 | } 95 | } 96 | 97 | extension BasicAuthMiddlewareTests { 98 | public static var allTests: [(String, (BasicAuthMiddlewareTests) -> () throws -> Void)] { 99 | return [ 100 | ("testBasicAuthMiddleware", testBasicAuthMiddleware), 101 | ("testInvalidRequests", testInvalidRequests), 102 | ] 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Middleware/BufferClientContentNegotiationMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class BufferClientContentNegotiationMiddlewareTests : XCTestCase { 5 | let contentNegotiation = ContentNegotiationMiddleware(mediaTypes: [.json, .urlEncodedForm], mode: .client, serializationMode: .buffer) 6 | 7 | func testClientRequestJSONResponse() throws { 8 | let request = Request(content: ["foo": "bar"]) 9 | 10 | let responder = BasicResponder { request in 11 | XCTAssertEqual(request.headers["Content-Type"], "application/json; charset=utf-8") 12 | XCTAssertEqual(request.headers["Accept"], "application/json, application/x-www-form-urlencoded") 13 | XCTAssertEqual(request.body, .buffer(Buffer("{\"foo\":\"bar\"}"))) 14 | XCTAssertNil(request.transferEncoding) 15 | 16 | return Response( 17 | headers: [ 18 | "Content-Type": "application/json; charset=utf-8", 19 | ], 20 | body: "{\"fuu\":\"baz\"}" 21 | ) 22 | } 23 | 24 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 25 | 26 | // Because there was no Accept header we serializer with the first media type in the 27 | // content negotiation middleware media type list. In this case JSON. 28 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 29 | XCTAssertEqual(response.content, ["fuu": "baz"]) 30 | } 31 | 32 | func testClientRequestURLEncodedFormResponse() throws { 33 | let request = Request(content: ["foo": "bar"]) 34 | 35 | let responder = BasicResponder { request in 36 | XCTAssertEqual(request.headers["Content-Type"], "application/json; charset=utf-8") 37 | XCTAssertEqual(request.headers["Accept"], "application/json, application/x-www-form-urlencoded") 38 | XCTAssertEqual(request.body, .buffer(Buffer("{\"foo\":\"bar\"}"))) 39 | XCTAssertNil(request.transferEncoding) 40 | 41 | return Response( 42 | headers: [ 43 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 44 | ], 45 | body: "fuu=baz" 46 | ) 47 | } 48 | 49 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 50 | 51 | // Because there was no Accept header we serializer with the first media type in the 52 | // content negotiation middleware media type list. In this case JSON. 53 | XCTAssertEqual(response.headers["Content-Type"], "application/x-www-form-urlencoded; charset=utf-8") 54 | XCTAssertEqual(response.content, ["fuu": "baz"]) 55 | } 56 | } 57 | 58 | extension BufferClientContentNegotiationMiddlewareTests { 59 | public static var allTests: [(String, (BufferClientContentNegotiationMiddlewareTests) -> () throws -> Void)] { 60 | return [ 61 | ("testClientRequestJSONResponse", testClientRequestJSONResponse), 62 | ("testClientRequestURLEncodedFormResponse", testClientRequestURLEncodedFormResponse), 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Middleware/BufferServerContentNegotiationMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class BufferServerContentNegotiationMiddlewareTests : XCTestCase { 5 | let contentNegotiation = ContentNegotiationMiddleware(mediaTypes: [.json, .urlEncodedForm], serializationMode: .buffer) 6 | 7 | func testJSONRequestDefaultResponse() throws { 8 | let request = Request( 9 | headers: [ 10 | "Content-Type": "application/json; charset=utf-8" 11 | ], 12 | body: "{\"foo\":\"bar\"}" 13 | ) 14 | 15 | let responder = BasicResponder { request in 16 | XCTAssertEqual(request.content, ["foo": "bar"]) 17 | return Response(content: ["fuu": "baz"]) 18 | } 19 | 20 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 21 | 22 | // Because there was no Accept header we serializer with the first media type in the 23 | // content negotiation middleware media type list. In this case JSON. 24 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 25 | XCTAssertEqual(response.body, .buffer(Buffer("{\"fuu\":\"baz\"}"))) 26 | XCTAssertNil(response.transferEncoding) 27 | } 28 | 29 | func testJSONRequestResponse() throws { 30 | let request = Request( 31 | headers: [ 32 | "Content-Type": "application/json; charset=utf-8", 33 | "Accept": "application/json" 34 | ], 35 | body: "{\"foo\":\"bar\"}" 36 | ) 37 | 38 | let responder = BasicResponder { request in 39 | XCTAssertEqual(request.content, ["foo": "bar"]) 40 | return Response(content: ["fuu": "baz"]) 41 | } 42 | 43 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 44 | 45 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 46 | XCTAssertEqual(response.body, .buffer(Buffer("{\"fuu\":\"baz\"}"))) 47 | XCTAssertNil(response.transferEncoding) 48 | } 49 | 50 | func testJSONRequestURLEncodedFormResponse() throws { 51 | let request = Request( 52 | headers: [ 53 | "Content-Type": "application/json; charset=utf-8", 54 | "Accept": "application/x-www-form-urlencoded" 55 | ], 56 | body: "{\"foo\":\"bar\"}" 57 | ) 58 | 59 | let responder = BasicResponder { request in 60 | XCTAssertEqual(request.content, ["foo": "bar"]) 61 | return Response(content: ["fuu": "baz"]) 62 | } 63 | 64 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 65 | 66 | XCTAssertEqual(response.headers["Content-Type"], "application/x-www-form-urlencoded; charset=utf-8") 67 | XCTAssertEqual(response.body, .buffer(Buffer("fuu=baz"))) 68 | XCTAssertNil(response.transferEncoding) 69 | } 70 | 71 | func testURLEncodedFormRequestDefaultResponse() throws { 72 | let request = Request( 73 | headers: [ 74 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" 75 | ], 76 | body: "foo=bar" 77 | ) 78 | 79 | let responder = BasicResponder { request in 80 | XCTAssertEqual(request.content, ["foo": "bar"]) 81 | return Response(content: ["fuu": "baz"]) 82 | } 83 | 84 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 85 | 86 | // Because there was no Accept header we serializer with the first media type in the 87 | // content negotiation middleware media type list. In this case JSON. 88 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 89 | XCTAssertEqual(response.body, .buffer(Buffer("{\"fuu\":\"baz\"}"))) 90 | XCTAssertNil(response.transferEncoding) 91 | } 92 | 93 | func testURLEncodedFormRequestResponse() throws { 94 | let request = Request( 95 | headers: [ 96 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 97 | "Accept": "application/x-www-form-urlencoded" 98 | ], 99 | body: "foo=bar" 100 | ) 101 | 102 | let responder = BasicResponder { request in 103 | XCTAssertEqual(request.content, ["foo": "bar"]) 104 | return Response(content: ["fuu": "baz"]) 105 | } 106 | 107 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 108 | 109 | XCTAssertEqual(response.headers["Content-Type"], "application/x-www-form-urlencoded; charset=utf-8") 110 | XCTAssertEqual(response.body, .buffer(Buffer("fuu=baz"))) 111 | XCTAssertNil(response.transferEncoding) 112 | } 113 | 114 | func testURLEncodedFormRequestJSONResponse() throws { 115 | let request = Request( 116 | headers: [ 117 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 118 | "Accept": "application/json" 119 | ], 120 | body: "foo=bar" 121 | ) 122 | 123 | let responder = BasicResponder { request in 124 | XCTAssertEqual(request.content, ["foo": "bar"]) 125 | return Response(content: ["fuu": "baz"]) 126 | } 127 | 128 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 129 | 130 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 131 | XCTAssertEqual(response.body, .buffer(Buffer("{\"fuu\":\"baz\"}"))) 132 | XCTAssertNil(response.transferEncoding) 133 | } 134 | } 135 | 136 | extension BufferServerContentNegotiationMiddlewareTests { 137 | public static var allTests: [(String, (BufferServerContentNegotiationMiddlewareTests) -> () throws -> Void)] { 138 | return [ 139 | ("testJSONRequestDefaultResponse", testJSONRequestDefaultResponse), 140 | ("testJSONRequestResponse", testJSONRequestResponse), 141 | ("testJSONRequestURLEncodedFormResponse", testJSONRequestURLEncodedFormResponse), 142 | ("testURLEncodedFormRequestDefaultResponse", testURLEncodedFormRequestDefaultResponse), 143 | ("testURLEncodedFormRequestResponse", testURLEncodedFormRequestResponse), 144 | ("testURLEncodedFormRequestJSONResponse", testURLEncodedFormRequestJSONResponse), 145 | ] 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Middleware/LogMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class LogMiddlewareTests : XCTestCase { 5 | func testLogMiddleware() throws { 6 | let stream = BufferStream() 7 | let log = LogMiddleware(stream: stream) 8 | let request = Request() 9 | 10 | let responder = BasicResponder { _ in 11 | return Response() 12 | } 13 | 14 | _ = try log.respond(to: request, chainingTo: responder) 15 | 16 | XCTAssertEqual(try String(buffer: stream.buffer), "================================================================================\nRequest:\n\nGET / HTTP/1.1\nContent-Length: 0\n\n--------------------------------------------------------------------------------\nResponse:\n\nHTTP/1.1 200 OK\nContent-Length: 0\n\n================================================================================\n") 17 | } 18 | 19 | func testDebugLogMiddleware() throws { 20 | let stream = BufferStream() 21 | let log = LogMiddleware(debug: true, stream: stream) 22 | let request = Request() 23 | 24 | let responder = BasicResponder { _ in 25 | return Response() 26 | } 27 | 28 | _ = try log.respond(to: request, chainingTo: responder) 29 | 30 | XCTAssertEqual(try String(buffer: stream.buffer), "================================================================================\nRequest:\n\nGET / HTTP/1.1\nContent-Length: 0\n\nStorage:\n-\n--------------------------------------------------------------------------------\nResponse:\n\nHTTP/1.1 200 OK\nContent-Length: 0\n\nStorage:\n-\n================================================================================\n") 31 | } 32 | } 33 | 34 | extension LogMiddlewareTests { 35 | public static var allTests: [(String, (LogMiddlewareTests) -> () throws -> Void)] { 36 | return [ 37 | ("testLogMiddleware", testLogMiddleware), 38 | ("testDebugLogMiddleware", testDebugLogMiddleware), 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Middleware/RecoveryMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | enum RecoveryMiddlewareTestError : Error { 5 | case error 6 | } 7 | 8 | public class RecoveryMiddlewareTests : XCTestCase { 9 | func testRecoveryMiddleware() throws { 10 | let request = Request() 11 | 12 | var responder = BasicResponder { _ in 13 | throw HTTPError.badRequest 14 | } 15 | 16 | var recovery = RecoveryMiddleware() 17 | var response = try recovery.respond(to: request, chainingTo: responder) 18 | XCTAssertEqual(response.status, .badRequest) 19 | 20 | responder = BasicResponder { _ in 21 | throw RecoveryMiddlewareTestError.error 22 | } 23 | 24 | recovery = RecoveryMiddleware() 25 | XCTAssertThrowsError(try recovery.respond(to: request, chainingTo: responder)) 26 | 27 | responder = BasicResponder { _ in 28 | throw RecoveryMiddlewareTestError.error 29 | } 30 | 31 | recovery = RecoveryMiddleware { error in 32 | switch error { 33 | case RecoveryMiddlewareTestError.error: 34 | return Response(status: .internalServerError) 35 | default: 36 | XCTFail("Should've recovered") 37 | throw error 38 | } 39 | } 40 | 41 | response = try recovery.respond(to: request, chainingTo: responder) 42 | XCTAssertEqual(response.status, .internalServerError) 43 | 44 | responder = BasicResponder { _ in 45 | return Response() 46 | } 47 | 48 | recovery = RecoveryMiddleware { error in 49 | XCTFail("Should've not been called") 50 | throw error 51 | } 52 | 53 | response = try recovery.respond(to: request, chainingTo: responder) 54 | XCTAssertEqual(response.status, .ok) 55 | } 56 | } 57 | 58 | extension RecoveryMiddlewareTests { 59 | public static var allTests: [(String, (RecoveryMiddlewareTests) -> () throws -> Void)] { 60 | return [ 61 | ("testRecoveryMiddleware", testRecoveryMiddleware), 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Middleware/RedirectMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class RedirectMiddlewareTests : XCTestCase { 5 | let redirect = RedirectMiddleware(redirectTo: "/over-there", if: { $0.method == .get }) 6 | 7 | func testDoesRedirect() throws { 8 | let request = Request() 9 | 10 | let responder = BasicResponder { _ in 11 | XCTFail("Should have redirected") 12 | return Response() 13 | } 14 | 15 | let response = try redirect.respond(to: request, chainingTo: responder) 16 | 17 | XCTAssertEqual(response.status, .found) 18 | XCTAssertEqual(response.headers["location"], "/over-there") 19 | } 20 | 21 | func testDoesntRedirect() throws { 22 | let request = Request(method: .post) 23 | 24 | let responder = BasicResponder { _ in 25 | return Response() 26 | } 27 | 28 | let response = try redirect.respond(to: request, chainingTo: responder) 29 | 30 | XCTAssertEqual(response.status, .ok) 31 | XCTAssertNotEqual(response.headers["location"], "/over-there") 32 | } 33 | } 34 | 35 | extension RedirectMiddlewareTests { 36 | public static var allTests: [(String, (RedirectMiddlewareTests) -> () throws -> Void)] { 37 | return [ 38 | ("testDoesRedirect", testDoesRedirect), 39 | ("testDoesntRedirect", testDoesntRedirect), 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Middleware/SessionMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class SessionMiddlewareTests : XCTestCase { 5 | let middleware = SessionMiddleware() 6 | 7 | func testCookieIsAdded() throws { 8 | let request = Request() 9 | 10 | let response = try middleware.respond(to: request, chainingTo: BasicResponder { request in 11 | XCTAssertNotNil(request.session) 12 | return Response() 13 | }) 14 | 15 | XCTAssertEqual(response.cookieHeaders.count, 1) 16 | } 17 | 18 | func testSessionPersists() throws { 19 | let request1 = Request() 20 | var request2: Request! 21 | 22 | let response1 = try middleware.respond(to: request1, chainingTo: BasicResponder { req in 23 | request2 = req 24 | return Response() 25 | }) 26 | 27 | let session1: Session! = request2.session 28 | XCTAssertNotNil(session1) 29 | XCTAssertEqual(response1.cookieHeaders.count, 1) 30 | 31 | let sessionToken = session1.token 32 | session1["key"] = "value" 33 | 34 | guard let responseCookie = response1.cookies.first else { 35 | return XCTFail("Response should contain cookie") 36 | } 37 | 38 | // make another request, this time with the cookie 39 | var request3 = Request(headers: ["Cookies": response1.cookies.first!.value]) 40 | request3.cookies.insert(Cookie(name: responseCookie.name, value: responseCookie.value)) 41 | var request4: Request! 42 | 43 | let _ = try middleware.respond(to: request3, chainingTo: BasicResponder { req in 44 | request4 = req 45 | return Response() 46 | }) 47 | 48 | // make sure session is still there 49 | let session2: Session! = request4.session 50 | XCTAssertNotNil(session2) 51 | 52 | // make sure its the same session 53 | XCTAssertEqual(session2.token, sessionToken) 54 | 55 | // make sure that the session persists information 56 | let value: Any! = session2["key"] 57 | XCTAssertNotNil(value) 58 | XCTAssertNotNil(value as? String) 59 | XCTAssertEqual(value as? String, "value") 60 | } 61 | } 62 | 63 | extension SessionMiddlewareTests { 64 | public static var allTests: [(String, (SessionMiddlewareTests) -> () throws -> Void)] { 65 | return [ 66 | ("testCookieIsAdded", testCookieIsAdded), 67 | ("testSessionPersists", testSessionPersists) 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Middleware/StreamClientContentNegotiationMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class StreamClientContentNegotiationMiddlewareTests : XCTestCase { 5 | let contentNegotiation = ContentNegotiationMiddleware(mediaTypes: [.json, .urlEncodedForm], mode: .client, serializationMode: .stream) 6 | 7 | func testClientRequestJSONResponse() throws { 8 | let request = Request(content: ["foo": "bar"]) 9 | 10 | let responder = BasicResponder { request in 11 | XCTAssertEqual(request.headers["Content-Type"], "application/json; charset=utf-8") 12 | XCTAssertEqual(request.headers["Accept"], "application/json, application/x-www-form-urlencoded") 13 | XCTAssertEqual(request.transferEncoding, "chunked") 14 | XCTAssertNil(request.contentLength) 15 | 16 | let stream = BufferStream() 17 | switch request.body { 18 | case .writer(let writer): 19 | try writer(stream) 20 | XCTAssertEqual(stream.buffer, Buffer("{\"foo\":\"bar\"}")) 21 | default: 22 | XCTFail() 23 | } 24 | return Response( 25 | headers: [ 26 | "Content-Type": "application/json; charset=utf-8", 27 | ], 28 | body: "{\"fuu\":\"baz\"}" 29 | ) 30 | } 31 | 32 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 33 | 34 | // Because there was no Accept header we serializer with the first media type in the 35 | // content negotiation middleware media type list. In this case JSON. 36 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 37 | XCTAssertEqual(response.content, ["fuu": "baz"]) 38 | } 39 | 40 | func testClientRequestURLEncodedFormResponse() throws { 41 | let request = Request(content: ["foo": "bar"]) 42 | 43 | let responder = BasicResponder { request in 44 | XCTAssertEqual(request.headers["Content-Type"], "application/json; charset=utf-8") 45 | XCTAssertEqual(request.headers["Accept"], "application/json, application/x-www-form-urlencoded") 46 | XCTAssertEqual(request.transferEncoding, "chunked") 47 | XCTAssertNil(request.contentLength) 48 | 49 | let stream = BufferStream() 50 | switch request.body { 51 | case .writer(let writer): 52 | try writer(stream) 53 | XCTAssertEqual(stream.buffer, Buffer("{\"foo\":\"bar\"}")) 54 | default: 55 | XCTFail() 56 | } 57 | return Response( 58 | headers: [ 59 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 60 | ], 61 | body: "fuu=baz" 62 | ) 63 | } 64 | 65 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 66 | 67 | // Because there was no Accept header we serializer with the first media type in the 68 | // content negotiation middleware media type list. In this case JSON. 69 | XCTAssertEqual(response.headers["Content-Type"], "application/x-www-form-urlencoded; charset=utf-8") 70 | XCTAssertEqual(response.content, ["fuu": "baz"]) 71 | } 72 | } 73 | 74 | extension StreamClientContentNegotiationMiddlewareTests { 75 | public static var allTests: [(String, (StreamClientContentNegotiationMiddlewareTests) -> () throws -> Void)] { 76 | return [ 77 | ("testClientRequestJSONResponse", testClientRequestJSONResponse), 78 | ("testClientRequestURLEncodedFormResponse", testClientRequestURLEncodedFormResponse), 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Middleware/StreamServerContentNegotiationMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | public class StreamServerContentNegotiationMiddlewareTests : XCTestCase { 5 | let contentNegotiation = ContentNegotiationMiddleware(mediaTypes: [.json, .urlEncodedForm]) 6 | 7 | func testJSONRequestDefaultResponse() throws { 8 | let request = Request( 9 | headers: [ 10 | "Content-Type": "application/json; charset=utf-8" 11 | ], 12 | body: "{\"foo\":\"bar\"}" 13 | ) 14 | 15 | let responder = BasicResponder { request in 16 | XCTAssertEqual(request.content, ["foo": "bar"]) 17 | return Response(content: ["fuu": "baz"]) 18 | } 19 | 20 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 21 | 22 | // Because there was no Accept header we serializer with the first media type in the 23 | // content negotiation middleware media type list. In this case JSON. 24 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 25 | XCTAssertEqual(response.transferEncoding, "chunked") 26 | XCTAssertNil(response.contentLength) 27 | 28 | let stream = BufferStream() 29 | switch response.body { 30 | case .writer(let writer): 31 | try writer(stream) 32 | XCTAssertEqual(stream.buffer, Buffer("{\"fuu\":\"baz\"}")) 33 | default: 34 | XCTFail() 35 | } 36 | } 37 | 38 | func testJSONRequestResponse() throws { 39 | let request = Request( 40 | headers: [ 41 | "Content-Type": "application/json; charset=utf-8", 42 | "Accept": "application/json" 43 | ], 44 | body: "{\"foo\":\"bar\"}" 45 | ) 46 | 47 | let responder = BasicResponder { request in 48 | XCTAssertEqual(request.content, ["foo": "bar"]) 49 | return Response(content: ["fuu": "baz"]) 50 | } 51 | 52 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 53 | 54 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 55 | XCTAssertEqual(response.transferEncoding, "chunked") 56 | XCTAssertNil(response.contentLength) 57 | 58 | let stream = BufferStream() 59 | switch response.body { 60 | case .writer(let writer): 61 | try writer(stream) 62 | XCTAssertEqual(stream.buffer, Buffer("{\"fuu\":\"baz\"}")) 63 | default: 64 | XCTFail() 65 | } 66 | } 67 | 68 | func testJSONRequestURLEncodedFormResponse() throws { 69 | let request = Request( 70 | headers: [ 71 | "Content-Type": "application/json; charset=utf-8", 72 | "Accept": "application/x-www-form-urlencoded" 73 | ], 74 | body: "{\"foo\":\"bar\"}" 75 | ) 76 | 77 | let responder = BasicResponder { request in 78 | XCTAssertEqual(request.content, ["foo": "bar"]) 79 | return Response(content: ["fuu": "baz"]) 80 | } 81 | 82 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 83 | 84 | XCTAssertEqual(response.headers["Content-Type"], "application/x-www-form-urlencoded; charset=utf-8") 85 | XCTAssertEqual(response.transferEncoding, "chunked") 86 | XCTAssertNil(response.contentLength) 87 | 88 | let stream = BufferStream() 89 | switch response.body { 90 | case .writer(let writer): 91 | try writer(stream) 92 | XCTAssertEqual(stream.buffer, Buffer("fuu=baz")) 93 | default: 94 | XCTFail() 95 | } 96 | } 97 | 98 | func testURLEncodedFormRequestDefaultResponse() throws { 99 | let request = Request( 100 | headers: [ 101 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" 102 | ], 103 | body: "foo=bar" 104 | ) 105 | 106 | let responder = BasicResponder { request in 107 | XCTAssertEqual(request.content, ["foo": "bar"]) 108 | return Response(content: ["fuu": "baz"]) 109 | } 110 | 111 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 112 | 113 | // Because there was no Accept header we serializer with the first media type in the 114 | // content negotiation middleware media type list. In this case JSON. 115 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 116 | XCTAssertEqual(response.transferEncoding, "chunked") 117 | XCTAssertNil(response.contentLength) 118 | 119 | let stream = BufferStream() 120 | switch response.body { 121 | case .writer(let writer): 122 | try writer(stream) 123 | XCTAssertEqual(stream.buffer, Buffer("{\"fuu\":\"baz\"}")) 124 | default: 125 | XCTFail() 126 | } 127 | } 128 | 129 | func testURLEncodedFormRequestResponse() throws { 130 | let request = Request( 131 | headers: [ 132 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 133 | "Accept": "application/x-www-form-urlencoded" 134 | ], 135 | body: "foo=bar" 136 | ) 137 | 138 | let responder = BasicResponder { request in 139 | XCTAssertEqual(request.content, ["foo": "bar"]) 140 | return Response(content: ["fuu": "baz"]) 141 | } 142 | 143 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 144 | 145 | XCTAssertEqual(response.headers["Content-Type"], "application/x-www-form-urlencoded; charset=utf-8") 146 | XCTAssertEqual(response.transferEncoding, "chunked") 147 | XCTAssertNil(response.contentLength) 148 | 149 | let stream = BufferStream() 150 | switch response.body { 151 | case .writer(let writer): 152 | try writer(stream) 153 | XCTAssertEqual(stream.buffer, Buffer("fuu=baz")) 154 | default: 155 | XCTFail() 156 | } 157 | } 158 | 159 | func testURLEncodedFormRequestJSONResponse() throws { 160 | let request = Request( 161 | headers: [ 162 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 163 | "Accept": "application/json" 164 | ], 165 | body: "foo=bar" 166 | ) 167 | 168 | let responder = BasicResponder { request in 169 | XCTAssertEqual(request.content, ["foo": "bar"]) 170 | return Response(content: ["fuu": "baz"]) 171 | } 172 | 173 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 174 | 175 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 176 | XCTAssertEqual(response.transferEncoding, "chunked") 177 | XCTAssertNil(response.contentLength) 178 | 179 | let stream = BufferStream() 180 | switch response.body { 181 | case .writer(let writer): 182 | try writer(stream) 183 | XCTAssertEqual(stream.buffer, Buffer("{\"fuu\":\"baz\"}")) 184 | default: 185 | XCTFail() 186 | } 187 | } 188 | 189 | func testEmptyBodyRequest() throws { 190 | let request = Request( 191 | headers: [ 192 | "Content-Type": "application/json; charset=utf-8" 193 | ] 194 | ) 195 | 196 | let responder = BasicResponder { request in 197 | XCTAssertNil(request.content) 198 | return try Response(content: ["items": []]) 199 | } 200 | 201 | let response = try contentNegotiation.respond(to: request, chainingTo: responder) 202 | 203 | XCTAssertEqual(response.headers["Content-Type"], "application/json; charset=utf-8") 204 | XCTAssertEqual(response.transferEncoding, "chunked") 205 | XCTAssertNil(response.contentLength) 206 | 207 | let stream = BufferStream() 208 | switch response.body { 209 | case .writer(let writer): 210 | try writer(stream) 211 | XCTAssertEqual(stream.buffer, Buffer("{\"items\":[]}")) 212 | default: 213 | XCTFail() 214 | } 215 | } 216 | } 217 | 218 | extension StreamServerContentNegotiationMiddlewareTests { 219 | public static var allTests: [(String, (StreamServerContentNegotiationMiddlewareTests) -> () throws -> Void)] { 220 | return [ 221 | ("testJSONRequestDefaultResponse", testJSONRequestDefaultResponse), 222 | ("testJSONRequestResponse", testJSONRequestResponse), 223 | ("testJSONRequestURLEncodedFormResponse", testJSONRequestURLEncodedFormResponse), 224 | ("testURLEncodedFormRequestDefaultResponse", testURLEncodedFormRequestDefaultResponse), 225 | ("testURLEncodedFormRequestResponse", testURLEncodedFormRequestResponse), 226 | ("testURLEncodedFormRequestJSONResponse", testURLEncodedFormRequestJSONResponse), 227 | ("testEmptyBodyRequest", testEmptyBodyRequest) 228 | ] 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Parser/RequestParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | import CHTTPParser 4 | 5 | let requestCount = [ 6 | 1, 7 | 2, 8 | 5 9 | ] 10 | 11 | let bufferSizes = [ 12 | 1, 13 | 2, 14 | 4, 15 | 32, 16 | 512, 17 | 2048 18 | ] 19 | 20 | let methods: [Request.Method] = [ 21 | .delete, 22 | .get, 23 | .head, 24 | .post, 25 | .put, 26 | .options, 27 | .trace, 28 | .patch, 29 | .other(method: "COPY"), 30 | .other(method: "LOCK"), 31 | .other(method: "MKCOL"), 32 | .other(method: "MOVE"), 33 | .other(method: "PROPFIND"), 34 | .other(method: "PROPPATCH"), 35 | .other(method: "SEARCH"), 36 | .other(method: "UNLOCK"), 37 | .other(method: "BIND"), 38 | .other(method: "REBIND"), 39 | .other(method: "UNBIND"), 40 | .other(method: "ACL"), 41 | .other(method: "REPORT"), 42 | .other(method: "MKACTIVITY"), 43 | .other(method: "CHECKOUT"), 44 | .other(method: "MERGE"), 45 | .other(method: "M-SEARCH"), 46 | .other(method: "NOTIFY"), 47 | .other(method: "SUBSCRIBE"), 48 | .other(method: "UNSUBSCRIBE"), 49 | .other(method: "PURGE"), 50 | .other(method: "MKCALENDAR"), 51 | .other(method: "LINK"), 52 | .other(method: "UNLINK"), 53 | ] 54 | 55 | public class RequestParserTests : XCTestCase { 56 | func testInvalidMethod() { 57 | let data = "INVALID / HTTP/1.1\r\n\r\n" 58 | let parser = MessageParser(mode: .request) 59 | XCTAssertThrowsError(try parser.parse(data)) 60 | } 61 | 62 | func testInvalidURL() { 63 | let data = "GET huehue HTTP/1.1\r\n\r\n" 64 | let parser = MessageParser(mode: .request) 65 | XCTAssertThrowsError(try parser.parse(data)) 66 | } 67 | 68 | func testNoURL() { 69 | let data = "GET HTTP/1.1\r\n\r\n" 70 | let parser = MessageParser(mode: .request) 71 | XCTAssertThrowsError(try parser.parse(data)) 72 | } 73 | 74 | func testInvalidHTTPVersion() { 75 | let data = "GET / HUEHUE\r\n\r\n" 76 | let parser = MessageParser(mode: .request) 77 | XCTAssertThrowsError(try parser.parse(data)) 78 | } 79 | 80 | func testInvalidDoubleConnectMethod() { 81 | let data = "CONNECT / HTTP/1.1\r\n\r\nCONNECT / HTTP/1.1\r\n\r\n" 82 | let parser = MessageParser(mode: .request) 83 | XCTAssertThrowsError(try parser.parse(data)) 84 | } 85 | 86 | func testConnectMethod() throws { 87 | let data = "CONNECT / HTTP/1.1\r\n\r\n" 88 | let parser = MessageParser(mode: .request) 89 | let request = try parser.parse(data).first! as! Request 90 | XCTAssert(request.method == .connect) 91 | XCTAssert(request.url.path == "/") 92 | XCTAssert(request.version.major == 1) 93 | XCTAssert(request.version.minor == 1) 94 | XCTAssertEqual(request.headers.count, 0) 95 | } 96 | 97 | func check(request: String, count: Int, bufferSize: Int, test: @escaping (Request) -> Void) throws { 98 | var data = "" 99 | 100 | for _ in 0 ..< count { 101 | data += request 102 | } 103 | 104 | let parser = MessageParser(mode: .request) 105 | for message in try parser.parse(data) { 106 | let request = message as! Request 107 | test(request) 108 | } 109 | } 110 | 111 | func testShortRequests() throws { 112 | for bufferSize in bufferSizes { 113 | for count in requestCount { 114 | for method in methods { 115 | let request = "\(method) / HTTP/1.1\r\n\r\n" 116 | try check(request: request, count: count, bufferSize: bufferSize) { request in 117 | XCTAssert(request.method == method) 118 | XCTAssert(request.url.path == "/") 119 | XCTAssert(request.version.major == 1) 120 | XCTAssert(request.version.minor == 1) 121 | XCTAssert(request.headers.count == 0) 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | func testMediumRequests() throws { 129 | for bufferSize in bufferSizes { 130 | for count in requestCount { 131 | for method in methods { 132 | let request = "\(method) / HTTP/1.1\r\nHost: zewo.co\r\n\r\n" 133 | try check(request: request, count: count, bufferSize: bufferSize) { request in 134 | XCTAssert(request.method == method) 135 | XCTAssert(request.url.path == "/") 136 | XCTAssert(request.version.major == 1) 137 | XCTAssert(request.version.minor == 1) 138 | XCTAssert(request.headers["Host"] == "zewo.co") 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | func testCookiesRequest() throws { 146 | for bufferSize in bufferSizes { 147 | for count in requestCount { 148 | for method in methods { 149 | let request = "\(method) / HTTP/1.1\r\nHost: zewo.co\r\nCookie: server=zewo, lang=swift\r\n\r\n" 150 | try check(request: request, count: count, bufferSize: bufferSize) { request in 151 | XCTAssert(request.method == method) 152 | XCTAssert(request.url.path == "/") 153 | XCTAssert(request.version.major == 1) 154 | XCTAssert(request.version.minor == 1) 155 | XCTAssert(request.headers["Host"] == "zewo.co") 156 | XCTAssert(request.headers["Cookie"] == "server=zewo, lang=swift") 157 | } 158 | } 159 | } 160 | } 161 | } 162 | 163 | func testBodyRequest() throws { 164 | for bufferSize in bufferSizes { 165 | for count in requestCount { 166 | for method in methods { 167 | let request = "\(method) / HTTP/1.1\r\nContent-Length: 4\r\n\r\nZewo" 168 | try check(request: request, count: count, bufferSize: bufferSize) { request in 169 | XCTAssert(request.method == method) 170 | XCTAssert(request.url.path == "/") 171 | XCTAssert(request.version.major == 1) 172 | XCTAssert(request.version.minor == 1) 173 | XCTAssert(request.headers["Content-Length"] == "4") 174 | XCTAssert(request.body == .buffer(Buffer("Zewo"))) 175 | } 176 | } 177 | } 178 | } 179 | } 180 | 181 | func testManyRequests() { 182 | var request = "" 183 | 184 | for _ in 0 ..< 100 { 185 | request += "POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nZewo" 186 | } 187 | 188 | measure { 189 | do { 190 | try self.check(request: request, count: 1, bufferSize: 4096) { request in 191 | XCTAssert(request.method == .post) 192 | XCTAssert(request.url.path == "/") 193 | XCTAssert(request.version.major == 1) 194 | XCTAssert(request.version.minor == 1) 195 | XCTAssert(request.headers["Content-Length"] == "4") 196 | XCTAssert(request.body == .buffer(Buffer("Zewo"))) 197 | } 198 | } catch { 199 | XCTFail() 200 | } 201 | } 202 | } 203 | 204 | func testErrorDescription() { 205 | XCTAssertEqual(String(describing: HPE_OK), "success") 206 | } 207 | 208 | func testUnknownMethod() { 209 | XCTAssertEqual(Request.Method(code: http_method(rawValue: 1969)), .other(method: "UNKNOWN")) 210 | } 211 | 212 | func testDuplicateHeaders() throws { 213 | for bufferSize in bufferSizes { 214 | for count in requestCount { 215 | for method in methods { 216 | let request = "\(method) / HTTP/1.1\r\nX-Custom-Header: foo\r\nX-Custom-Header: bar\r\n\r\n" 217 | try check(request: request, count: count, bufferSize: bufferSize) { request in 218 | XCTAssert(request.method == method) 219 | XCTAssert(request.url.path == "/") 220 | XCTAssert(request.version.major == 1) 221 | XCTAssert(request.version.minor == 1) 222 | XCTAssert(request.headers["X-Custom-Header"] == "foo, bar") 223 | } 224 | } 225 | } 226 | } 227 | } 228 | 229 | func testChunkedTransferEncodingBody() throws { 230 | let data = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n" 231 | let parser = MessageParser(mode: .request) 232 | let request = try parser.parse(data).first! as! Request 233 | XCTAssert(request.method == .post) 234 | XCTAssert(request.url.path == "/") 235 | XCTAssert(request.version.major == 1) 236 | XCTAssert(request.version.minor == 1) 237 | XCTAssertEqual(request.headers.count, 1) 238 | XCTAssertEqual(request.transferEncoding, "chunked") 239 | } 240 | } 241 | 242 | extension RequestParserTests { 243 | public static var allTests: [(String, (RequestParserTests) -> () throws -> Void)] { 244 | return [ 245 | ("testInvalidMethod", testInvalidMethod), 246 | ("testInvalidURL", testInvalidURL), 247 | ("testNoURL", testNoURL), 248 | ("testInvalidHTTPVersion", testInvalidHTTPVersion), 249 | ("testInvalidDoubleConnectMethod", testInvalidDoubleConnectMethod), 250 | ("testConnectMethod", testConnectMethod), 251 | ("testShortRequests", testShortRequests), 252 | ("testMediumRequests", testMediumRequests), 253 | ("testCookiesRequest", testCookiesRequest), 254 | ("testBodyRequest", testBodyRequest), 255 | ("testManyRequests", testManyRequests), 256 | ("testErrorDescription", testErrorDescription), 257 | ("testUnknownMethod", testUnknownMethod), 258 | ("testDuplicateHeaders", testDuplicateHeaders), 259 | ] 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Parser/ResponseParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTTP 3 | 4 | let responseCount = [ 5 | 1, 6 | 2, 7 | 5 8 | ] 9 | 10 | let statuses: [Response.Status] = [ 11 | .`continue`, 12 | .switchingProtocols, 13 | .processing, 14 | 15 | .ok, 16 | .created, 17 | .accepted, 18 | .nonAuthoritativeInformation, 19 | .noContent, 20 | .resetContent, 21 | .partialContent, 22 | 23 | .multipleChoices, 24 | .movedPermanently, 25 | .found, 26 | .seeOther, 27 | .notModified, 28 | .useProxy, 29 | .switchProxy, 30 | .temporaryRedirect, 31 | .permanentRedirect, 32 | 33 | .badRequest, 34 | .unauthorized, 35 | .paymentRequired, 36 | .forbidden, 37 | .notFound, 38 | .methodNotAllowed, 39 | .notAcceptable, 40 | .proxyAuthenticationRequired, 41 | .requestTimeout, 42 | .conflict, 43 | .gone, 44 | .lengthRequired, 45 | .preconditionFailed, 46 | .requestEntityTooLarge, 47 | .requestURITooLong, 48 | .unsupportedMediaType, 49 | .requestedRangeNotSatisfiable, 50 | .expectationFailed, 51 | .imATeapot, 52 | .authenticationTimeout, 53 | .enhanceYourCalm, 54 | .unprocessableEntity, 55 | .locked, 56 | .failedDependency, 57 | .preconditionRequired, 58 | .tooManyRequests, 59 | .requestHeaderFieldsTooLarge, 60 | 61 | .internalServerError, 62 | .notImplemented, 63 | .badGateway, 64 | .serviceUnavailable, 65 | .gatewayTimeout, 66 | .httpVersionNotSupported, 67 | .variantAlsoNegotiates, 68 | .insufficientStorage, 69 | .loopDetected, 70 | .notExtended, 71 | .networkAuthenticationRequired, 72 | ] 73 | 74 | public class ResponseParserTests : XCTestCase { 75 | func testInvalidHTTPVersion() throws { 76 | let data = Buffer("HUEHUE 200 OK\r\n\r\n") 77 | let parser = MessageParser(mode: .response) 78 | XCTAssertThrowsError(try parser.parse(data)) 79 | } 80 | 81 | func check(response: String, count: Int, bufferSize: Int, test: (Response) -> Void) throws { 82 | var data = "" 83 | 84 | for _ in 0 ..< count { 85 | data += response 86 | } 87 | 88 | let parser = MessageParser(mode: .response) 89 | 90 | for _ in 0 ..< count { 91 | for message in try parser.parse(data) { 92 | let response = message as! Response 93 | test(response) 94 | } 95 | } 96 | } 97 | 98 | func testShortResponses() throws { 99 | for bufferSize in bufferSizes { 100 | for count in responseCount { 101 | for status in statuses { 102 | let response = "HTTP/1.1 \(status.statusCode) \(status.reasonPhrase)\r\nContent-Length: 0\r\n\r\n" 103 | try check(response: response, count: count, bufferSize: bufferSize) { response in 104 | XCTAssert(response.status == status) 105 | XCTAssert(response.version.major == 1) 106 | XCTAssert(response.version.minor == 1) 107 | XCTAssert(response.headers.count == 1) 108 | XCTAssert(response.headers["Content-Length"] == "0") 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | func testCookiesResponse() throws { 116 | for bufferSize in bufferSizes { 117 | for count in responseCount { 118 | for status in statuses { 119 | let response = "HTTP/1.1 \(status.statusCode) \(status.reasonPhrase)\r\nContent-Length: 0\r\nHost: zewo.co\r\nSet-Cookie: server=zewo\r\nSet-Cookie: lang=swift\r\n\r\n" 120 | try check(response: response, count: count, bufferSize: bufferSize) { response in 121 | XCTAssert(response.status == status) 122 | XCTAssert(response.version.major == 1) 123 | XCTAssert(response.version.minor == 1) 124 | XCTAssert(response.headers["Host"] == "zewo.co") 125 | XCTAssert(response.cookies.contains(AttributedCookie(name: "server", value: "zewo"))) 126 | XCTAssert(response.cookies.contains(AttributedCookie(name: "lang", value: "swift"))) 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | func testBodyResponse() throws { 134 | for bufferSize in bufferSizes { 135 | for count in responseCount { 136 | for status in statuses { 137 | let response = "HTTP/1.1 \(status.statusCode) \(status.reasonPhrase)\r\nContent-Length: 4\r\n\r\nZewo" 138 | try check(response: response, count: count, bufferSize: bufferSize) { response in 139 | XCTAssert(response.status == status) 140 | XCTAssert(response.version.major == 1) 141 | XCTAssert(response.version.minor == 1) 142 | XCTAssert(response.headers["Content-Length"] == "4") 143 | XCTAssert(response.body == .buffer(Buffer("Zewo"))) 144 | } 145 | } 146 | } 147 | } 148 | } 149 | 150 | func testManyResponses() { 151 | var response = "" 152 | 153 | for _ in 0 ..< 100 { 154 | response += "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nZewo" 155 | } 156 | 157 | measure { 158 | do { 159 | try self.check(response: response, count: 1, bufferSize: 4096) { response in 160 | XCTAssert(response.status == .ok) 161 | XCTAssert(response.version.major == 1) 162 | XCTAssert(response.version.minor == 1) 163 | XCTAssert(response.headers["Content-Length"] == "4") 164 | XCTAssert(response.body == .buffer(Buffer("Zewo"))) 165 | } 166 | } catch { 167 | XCTFail() 168 | } 169 | } 170 | } 171 | 172 | func testDuplicateHeaders() throws { 173 | for bufferSize in bufferSizes { 174 | for count in requestCount { 175 | for status in statuses { 176 | let response = "HTTP/1.1 \(status.statusCode) \(status.reasonPhrase)\r\nContent-Length: 0\r\nX-Custom-Header: foo\r\nX-Custom-Header: bar\r\n\r\n" 177 | try check(response: response, count: count, bufferSize: bufferSize) { response in 178 | XCTAssert(response.status == status) 179 | XCTAssert(response.version.major == 1) 180 | XCTAssert(response.version.minor == 1) 181 | XCTAssert(response.headers["X-Custom-Header"] == "foo, bar") 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } 188 | 189 | extension ResponseParserTests { 190 | public static var allTests: [(String, (ResponseParserTests) -> () throws -> Void)] { 191 | return [ 192 | ("testInvalidHTTPVersion", testInvalidHTTPVersion), 193 | ("testShortResponses", testShortResponses), 194 | ("testCookiesResponse", testCookiesResponse), 195 | ("testBodyResponse", testBodyResponse), 196 | ("testManyResponses", testManyResponses), 197 | ("testDuplicateHeaders", testDuplicateHeaders), 198 | ] 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Tests/HTTPTests/Serializer/HTTPSerializerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Axis 3 | @testable import HTTP 4 | 5 | public class HTTPSerializerTests : XCTestCase { 6 | func testResponseSerializeBuffer() throws { 7 | let outStream = BufferStream() 8 | let serializer = ResponseSerializer(stream: outStream) 9 | var response = Response(body: "foo") 10 | response.cookies = [AttributedCookie(name: "foo", value: "bar")] 11 | 12 | try serializer.serialize(response, deadline: 1.second.fromNow()) 13 | XCTAssertEqual(outStream.buffer, Buffer("HTTP/1.1 200 OK\r\nContent-Length: 3\r\nSet-Cookie: foo=bar\r\n\r\nfoo")) 14 | } 15 | 16 | func testResponseSerializeReaderStream() throws { 17 | let inStream = BufferStream(buffer: Buffer("foo")) 18 | let outStream = BufferStream() 19 | let serializer = ResponseSerializer(stream: outStream) 20 | let response = Response(body: inStream) 21 | 22 | try serializer.serialize(response, deadline: 1.second.fromNow()) 23 | XCTAssertEqual(outStream.buffer, Buffer("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n0\r\n\r\n")) 24 | } 25 | 26 | func testResponseSerializeWriterStream() throws { 27 | let outStream = BufferStream() 28 | let serializer = ResponseSerializer(stream: outStream) 29 | 30 | let response = Response(body: { stream in 31 | try stream.write("foo", deadline: 1.second.fromNow()) 32 | try stream.flush(deadline: 1.second.fromNow()) 33 | }) 34 | 35 | try serializer.serialize(response, deadline: 1.second.fromNow()) 36 | XCTAssertEqual(outStream.buffer, Buffer("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n0\r\n\r\n")) 37 | } 38 | 39 | func testRequestSerializeBuffer() throws { 40 | let outStream = BufferStream() 41 | let serializer = RequestSerializer(stream: outStream) 42 | let request = Request(body: "foo") 43 | 44 | try serializer.serialize(request, deadline: 1.second.fromNow()) 45 | XCTAssertEqual(outStream.buffer, Buffer("GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\nfoo")) 46 | } 47 | 48 | func testRequestSerializeReaderStream() throws { 49 | let inStream = BufferStream(buffer: "foo") 50 | let outStream = BufferStream() 51 | let serializer = RequestSerializer(stream: outStream) 52 | let request = Request(body: inStream) 53 | 54 | try serializer.serialize(request, deadline: 1.second.fromNow()) 55 | XCTAssertEqual(outStream.buffer, Buffer("GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n0\r\n\r\n")) 56 | } 57 | 58 | func testRequestSerializeWriterStream() throws { 59 | let outStream = BufferStream() 60 | let serializer = RequestSerializer(stream: outStream) 61 | 62 | let request = Request(body: { stream in 63 | try stream.write("foo", deadline: 1.second.fromNow()) 64 | try stream.flush(deadline: 1.second.fromNow()) 65 | }) 66 | 67 | try serializer.serialize(request, deadline: 1.second.fromNow()) 68 | XCTAssertEqual(outStream.buffer, Buffer("GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n0\r\n\r\n")) 69 | } 70 | 71 | func testBodyStream() throws { 72 | let transport = BufferStream() 73 | let bodyStream = BodyStream(transport) 74 | bodyStream.close() 75 | XCTAssertEqual(bodyStream.closed, true) 76 | do { 77 | try bodyStream.write([1, 2, 3], deadline: 1.second.fromNow()) 78 | try bodyStream.flush(deadline: 1.second.fromNow()) 79 | XCTFail() 80 | } catch {} 81 | bodyStream.closed = false 82 | XCTAssertThrowsError(try bodyStream.read(upTo: 1, deadline: 1.second.fromNow())) 83 | } 84 | } 85 | 86 | extension HTTPSerializerTests { 87 | public static var allTests: [(String, (HTTPSerializerTests) -> () throws -> Void)] { 88 | return [ 89 | ("testResponseSerializeBuffer", testResponseSerializeBuffer), 90 | ("testResponseSerializeBuffer", testResponseSerializeReaderStream), 91 | ("testResponseSerializeBuffer", testResponseSerializeWriterStream), 92 | ("testResponseSerializeBuffer", testRequestSerializeBuffer), 93 | ("testResponseSerializeBuffer", testRequestSerializeReaderStream), 94 | ("testResponseSerializeBuffer", testRequestSerializeWriterStream), 95 | ("testResponseSerializeBuffer", testBodyStream), 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import HTTPTests 3 | 4 | XCTMain([ 5 | testCase(RequestContentTests.allTests), 6 | testCase(ResponseContentTests.allTests), 7 | testCase(ErrorTests.allTests), 8 | testCase(AttributedCookieTests.allTests), 9 | testCase(BodyTests.allTests), 10 | testCase(CookieTests.allTests), 11 | testCase(MessageTests.allTests), 12 | testCase(RequestMethodTests.allTests), 13 | testCase(RequestTests.allTests), 14 | testCase(ResponseStatusTests.allTests), 15 | testCase(ResponseTests.allTests), 16 | testCase(BasicAuthMiddlewareTests.allTests), 17 | testCase(BufferClientContentNegotiationMiddlewareTests.allTests), 18 | testCase(BufferServerContentNegotiationMiddlewareTests.allTests), 19 | testCase(LogMiddlewareTests.allTests), 20 | testCase(RecoveryMiddlewareTests.allTests), 21 | testCase(RedirectMiddlewareTests.allTests), 22 | testCase(SessionMiddlewareTests.allTests), 23 | testCase(StreamClientContentNegotiationMiddlewareTests.allTests), 24 | testCase(StreamServerContentNegotiationMiddlewareTests.allTests), 25 | testCase(RequestParserTests.allTests), 26 | testCase(ResponseParserTests.allTests), 27 | testCase(HTTPSerializerTests.allTests), 28 | ]) 29 | --------------------------------------------------------------------------------