Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8516cc725 | |||
| 811940bb05 | |||
| 3aa8b03c06 | |||
| 2ab754e00f | |||
| defd4c7005 | |||
| d81bed005d | |||
| 00adc53d0f | |||
| 280caad31d | |||
| 327e890e55 | |||
| a8205d2b7a | |||
| 62aba99de0 | |||
| ee4b4d9620 | |||
| fee5702695 | |||
| 055a757e15 | |||
| 6b5b27edf0 | |||
| d53f9432ce | |||
| 8d278305c1 | |||
| 75f022c06d | |||
| 6c0317d239 | |||
| 0a09bcac28 | |||
| ab8f280798 | |||
| 92f3ee4dc0 | |||
| 8f1f900474 | |||
| 6e6ffe2124 | |||
| 0da680a181 | |||
| 378286eea3 | |||
| 48d155f3ed | |||
| 43702318d4 | |||
| 8ad751d04d | |||
| af9c374f5c | |||
| 7e366e4687 | |||
| 002d9ce17b | |||
| 5a16db4ccb | |||
| f75476055d | |||
| 3a21866782 | |||
| 9691a6a526 | |||
| 02ef28a81e | |||
| 205a2652fc | |||
| 9c12770490 | |||
| fbd4208900 | |||
| 990a38227a | |||
| 6c6c1b1ad4 | |||
| 80111f16b1 | |||
| 58eb012735 | |||
| db44cf0d1a | |||
| 7c04fa59d3 | |||
| f66aa823e0 | |||
| 8feb666b6f | |||
| 885d1f1ec4 | |||
| 9fdf40ff74 | |||
| dd9e50172c | |||
| 8b610c9655 | |||
| cbb6387f00 | |||
| 12cec86860 | |||
| addb1029cf |
@@ -73,3 +73,4 @@ fastlane/test_output
|
||||
*.orig
|
||||
/.idea
|
||||
/Package.pins
|
||||
docs
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
clean: true
|
||||
module: HTTP
|
||||
author: Swift Server API project
|
||||
github_url: https://github.com/swift-server/http/
|
||||
|
||||
theme: fullwidth
|
||||
clean: true
|
||||
exclude: [Packages]
|
||||
|
||||
readme: README.md
|
||||
|
||||
skip_undocumented: false
|
||||
hide_documentation_coverage: false
|
||||
|
||||
xcodebuild_arguments: [-project, SwiftServerHTTP.xcodeproj, -target, HTTP]
|
||||
|
||||
custom_categories:
|
||||
|
||||
- name: HTTP Server
|
||||
children:
|
||||
- HTTPServer
|
||||
- HTTPServing
|
||||
- HTTPRequestHandler
|
||||
- HTTPRequestHandling
|
||||
|
||||
- name: HTTP Headers
|
||||
children:
|
||||
- HTTPHeaders
|
||||
- HTTPVersion
|
||||
|
||||
- name: HTTP Request
|
||||
children:
|
||||
- HTTPRequest
|
||||
- HTTPMethod
|
||||
- HTTPBodyHandler
|
||||
- HTTPBodyProcessing
|
||||
- HTTPBodyChunk
|
||||
|
||||
- name: HTTP Response
|
||||
children:
|
||||
- HTTPResponse
|
||||
- HTTPResponseStatus
|
||||
- HTTPResponseWriter
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.1.1-RELEASE
|
||||
4.0
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
included:
|
||||
- Sources
|
||||
- Tests
|
||||
|
||||
opt_in_rules:
|
||||
- closure_end_indentation
|
||||
- closure_spacing
|
||||
- fatal_error_message
|
||||
- operator_usage_whitespace
|
||||
- redundant_nil_coalescing
|
||||
- switch_case_on_newline
|
||||
- attributes
|
||||
- no_extension_access_modifier
|
||||
|
||||
disabled_rules:
|
||||
- trailing_comma
|
||||
- line_length
|
||||
- file_length
|
||||
- function_body_length
|
||||
- type_body_length
|
||||
- todo
|
||||
|
||||
identifier_name:
|
||||
excluded:
|
||||
- i
|
||||
- ok
|
||||
- im
|
||||
- if
|
||||
- te
|
||||
|
||||
large_tuple: 4
|
||||
|
||||
cyclomatic_complexity: 15
|
||||
|
||||
nesting:
|
||||
type_level: 2
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
branches:
|
||||
except:
|
||||
- gh-pages
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode9
|
||||
|
||||
before_install:
|
||||
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update -y ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then wget https://swift.org/builds/swift-4.0-release/ubuntu1404/swift-4.0-RELEASE/swift-4.0-RELEASE-ubuntu14.04.tar.gz ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then tar xzvf swift-4.0-RELEASE-ubuntu14.04.tar.gz ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export PATH=swift-4.0-RELEASE-ubuntu14.04/usr/bin:$PATH ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -y install clang libicu-dev ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CC=/usr/bin/clang; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=/usr/bin/clang ; fi
|
||||
|
||||
script:
|
||||
- swift build
|
||||
- swift test
|
||||
@@ -0,0 +1,412 @@
|
||||
```
|
||||
/// HTTP Request NOT INCLUDING THE BODY. This allows for streaming
|
||||
public struct HTTPRequest {
|
||||
public var method : HTTPMethod
|
||||
public var target : String /* e.g. "/foo/bar?buz=qux" */
|
||||
public var httpVersion : HTTPVersion
|
||||
public var headers : HTTPHeaders
|
||||
}
|
||||
|
||||
/// Object that code writes the response and response body to.
|
||||
public protocol HTTPResponseWriter : class {
|
||||
func writeHeader(status: HTTPResponseStatus, headers: HTTPHeaders, completion: @escaping (Result) -> Void)
|
||||
func writeTrailer(_ trailers: HTTPHeaders, completion: @escaping (Result) -> Void)
|
||||
func writeBody(_ data: UnsafeHTTPResponseBody, completion: @escaping (Result) -> Void)
|
||||
func done(completion: @escaping (Result) -> Void)
|
||||
func abort()
|
||||
}
|
||||
|
||||
/// Convenience methods for HTTP response writer.
|
||||
extension HTTPResponseWriter {
|
||||
public func writeHeader(status: HTTPResponseStatus, headers: HTTPHeaders)
|
||||
public func writeHeader(status: HTTPResponseStatus)
|
||||
public func writeTrailer(_ trailers: HTTPHeaders)
|
||||
public func writeBody(_ data: UnsafeHTTPResponseBody)
|
||||
public func done()
|
||||
}
|
||||
|
||||
public protocol UnsafeHTTPResponseBody {
|
||||
func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
|
||||
}
|
||||
|
||||
extension UnsafeRawBufferPointer : UnsafeHTTPResponseBody {
|
||||
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
|
||||
}
|
||||
|
||||
public protocol HTTPResponseBody : UnsafeHTTPResponseBody {}
|
||||
|
||||
extension Data : HTTPResponseBody {
|
||||
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
|
||||
}
|
||||
|
||||
extension DispatchData : HTTPResponseBody {
|
||||
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
|
||||
}
|
||||
|
||||
extension String : HTTPResponseBody {
|
||||
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
|
||||
}
|
||||
|
||||
/// Method that takes a chunk of request body and is expected to write to the ResponseWriter
|
||||
public typealias HTTPBodyHandler = (HTTPBodyChunk, inout Bool) -> Void /* the Bool can be set to true when we don't want to process anything further */
|
||||
|
||||
/// Indicates whether the body is going to be processed or ignored
|
||||
public enum HTTPBodyProcessing {
|
||||
case discardBody /* if you're not interested in the body */
|
||||
case processBody(handler: HTTPBodyHandler)
|
||||
}
|
||||
|
||||
/// Part (or maybe all) of the incoming request body
|
||||
public enum HTTPBodyChunk {
|
||||
case chunk(data: DispatchData, finishedProcessing: () -> Void) /* a new bit of the HTTP request body has arrived, finishedProcessing() must be called when done with that chunk */
|
||||
case failed(error: /*HTTPParser*/ Error) /* error while streaming the HTTP request body, eg. connection closed */
|
||||
case trailer(key: String, value: String) /* trailer has arrived (this we actually haven't implemented yet) */
|
||||
case end /* body and trailers finished */
|
||||
}
|
||||
|
||||
/// Headers structure.
|
||||
public struct HTTPHeaders : Sequence, ExpressibleByDictionaryLiteral {
|
||||
public subscript(name: Name) -> String?
|
||||
public subscript(valuesFor name: Name) -> [String]
|
||||
|
||||
public struct Literal : ExpressibleByDictionaryLiteral { }
|
||||
public mutating func append(_ literal: HTTPHeaders.Literal)
|
||||
public mutating func replace(_ literal: HTTPHeaders.Literal)
|
||||
|
||||
public func makeIterator() -> AnyIterator<(name: Name, value: String)>
|
||||
|
||||
public struct Name : Hashable, ExpressibleByStringLiteral, CustomStringConvertible {
|
||||
public init(_ name: String)
|
||||
|
||||
// https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
||||
// Permanent Message Header Field Names
|
||||
public static let aIM
|
||||
public static let accept
|
||||
public static let acceptAdditions
|
||||
public static let acceptCharset
|
||||
public static let acceptDatetime
|
||||
public static let acceptEncoding
|
||||
public static let acceptFeatures
|
||||
public static let acceptLanguage
|
||||
public static let acceptPatch
|
||||
public static let acceptPost
|
||||
public static let acceptRanges
|
||||
public static let age
|
||||
public static let allow
|
||||
public static let alpn
|
||||
public static let altSvc
|
||||
public static let altUsed
|
||||
public static let alternates
|
||||
public static let applyToRedirectRef
|
||||
public static let authenticationControl
|
||||
public static let authenticationInfo
|
||||
public static let authorization
|
||||
public static let cExt
|
||||
public static let cMan
|
||||
public static let cOpt
|
||||
public static let cPEP
|
||||
public static let cPEPInfo
|
||||
public static let cacheControl
|
||||
public static let calDAVTimezones
|
||||
public static let close
|
||||
public static let connection
|
||||
public static let contentBase
|
||||
public static let contentDisposition
|
||||
public static let contentEncoding
|
||||
public static let contentID
|
||||
public static let contentLanguage
|
||||
public static let contentLength
|
||||
public static let contentLocation
|
||||
public static let contentMD5
|
||||
public static let contentRange
|
||||
public static let contentScriptType
|
||||
public static let contentStyleType
|
||||
public static let contentType
|
||||
public static let contentVersion
|
||||
public static let cookie
|
||||
public static let cookie2
|
||||
public static let dasl
|
||||
public static let dav
|
||||
public static let date
|
||||
public static let defaultStyle
|
||||
public static let deltaBase
|
||||
public static let depth
|
||||
public static let derivedFrom
|
||||
public static let destination
|
||||
public static let differentialID
|
||||
public static let digest
|
||||
public static let eTag
|
||||
public static let expect
|
||||
public static let expires
|
||||
public static let ext
|
||||
public static let forwarded
|
||||
public static let from
|
||||
public static let getProfile
|
||||
public static let hobareg
|
||||
public static let host
|
||||
public static let http2Settings
|
||||
public static let im
|
||||
public static let `if`
|
||||
public static let ifMatch
|
||||
public static let ifModifiedSince
|
||||
public static let ifNoneMatch
|
||||
public static let ifRange
|
||||
public static let ifScheduleTagMatch
|
||||
public static let ifUnmodifiedSince
|
||||
public static let keepAlive
|
||||
public static let label
|
||||
public static let lastModified
|
||||
public static let link
|
||||
public static let location
|
||||
public static let lockToken
|
||||
public static let man
|
||||
public static let maxForwards
|
||||
public static let mementoDatetime
|
||||
public static let meter
|
||||
public static let mimeVersion
|
||||
public static let negotiate
|
||||
public static let opt
|
||||
public static let optionalWWWAuthenticate
|
||||
public static let orderingType
|
||||
public static let origin
|
||||
public static let overwrite
|
||||
public static let p3p
|
||||
public static let pep
|
||||
public static let picsLabel
|
||||
public static let pepInfo
|
||||
public static let position
|
||||
public static let pragma
|
||||
public static let prefer
|
||||
public static let preferenceApplied
|
||||
public static let profileObject
|
||||
public static let `protocol`
|
||||
public static let protocolInfo
|
||||
public static let protocolQuery
|
||||
public static let protocolRequest
|
||||
public static let proxyAuthenticate
|
||||
public static let proxyAuthenticationInfo
|
||||
public static let proxyAuthorization
|
||||
public static let proxyFeatures
|
||||
public static let proxyInstruction
|
||||
public static let `public`
|
||||
public static let publicKeyPins
|
||||
public static let publicKeyPinsReportOnly
|
||||
public static let range
|
||||
public static let redirectRef
|
||||
public static let referer
|
||||
public static let retryAfter
|
||||
public static let safe
|
||||
public static let scheduleReply
|
||||
public static let scheduleTag
|
||||
public static let secWebSocketAccept
|
||||
public static let secWebSocketExtensions
|
||||
public static let secWebSocketKey
|
||||
public static let secWebSocketProtocol
|
||||
public static let secWebSocketVersion
|
||||
public static let securityScheme
|
||||
public static let server
|
||||
public static let setCookie
|
||||
public static let setCookie2
|
||||
public static let setProfile
|
||||
public static let slug
|
||||
public static let soapAction
|
||||
public static let statusURI
|
||||
public static let strictTransportSecurity
|
||||
public static let surrogateCapability
|
||||
public static let surrogateControl
|
||||
public static let tcn
|
||||
public static let te
|
||||
public static let timeout
|
||||
public static let topic
|
||||
public static let trailer
|
||||
public static let transferEncoding
|
||||
public static let ttl
|
||||
public static let urgency
|
||||
public static let uri
|
||||
public static let upgrade
|
||||
public static let userAgent
|
||||
public static let variantVary
|
||||
public static let vary
|
||||
public static let via
|
||||
public static let wwwAuthenticate
|
||||
public static let wantDigest
|
||||
public static let warning
|
||||
public static let xFrameOptions
|
||||
|
||||
// https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
||||
// Provisional Message Header Field Names
|
||||
public static let accessControl
|
||||
public static let accessControlAllowCredentials
|
||||
public static let accessControlAllowHeaders
|
||||
public static let accessControlAllowMethods
|
||||
public static let accessControlAllowOrigin
|
||||
public static let accessControlMaxAge
|
||||
public static let accessControlRequestMethod
|
||||
public static let accessControlRequestHeaders
|
||||
public static let compliance
|
||||
public static let contentTransferEncoding
|
||||
public static let cost
|
||||
public static let ediintFeatures
|
||||
public static let messageID
|
||||
public static let methodCheck
|
||||
public static let methodCheckExpires
|
||||
public static let nonCompliance
|
||||
public static let optional
|
||||
public static let refererRoot
|
||||
public static let resolutionHint
|
||||
public static let resolverLocation
|
||||
public static let subOK
|
||||
public static let subst
|
||||
public static let title
|
||||
public static let uaColor
|
||||
public static let uaMedia
|
||||
public static let uaPixels
|
||||
public static let uaResolution
|
||||
public static let uaWindowpixels
|
||||
public static let version
|
||||
public static let xDeviceAccept
|
||||
public static let xDeviceAcceptCharset
|
||||
public static let xDeviceAcceptEncoding
|
||||
public static let xDeviceAcceptLanguage
|
||||
public static let xDeviceUserAgent
|
||||
}
|
||||
}
|
||||
|
||||
/// Version number of the HTTP Protocol
|
||||
public struct HTTPVersion {
|
||||
/// Major version component.
|
||||
public var major: Int
|
||||
/// Minor version component.
|
||||
public var minor: Int
|
||||
|
||||
public init(major: Int, minor: Int)
|
||||
}
|
||||
|
||||
public enum HTTPTransferEncoding {
|
||||
case identity(contentLength: UInt)
|
||||
case chunked
|
||||
}
|
||||
|
||||
/// Response status (200 ok, 404 not found, etc)
|
||||
public struct HTTPResponseStatus: Equatable, CustomStringConvertible, ExpressibleByIntegerLiteral {
|
||||
public let code: Int
|
||||
public let reasonPhrase: String
|
||||
|
||||
public init(code: Int, reasonPhrase: String)
|
||||
public init(code: Int)
|
||||
|
||||
/* all the codes from http://www.iana.org/assignments/http-status-codes */
|
||||
public static let `continue`
|
||||
public static let switchingProtocols
|
||||
public static let ok
|
||||
public static let created
|
||||
public static let accepted
|
||||
public static let nonAuthoritativeInformation
|
||||
public static let noContent
|
||||
public static let resetContent
|
||||
public static let partialContent
|
||||
public static let multiStatus
|
||||
public static let alreadyReported
|
||||
public static let imUsed
|
||||
public static let multipleChoices
|
||||
public static let movedPermanently
|
||||
public static let found
|
||||
public static let seeOther
|
||||
public static let notModified
|
||||
public static let useProxy
|
||||
public static let temporaryRedirect
|
||||
public static let permanentRedirect
|
||||
public static let badRequest
|
||||
public static let unauthorized
|
||||
public static let paymentRequired
|
||||
public static let forbidden
|
||||
public static let notFound
|
||||
public static let methodNotAllowed
|
||||
public static let notAcceptable
|
||||
public static let proxyAuthenticationRequired
|
||||
public static let requestTimeout
|
||||
public static let conflict
|
||||
public static let gone
|
||||
public static let lengthRequired
|
||||
public static let preconditionFailed
|
||||
public static let payloadTooLarge
|
||||
public static let uriTooLong
|
||||
public static let unsupportedMediaType
|
||||
public static let rangeNotSatisfiable
|
||||
public static let expectationFailed
|
||||
public static let misdirectedRequest
|
||||
public static let unprocessableEntity
|
||||
public static let locked
|
||||
public static let failedDependency
|
||||
public static let upgradeRequired
|
||||
public static let preconditionRequired
|
||||
public static let tooManyRequests
|
||||
public static let requestHeaderFieldsTooLarge
|
||||
public static let unavailableForLegalReasons
|
||||
public static let internalServerError
|
||||
public static let notImplemented
|
||||
public static let badGateway
|
||||
public static let serviceUnavailable
|
||||
public static let gatewayTimeout
|
||||
public static let httpVersionNotSupported
|
||||
public static let variantAlsoNegotiates
|
||||
public static let insufficientStorage
|
||||
public static let loopDetected
|
||||
public static let notExtended
|
||||
public static let networkAuthenticationRequired
|
||||
|
||||
public var `class`: Class
|
||||
|
||||
public enum Class {
|
||||
case informational
|
||||
case successful
|
||||
case redirection
|
||||
case clientError
|
||||
case serverError
|
||||
case invalidStatus
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP Methods handled by http_parser.[ch] supports
|
||||
public struct HTTPMethod : Hashable, CustomStringConvertible, ExpressibleByIntegerLiteral {
|
||||
|
||||
public let method: String
|
||||
|
||||
public init(_ method: String)
|
||||
|
||||
/* Constants for everything that http_parser.[ch] supports */
|
||||
public static let delete
|
||||
public static let get
|
||||
public static let head
|
||||
public static let post
|
||||
public static let put
|
||||
public static let connect
|
||||
public static let options
|
||||
public static let trace
|
||||
public static let copy
|
||||
public static let lock
|
||||
public static let mkol
|
||||
public static let move
|
||||
public static let propfind
|
||||
public static let proppatch
|
||||
public static let search
|
||||
public static let unlock
|
||||
public static let bind
|
||||
public static let rebind
|
||||
public static let unbind
|
||||
public static let acl
|
||||
public static let report
|
||||
public static let mkactivity
|
||||
public static let checkout
|
||||
public static let merge
|
||||
public static let msearch
|
||||
public static let notify
|
||||
public static let subscribe
|
||||
public static let unsubscribe
|
||||
public static let patch
|
||||
public static let purge
|
||||
public static let mkcalendar
|
||||
public static let link
|
||||
public static let unlink
|
||||
}
|
||||
```
|
||||
@@ -2,9 +2,9 @@
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
@@ -63,14 +63,14 @@
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
@@ -86,7 +86,7 @@
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
@@ -127,7 +127,7 @@
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
@@ -135,12 +135,12 @@
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
@@ -150,7 +150,7 @@
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
@@ -162,7 +162,7 @@
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
@@ -173,12 +173,12 @@
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
@@ -186,16 +186,26 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
## Runtime Library Exception to the Apache 2.0 License: ##
|
||||
|
||||
|
||||
As an exception, if you use this Software to compile your source code and
|
||||
portions of this Software are embedded into the binary product as a result,
|
||||
you may redistribute such product without providing attribution as would
|
||||
otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.
|
||||
|
||||
+23
-8
@@ -1,16 +1,31 @@
|
||||
// swift-tools-version:3.1
|
||||
// swift-tools-version:4.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftServerHttp",
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
.library(
|
||||
name: "HTTP",
|
||||
targets: ["HTTP"]),
|
||||
],
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/IBM-Swift/CHTTPParser.git", majorVersion: 0, minor: 1),
|
||||
.Package(url: "https://github.com/IBM-Swift/BlueSocket.git", majorVersion: 0, minor: 12),
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
||||
.target(
|
||||
name: "CHTTPParser",
|
||||
dependencies: []),
|
||||
.target(
|
||||
name: "HTTP",
|
||||
dependencies: ["CHTTPParser"]),
|
||||
.testTarget(
|
||||
name: "HTTPTests",
|
||||
dependencies: ["HTTP"]),
|
||||
]
|
||||
)
|
||||
|
||||
#if os(Linux)
|
||||
package.dependencies.append(
|
||||
.Package(url: "https://github.com/IBM-Swift/BlueSignals.git", majorVersion: 0, minor: 9))
|
||||
#endif
|
||||
|
||||
@@ -1,3 +1,74 @@
|
||||
# SwiftServerHttp
|
||||
# Swift Server Project HTTP APIs
|
||||
|
||||
Sample prototype implementation of @weissi's HTTP Sketch v2 from https://lists.swift.org/pipermail/swift-server-dev/Week-of-Mon-20170403/000422.html for discussion.
|
||||
This is an early implementation of the Swift Server Project's HTTP APIs. This provides simple HTTP server on which rich web application frameworks can be built.
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
### Hello World
|
||||
The following code implements a very simple "Hello World!" server:
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
import HTTP
|
||||
|
||||
func hello(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
response.writeHeader(status: .ok)
|
||||
response.writeBody("Hello, World!")
|
||||
response.done()
|
||||
return .discardBody
|
||||
}
|
||||
|
||||
let server = HTTPServer()
|
||||
try! server.start(port: 8080, handler: hello)
|
||||
|
||||
CFRunLoopRun()
|
||||
```
|
||||
|
||||
The `hello()` function receives a `HTTPRequest` that describes the request and a `HTTPResponseWriter` used to write a response.
|
||||
|
||||
Data that is received as part of the request body is made available to the closure that is returned by the `hello()` function. In the "Hello World!" example the request body is not used, so `.discardBody` is returned.
|
||||
|
||||
### Echo Server
|
||||
The following code implements a very simple Echo server that responds with the contents of the incoming request:
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
import HTTP
|
||||
|
||||
func echo(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
response.writeHeader(status: .ok)
|
||||
return .processBody { (chunk, stop) in
|
||||
switch chunk {
|
||||
case .chunk(let data, let finishedProcessing):
|
||||
response.writeBody(data) { _ in
|
||||
finishedProcessing()
|
||||
}
|
||||
case .end:
|
||||
response.done()
|
||||
default:
|
||||
stop = true
|
||||
response.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let server = HTTPServer()
|
||||
try! server.start(port: 8080, handler: echo)
|
||||
|
||||
CFRunLoopRun()
|
||||
```
|
||||
As the Echo server needs to process the request body data and return it in the reponse, the `echo()` function returns a `.processBody` closure. This closure is called with `.chunk` when data is available for processing from the request, and `.end` when no more data is available.
|
||||
|
||||
Once any data chunk has been processed, `finishedProcessing()` should be called to signify that it has been handled.
|
||||
|
||||
When the response is complete, `response.done()` should be called.
|
||||
|
||||
## API Documentation
|
||||
Full Jazzy documentation of the API is available here:
|
||||
<https://swift-server.github.io/http/>
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
This project is based on an inital proposal from @weissi on the swift-server-dev mailing list:
|
||||
<https://lists.swift.org/pipermail/swift-server-dev/Week-of-Mon-20170403/000422.html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,431 @@
|
||||
/* Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef http_parser_h
|
||||
#define http_parser_h
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Also update SONAME in the Makefile whenever you change these. */
|
||||
#define HTTP_PARSER_VERSION_MAJOR 2
|
||||
#define HTTP_PARSER_VERSION_MINOR 7
|
||||
#define HTTP_PARSER_VERSION_PATCH 1
|
||||
|
||||
#include <stddef.h>
|
||||
#if defined(_WIN32) && !defined(__MINGW32__) && \
|
||||
(!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__)
|
||||
#include <BaseTsd.h>
|
||||
typedef __int8 int8_t;
|
||||
typedef unsigned __int8 uint8_t;
|
||||
typedef __int16 int16_t;
|
||||
typedef unsigned __int16 uint16_t;
|
||||
typedef __int32 int32_t;
|
||||
typedef unsigned __int32 uint32_t;
|
||||
typedef __int64 int64_t;
|
||||
typedef unsigned __int64 uint64_t;
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
/* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run
|
||||
* faster
|
||||
*/
|
||||
#ifndef HTTP_PARSER_STRICT
|
||||
# define HTTP_PARSER_STRICT 1
|
||||
#endif
|
||||
|
||||
/* Maximium header size allowed. If the macro is not defined
|
||||
* before including this header then the default is used. To
|
||||
* change the maximum header size, define the macro in the build
|
||||
* environment (e.g. -DHTTP_MAX_HEADER_SIZE=<value>). To remove
|
||||
* the effective limit on the size of the header, define the macro
|
||||
* to a very large number (e.g. -DHTTP_MAX_HEADER_SIZE=0x7fffffff)
|
||||
*/
|
||||
#ifndef HTTP_MAX_HEADER_SIZE
|
||||
# define HTTP_MAX_HEADER_SIZE (80*1024)
|
||||
#endif
|
||||
|
||||
typedef struct http_parser http_parser;
|
||||
typedef struct http_parser_settings http_parser_settings;
|
||||
|
||||
|
||||
/* Callbacks should return non-zero to indicate an error. The parser will
|
||||
* then halt execution.
|
||||
*
|
||||
* The one exception is on_headers_complete. In a HTTP_RESPONSE parser
|
||||
* returning '1' from on_headers_complete will tell the parser that it
|
||||
* should not expect a body. This is used when receiving a response to a
|
||||
* HEAD request which may contain 'Content-Length' or 'Transfer-Encoding:
|
||||
* chunked' headers that indicate the presence of a body.
|
||||
*
|
||||
* Returning `2` from on_headers_complete will tell parser that it should not
|
||||
* expect neither a body nor any futher responses on this connection. This is
|
||||
* useful for handling responses to a CONNECT request which may not contain
|
||||
* `Upgrade` or `Connection: upgrade` headers.
|
||||
*
|
||||
* http_data_cb does not return data chunks. It will be called arbitrarily
|
||||
* many times for each string. E.G. you might get 10 callbacks for "on_url"
|
||||
* each providing just a few characters more data.
|
||||
*/
|
||||
typedef int (*http_data_cb) (http_parser*, const char *at, size_t length);
|
||||
typedef int (*http_cb) (http_parser*);
|
||||
|
||||
|
||||
/* Status Codes */
|
||||
#define HTTP_STATUS_MAP(XX) \
|
||||
XX(100, CONTINUE, Continue) \
|
||||
XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \
|
||||
XX(102, PROCESSING, Processing) \
|
||||
XX(200, OK, OK) \
|
||||
XX(201, CREATED, Created) \
|
||||
XX(202, ACCEPTED, Accepted) \
|
||||
XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \
|
||||
XX(204, NO_CONTENT, No Content) \
|
||||
XX(205, RESET_CONTENT, Reset Content) \
|
||||
XX(206, PARTIAL_CONTENT, Partial Content) \
|
||||
XX(207, MULTI_STATUS, Multi-Status) \
|
||||
XX(208, ALREADY_REPORTED, Already Reported) \
|
||||
XX(226, IM_USED, IM Used) \
|
||||
XX(300, MULTIPLE_CHOICES, Multiple Choices) \
|
||||
XX(301, MOVED_PERMANENTLY, Moved Permanently) \
|
||||
XX(302, FOUND, Found) \
|
||||
XX(303, SEE_OTHER, See Other) \
|
||||
XX(304, NOT_MODIFIED, Not Modified) \
|
||||
XX(305, USE_PROXY, Use Proxy) \
|
||||
XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \
|
||||
XX(308, PERMANENT_REDIRECT, Permanent Redirect) \
|
||||
XX(400, BAD_REQUEST, Bad Request) \
|
||||
XX(401, UNAUTHORIZED, Unauthorized) \
|
||||
XX(402, PAYMENT_REQUIRED, Payment Required) \
|
||||
XX(403, FORBIDDEN, Forbidden) \
|
||||
XX(404, NOT_FOUND, Not Found) \
|
||||
XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \
|
||||
XX(406, NOT_ACCEPTABLE, Not Acceptable) \
|
||||
XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \
|
||||
XX(408, REQUEST_TIMEOUT, Request Timeout) \
|
||||
XX(409, CONFLICT, Conflict) \
|
||||
XX(410, GONE, Gone) \
|
||||
XX(411, LENGTH_REQUIRED, Length Required) \
|
||||
XX(412, PRECONDITION_FAILED, Precondition Failed) \
|
||||
XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \
|
||||
XX(414, URI_TOO_LONG, URI Too Long) \
|
||||
XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \
|
||||
XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \
|
||||
XX(417, EXPECTATION_FAILED, Expectation Failed) \
|
||||
XX(421, MISDIRECTED_REQUEST, Misdirected Request) \
|
||||
XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \
|
||||
XX(423, LOCKED, Locked) \
|
||||
XX(424, FAILED_DEPENDENCY, Failed Dependency) \
|
||||
XX(426, UPGRADE_REQUIRED, Upgrade Required) \
|
||||
XX(428, PRECONDITION_REQUIRED, Precondition Required) \
|
||||
XX(429, TOO_MANY_REQUESTS, Too Many Requests) \
|
||||
XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \
|
||||
XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \
|
||||
XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \
|
||||
XX(501, NOT_IMPLEMENTED, Not Implemented) \
|
||||
XX(502, BAD_GATEWAY, Bad Gateway) \
|
||||
XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \
|
||||
XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \
|
||||
XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \
|
||||
XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \
|
||||
XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \
|
||||
XX(508, LOOP_DETECTED, Loop Detected) \
|
||||
XX(510, NOT_EXTENDED, Not Extended) \
|
||||
XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \
|
||||
|
||||
enum http_status
|
||||
{
|
||||
#define XX(num, name, string) HTTP_STATUS_##name = num,
|
||||
HTTP_STATUS_MAP(XX)
|
||||
#undef XX
|
||||
};
|
||||
|
||||
|
||||
/* Request Methods */
|
||||
#define HTTP_METHOD_MAP(XX) \
|
||||
XX(0, DELETE, DELETE) \
|
||||
XX(1, GET, GET) \
|
||||
XX(2, HEAD, HEAD) \
|
||||
XX(3, POST, POST) \
|
||||
XX(4, PUT, PUT) \
|
||||
/* pathological */ \
|
||||
XX(5, CONNECT, CONNECT) \
|
||||
XX(6, OPTIONS, OPTIONS) \
|
||||
XX(7, TRACE, TRACE) \
|
||||
/* WebDAV */ \
|
||||
XX(8, COPY, COPY) \
|
||||
XX(9, LOCK, LOCK) \
|
||||
XX(10, MKCOL, MKCOL) \
|
||||
XX(11, MOVE, MOVE) \
|
||||
XX(12, PROPFIND, PROPFIND) \
|
||||
XX(13, PROPPATCH, PROPPATCH) \
|
||||
XX(14, SEARCH, SEARCH) \
|
||||
XX(15, UNLOCK, UNLOCK) \
|
||||
XX(16, BIND, BIND) \
|
||||
XX(17, REBIND, REBIND) \
|
||||
XX(18, UNBIND, UNBIND) \
|
||||
XX(19, ACL, ACL) \
|
||||
/* subversion */ \
|
||||
XX(20, REPORT, REPORT) \
|
||||
XX(21, MKACTIVITY, MKACTIVITY) \
|
||||
XX(22, CHECKOUT, CHECKOUT) \
|
||||
XX(23, MERGE, MERGE) \
|
||||
/* upnp */ \
|
||||
XX(24, MSEARCH, M-SEARCH) \
|
||||
XX(25, NOTIFY, NOTIFY) \
|
||||
XX(26, SUBSCRIBE, SUBSCRIBE) \
|
||||
XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \
|
||||
/* RFC-5789 */ \
|
||||
XX(28, PATCH, PATCH) \
|
||||
XX(29, PURGE, PURGE) \
|
||||
/* CalDAV */ \
|
||||
XX(30, MKCALENDAR, MKCALENDAR) \
|
||||
/* RFC-2068, section 19.6.1.2 */ \
|
||||
XX(31, LINK, LINK) \
|
||||
XX(32, UNLINK, UNLINK) \
|
||||
|
||||
enum http_method
|
||||
{
|
||||
#define XX(num, name, string) HTTP_##name = num,
|
||||
HTTP_METHOD_MAP(XX)
|
||||
#undef XX
|
||||
};
|
||||
|
||||
|
||||
enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH };
|
||||
|
||||
|
||||
/* Flag values for http_parser.flags field */
|
||||
enum flags
|
||||
{ F_CHUNKED = 1 << 0
|
||||
, F_CONNECTION_KEEP_ALIVE = 1 << 1
|
||||
, F_CONNECTION_CLOSE = 1 << 2
|
||||
, F_CONNECTION_UPGRADE = 1 << 3
|
||||
, F_TRAILING = 1 << 4
|
||||
, F_UPGRADE = 1 << 5
|
||||
, F_SKIPBODY = 1 << 6
|
||||
, F_CONTENTLENGTH = 1 << 7
|
||||
};
|
||||
|
||||
|
||||
/* Map for errno-related constants
|
||||
*
|
||||
* The provided argument should be a macro that takes 2 arguments.
|
||||
*/
|
||||
#define HTTP_ERRNO_MAP(XX) \
|
||||
/* No error */ \
|
||||
XX(OK, "success") \
|
||||
\
|
||||
/* Callback-related errors */ \
|
||||
XX(CB_message_begin, "the on_message_begin callback failed") \
|
||||
XX(CB_url, "the on_url callback failed") \
|
||||
XX(CB_header_field, "the on_header_field callback failed") \
|
||||
XX(CB_header_value, "the on_header_value callback failed") \
|
||||
XX(CB_headers_complete, "the on_headers_complete callback failed") \
|
||||
XX(CB_body, "the on_body callback failed") \
|
||||
XX(CB_message_complete, "the on_message_complete callback failed") \
|
||||
XX(CB_status, "the on_status callback failed") \
|
||||
XX(CB_chunk_header, "the on_chunk_header callback failed") \
|
||||
XX(CB_chunk_complete, "the on_chunk_complete callback failed") \
|
||||
\
|
||||
/* Parsing-related errors */ \
|
||||
XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \
|
||||
XX(HEADER_OVERFLOW, \
|
||||
"too many header bytes seen; overflow detected") \
|
||||
XX(CLOSED_CONNECTION, \
|
||||
"data received after completed connection: close message") \
|
||||
XX(INVALID_VERSION, "invalid HTTP version") \
|
||||
XX(INVALID_STATUS, "invalid HTTP status code") \
|
||||
XX(INVALID_METHOD, "invalid HTTP method") \
|
||||
XX(INVALID_URL, "invalid URL") \
|
||||
XX(INVALID_HOST, "invalid host") \
|
||||
XX(INVALID_PORT, "invalid port") \
|
||||
XX(INVALID_PATH, "invalid path") \
|
||||
XX(INVALID_QUERY_STRING, "invalid query string") \
|
||||
XX(INVALID_FRAGMENT, "invalid fragment") \
|
||||
XX(LF_EXPECTED, "LF character expected") \
|
||||
XX(INVALID_HEADER_TOKEN, "invalid character in header") \
|
||||
XX(INVALID_CONTENT_LENGTH, \
|
||||
"invalid character in content-length header") \
|
||||
XX(UNEXPECTED_CONTENT_LENGTH, \
|
||||
"unexpected content-length header") \
|
||||
XX(INVALID_CHUNK_SIZE, \
|
||||
"invalid character in chunk size header") \
|
||||
XX(INVALID_CONSTANT, "invalid constant string") \
|
||||
XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\
|
||||
XX(STRICT, "strict mode assertion failed") \
|
||||
XX(PAUSED, "parser is paused") \
|
||||
XX(UNKNOWN, "an unknown error occurred")
|
||||
|
||||
|
||||
/* Define HPE_* values for each errno value above */
|
||||
#define HTTP_ERRNO_GEN(n, s) HPE_##n,
|
||||
enum http_errno {
|
||||
HTTP_ERRNO_MAP(HTTP_ERRNO_GEN)
|
||||
};
|
||||
#undef HTTP_ERRNO_GEN
|
||||
|
||||
|
||||
/* Get an http_errno value from an http_parser */
|
||||
#define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno)
|
||||
|
||||
|
||||
struct http_parser {
|
||||
/** PRIVATE **/
|
||||
unsigned int type : 2; /* enum http_parser_type */
|
||||
unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */
|
||||
unsigned int state : 7; /* enum state from http_parser.c */
|
||||
unsigned int header_state : 7; /* enum header_state from http_parser.c */
|
||||
unsigned int index : 7; /* index into current matcher */
|
||||
unsigned int lenient_http_headers : 1;
|
||||
|
||||
uint32_t nread; /* # bytes read in various scenarios */
|
||||
uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */
|
||||
|
||||
/** READ-ONLY **/
|
||||
unsigned short http_major;
|
||||
unsigned short http_minor;
|
||||
unsigned int status_code : 16; /* responses only */
|
||||
unsigned int method : 8; /* requests only */
|
||||
unsigned int http_errno : 7;
|
||||
|
||||
/* 1 = Upgrade header was present and the parser has exited because of that.
|
||||
* 0 = No upgrade header present.
|
||||
* Should be checked when http_parser_execute() returns in addition to
|
||||
* error checking.
|
||||
*/
|
||||
unsigned int upgrade : 1;
|
||||
|
||||
/** PUBLIC **/
|
||||
void *data; /* A pointer to get hook to the "connection" or "socket" object */
|
||||
};
|
||||
|
||||
|
||||
struct http_parser_settings {
|
||||
http_cb on_message_begin;
|
||||
http_data_cb on_url;
|
||||
http_data_cb on_status;
|
||||
http_data_cb on_header_field;
|
||||
http_data_cb on_header_value;
|
||||
http_cb on_headers_complete;
|
||||
http_data_cb on_body;
|
||||
http_cb on_message_complete;
|
||||
/* When on_chunk_header is called, the current chunk length is stored
|
||||
* in parser->content_length.
|
||||
*/
|
||||
http_cb on_chunk_header;
|
||||
http_cb on_chunk_complete;
|
||||
};
|
||||
|
||||
|
||||
enum http_parser_url_fields
|
||||
{ UF_SCHEMA = 0
|
||||
, UF_HOST = 1
|
||||
, UF_PORT = 2
|
||||
, UF_PATH = 3
|
||||
, UF_QUERY = 4
|
||||
, UF_FRAGMENT = 5
|
||||
, UF_USERINFO = 6
|
||||
, UF_MAX = 7
|
||||
};
|
||||
|
||||
|
||||
/* Result structure for http_parser_parse_url().
|
||||
*
|
||||
* Callers should index into field_data[] with UF_* values iff field_set
|
||||
* has the relevant (1 << UF_*) bit set. As a courtesy to clients (and
|
||||
* because we probably have padding left over), we convert any port to
|
||||
* a uint16_t.
|
||||
*/
|
||||
struct http_parser_url {
|
||||
uint16_t field_set; /* Bitmask of (1 << UF_*) values */
|
||||
uint16_t port; /* Converted UF_PORT string */
|
||||
|
||||
struct {
|
||||
uint16_t off; /* Offset into buffer in which field starts */
|
||||
uint16_t len; /* Length of run in buffer */
|
||||
} field_data[UF_MAX];
|
||||
};
|
||||
|
||||
|
||||
/* Returns the library version. Bits 16-23 contain the major version number,
|
||||
* bits 8-15 the minor version number and bits 0-7 the patch level.
|
||||
* Usage example:
|
||||
*
|
||||
* unsigned long version = http_parser_version();
|
||||
* unsigned major = (version >> 16) & 255;
|
||||
* unsigned minor = (version >> 8) & 255;
|
||||
* unsigned patch = version & 255;
|
||||
* printf("http_parser v%u.%u.%u\n", major, minor, patch);
|
||||
*/
|
||||
unsigned long http_parser_version(void);
|
||||
|
||||
void http_parser_init(http_parser *parser, enum http_parser_type type);
|
||||
|
||||
|
||||
/* Initialize http_parser_settings members to 0
|
||||
*/
|
||||
void http_parser_settings_init(http_parser_settings *settings);
|
||||
|
||||
|
||||
/* Executes the parser. Returns number of parsed bytes. Sets
|
||||
* `parser->http_errno` on error. */
|
||||
size_t http_parser_execute(http_parser *parser,
|
||||
const http_parser_settings *settings,
|
||||
const char *data,
|
||||
size_t len);
|
||||
|
||||
|
||||
/* If http_should_keep_alive() in the on_headers_complete or
|
||||
* on_message_complete callback returns 0, then this should be
|
||||
* the last message on the connection.
|
||||
* If you are the server, respond with the "Connection: close" header.
|
||||
* If you are the client, close the connection.
|
||||
*/
|
||||
int http_should_keep_alive(const http_parser *parser);
|
||||
|
||||
/* Returns a string version of the HTTP method. */
|
||||
const char *http_method_str(enum http_method m);
|
||||
|
||||
/* Return a string name of the given error */
|
||||
const char *http_errno_name(enum http_errno err);
|
||||
|
||||
/* Return a string description of the given error */
|
||||
const char *http_errno_description(enum http_errno err);
|
||||
|
||||
/* Initialize all http_parser_url members to 0 */
|
||||
void http_parser_url_init(struct http_parser_url *u);
|
||||
|
||||
/* Parse a URL; return nonzero on failure */
|
||||
int http_parser_parse_url(const char *buf, size_t buflen,
|
||||
int is_connect,
|
||||
struct http_parser_url *u);
|
||||
|
||||
/* Pause or un-pause the parser; a nonzero value pauses */
|
||||
void http_parser_pause(http_parser *parser, int paused);
|
||||
|
||||
/* Checks if this is the final chunk of the body. */
|
||||
int http_body_is_final(const http_parser *parser);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,54 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Typealias for a closure that handles an incoming HTTP request
|
||||
/// The following is an example of an echo `HTTPRequestHandler` that returns the request it receives as a response:
|
||||
/// ```swift
|
||||
/// func echo(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
/// response.writeHeader(status: .ok)
|
||||
/// return .processBody { (chunk, stop) in
|
||||
/// switch chunk {
|
||||
/// case .chunk(let data, let finishedProcessing):
|
||||
/// response.writeBody(data) { _ in
|
||||
/// finishedProcessing()
|
||||
/// }
|
||||
/// case .end:
|
||||
/// response.done()
|
||||
/// default:
|
||||
/// stop = true
|
||||
/// response.abort()
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// This then needs to be registered with the server using `HTTPServer.start(port:handler:)`
|
||||
/// - Parameter req: the incoming HTTP request.
|
||||
/// - Parameter res: a writer providing functions to create an HTTP reponse to the request.
|
||||
/// - Returns HTTPBodyProcessing: a enum that either discards the request data, or provides a closure to process it.
|
||||
public typealias HTTPRequestHandler = (HTTPRequest, HTTPResponseWriter) -> HTTPBodyProcessing
|
||||
|
||||
/// Class protocol containing a `handle()` function that implements `HTTPRequestHandler` to respond to incoming HTTP requests.
|
||||
/// - See: `HTTPRequestHandler` for more information
|
||||
public protocol HTTPRequestHandling: class {
|
||||
/// handle: function that implements `HTTPRequestHandler` and is called when a new HTTP request is received by the HTTP server.
|
||||
/// - Parameter request: the incoming HTTP request.
|
||||
/// - Parameter response: a writer providing functions to create an HTTP response to the request.
|
||||
/// - Returns HTTPBodyProcessing: a enum that either discards the request data, or provides a closure to process it.
|
||||
/// - See: `HTTPRequestHandler` for more information
|
||||
func handle(request: HTTPRequest, response: HTTPResponseWriter) -> HTTPBodyProcessing
|
||||
}
|
||||
|
||||
/// The result returned as part of a completion handler
|
||||
public enum Result {
|
||||
/// The action was successful
|
||||
case ok
|
||||
/// An error occurred during the processing of the action
|
||||
case error(Error)
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
/// Representation of the HTTP headers associated with a `HTTPRequest` or `HTTPResponse`.
|
||||
/// Headers are subscriptable using case-insensitive comparison or provide `Name` constants. eg.
|
||||
/// ```swift
|
||||
/// let contentLength = headers["content-length"]
|
||||
/// ```
|
||||
/// or
|
||||
/// ```swift
|
||||
/// let contentLength = headers[.contentLength]
|
||||
/// ```
|
||||
public struct HTTPHeaders {
|
||||
var original: [(name: Name, value: String)]?
|
||||
var storage: [Name: [String]] {
|
||||
didSet { original = nil }
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public subscript(name: Name) -> String? {
|
||||
get {
|
||||
guard let value = storage[name] else { return nil }
|
||||
switch name {
|
||||
case Name.setCookie: // Exception, see note in [RFC7230, section 3.2.2]
|
||||
return value.isEmpty ? nil : value[0]
|
||||
default:
|
||||
return value.joined(separator: ",")
|
||||
}
|
||||
}
|
||||
set {
|
||||
storage[name] = newValue.map { [$0] }
|
||||
}
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public subscript(valuesFor name: Name) -> [String] {
|
||||
get { return storage[name] ?? [] }
|
||||
set { storage[name] = newValue.isEmpty ? nil : newValue }
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPHeaders : ExpressibleByDictionaryLiteral {
|
||||
/// Creates HTTP headers.
|
||||
public init(dictionaryLiteral: (Name, String)...) {
|
||||
storage = [:]
|
||||
for (name, value) in dictionaryLiteral {
|
||||
#if swift(>=4.0)
|
||||
storage[name, default: []].append(value)
|
||||
#else
|
||||
if storage[name] == nil {
|
||||
storage[name] = [value]
|
||||
} else {
|
||||
storage[name]!.append(value)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
original = dictionaryLiteral
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPHeaders {
|
||||
// Used instead of HTTPHeaders to save CPU on dictionary construction
|
||||
/// :nodoc:
|
||||
public struct Literal: ExpressibleByDictionaryLiteral {
|
||||
let fields: [(name: Name, value: String)]
|
||||
|
||||
public init(dictionaryLiteral: (Name, String)...) {
|
||||
fields = dictionaryLiteral
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends a header to the headers
|
||||
public mutating func append(_ literal: HTTPHeaders.Literal) {
|
||||
for (name, value) in literal.fields {
|
||||
#if swift(>=4.0)
|
||||
storage[name, default: []].append(value)
|
||||
#else
|
||||
if storage[name] == nil {
|
||||
storage[name] = [value]
|
||||
} else {
|
||||
storage[name]!.append(value)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces a header in the headers
|
||||
public mutating func replace(_ literal: HTTPHeaders.Literal) {
|
||||
for (name, _) in literal.fields {
|
||||
storage[name] = []
|
||||
}
|
||||
for (name, value) in literal.fields {
|
||||
storage[name]!.append(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPHeaders : Sequence {
|
||||
/// :nodoc:
|
||||
public func makeIterator() -> AnyIterator<(name: Name, value: String)> {
|
||||
if let original = original {
|
||||
return AnyIterator(original.makeIterator())
|
||||
} else {
|
||||
return AnyIterator(StorageIterator(storage.makeIterator()))
|
||||
}
|
||||
}
|
||||
|
||||
struct StorageIterator: IteratorProtocol {
|
||||
var headers: DictionaryIterator<Name, [String]>
|
||||
var header: (name: Name, values: IndexingIterator<[String]>)?
|
||||
|
||||
init(_ iterator: DictionaryIterator<Name, [String]>) {
|
||||
headers = iterator
|
||||
header = headers.next().map { (name: $0.key, values: $0.value.makeIterator()) }
|
||||
}
|
||||
|
||||
mutating func next() -> (name: Name, value: String)? {
|
||||
while header != nil {
|
||||
if let value = header!.values.next() {
|
||||
return (name: header!.name, value: value)
|
||||
} else {
|
||||
header = headers.next().map { (name: $0.key, values: $0.value.makeIterator()) }
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTPHeaders structure.
|
||||
extension HTTPHeaders {
|
||||
/// Type used for the name of a HTTP header in the `HTTPHeaders` storage.
|
||||
public struct Name: Hashable, ExpressibleByStringLiteral, CustomStringConvertible {
|
||||
let original: String
|
||||
let lowercased: String
|
||||
public let hashValue: Int
|
||||
|
||||
/// Create a HTTP header name with the provided String.
|
||||
public init(_ name: String) {
|
||||
original = name
|
||||
lowercased = name.lowercased()
|
||||
hashValue = lowercased.hashValue
|
||||
}
|
||||
|
||||
public init(stringLiteral: String) {
|
||||
self.init(stringLiteral)
|
||||
}
|
||||
|
||||
public init(unicodeScalarLiteral: String) {
|
||||
self.init(unicodeScalarLiteral)
|
||||
}
|
||||
|
||||
public init(extendedGraphemeClusterLiteral: String) {
|
||||
self.init(extendedGraphemeClusterLiteral)
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public var description: String {
|
||||
return original
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public static func == (lhs: Name, rhs: Name) -> Bool {
|
||||
return lhs.lowercased == rhs.lowercased
|
||||
}
|
||||
|
||||
// https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
||||
// Permanent Message Header Field Names
|
||||
|
||||
/// A-IM header.
|
||||
public static let aIM = Name("A-IM")
|
||||
/// Accept header.
|
||||
public static let accept = Name("Accept")
|
||||
/// Accept-Additions header.
|
||||
public static let acceptAdditions = Name("Accept-Additions")
|
||||
/// Accept-Charset header.
|
||||
public static let acceptCharset = Name("Accept-Charset")
|
||||
/// Accept-Datetime header.
|
||||
public static let acceptDatetime = Name("Accept-Datetime")
|
||||
/// Accept-Encoding header.
|
||||
public static let acceptEncoding = Name("Accept-Encoding")
|
||||
/// Accept-Features header.
|
||||
public static let acceptFeatures = Name("Accept-Features")
|
||||
/// Accept-Language header.
|
||||
public static let acceptLanguage = Name("Accept-Language")
|
||||
/// Accept-Patch header.
|
||||
public static let acceptPatch = Name("Accept-Patch")
|
||||
/// Accept-Post header.
|
||||
public static let acceptPost = Name("Accept-Post")
|
||||
/// Accept-Ranges header.
|
||||
public static let acceptRanges = Name("Accept-Ranges")
|
||||
/// Accept-Age header.
|
||||
public static let age = Name("Age")
|
||||
/// Accept-Allow header.
|
||||
public static let allow = Name("Allow")
|
||||
/// ALPN header.
|
||||
public static let alpn = Name("ALPN")
|
||||
/// Alt-Svc header.
|
||||
public static let altSvc = Name("Alt-Svc")
|
||||
/// Alt-Used header.
|
||||
public static let altUsed = Name("Alt-Used")
|
||||
/// Alternatives header.
|
||||
public static let alternates = Name("Alternates")
|
||||
/// Apply-To-Redirect-Ref header.
|
||||
public static let applyToRedirectRef = Name("Apply-To-Redirect-Ref")
|
||||
/// Authentication-Control header.
|
||||
public static let authenticationControl = Name("Authentication-Control")
|
||||
/// Authentication-Info header.
|
||||
public static let authenticationInfo = Name("Authentication-Info")
|
||||
/// Authorization header.
|
||||
public static let authorization = Name("Authorization")
|
||||
/// C-Ext header.
|
||||
public static let cExt = Name("C-Ext")
|
||||
/// C-Man header.
|
||||
public static let cMan = Name("C-Man")
|
||||
/// C-Opt header.
|
||||
public static let cOpt = Name("C-Opt")
|
||||
/// C-PEP header.
|
||||
public static let cPEP = Name("C-PEP")
|
||||
/// C-PEP-Indo header.
|
||||
public static let cPEPInfo = Name("C-PEP-Info")
|
||||
/// Cache-Control header.
|
||||
public static let cacheControl = Name("Cache-Control")
|
||||
/// CalDav-Timezones header.
|
||||
public static let calDAVTimezones = Name("CalDAV-Timezones")
|
||||
/// Close header.
|
||||
public static let close = Name("Close")
|
||||
/// Connection header.
|
||||
public static let connection = Name("Connection")
|
||||
/// Content-Base.
|
||||
public static let contentBase = Name("Content-Base")
|
||||
/// Content-Disposition header.
|
||||
public static let contentDisposition = Name("Content-Disposition")
|
||||
/// Content-Encoding header.
|
||||
public static let contentEncoding = Name("Content-Encoding")
|
||||
/// Content-ID header.
|
||||
public static let contentID = Name("Content-ID")
|
||||
/// Content-Language header.
|
||||
public static let contentLanguage = Name("Content-Language")
|
||||
/// Content-Length header.
|
||||
public static let contentLength = Name("Content-Length")
|
||||
/// Content-Location header.
|
||||
public static let contentLocation = Name("Content-Location")
|
||||
/// Content-MD5 header.
|
||||
public static let contentMD5 = Name("Content-MD5")
|
||||
/// Content-Range header.
|
||||
public static let contentRange = Name("Content-Range")
|
||||
/// Content-Script-Type header.
|
||||
public static let contentScriptType = Name("Content-Script-Type")
|
||||
/// Content-Style-Type header.
|
||||
public static let contentStyleType = Name("Content-Style-Type")
|
||||
/// Content-Type header.
|
||||
public static let contentType = Name("Content-Type")
|
||||
/// Content-Version header.
|
||||
public static let contentVersion = Name("Content-Version")
|
||||
/// Content-Cookie header.
|
||||
public static let cookie = Name("Cookie")
|
||||
/// Content-Cookie2 header.
|
||||
public static let cookie2 = Name("Cookie2")
|
||||
/// DASL header.
|
||||
public static let dasl = Name("DASL")
|
||||
/// DASV header.
|
||||
public static let dav = Name("DAV")
|
||||
/// Date header.
|
||||
public static let date = Name("Date")
|
||||
/// Default-Style header.
|
||||
public static let defaultStyle = Name("Default-Style")
|
||||
/// Delta-Base header.
|
||||
public static let deltaBase = Name("Delta-Base")
|
||||
/// Depth header.
|
||||
public static let depth = Name("Depth")
|
||||
/// Derived-From header.
|
||||
public static let derivedFrom = Name("Derived-From")
|
||||
/// Destination header.
|
||||
public static let destination = Name("Destination")
|
||||
/// Differential-ID header.
|
||||
public static let differentialID = Name("Differential-ID")
|
||||
/// Digest header.
|
||||
public static let digest = Name("Digest")
|
||||
/// ETag header.
|
||||
public static let eTag = Name("ETag")
|
||||
/// Expect header.
|
||||
public static let expect = Name("Expect")
|
||||
/// Expires header.
|
||||
public static let expires = Name("Expires")
|
||||
/// Ext header.
|
||||
public static let ext = Name("Ext")
|
||||
/// Forwarded header.
|
||||
public static let forwarded = Name("Forwarded")
|
||||
/// From header.
|
||||
public static let from = Name("From")
|
||||
/// GetProfile header.
|
||||
public static let getProfile = Name("GetProfile")
|
||||
/// Hobareg header.
|
||||
public static let hobareg = Name("Hobareg")
|
||||
/// Host header.
|
||||
public static let host = Name("Host")
|
||||
/// HTTP2-Settings header.
|
||||
public static let http2Settings = Name("HTTP2-Settings")
|
||||
/// IM header.
|
||||
public static let im = Name("IM")
|
||||
/// If header.
|
||||
public static let `if` = Name("If")
|
||||
/// If-Match header.
|
||||
public static let ifMatch = Name("If-Match")
|
||||
/// If-Modified-Since header.
|
||||
public static let ifModifiedSince = Name("If-Modified-Since")
|
||||
/// If-None-Match header.
|
||||
public static let ifNoneMatch = Name("If-None-Match")
|
||||
/// If-Range header.
|
||||
public static let ifRange = Name("If-Range")
|
||||
/// If-Schedule-Tag-Match header.
|
||||
public static let ifScheduleTagMatch = Name("If-Schedule-Tag-Match")
|
||||
/// If-Unmodified-Since header.
|
||||
public static let ifUnmodifiedSince = Name("If-Unmodified-Since")
|
||||
/// Keep-Alive header.
|
||||
public static let keepAlive = Name("Keep-Alive")
|
||||
/// Label header.
|
||||
public static let label = Name("Label")
|
||||
/// Last-Modified header.
|
||||
public static let lastModified = Name("Last-Modified")
|
||||
/// Link header.
|
||||
public static let link = Name("Link")
|
||||
/// Location header.
|
||||
public static let location = Name("Location")
|
||||
/// Lock-Token header.
|
||||
public static let lockToken = Name("Lock-Token")
|
||||
/// Man header.
|
||||
public static let man = Name("Man")
|
||||
/// Max-Forwards header.
|
||||
public static let maxForwards = Name("Max-Forwards")
|
||||
/// Memento-Date header.
|
||||
public static let mementoDatetime = Name("Memento-Datetime")
|
||||
/// Meter header.
|
||||
public static let meter = Name("Meter")
|
||||
/// MIME-Version header.
|
||||
public static let mimeVersion = Name("MIME-Version")
|
||||
/// Negotiate header.
|
||||
public static let negotiate = Name("Negotiate")
|
||||
/// Opt header.
|
||||
public static let opt = Name("Opt")
|
||||
/// Optional-WWW-Authenticate header.
|
||||
public static let optionalWWWAuthenticate = Name("Optional-WWW-Authenticate")
|
||||
/// Ordering-Type header.
|
||||
public static let orderingType = Name("Ordering-Type")
|
||||
/// Origin header.
|
||||
public static let origin = Name("Origin")
|
||||
/// Overwrite header.
|
||||
public static let overwrite = Name("Overwrite")
|
||||
/// P3P header.
|
||||
public static let p3p = Name("P3P")
|
||||
/// PEP header.
|
||||
public static let pep = Name("PEP")
|
||||
/// PICS-Label header.
|
||||
public static let picsLabel = Name("PICS-Label")
|
||||
/// Pep-Info header.
|
||||
public static let pepInfo = Name("Pep-Info")
|
||||
/// Position header.
|
||||
public static let position = Name("Position")
|
||||
/// Pragma header.
|
||||
public static let pragma = Name("Pragma")
|
||||
/// Prefer header.
|
||||
public static let prefer = Name("Prefer")
|
||||
/// Preference-Applied header.
|
||||
public static let preferenceApplied = Name("Preference-Applied")
|
||||
/// ProfileObject header.
|
||||
public static let profileObject = Name("ProfileObject")
|
||||
/// Protocol header.
|
||||
public static let `protocol` = Name("Protocol")
|
||||
/// Protocol-Info header.
|
||||
public static let protocolInfo = Name("Protocol-Info")
|
||||
/// Protocol-Query header.
|
||||
public static let protocolQuery = Name("Protocol-Query")
|
||||
/// Protocol-Request header.
|
||||
public static let protocolRequest = Name("Protocol-Request")
|
||||
/// Proxy-Authenticate header.
|
||||
public static let proxyAuthenticate = Name("Proxy-Authenticate")
|
||||
/// Proxy-Authentication-Info header.
|
||||
public static let proxyAuthenticationInfo = Name("Proxy-Authentication-Info")
|
||||
/// Proxy-Authorization header.
|
||||
public static let proxyAuthorization = Name("Proxy-Authorization")
|
||||
/// Proxy-Features header.
|
||||
public static let proxyFeatures = Name("Proxy-Features")
|
||||
/// Proxy-Instruction header.
|
||||
public static let proxyInstruction = Name("Proxy-Instruction")
|
||||
/// Public header.
|
||||
public static let `public` = Name("Public")
|
||||
/// Public-Key-Pins header.
|
||||
public static let publicKeyPins = Name("Public-Key-Pins")
|
||||
/// Public-Key-Pins-Report-Only header.
|
||||
public static let publicKeyPinsReportOnly = Name("Public-Key-Pins-Report-Only")
|
||||
/// Range header.
|
||||
public static let range = Name("Range")
|
||||
/// Redirect-Ref header.
|
||||
public static let redirectRef = Name("Redirect-Ref")
|
||||
/// Referer header.
|
||||
public static let referer = Name("Referer")
|
||||
/// Retry-After header.
|
||||
public static let retryAfter = Name("Retry-After")
|
||||
/// Safe header.
|
||||
public static let safe = Name("Safe")
|
||||
/// Schedule-Reply header.
|
||||
public static let scheduleReply = Name("Schedule-Reply")
|
||||
/// Schedule-Tag header.
|
||||
public static let scheduleTag = Name("Schedule-Tag")
|
||||
/// Sec-WebSocket-Accept header.
|
||||
public static let secWebSocketAccept = Name("Sec-WebSocket-Accept")
|
||||
/// Sec-WebSocket-Extensions header.
|
||||
public static let secWebSocketExtensions = Name("Sec-WebSocket-Extensions")
|
||||
/// Sec-WebSocket-Key header.
|
||||
public static let secWebSocketKey = Name("Sec-WebSocket-Key")
|
||||
/// Sec-WebSocket-Protocol header.
|
||||
public static let secWebSocketProtocol = Name("Sec-WebSocket-Protocol")
|
||||
/// Sec-WebSocket-Version header.
|
||||
public static let secWebSocketVersion = Name("Sec-WebSocket-Version")
|
||||
/// Security-Scheme header.
|
||||
public static let securityScheme = Name("Security-Scheme")
|
||||
/// Server header.
|
||||
public static let server = Name("Server")
|
||||
/// Set-Cookie header.
|
||||
public static let setCookie = Name("Set-Cookie")
|
||||
/// Set-Cookie2 header.
|
||||
public static let setCookie2 = Name("Set-Cookie2")
|
||||
/// SetProfile header.
|
||||
public static let setProfile = Name("SetProfile")
|
||||
/// SLUG header.
|
||||
public static let slug = Name("SLUG")
|
||||
/// SoapAction header.
|
||||
public static let soapAction = Name("SoapAction")
|
||||
/// Status-URI header.
|
||||
public static let statusURI = Name("Status-URI")
|
||||
/// Strict-Transport-Security header.
|
||||
public static let strictTransportSecurity = Name("Strict-Transport-Security")
|
||||
/// Surrogate-Capability header.
|
||||
public static let surrogateCapability = Name("Surrogate-Capability")
|
||||
/// Surrogate-Control header.
|
||||
public static let surrogateControl = Name("Surrogate-Control")
|
||||
/// TCN header.
|
||||
public static let tcn = Name("TCN")
|
||||
/// TE header.
|
||||
public static let te = Name("TE")
|
||||
/// Timeout header.
|
||||
public static let timeout = Name("Timeout")
|
||||
/// Topic header.
|
||||
public static let topic = Name("Topic")
|
||||
/// Trailer header.
|
||||
public static let trailer = Name("Trailer")
|
||||
/// Transfer-Encoding header.
|
||||
public static let transferEncoding = Name("Transfer-Encoding")
|
||||
/// TTL header.
|
||||
public static let ttl = Name("TTL")
|
||||
/// Urgency header.
|
||||
public static let urgency = Name("Urgency")
|
||||
/// URI header.
|
||||
public static let uri = Name("URI")
|
||||
/// Upgrade header.
|
||||
public static let upgrade = Name("Upgrade")
|
||||
/// User-Agent header.
|
||||
public static let userAgent = Name("User-Agent")
|
||||
/// Variant-Vary header.
|
||||
public static let variantVary = Name("Variant-Vary")
|
||||
/// Vary header.
|
||||
public static let vary = Name("Vary")
|
||||
/// Via header.
|
||||
public static let via = Name("Via")
|
||||
/// WWW-Authenticate header.
|
||||
public static let wwwAuthenticate = Name("WWW-Authenticate")
|
||||
/// Want-Digest header.
|
||||
public static let wantDigest = Name("Want-Digest")
|
||||
/// Warning header.
|
||||
public static let warning = Name("Warning")
|
||||
/// X-Frame-Options header.
|
||||
public static let xFrameOptions = Name("X-Frame-Options")
|
||||
|
||||
// https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
||||
// Provisional Message Header Field Names
|
||||
/// Access-Control header.
|
||||
public static let accessControl = Name("Access-Control")
|
||||
/// Access-Control-Allow-Credentials header.
|
||||
public static let accessControlAllowCredentials = Name("Access-Control-Allow-Credentials")
|
||||
/// Access-Control-Allow-Headers header.
|
||||
public static let accessControlAllowHeaders = Name("Access-Control-Allow-Headers")
|
||||
/// Access-Control-Allow-Methods header.
|
||||
public static let accessControlAllowMethods = Name("Access-Control-Allow-Methods")
|
||||
/// Access-Control-Allow-Origin header.
|
||||
public static let accessControlAllowOrigin = Name("Access-Control-Allow-Origin")
|
||||
/// Access-Control-Max-Age header.
|
||||
public static let accessControlMaxAge = Name("Access-Control-Max-Age")
|
||||
/// Access-Control-Request-Method header.
|
||||
public static let accessControlRequestMethod = Name("Access-Control-Request-Method")
|
||||
/// Access-Control-Request-Headers header.
|
||||
public static let accessControlRequestHeaders = Name("Access-Control-Request-Headers")
|
||||
/// Compliance header.
|
||||
public static let compliance = Name("Compliance")
|
||||
/// Content-Transfer-Encoding header.
|
||||
public static let contentTransferEncoding = Name("Content-Transfer-Encoding")
|
||||
/// Cost header.
|
||||
public static let cost = Name("Cost")
|
||||
/// EDIINT-Features header.
|
||||
public static let ediintFeatures = Name("EDIINT-Features")
|
||||
/// Message-ID header.
|
||||
public static let messageID = Name("Message-ID")
|
||||
/// Method-Check header.
|
||||
public static let methodCheck = Name("Method-Check")
|
||||
/// Method-Check-Expires header.
|
||||
public static let methodCheckExpires = Name("Method-Check-Expires")
|
||||
/// Non-Compliance header.
|
||||
public static let nonCompliance = Name("Non-Compliance")
|
||||
/// Optional header.
|
||||
public static let optional = Name("Optional")
|
||||
/// Referer-Root header.
|
||||
public static let refererRoot = Name("Referer-Root")
|
||||
/// Resolution-Hint header.
|
||||
public static let resolutionHint = Name("Resolution-Hint")
|
||||
/// Resolver-Location header.
|
||||
public static let resolverLocation = Name("Resolver-Location")
|
||||
/// SubOK header.
|
||||
public static let subOK = Name("SubOK")
|
||||
/// Subst header.
|
||||
public static let subst = Name("Subst")
|
||||
/// Title header.
|
||||
public static let title = Name("Title")
|
||||
/// UA-Color header.
|
||||
public static let uaColor = Name("UA-Color")
|
||||
/// UA-Media header.
|
||||
public static let uaMedia = Name("UA-Media")
|
||||
/// UA-Pixels header.
|
||||
public static let uaPixels = Name("UA-Pixels")
|
||||
/// UA-Resolution header.
|
||||
public static let uaResolution = Name("UA-Resolution")
|
||||
/// UA-Windowpixels header.
|
||||
public static let uaWindowpixels = Name("UA-Windowpixels")
|
||||
/// Version header.
|
||||
public static let version = Name("Version")
|
||||
/// X-Device-Accept header.
|
||||
public static let xDeviceAccept = Name("X-Device-Accept")
|
||||
/// X-Device-Accept-Charset header.
|
||||
public static let xDeviceAcceptCharset = Name("X-Device-Accept-Charset")
|
||||
/// X-Device-Accept-Encoding header.
|
||||
public static let xDeviceAcceptEncoding = Name("X-Device-Accept-Encoding")
|
||||
/// X-Device-Accept-Language header.
|
||||
public static let xDeviceAcceptLanguage = Name("X-Device-Accept-Language")
|
||||
/// X-Device-User-Agent header.
|
||||
public static let xDeviceUserAgent = Name("X-Device-User-Agent")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
/// HTTP method structure
|
||||
public struct HTTPMethod {
|
||||
/// HTTP method
|
||||
public let method: String
|
||||
|
||||
/// Creates an HTTP method
|
||||
public init(_ method: String) {
|
||||
self.method = method.uppercased()
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP method constants
|
||||
extension HTTPMethod {
|
||||
/// DELETE method.
|
||||
public static let delete = HTTPMethod("DELETE")
|
||||
/// GET method.
|
||||
public static let get = HTTPMethod("GET")
|
||||
/// HEAD method.
|
||||
public static let head = HTTPMethod("HEAD")
|
||||
/// POST method.
|
||||
public static let post = HTTPMethod("POST")
|
||||
/// PUT method.
|
||||
public static let put = HTTPMethod("PUT")
|
||||
/// CONNECT method.
|
||||
public static let connect = HTTPMethod("CONNECT")
|
||||
/// OPTIONS method.
|
||||
public static let options = HTTPMethod("OPTIONS")
|
||||
/// TRACE method.
|
||||
public static let trace = HTTPMethod("TRACE")
|
||||
/// COPY method.
|
||||
public static let copy = HTTPMethod("COPY")
|
||||
/// LOCK method.
|
||||
public static let lock = HTTPMethod("LOCK")
|
||||
/// MKCOL method.
|
||||
public static let mkol = HTTPMethod("MKCOL")
|
||||
/// MOVE method.
|
||||
public static let move = HTTPMethod("MOVE")
|
||||
/// PROPFIND method.
|
||||
public static let propfind = HTTPMethod("PROPFIND")
|
||||
/// PROPPATCH method.
|
||||
public static let proppatch = HTTPMethod("PROPPATCH")
|
||||
/// SEARCH method.
|
||||
public static let search = HTTPMethod("SEARCH")
|
||||
/// UNLOCK method.
|
||||
public static let unlock = HTTPMethod("UNLOCK")
|
||||
/// BIND method.
|
||||
public static let bind = HTTPMethod("BIND")
|
||||
/// REBIND method.
|
||||
public static let rebind = HTTPMethod("REBIND")
|
||||
/// UNBIND method.
|
||||
public static let unbind = HTTPMethod("UNBIND")
|
||||
/// ACL method.
|
||||
public static let acl = HTTPMethod("ACL")
|
||||
/// REPORT method.
|
||||
public static let report = HTTPMethod("REPORT")
|
||||
/// MKACTIVITY method.
|
||||
public static let mkactivity = HTTPMethod("MKACTIVITY")
|
||||
/// CHECKOUT method.
|
||||
public static let checkout = HTTPMethod("CHECKOUT")
|
||||
/// MERGE method.
|
||||
public static let merge = HTTPMethod("MERGE")
|
||||
/// MSEARCH method.
|
||||
public static let msearch = HTTPMethod("MSEARCH")
|
||||
/// NOTIFY method.
|
||||
public static let notify = HTTPMethod("NOTIFY")
|
||||
/// SUBSCRIBE method.
|
||||
public static let subscribe = HTTPMethod("SUBSCRIBE")
|
||||
/// UNSUBSCRIBE method.
|
||||
public static let unsubscribe = HTTPMethod("UNSUBSCRIBE")
|
||||
/// PATCH method.
|
||||
public static let patch = HTTPMethod("PATCH")
|
||||
/// PURGE method.
|
||||
public static let purge = HTTPMethod("PURGE")
|
||||
/// MKCALENDAR method.
|
||||
public static let mkcalendar = HTTPMethod("MKCALENDAR")
|
||||
/// LINK method.
|
||||
public static let link = HTTPMethod("LINK")
|
||||
/// UNLINK method.
|
||||
public static let unlink = HTTPMethod("UNLINK")
|
||||
}
|
||||
|
||||
extension HTTPMethod : Hashable {
|
||||
public var hashValue: Int {
|
||||
return method.hashValue
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public static func == (lhs: HTTPMethod, rhs: HTTPMethod) -> Bool {
|
||||
return lhs.method == rhs.method
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public static func ~= (match: HTTPMethod, version: HTTPMethod) -> Bool {
|
||||
return match == version
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPMethod : ExpressibleByStringLiteral {
|
||||
/// :nodoc:
|
||||
public init(stringLiteral: String) {
|
||||
self.init(stringLiteral)
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public init(unicodeScalarLiteral: String) {
|
||||
self.init(unicodeScalarLiteral)
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public init(extendedGraphemeClusterLiteral: String) {
|
||||
self.init(extendedGraphemeClusterLiteral)
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPMethod : CustomStringConvertible {
|
||||
/// :nodoc:
|
||||
public var description: String {
|
||||
return method
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
|
||||
/// A structure representing the headers from a HTTP request, without the body of the request.
|
||||
public struct HTTPRequest {
|
||||
/// HTTP request method.
|
||||
public var method: HTTPMethod
|
||||
/// HTTP request URI, eg. "/foo/bar?buz=qux"
|
||||
public var target: String
|
||||
/// HTTP request version
|
||||
public var httpVersion: HTTPVersion
|
||||
/// HTTP request headers
|
||||
public var headers: HTTPHeaders
|
||||
}
|
||||
|
||||
/// Method that takes a chunk of request body and is expected to write to the `HTTPResponseWriter`
|
||||
/// - Parameter HTTPBodyChunk: `HTTPBodyChunk` representing some or all of the incoming request body
|
||||
/// - Parameter Bool: A boolean flag that can be set to true in order to prevent further processing
|
||||
public typealias HTTPBodyHandler = (HTTPBodyChunk, inout Bool) -> Void
|
||||
|
||||
/// Indicates whether the body is going to be processed or ignored
|
||||
public enum HTTPBodyProcessing {
|
||||
/// Used to discard the body data associated with the incoming HTTP request
|
||||
case discardBody
|
||||
/// Used to process the body data associated with the imcoming HTTP request using a `HTTPBodyHandler`
|
||||
case processBody(handler: HTTPBodyHandler)
|
||||
}
|
||||
|
||||
/// Part or all of the incoming request body
|
||||
public enum HTTPBodyChunk {
|
||||
/// A new chunk of the incoming HTTP reqest body data has arrived. `finishedProcessing()` must be called when
|
||||
/// that chunk has been processed.
|
||||
case chunk(data: DispatchData, finishedProcessing: () -> Void)
|
||||
/// An error has occurred whilst streaming the incoming HTTP request data, eg. the connection closed
|
||||
case failed(error: Error)
|
||||
/// A trailer header has arrived during the processing of the incoming HTTP request data.
|
||||
/// This is currently unimplemented.
|
||||
case trailer(key: String, value: String)
|
||||
/// The stream of incoming HTTP request data has completed.
|
||||
case end
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dispatch
|
||||
|
||||
/// A structure representing the headers for a HTTP response, without the body of the response.
|
||||
public struct HTTPResponse {
|
||||
/// HTTP response version
|
||||
public var httpVersion: HTTPVersion
|
||||
/// HTTP response status
|
||||
public var status: HTTPResponseStatus
|
||||
/// HTTP response headers
|
||||
public var headers: HTTPHeaders
|
||||
}
|
||||
|
||||
/// HTTPResponseWriter provides functions to create an HTTP response
|
||||
public protocol HTTPResponseWriter : class {
|
||||
/// Writer function to create the headers for an HTTP response
|
||||
/// - Parameter status: The status code to include in the HTTP response
|
||||
/// - Parameter headers: The HTTP headers to include in the HTTP response
|
||||
/// - Parameter completion: Closure that is called when the HTTP headers have been written to the HTTP respose
|
||||
func writeHeader(status: HTTPResponseStatus, headers: HTTPHeaders, completion: @escaping (Result) -> Void)
|
||||
|
||||
/// Writer function to write a trailer header as part of the HTTP response
|
||||
/// - Parameter trailers: The trailers to write as part of the HTTP response
|
||||
/// - Parameter completion: Closure that is called when the trailers has been written to the HTTP response
|
||||
/// This is not currently implemented
|
||||
func writeTrailer(_ trailers: HTTPHeaders, completion: @escaping (Result) -> Void)
|
||||
|
||||
/// Writer function to write data to the body of the HTTP response
|
||||
/// - Parameter data: The data to write as part of the HTTP response
|
||||
/// - Parameter completion: Closure that is called when the data has been written to the HTTP response
|
||||
func writeBody(_ data: UnsafeHTTPResponseBody, completion: @escaping (Result) -> Void)
|
||||
|
||||
/// Writer function to complete the HTTP response
|
||||
/// - Parameter completion: Closure that is called when the HTTP response has been completed
|
||||
func done(completion: @escaping (Result) -> Void)
|
||||
|
||||
/// abort: Abort the HTTP response
|
||||
func abort()
|
||||
}
|
||||
|
||||
/// Convenience methods for HTTP response writer.
|
||||
extension HTTPResponseWriter {
|
||||
/// Convenience function to write the headers for an HTTP response without a completion handler
|
||||
/// - See: `writeHeader(status:headers:completion:)`
|
||||
public func writeHeader(status: HTTPResponseStatus, headers: HTTPHeaders) {
|
||||
writeHeader(status: status, headers: headers) { _ in }
|
||||
}
|
||||
|
||||
/// Convenience function to write a HTTP response with no headers or completion handler
|
||||
/// - See: `writeHeader(status:headers:completion:)`
|
||||
public func writeHeader(status: HTTPResponseStatus) {
|
||||
writeHeader(status: status, headers: [:])
|
||||
}
|
||||
|
||||
/// Convenience function to write a trailer header as part of the HTTP response without a completion handler
|
||||
/// - See: `writeTrailer(_:completion:)`
|
||||
public func writeTrailer(_ trailers: HTTPHeaders) {
|
||||
writeTrailer(trailers) { _ in }
|
||||
}
|
||||
|
||||
/// Convenience function for writing `data` to the body of the HTTP response without a completion handler.
|
||||
/// - See: writeBody(_:completion:)
|
||||
public func writeBody(_ data: UnsafeHTTPResponseBody) {
|
||||
return writeBody(data) { _ in }
|
||||
}
|
||||
|
||||
/// Convenience function to complete the HTTP response without a completion handler.
|
||||
/// - See: done(completion:)
|
||||
public func done() {
|
||||
done { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
/// The response status for the HTTP response
|
||||
/// - See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml for more information
|
||||
public struct HTTPResponseStatus: Equatable, CustomStringConvertible, ExpressibleByIntegerLiteral {
|
||||
/// The status code, eg. 200 or 404
|
||||
public let code: Int
|
||||
/// The reason phrase for the status code
|
||||
public let reasonPhrase: String
|
||||
|
||||
/// Creates an HTTP response status
|
||||
/// - Parameter code: The status code used for the response status
|
||||
/// - Parameter reasonPhrase: The reason phrase to use for the response status
|
||||
public init(code: Int, reasonPhrase: String) {
|
||||
self.code = code
|
||||
self.reasonPhrase = reasonPhrase
|
||||
}
|
||||
|
||||
/// Creates an HTTP response status
|
||||
/// The reason phrase is added for the status code, or "http_(code)" if the code is not well known
|
||||
/// - Parameter code: The status code used for the response status
|
||||
public init(code: Int) {
|
||||
self.init(code: code, reasonPhrase: HTTPResponseStatus.defaultReasonPhrase(forCode: code))
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public init(integerLiteral: Int) {
|
||||
self.init(code: integerLiteral)
|
||||
}
|
||||
|
||||
/* all the codes from http://www.iana.org/assignments/http-status-codes */
|
||||
/// 100 Continue
|
||||
public static let `continue` = HTTPResponseStatus(code: 100)
|
||||
/// 101 Switching Protocols
|
||||
public static let switchingProtocols = HTTPResponseStatus(code: 101)
|
||||
/// 200 OK
|
||||
public static let ok = HTTPResponseStatus(code: 200)
|
||||
/// 201 Created
|
||||
public static let created = HTTPResponseStatus(code: 201)
|
||||
/// 202 Accepted
|
||||
public static let accepted = HTTPResponseStatus(code: 202)
|
||||
/// 203 Non-Authoritative Information
|
||||
public static let nonAuthoritativeInformation = HTTPResponseStatus(code: 203)
|
||||
/// 204 No Content
|
||||
public static let noContent = HTTPResponseStatus(code: 204)
|
||||
/// 205 Reset Content
|
||||
public static let resetContent = HTTPResponseStatus(code: 205)
|
||||
/// 206 Partial Content
|
||||
public static let partialContent = HTTPResponseStatus(code: 206)
|
||||
/// 207 Multi-Status
|
||||
public static let multiStatus = HTTPResponseStatus(code: 207)
|
||||
/// 208 Already Reported
|
||||
public static let alreadyReported = HTTPResponseStatus(code: 208)
|
||||
/// 226 IM Used
|
||||
public static let imUsed = HTTPResponseStatus(code: 226)
|
||||
/// 300 Multiple Choices
|
||||
public static let multipleChoices = HTTPResponseStatus(code: 300)
|
||||
/// 301 Moved Permanently
|
||||
public static let movedPermanently = HTTPResponseStatus(code: 301)
|
||||
/// 302 Found
|
||||
public static let found = HTTPResponseStatus(code: 302)
|
||||
/// 303 See Other
|
||||
public static let seeOther = HTTPResponseStatus(code: 303)
|
||||
/// 304 Not Modified
|
||||
public static let notModified = HTTPResponseStatus(code: 304)
|
||||
/// 305 Use Proxy
|
||||
public static let useProxy = HTTPResponseStatus(code: 305)
|
||||
/// 307 Temporary Redirect
|
||||
public static let temporaryRedirect = HTTPResponseStatus(code: 307)
|
||||
/// 308 Permanent Redirect
|
||||
public static let permanentRedirect = HTTPResponseStatus(code: 308)
|
||||
/// 400 Bad Request
|
||||
public static let badRequest = HTTPResponseStatus(code: 400)
|
||||
/// 401 Unauthorized
|
||||
public static let unauthorized = HTTPResponseStatus(code: 401)
|
||||
/// 402 Payment Required
|
||||
public static let paymentRequired = HTTPResponseStatus(code: 402)
|
||||
/// 403 Forbidden
|
||||
public static let forbidden = HTTPResponseStatus(code: 403)
|
||||
/// 404 Not Found
|
||||
public static let notFound = HTTPResponseStatus(code: 404)
|
||||
/// 405 Method Not Allowed
|
||||
public static let methodNotAllowed = HTTPResponseStatus(code: 405)
|
||||
/// 406 Not Acceptable
|
||||
public static let notAcceptable = HTTPResponseStatus(code: 406)
|
||||
/// 407 Proxy Authentication Required
|
||||
public static let proxyAuthenticationRequired = HTTPResponseStatus(code: 407)
|
||||
/// 408 Request Timeout
|
||||
public static let requestTimeout = HTTPResponseStatus(code: 408)
|
||||
/// 409 Conflict
|
||||
public static let conflict = HTTPResponseStatus(code: 409)
|
||||
/// 410 Gone
|
||||
public static let gone = HTTPResponseStatus(code: 410)
|
||||
/// 411 Length Required
|
||||
public static let lengthRequired = HTTPResponseStatus(code: 411)
|
||||
/// 412 Precondition Failed
|
||||
public static let preconditionFailed = HTTPResponseStatus(code: 412)
|
||||
/// 413 Payload Too Large
|
||||
public static let payloadTooLarge = HTTPResponseStatus(code: 413)
|
||||
/// 414 URI Too Long
|
||||
public static let uriTooLong = HTTPResponseStatus(code: 414)
|
||||
/// 415 Unsupported Media Type
|
||||
public static let unsupportedMediaType = HTTPResponseStatus(code: 415)
|
||||
/// 416 Range Not Satisfiable
|
||||
public static let rangeNotSatisfiable = HTTPResponseStatus(code: 416)
|
||||
/// 417 Expectation Failed
|
||||
public static let expectationFailed = HTTPResponseStatus(code: 417)
|
||||
/// 421 Misdirected Request
|
||||
public static let misdirectedRequest = HTTPResponseStatus(code: 421)
|
||||
/// 422 Unprocessable Entity
|
||||
public static let unprocessableEntity = HTTPResponseStatus(code: 422)
|
||||
/// 423 Locked
|
||||
public static let locked = HTTPResponseStatus(code: 423)
|
||||
/// 424 Failed Dependency
|
||||
public static let failedDependency = HTTPResponseStatus(code: 424)
|
||||
/// 426 Upgrade Required
|
||||
public static let upgradeRequired = HTTPResponseStatus(code: 426)
|
||||
/// 428 Precondition Required
|
||||
public static let preconditionRequired = HTTPResponseStatus(code: 428)
|
||||
/// 429 Too Many Requests
|
||||
public static let tooManyRequests = HTTPResponseStatus(code: 429)
|
||||
/// 431 Request Header Fields Too Large
|
||||
public static let requestHeaderFieldsTooLarge = HTTPResponseStatus(code: 431)
|
||||
/// 451 Unavailable For Legal Reasons
|
||||
public static let unavailableForLegalReasons = HTTPResponseStatus(code: 451)
|
||||
/// 500 Internal Server Error
|
||||
public static let internalServerError = HTTPResponseStatus(code: 500)
|
||||
/// 501 Not Implemented
|
||||
public static let notImplemented = HTTPResponseStatus(code: 501)
|
||||
/// 502 Bad Gateway
|
||||
public static let badGateway = HTTPResponseStatus(code: 502)
|
||||
/// 503 Service Unavailable
|
||||
public static let serviceUnavailable = HTTPResponseStatus(code: 503)
|
||||
/// 504 Gateway Timeout
|
||||
public static let gatewayTimeout = HTTPResponseStatus(code: 504)
|
||||
/// 505 HTTP Version Not Supported
|
||||
public static let httpVersionNotSupported = HTTPResponseStatus(code: 505)
|
||||
/// 506 Variant Also Negotiates
|
||||
public static let variantAlsoNegotiates = HTTPResponseStatus(code: 506)
|
||||
/// 507 Insufficient Storage
|
||||
public static let insufficientStorage = HTTPResponseStatus(code: 507)
|
||||
/// 508 Loop Detected
|
||||
public static let loopDetected = HTTPResponseStatus(code: 508)
|
||||
/// 510 Not Extended
|
||||
public static let notExtended = HTTPResponseStatus(code: 510)
|
||||
/// 511 Network Authentication Required
|
||||
public static let networkAuthenticationRequired = HTTPResponseStatus(code: 511)
|
||||
|
||||
// swiftlint:disable cyclomatic_complexity switch_case_on_newline
|
||||
static func defaultReasonPhrase(forCode code: Int) -> String {
|
||||
switch code {
|
||||
case 100: return "Continue"
|
||||
case 101: return "Switching Protocols"
|
||||
case 200: return "OK"
|
||||
case 201: return "Created"
|
||||
case 202: return "Accepted"
|
||||
case 203: return "Non-Authoritative Information"
|
||||
case 204: return "No Content"
|
||||
case 205: return "Reset Content"
|
||||
case 206: return "Partial Content"
|
||||
case 207: return "Multi-Status"
|
||||
case 208: return "Already Reported"
|
||||
case 226: return "IM Used"
|
||||
case 300: return "Multiple Choices"
|
||||
case 301: return "Moved Permanently"
|
||||
case 302: return "Found"
|
||||
case 303: return "See Other"
|
||||
case 304: return "Not Modified"
|
||||
case 305: return "Use Proxy"
|
||||
case 307: return "Temporary Redirect"
|
||||
case 308: return "Permanent Redirect"
|
||||
case 400: return "Bad Request"
|
||||
case 401: return "Unauthorized"
|
||||
case 402: return "Payment Required"
|
||||
case 403: return "Forbidden"
|
||||
case 404: return "Not Found"
|
||||
case 405: return "Method Not Allowed"
|
||||
case 406: return "Not Acceptable"
|
||||
case 407: return "Proxy Authentication Required"
|
||||
case 408: return "Request Timeout"
|
||||
case 409: return "Conflict"
|
||||
case 410: return "Gone"
|
||||
case 411: return "Length Required"
|
||||
case 412: return "Precondition Failed"
|
||||
case 413: return "Payload Too Large"
|
||||
case 414: return "URI Too Long"
|
||||
case 415: return "Unsupported Media Type"
|
||||
case 416: return "Range Not Satisfiable"
|
||||
case 417: return "Expectation Failed"
|
||||
case 421: return "Misdirected Request"
|
||||
case 422: return "Unprocessable Entity"
|
||||
case 423: return "Locked"
|
||||
case 424: return "Failed Dependency"
|
||||
case 426: return "Upgrade Required"
|
||||
case 428: return "Precondition Required"
|
||||
case 429: return "Too Many Requests"
|
||||
case 431: return "Request Header Fields Too Large"
|
||||
case 451: return "Unavailable For Legal Reasons"
|
||||
case 500: return "Internal Server Error"
|
||||
case 501: return "Not Implemented"
|
||||
case 502: return "Bad Gateway"
|
||||
case 503: return "Service Unavailable"
|
||||
case 504: return "Gateway Timeout"
|
||||
case 505: return "HTTP Version Not Supported"
|
||||
case 506: return "Variant Also Negotiates"
|
||||
case 507: return "Insufficient Storage"
|
||||
case 508: return "Loop Detected"
|
||||
case 510: return "Not Extended"
|
||||
case 511: return "Network Authentication Required"
|
||||
default: return "http_\(code)"
|
||||
}
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public var description: String {
|
||||
return "\(code) \(reasonPhrase)"
|
||||
}
|
||||
|
||||
/// - The `Class` representing the class of status code for this response status
|
||||
public var `class`: Class {
|
||||
return Class(code: code)
|
||||
}
|
||||
|
||||
/// The class of a `HTTPResponseStatus` code
|
||||
/// - See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml for more information
|
||||
public enum Class {
|
||||
/// Informational: the request was received, and is continuing to be processed
|
||||
case informational
|
||||
/// Success: the action was successfully received, understood, and accepted
|
||||
case successful
|
||||
/// Redirection: further action must be taken in order to complete the request
|
||||
case redirection
|
||||
/// Client Error: the request contains bad syntax or cannot be fulfilled
|
||||
case clientError
|
||||
/// Server Error: the server failed to fulfill an apparently valid request
|
||||
case serverError
|
||||
/// Invalid: the code does not map to a well known status code class
|
||||
case invalidStatus
|
||||
|
||||
init(code: Int) {
|
||||
switch code {
|
||||
case 100..<200: self = .informational
|
||||
case 200..<300: self = .successful
|
||||
case 300..<400: self = .redirection
|
||||
case 400..<500: self = .clientError
|
||||
case 500..<600: self = .serverError
|
||||
default: self = .invalidStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [RFC2616, section 4.4]
|
||||
var bodyAllowed: Bool {
|
||||
switch code {
|
||||
case 100..<200: return false
|
||||
case 204: return false
|
||||
case 304: return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
var suppressedHeaders: [HTTPHeaders.Name] {
|
||||
if self == .notModified {
|
||||
return ["Content-Type", "Content-Length", "Transfer-Encoding"]
|
||||
} else if !bodyAllowed {
|
||||
return ["Content-Length", "Transfer-Encoding"]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public static func == (lhs: HTTPResponseStatus, rhs: HTTPResponseStatus) -> Bool {
|
||||
return lhs.code == rhs.code
|
||||
}
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public protocol UnsafeHTTPResponseBody {
|
||||
func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
extension UnsafeRawBufferPointer: UnsafeHTTPResponseBody {
|
||||
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
|
||||
return try body(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public protocol HTTPResponseBody: UnsafeHTTPResponseBody {}
|
||||
|
||||
extension Data: HTTPResponseBody {
|
||||
/// :nodoc:
|
||||
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
|
||||
return try withUnsafeBytes { try body(UnsafeRawBufferPointer(start: $0, count: count)) }
|
||||
}
|
||||
}
|
||||
|
||||
extension DispatchData: HTTPResponseBody {
|
||||
/// :nodoc:
|
||||
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
|
||||
return try withUnsafeBytes { try body(UnsafeRawBufferPointer(start: $0, count: count)) }
|
||||
}
|
||||
}
|
||||
|
||||
extension String: HTTPResponseBody {
|
||||
/// :nodoc:
|
||||
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
|
||||
return try ContiguousArray(utf8).withUnsafeBytes(body)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
/// Definition of an HTTP server.
|
||||
public protocol HTTPServing : class {
|
||||
|
||||
/// Start the HTTP server on the given `port`, using `handler` to process incoming requests
|
||||
func start(port: Int, handler: @escaping HTTPRequestHandler) throws
|
||||
|
||||
/// Stop the server
|
||||
func stop()
|
||||
|
||||
/// The port the server is listening on
|
||||
var port: Int { get }
|
||||
|
||||
/// The number of current connections
|
||||
var connectionCount: Int { get }
|
||||
}
|
||||
|
||||
/// A basic HTTP server. Currently this is implemented using the PoCSocket
|
||||
/// abstraction, but the intention is to remove this dependency and reimplement
|
||||
/// the class using transport APIs provided by the Server APIs working group.
|
||||
public class HTTPServer: HTTPServing {
|
||||
private let server = PoCSocketSimpleServer()
|
||||
|
||||
/// Create an instance of the server. This needs to be followed with a call to `start(port:handler:)`
|
||||
public init() {
|
||||
}
|
||||
|
||||
/// Start the HTTP server on the given `port` number, using a `HTTPRequestHandler` to process incoming requests.
|
||||
public func start(port: Int = 0, handler: @escaping HTTPRequestHandler) throws {
|
||||
try server.start(port: port, handler: handler)
|
||||
}
|
||||
|
||||
/// Stop the server
|
||||
public func stop() {
|
||||
server.stop()
|
||||
}
|
||||
|
||||
/// The port number the server is listening on
|
||||
public var port: Int {
|
||||
return server.port
|
||||
}
|
||||
|
||||
/// The number of current connections
|
||||
public var connectionCount: Int {
|
||||
return server.connectionCount
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import CHTTPParser
|
||||
import Foundation
|
||||
import Dispatch
|
||||
|
||||
/// Class that wraps the CHTTPParser and calls the `HTTPRequestHandler` to get the response
|
||||
/// :nodoc:
|
||||
public class StreamingParser: HTTPResponseWriter {
|
||||
|
||||
let handle: HTTPRequestHandler
|
||||
|
||||
/// Time to leave socket open waiting for next request to start
|
||||
public let keepAliveTimeout: TimeInterval
|
||||
|
||||
/// Flag to track if the client wants to send consecutive requests on the same TCP connection
|
||||
var clientRequestedKeepAlive = false
|
||||
|
||||
/// Tracks when socket should be closed. Needs to have a lock, since it's updated often
|
||||
private let _keepAliveUntilLock = DispatchSemaphore(value: 1)
|
||||
private var _keepAliveUntil: TimeInterval?
|
||||
public var keepAliveUntil: TimeInterval? {
|
||||
get {
|
||||
_keepAliveUntilLock.wait()
|
||||
defer {
|
||||
_keepAliveUntilLock.signal()
|
||||
}
|
||||
return _keepAliveUntil
|
||||
}
|
||||
set {
|
||||
_keepAliveUntilLock.wait()
|
||||
defer {
|
||||
_keepAliveUntilLock.signal()
|
||||
}
|
||||
_keepAliveUntil = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional delegate that can tell us how many connections are in-flight.
|
||||
public weak var connectionCounter: CurrentConnectionCounting?
|
||||
|
||||
/// Holds the bytes that come from the CHTTPParser until we have enough of them to do something with it
|
||||
var parserBuffer: Data?
|
||||
|
||||
/// HTTP Parser
|
||||
var httpParser = http_parser()
|
||||
var httpParserSettings = http_parser_settings()
|
||||
|
||||
/// Block that takes a chunk from the HTTPParser as input and writes to a Response as a result
|
||||
var httpBodyProcessingCallback: HTTPBodyProcessing?
|
||||
|
||||
//Note: we want this to be strong so it holds onto the connector until it's explicitly cleared
|
||||
/// Protocol that we use to send data (and status info) back to the Network layer
|
||||
public var parserConnector: ParserConnecting?
|
||||
|
||||
///Flag to track whether our handler has told us not to call it anymore
|
||||
private let _shouldStopProcessingBodyLock = DispatchSemaphore(value: 1)
|
||||
private var _shouldStopProcessingBody: Bool = false
|
||||
private var shouldStopProcessingBody: Bool {
|
||||
get {
|
||||
_shouldStopProcessingBodyLock.wait()
|
||||
defer {
|
||||
_shouldStopProcessingBodyLock.signal()
|
||||
}
|
||||
return _shouldStopProcessingBody
|
||||
}
|
||||
set {
|
||||
_shouldStopProcessingBodyLock.wait()
|
||||
defer {
|
||||
_shouldStopProcessingBodyLock.signal()
|
||||
}
|
||||
_shouldStopProcessingBody = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var lastCallBack = CallbackRecord.idle
|
||||
var lastHeaderName: String?
|
||||
var parsedHeaders = HTTPHeaders()
|
||||
var parsedHTTPMethod: HTTPMethod?
|
||||
var parsedHTTPVersion: HTTPVersion?
|
||||
var parsedURL: String?
|
||||
|
||||
/// Is the currently parsed request an upgrade request?
|
||||
public private(set) var upgradeRequested = false
|
||||
|
||||
/// Class that wraps the CHTTPParser and calls the `HTTPRequestHandler` to get the response
|
||||
///
|
||||
/// - Parameter handler: function that is used to create the response
|
||||
public init(handler: @escaping HTTPRequestHandler, connectionCounter: CurrentConnectionCounting? = nil, keepAliveTimeout: Double = 5.0) {
|
||||
self.handle = handler
|
||||
self.connectionCounter = connectionCounter
|
||||
self.keepAliveTimeout = keepAliveTimeout
|
||||
|
||||
//Set up all the callbacks for the CHTTPParser library
|
||||
httpParserSettings.on_message_begin = { parser -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return Int32(0)
|
||||
}
|
||||
return listener.messageBegan()
|
||||
}
|
||||
|
||||
httpParserSettings.on_message_complete = { parser -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return 0
|
||||
}
|
||||
return listener.messageCompleted()
|
||||
}
|
||||
|
||||
httpParserSettings.on_headers_complete = { parser -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return 0
|
||||
}
|
||||
let methodId = parser?.pointee.method
|
||||
let methodName = String(validatingUTF8: http_method_str(http_method(rawValue: methodId ?? 0))) ?? "GET"
|
||||
let major = Int(parser?.pointee.http_major ?? 0)
|
||||
let minor = Int(parser?.pointee.http_minor ?? 0)
|
||||
|
||||
//This needs to be set here and not messageCompleted if it's going to work here
|
||||
let keepAlive = http_should_keep_alive(parser) == 1
|
||||
let upgradeRequested = parser?.pointee.upgrade == 1
|
||||
|
||||
return listener.headersCompleted(methodName: methodName,
|
||||
majorVersion: major,
|
||||
minorVersion: minor,
|
||||
keepAlive: keepAlive,
|
||||
upgrade: upgradeRequested)
|
||||
}
|
||||
|
||||
httpParserSettings.on_header_field = { (parser, chunk, length) -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return 0
|
||||
}
|
||||
return listener.headerFieldReceived(data: chunk, length: length)
|
||||
}
|
||||
|
||||
httpParserSettings.on_header_value = { (parser, chunk, length) -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return 0
|
||||
}
|
||||
return listener.headerValueReceived(data: chunk, length: length)
|
||||
}
|
||||
|
||||
httpParserSettings.on_body = { (parser, chunk, length) -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return 0
|
||||
}
|
||||
return listener.bodyReceived(data: chunk, length: length)
|
||||
}
|
||||
|
||||
httpParserSettings.on_url = { (parser, chunk, length) -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return 0
|
||||
}
|
||||
return listener.urlReceived(data: chunk, length: length)
|
||||
}
|
||||
|
||||
http_parser_init(&httpParser, HTTP_REQUEST)
|
||||
|
||||
self.httpParser.data = Unmanaged.passUnretained(self).toOpaque()
|
||||
}
|
||||
|
||||
/// Read a stream from the network, pass it to the parser and return number of bytes consumed
|
||||
///
|
||||
/// - Parameter data: data coming from network
|
||||
/// - Returns: number of bytes that we sent to the parser
|
||||
public func readStream(data: Data) -> Int {
|
||||
return data.withUnsafeBytes { (ptr) -> Int in
|
||||
return http_parser_execute(&self.httpParser, &self.httpParserSettings, ptr, data.count)
|
||||
}
|
||||
}
|
||||
|
||||
/// States to track where we are in parsing the HTTP Stream from the client
|
||||
enum CallbackRecord {
|
||||
case idle, messageBegan, messageCompleted, headersCompleted, headerFieldReceived, headerValueReceived, bodyReceived, urlReceived
|
||||
}
|
||||
|
||||
/// Process change of state as we get more and more parser callbacks
|
||||
///
|
||||
/// - Parameter currentCallBack: state we are entering, as specified by the CHTTPParser
|
||||
/// - Returns: Whether or not the state actually changed
|
||||
@discardableResult
|
||||
func processCurrentCallback(_ currentCallBack: CallbackRecord) -> Bool {
|
||||
if lastCallBack == currentCallBack {
|
||||
return false
|
||||
}
|
||||
switch lastCallBack {
|
||||
case .headerFieldReceived:
|
||||
if let parserBuffer = self.parserBuffer {
|
||||
self.lastHeaderName = String(data: parserBuffer, encoding: .utf8)
|
||||
self.parserBuffer = nil
|
||||
} else {
|
||||
print("Missing parserBuffer after \(lastCallBack)")
|
||||
}
|
||||
case .headerValueReceived:
|
||||
if let parserBuffer = self.parserBuffer,
|
||||
let lastHeaderName = self.lastHeaderName,
|
||||
let headerValue = String(data:parserBuffer, encoding: .utf8) {
|
||||
self.parsedHeaders.append([HTTPHeaders.Name(lastHeaderName): headerValue])
|
||||
self.lastHeaderName = nil
|
||||
self.parserBuffer = nil
|
||||
} else {
|
||||
print("Missing parserBuffer after \(lastCallBack)")
|
||||
}
|
||||
case .headersCompleted:
|
||||
self.parserBuffer = nil
|
||||
|
||||
if !upgradeRequested {
|
||||
self.httpBodyProcessingCallback = self.handle(self.createRequest(), self)
|
||||
}
|
||||
case .urlReceived:
|
||||
if let parserBuffer = self.parserBuffer {
|
||||
//Under heaptrack, this may appear to leak via _CFGetTSDCreateIfNeeded,
|
||||
// apparently, that's because it triggers thread metadata to be created
|
||||
self.parsedURL = String(data:parserBuffer, encoding: .utf8)
|
||||
self.parserBuffer = nil
|
||||
} else {
|
||||
print("Missing parserBuffer after \(lastCallBack)")
|
||||
}
|
||||
case .idle:
|
||||
break
|
||||
case .messageBegan:
|
||||
break
|
||||
case .messageCompleted:
|
||||
break
|
||||
case .bodyReceived:
|
||||
break
|
||||
}
|
||||
lastCallBack = currentCallBack
|
||||
return true
|
||||
}
|
||||
|
||||
func messageBegan() -> Int32 {
|
||||
processCurrentCallback(.messageBegan)
|
||||
self.parserConnector?.responseBeginning()
|
||||
return 0
|
||||
}
|
||||
|
||||
func messageCompleted() -> Int32 {
|
||||
let didChangeState = processCurrentCallback(.messageCompleted)
|
||||
if let chunkHandler = self.httpBodyProcessingCallback, didChangeState {
|
||||
var dummy = false //We're sending `.end`, which means processing is stopping anyway, so the bool here is pointless
|
||||
switch chunkHandler {
|
||||
case .processBody(let handler):
|
||||
handler(.end, &dummy)
|
||||
case .discardBody:
|
||||
done()
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func headersCompleted(methodName: String,
|
||||
majorVersion: Int,
|
||||
minorVersion: Int,
|
||||
keepAlive: Bool,
|
||||
upgrade: Bool) -> Int32 {
|
||||
processCurrentCallback(.headersCompleted)
|
||||
self.parsedHTTPMethod = HTTPMethod(methodName)
|
||||
self.parsedHTTPVersion = HTTPVersion(major: majorVersion, minor: minorVersion)
|
||||
|
||||
//This needs to be set here and not messageCompleted if it's going to work here
|
||||
self.clientRequestedKeepAlive = keepAlive
|
||||
self.keepAliveUntil = Date(timeIntervalSinceNow: keepAliveTimeout).timeIntervalSinceReferenceDate
|
||||
self.upgradeRequested = upgrade
|
||||
return 0
|
||||
}
|
||||
|
||||
func headerFieldReceived(data: UnsafePointer<Int8>?, length: Int) -> Int32 {
|
||||
processCurrentCallback(.headerFieldReceived)
|
||||
guard let data = data else { return 0 }
|
||||
data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in
|
||||
if var parserBuffer = parserBuffer {
|
||||
parserBuffer.append(ptr, count: length)
|
||||
} else {
|
||||
parserBuffer = Data(bytes: data, count: length)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func headerValueReceived(data: UnsafePointer<Int8>?, length: Int) -> Int32 {
|
||||
processCurrentCallback(.headerValueReceived)
|
||||
guard let data = data else { return 0 }
|
||||
data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in
|
||||
if var parserBuffer = parserBuffer {
|
||||
parserBuffer.append(ptr, count: length)
|
||||
} else {
|
||||
parserBuffer = Data(bytes: data, count: length)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func bodyReceived(data: UnsafePointer<Int8>?, length: Int) -> Int32 {
|
||||
processCurrentCallback(.bodyReceived)
|
||||
guard let data = data else { return 0 }
|
||||
if shouldStopProcessingBody {
|
||||
return 0
|
||||
}
|
||||
data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in
|
||||
#if swift(>=4.0)
|
||||
let buff = UnsafeRawBufferPointer(start: ptr, count: length)
|
||||
#else
|
||||
let buff = UnsafeBufferPointer<UInt8>(start: ptr, count: length)
|
||||
#endif
|
||||
let chunk = DispatchData(bytes: buff)
|
||||
if let chunkHandler = self.httpBodyProcessingCallback {
|
||||
switch chunkHandler {
|
||||
case .processBody(let handler):
|
||||
//OK, this sucks. We can't access the value of the `inout` inside this block
|
||||
// due to exclusivity. Which means that if we were to pass a local variable, we'd
|
||||
// have to put a semaphore or something up here to wait for the block to be done before
|
||||
// we could get its value and pass that on to the instance variable. So instead, we're
|
||||
// just passing in a pointer to the internal ivar. But that ivar can't be modified in
|
||||
// more than one place, so we have to put a semaphore around it to prevent that.
|
||||
_shouldStopProcessingBodyLock.wait()
|
||||
handler(.chunk(data: chunk, finishedProcessing: {self._shouldStopProcessingBodyLock.signal()}), &_shouldStopProcessingBody)
|
||||
case .discardBody:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func urlReceived(data: UnsafePointer<Int8>?, length: Int) -> Int32 {
|
||||
processCurrentCallback(.urlReceived)
|
||||
guard let data = data else { return 0 }
|
||||
data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in
|
||||
if var parserBuffer = parserBuffer {
|
||||
parserBuffer.append(ptr, count: length)
|
||||
} else {
|
||||
parserBuffer = Data(bytes: data, count: length)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
static func getSelf(parser: UnsafeMutablePointer<http_parser>?) -> StreamingParser? {
|
||||
guard let pointee = parser?.pointee.data else { return nil }
|
||||
return Unmanaged<StreamingParser>.fromOpaque(pointee).takeUnretainedValue()
|
||||
}
|
||||
|
||||
var headersWritten = false
|
||||
var isChunked = false
|
||||
|
||||
/// Create a `HTTPRequest` struct from the parsed information
|
||||
public func createRequest() -> HTTPRequest {
|
||||
return HTTPRequest(method: parsedHTTPMethod!,
|
||||
target: parsedURL!,
|
||||
httpVersion: parsedHTTPVersion!,
|
||||
headers: parsedHeaders)
|
||||
}
|
||||
|
||||
public func writeHeader(status: HTTPResponseStatus, headers: HTTPHeaders, completion: @escaping (Result) -> Void) {
|
||||
|
||||
guard !headersWritten else {
|
||||
return
|
||||
}
|
||||
|
||||
var header = "HTTP/1.1 \(status.code) \(status.reasonPhrase)\r\n"
|
||||
|
||||
let isContinue = status == .continue
|
||||
|
||||
var headers = headers
|
||||
if !isContinue {
|
||||
adjustHeaders(status: status, headers: &headers)
|
||||
}
|
||||
|
||||
for (key, value) in headers {
|
||||
// TODO encode value using [RFC5987]
|
||||
header += "\(key): \(value)\r\n"
|
||||
}
|
||||
header.append("\r\n")
|
||||
|
||||
// FIXME headers are US-ASCII, anything else should be encoded using [RFC5987] some lines above
|
||||
// TODO use requested encoding if specified
|
||||
if let data = header.data(using: .utf8) {
|
||||
self.parserConnector?.queueSocketWrite(data, completion: completion)
|
||||
if !isContinue {
|
||||
headersWritten = true
|
||||
}
|
||||
} else {
|
||||
//TODO handle encoding error
|
||||
}
|
||||
}
|
||||
|
||||
func adjustHeaders(status: HTTPResponseStatus, headers: inout HTTPHeaders) {
|
||||
for header in status.suppressedHeaders {
|
||||
headers[header] = nil
|
||||
}
|
||||
|
||||
if headers[.contentLength] != nil {
|
||||
headers[.transferEncoding] = "identity"
|
||||
} else if parsedHTTPVersion! >= HTTPVersion(major: 1, minor: 1) {
|
||||
switch headers[.transferEncoding] {
|
||||
case .some("identity"): // identity without content-length
|
||||
clientRequestedKeepAlive = false
|
||||
case .some("chunked"):
|
||||
isChunked = true
|
||||
default:
|
||||
isChunked = true
|
||||
headers[.transferEncoding] = "chunked"
|
||||
}
|
||||
} else {
|
||||
// HTTP 1.0 does not support chunked
|
||||
clientRequestedKeepAlive = false
|
||||
headers[.transferEncoding] = nil
|
||||
}
|
||||
|
||||
|
||||
if clientRequestedKeepAlive {
|
||||
headers[.connection] = "Keep-Alive"
|
||||
} else {
|
||||
headers[.connection] = "Close"
|
||||
}
|
||||
}
|
||||
|
||||
public func writeTrailer(_ trailers: HTTPHeaders, completion: @escaping (Result) -> Void) {
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
public func writeBody(_ data: UnsafeHTTPResponseBody, completion: @escaping (Result) -> Void) {
|
||||
guard headersWritten else {
|
||||
//TODO error or default headers?
|
||||
return
|
||||
}
|
||||
|
||||
guard data.withUnsafeBytes({ $0.count > 0 }) else {
|
||||
completion(.ok)
|
||||
return
|
||||
}
|
||||
|
||||
let dataToWrite: Data
|
||||
if isChunked {
|
||||
dataToWrite = data.withUnsafeBytes {
|
||||
let chunkStart = (String($0.count, radix: 16) + "\r\n").data(using: .utf8)!
|
||||
var dataToWrite = chunkStart
|
||||
dataToWrite.append(UnsafeBufferPointer(start: $0.baseAddress?.assumingMemoryBound(to: UInt8.self), count: $0.count))
|
||||
let chunkEnd = "\r\n".data(using: .utf8)!
|
||||
dataToWrite.append(chunkEnd)
|
||||
return dataToWrite
|
||||
}
|
||||
} else if let data = data as? Data {
|
||||
dataToWrite = data
|
||||
} else {
|
||||
dataToWrite = data.withUnsafeBytes { Data($0) }
|
||||
}
|
||||
|
||||
self.parserConnector?.queueSocketWrite(dataToWrite, completion: completion)
|
||||
}
|
||||
|
||||
public func done(completion: @escaping (Result) -> Void) {
|
||||
if isChunked {
|
||||
let chunkTerminate = "0\r\n\r\n".data(using: .utf8)!
|
||||
self.parserConnector?.queueSocketWrite(chunkTerminate, completion: completion)
|
||||
}
|
||||
|
||||
self.parsedHTTPMethod = nil
|
||||
self.parsedURL = nil
|
||||
self.parsedHeaders = HTTPHeaders()
|
||||
self.lastHeaderName = nil
|
||||
self.parserBuffer = nil
|
||||
self.parsedHTTPMethod = nil
|
||||
self.parsedHTTPVersion = nil
|
||||
self.lastCallBack = .idle
|
||||
self.headersWritten = false
|
||||
self.httpBodyProcessingCallback = nil
|
||||
self.upgradeRequested = false
|
||||
self.shouldStopProcessingBody = false
|
||||
|
||||
//Note: This used to be passed into the completion block that `Result` used to have
|
||||
// But since that block was removed, we're calling it directly
|
||||
if self.clientRequestedKeepAlive {
|
||||
self.keepAliveUntil = Date(timeIntervalSinceNow: keepAliveTimeout).timeIntervalSinceReferenceDate
|
||||
self.parserConnector?.responseComplete()
|
||||
} else {
|
||||
self.parserConnector?.responseCompleteCloseWriter()
|
||||
}
|
||||
completion(.ok)
|
||||
}
|
||||
|
||||
public func abort() {
|
||||
fatalError("abort called, not sure what to do with it")
|
||||
}
|
||||
|
||||
deinit {
|
||||
httpParser.data = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Protocol implemented by the thing that sits in between us and the network layer
|
||||
/// :nodoc:
|
||||
public protocol ParserConnecting: class {
|
||||
/// Send data to the network do be written to the client
|
||||
func queueSocketWrite(_ from: Data, completion: @escaping (Result) -> Void)
|
||||
|
||||
/// Let the network know that a response has started to avoid closing a connection during a slow write
|
||||
func responseBeginning()
|
||||
|
||||
/// Let the network know that a response is complete, so it can be closed after timeout
|
||||
func responseComplete()
|
||||
|
||||
/// Let the network know that a response is complete and we're ready to close the connection
|
||||
func responseCompleteCloseWriter()
|
||||
|
||||
/// Used to let the network know we're ready to close the connection
|
||||
func closeWriter()
|
||||
}
|
||||
|
||||
/// Delegate that can tell us how many connections are in-flight so we can set the Keep-Alive header
|
||||
/// to the correct number of available connections
|
||||
/// :nodoc:
|
||||
public protocol CurrentConnectionCounting: class {
|
||||
/// Current number of active connections
|
||||
var connectionCount: Int { get }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
/// Version number of the HTTP Protocol
|
||||
public struct HTTPVersion {
|
||||
/// Major version component.
|
||||
public private(set) var major: Int
|
||||
/// Minor version component.
|
||||
public private(set) var minor: Int
|
||||
|
||||
/// Creates an HTTP version.
|
||||
public init(major: Int, minor: Int) {
|
||||
self.major = major
|
||||
self.minor = minor
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPVersion : Hashable {
|
||||
/// :nodoc:
|
||||
public var hashValue: Int {
|
||||
return (major << 8) | minor
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public static func == (lhs: HTTPVersion, rhs: HTTPVersion) -> Bool {
|
||||
return lhs.major == rhs.major && lhs.minor == rhs.minor
|
||||
}
|
||||
|
||||
/// :nodoc:
|
||||
public static func ~= (match: HTTPVersion, version: HTTPVersion) -> Bool {
|
||||
return match == version
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPVersion : Comparable {
|
||||
/// :nodoc:
|
||||
public static func < (lhs: HTTPVersion, rhs: HTTPVersion) -> Bool {
|
||||
if lhs.major != rhs.major {
|
||||
return lhs.major < rhs.major
|
||||
} else {
|
||||
return lhs.minor < rhs.minor
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HTTPVersion : CustomStringConvertible {
|
||||
/// :nodoc:
|
||||
public var description: String {
|
||||
return "HTTP/" + major.description + "." + minor.description
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dispatch
|
||||
|
||||
///:nodoc:
|
||||
public enum PoCSocketError: Error {
|
||||
case SocketOSError(errno: Int32)
|
||||
case InvalidSocketError
|
||||
case InvalidReadLengthError
|
||||
case InvalidWriteLengthError
|
||||
case InvalidBufferError
|
||||
}
|
||||
|
||||
/// Simple Wrapper around the `socket(2)` functions we need for Proof of Concept testing
|
||||
/// Intentionally a thin layer over `recv(2)`/`send(2)` so uses the same argument types.
|
||||
/// Note that no method names here are the same as any system call names.
|
||||
/// This is because we expect the caller might need functionality we haven't implemented here.
|
||||
internal class PoCSocket {
|
||||
|
||||
/// hold the file descriptor for the socket supplied by the OS. `-1` is invalid socket
|
||||
internal var socketfd: Int32 = -1
|
||||
|
||||
/// The TCP port the server is actually listening on. Set after system call completes
|
||||
internal var listeningPort: Int32 = -1
|
||||
|
||||
/// Track state between `listen(2)` and `shutdown(2)`
|
||||
internal private(set) var isListening = false
|
||||
|
||||
/// Track state between `accept(2)/bind(2)` and `close(2)`
|
||||
internal private(set) var isConnected = false
|
||||
|
||||
/// track whether a shutdown is in progress so we can suppress error messages
|
||||
private let _isShuttingDownLock = DispatchSemaphore(value: 1)
|
||||
private var _isShuttingDown: Bool = false
|
||||
private var isShuttingDown: Bool {
|
||||
get {
|
||||
_isShuttingDownLock.wait()
|
||||
defer {
|
||||
_isShuttingDownLock.signal()
|
||||
}
|
||||
return _isShuttingDown
|
||||
}
|
||||
set {
|
||||
_isShuttingDownLock.wait()
|
||||
defer {
|
||||
_isShuttingDownLock.signal()
|
||||
}
|
||||
_isShuttingDown = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Call recv(2) with buffer allocated by our caller and return the output
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - readBuffer: Buffer to read into. Note this needs to be `inout` because we're modfying it and we want Swift4+'s ownership checks to make sure no one else is at the same time
|
||||
/// - maxLength: Max length that can be read. Buffer *must* be at least this big!!!
|
||||
/// - Returns: Number of bytes read or -1 on failure as per `recv(2)`
|
||||
/// - Throws: PoCSocketError if sanity checks fail
|
||||
internal func socketRead(into readBuffer: inout UnsafeMutablePointer<Int8>, maxLength:Int) throws -> Int {
|
||||
if maxLength <= 0 || maxLength > Int(Int32.max) {
|
||||
throw PoCSocketError.InvalidReadLengthError
|
||||
}
|
||||
if socketfd <= 0 {
|
||||
throw PoCSocketError.InvalidSocketError
|
||||
}
|
||||
|
||||
//Make sure no one passed a nil pointer to us
|
||||
let readBufferPointer: UnsafeMutablePointer<Int8>! = readBuffer
|
||||
if readBufferPointer == nil {
|
||||
throw PoCSocketError.InvalidBufferError
|
||||
}
|
||||
|
||||
//Make sure data isn't re-used
|
||||
readBuffer.initialize(to: 0x0, count: maxLength)
|
||||
|
||||
let read = recv(self.socketfd, readBuffer, maxLength, Int32(0))
|
||||
//Leave this as a local variable to facilitate Setting a Watchpoint in lldb
|
||||
return read
|
||||
}
|
||||
|
||||
/// Pass buffer passed into to us into send(2).
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - buffer: buffer containing data to write.
|
||||
/// - bufSize: number of bytes to write. Buffer must be this long
|
||||
/// - Returns: number of bytes written or -1. See `send(2)`
|
||||
/// - Throws: PoCSocketError if sanity checks fail
|
||||
@discardableResult internal func socketWrite(from buffer: UnsafeRawPointer, bufSize: Int) throws -> Int {
|
||||
if socketfd <= 0 {
|
||||
throw PoCSocketError.InvalidSocketError
|
||||
}
|
||||
if bufSize < 0 || bufSize > Int(Int32.max) {
|
||||
throw PoCSocketError.InvalidWriteLengthError
|
||||
}
|
||||
|
||||
//Make sure we weren't handed a nil buffer
|
||||
let writeBufferPointer: UnsafeRawPointer! = buffer
|
||||
if writeBufferPointer == nil {
|
||||
throw PoCSocketError.InvalidBufferError
|
||||
}
|
||||
|
||||
let sent = send(self.socketfd, buffer, Int(bufSize), Int32(0))
|
||||
//Leave this as a local variable to facilitate Setting a Watchpoint in lldb
|
||||
return sent
|
||||
}
|
||||
|
||||
/// Calls `shutdown(2)` and `close(2)` on a socket
|
||||
internal func shutdownAndClose() {
|
||||
self.isShuttingDown = true
|
||||
if socketfd < 1 {
|
||||
//Nothing to do. Maybe it was closed already
|
||||
return
|
||||
}
|
||||
//print("Shutting down socket \(self.socketfd)")
|
||||
if self.isListening || self.isConnected {
|
||||
//print("Shutting down socket")
|
||||
_ = shutdown(self.socketfd, Int32(SHUT_RDWR))
|
||||
self.isListening = false
|
||||
}
|
||||
self.isConnected = false
|
||||
close(self.socketfd)
|
||||
}
|
||||
|
||||
/// Thin wrapper around `accept(2)`
|
||||
///
|
||||
/// - Returns: PoCSocket object for newly connected socket or nil if we've been told to shutdown
|
||||
/// - Throws: PoCSocketError on sanity check fails or if accept fails after several retries
|
||||
internal func acceptClientConnection() throws -> PoCSocket? {
|
||||
if socketfd <= 0 || !isListening {
|
||||
throw PoCSocketError.InvalidSocketError
|
||||
}
|
||||
|
||||
let retVal = PoCSocket()
|
||||
|
||||
var maxRetryCount = 100
|
||||
|
||||
var acceptFD: Int32 = -1
|
||||
repeat {
|
||||
var acceptAddr = sockaddr_in()
|
||||
var addrSize = socklen_t(MemoryLayout<sockaddr_in>.size)
|
||||
|
||||
acceptFD = withUnsafeMutablePointer(to: &acceptAddr) { pointer in
|
||||
return accept(self.socketfd, UnsafeMutableRawPointer(pointer).assumingMemoryBound(to: sockaddr.self), &addrSize)
|
||||
}
|
||||
if acceptFD < 0 && errno != EINTR {
|
||||
//fail
|
||||
if (isShuttingDown) {
|
||||
return nil
|
||||
}
|
||||
maxRetryCount = maxRetryCount - 1
|
||||
print("Could not accept on socket \(socketfd). Error is \(errno). Will retry.")
|
||||
}
|
||||
}
|
||||
while acceptFD < 0 && maxRetryCount > 0
|
||||
|
||||
if acceptFD < 0 {
|
||||
throw PoCSocketError.SocketOSError(errno: errno)
|
||||
}
|
||||
|
||||
retVal.isConnected = true
|
||||
retVal.socketfd = acceptFD
|
||||
|
||||
return retVal
|
||||
}
|
||||
|
||||
/// call `bind(2)` and `listen(2)`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - port: `sin_port` value, see `bind(2)`
|
||||
/// - maxBacklogSize: backlog argument to `listen(2)`
|
||||
/// - Throws: PoCSocketError
|
||||
internal func bindAndListen(on port: Int = 0, maxBacklogSize: Int32 = 100) throws {
|
||||
#if os(Linux)
|
||||
socketfd = socket(Int32(AF_INET), Int32(SOCK_STREAM.rawValue), Int32(IPPROTO_TCP))
|
||||
#else
|
||||
socketfd = socket(Int32(AF_INET), Int32(SOCK_STREAM), Int32(IPPROTO_TCP))
|
||||
#endif
|
||||
|
||||
if socketfd <= 0 {
|
||||
throw PoCSocketError.InvalidSocketError
|
||||
}
|
||||
|
||||
var on: Int32 = 1
|
||||
// Allow address reuse
|
||||
if setsockopt(self.socketfd, SOL_SOCKET, SO_REUSEADDR, &on, socklen_t(MemoryLayout<Int32>.size)) < 0 {
|
||||
throw PoCSocketError.SocketOSError(errno: errno)
|
||||
}
|
||||
|
||||
// Allow port reuse
|
||||
if setsockopt(self.socketfd, SOL_SOCKET, SO_REUSEPORT, &on, socklen_t(MemoryLayout<Int32>.size)) < 0 {
|
||||
throw PoCSocketError.SocketOSError(errno: errno)
|
||||
}
|
||||
|
||||
#if os(Linux)
|
||||
var addr = sockaddr_in(
|
||||
sin_family: sa_family_t(AF_INET),
|
||||
sin_port: htons(UInt16(port)),
|
||||
sin_addr: in_addr(s_addr: in_addr_t(0)),
|
||||
sin_zero:(0, 0, 0, 0, 0, 0, 0, 0))
|
||||
#else
|
||||
var addr = sockaddr_in(
|
||||
sin_len: UInt8(MemoryLayout<sockaddr_in>.stride),
|
||||
sin_family: UInt8(AF_INET),
|
||||
sin_port: (Int(OSHostByteOrder()) != OSLittleEndian ? UInt16(port) : _OSSwapInt16(UInt16(port))),
|
||||
sin_addr: in_addr(s_addr: in_addr_t(0)),
|
||||
sin_zero:(0, 0, 0, 0, 0, 0, 0, 0))
|
||||
#endif
|
||||
|
||||
let _ = withUnsafePointer(to: &addr) {
|
||||
bind(self.socketfd, UnsafePointer<sockaddr>(OpaquePointer($0)), socklen_t(MemoryLayout<sockaddr_in>.size))
|
||||
}
|
||||
|
||||
//print("bindResult is \(bindResult)")
|
||||
|
||||
let _ = listen(self.socketfd, maxBacklogSize)
|
||||
|
||||
isListening = true
|
||||
|
||||
//print("listenResult is \(listenResult)")
|
||||
|
||||
var addr_in = sockaddr_in()
|
||||
|
||||
listeningPort = try withUnsafePointer(to: &addr_in) { pointer in
|
||||
var len = socklen_t(MemoryLayout<sockaddr_in>.size)
|
||||
if getsockname(socketfd, UnsafeMutablePointer(OpaquePointer(pointer)), &len) != 0 {
|
||||
throw PoCSocketError.SocketOSError(errno: errno)
|
||||
}
|
||||
#if os(Linux)
|
||||
return Int32(ntohs(addr_in.sin_port))
|
||||
#else
|
||||
return Int32(Int(OSHostByteOrder()) != OSLittleEndian ? addr_in.sin_port.littleEndian : addr_in.sin_port.bigEndian)
|
||||
#endif
|
||||
}
|
||||
|
||||
//print("listeningPort is \(listeningPort)")
|
||||
}
|
||||
|
||||
/// Check to see if socket is being used
|
||||
///
|
||||
/// - Returns: whether socket is listening or connected
|
||||
internal func isOpen() -> Bool {
|
||||
return isListening || isConnected
|
||||
}
|
||||
|
||||
/// Sets the socket to Blocking or non-blocking mode.
|
||||
///
|
||||
/// - Parameter mode: true for blocking, false for nonBlocking
|
||||
/// - Returns: `fcntl(2)` flags
|
||||
/// - Throws: PoCSocketError if `fcntl` fails
|
||||
@discardableResult internal func setBlocking(mode: Bool) throws -> Int32 {
|
||||
let flags = fcntl(self.socketfd, F_GETFL)
|
||||
if flags < 0 {
|
||||
//Failed
|
||||
throw PoCSocketError.SocketOSError(errno: errno)
|
||||
}
|
||||
|
||||
let newFlags = mode ? flags & ~O_NONBLOCK : flags | O_NONBLOCK
|
||||
|
||||
let result = fcntl(self.socketfd, F_SETFL, newFlags)
|
||||
if result < 0 {
|
||||
//Failed
|
||||
throw PoCSocketError.SocketOSError(errno: errno)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dispatch
|
||||
|
||||
///:nodoc:
|
||||
public class PoCSocketConnectionListener: ParserConnecting {
|
||||
|
||||
///socket(2) wrapper object
|
||||
var socket: PoCSocket?
|
||||
|
||||
///ivar for the thing that manages the CHTTP Parser
|
||||
var parser: StreamingParser?
|
||||
|
||||
///Save the socket file descriptor so we can loook at it for debugging purposes
|
||||
var socketFD: Int32
|
||||
var shouldShutdown: Bool = false
|
||||
|
||||
/// Queues for managing access to the socket without blocking the world
|
||||
let socketReaderQueue: DispatchQueue
|
||||
let socketWriterQueue: DispatchQueue
|
||||
|
||||
///Event handler for reading from the socket
|
||||
private var readerSource: DispatchSourceRead?
|
||||
|
||||
///Flag to track whether we're in the middle of a response or not (with lock)
|
||||
private let _responseCompletedLock = DispatchSemaphore(value: 1)
|
||||
private var _responseCompleted: Bool = false
|
||||
var responseCompleted: Bool {
|
||||
get {
|
||||
_responseCompletedLock.wait()
|
||||
defer {
|
||||
_responseCompletedLock.signal()
|
||||
}
|
||||
return _responseCompleted
|
||||
}
|
||||
set {
|
||||
_responseCompletedLock.wait()
|
||||
defer {
|
||||
_responseCompletedLock.signal()
|
||||
}
|
||||
_responseCompleted = newValue
|
||||
}
|
||||
}
|
||||
|
||||
///Flag to track whether we've received a socket error or not (with lock)
|
||||
private let _errorOccurredLock = DispatchSemaphore(value: 1)
|
||||
private var _errorOccurred: Bool = false
|
||||
var errorOccurred: Bool {
|
||||
get {
|
||||
_errorOccurredLock.wait()
|
||||
defer {
|
||||
_errorOccurredLock.signal()
|
||||
}
|
||||
return _errorOccurred
|
||||
}
|
||||
set {
|
||||
_errorOccurredLock.wait()
|
||||
defer {
|
||||
_errorOccurredLock.signal()
|
||||
}
|
||||
_errorOccurred = newValue
|
||||
}
|
||||
}
|
||||
|
||||
///Largest number of bytes we're willing to allocate for a Read
|
||||
// it's an anti-heartbleed-type paranoia check
|
||||
private var maxReadLength: Int = 1048576
|
||||
|
||||
/// initializer
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - socket: thin PoCSocket wrapper around system calls
|
||||
/// - parser: Manager of the CHTTPParser library
|
||||
internal init(socket: PoCSocket, parser: StreamingParser, readQueue: DispatchQueue, writeQueue: DispatchQueue, maxReadLength: Int = 0) {
|
||||
self.socket = socket
|
||||
socketFD = socket.socketfd
|
||||
socketReaderQueue = readQueue
|
||||
socketWriterQueue = writeQueue
|
||||
self.parser = parser
|
||||
parser.parserConnector = self
|
||||
if maxReadLength > 0 {
|
||||
self.maxReadLength = maxReadLength
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if socket is still open. Used to decide whether it should be closed/pruned after timeout
|
||||
public var isOpen: Bool {
|
||||
guard let socket = self.socket else {
|
||||
return false
|
||||
}
|
||||
return socket.isOpen()
|
||||
}
|
||||
|
||||
/// Close the socket and free up memory unless we're in the middle of a request
|
||||
func close() {
|
||||
self.shouldShutdown = true
|
||||
|
||||
if !self.responseCompleted && !self.errorOccurred {
|
||||
return
|
||||
}
|
||||
if (self.socket?.socketfd ?? -1) > 0 {
|
||||
self.socket?.shutdownAndClose()
|
||||
}
|
||||
|
||||
//In a perfect world, we wouldn't have to clean this all up explicitly,
|
||||
// but KDE/heaptrack informs us we're in far from a perfect world
|
||||
|
||||
if !(self.readerSource?.isCancelled ?? true) {
|
||||
/*
|
||||
OK, so later macOS wants `cancel()` to be called from inside the readerSource,
|
||||
otherwise, there's a very intermittent thread-dependent crash, (ask me how I know)
|
||||
so in that case, we set a Bool variable and call `activate()`. Older macOS doesn't
|
||||
have `activate()` so we call back to calling `cancel()` directly.
|
||||
|
||||
Linux *DOES* have activate(), but it doesn't seem to do anything at present, so we call `cancel()`
|
||||
directly in that case, too (Although I suspect that might need to change in future releases).
|
||||
*/
|
||||
#if os(Linux)
|
||||
// Call Cancel directory on Linux
|
||||
self.readerSource?.cancel()
|
||||
self.cleanup()
|
||||
#else
|
||||
if #available(OSX 10.12, *) {
|
||||
//Set Flag and Activate the readerSource so it can run `cancel()` for us
|
||||
self.shouldShutdown = true
|
||||
self.readerSource?.activate()
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
self.readerSource?.cancel()
|
||||
self.cleanup()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by the parser to let us know that it's done with this socket
|
||||
public func closeWriter() {
|
||||
self.socketWriterQueue.async { [weak self] in
|
||||
if self?.readerSource?.isCancelled ?? true {
|
||||
self?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the socket is idle, and if so, call close()
|
||||
func closeIfIdleSocket() {
|
||||
let now = Date().timeIntervalSinceReferenceDate
|
||||
if let keepAliveUntil = parser?.keepAliveUntil, now >= keepAliveUntil {
|
||||
print("Closing idle socket \(socketFD)")
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
self.readerSource?.setEventHandler(handler: nil)
|
||||
self.readerSource?.setCancelHandler(handler: nil)
|
||||
|
||||
self.readerSource = nil
|
||||
self.socket = nil
|
||||
self.parser?.parserConnector = nil //allows for memory to be reclaimed
|
||||
self.parser = nil
|
||||
}
|
||||
|
||||
/// Called by the parser to let us know that a response has started being created
|
||||
public func responseBeginning() {
|
||||
self.socketWriterQueue.async { [weak self] in
|
||||
self?.responseCompleted = false
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by the parser to let us know that a response is complete, and we can close after timeout
|
||||
public func responseComplete() {
|
||||
self.socketWriterQueue.async { [weak self] in
|
||||
self?.responseCompleted = true
|
||||
if self?.readerSource?.isCancelled ?? true {
|
||||
self?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by the parser to let us know that a response is complete and we should close the socket
|
||||
public func responseCompleteCloseWriter() {
|
||||
self.socketWriterQueue.async { [weak self] in
|
||||
self?.responseCompleted = true
|
||||
self?.close()
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts reading from the socket and feeding that data to the parser
|
||||
public func process() {
|
||||
try! socket?.setBlocking(mode: true)
|
||||
|
||||
let tempReaderSource = DispatchSource.makeReadSource(fileDescriptor: socket?.socketfd ?? -1,
|
||||
queue: socketReaderQueue)
|
||||
|
||||
tempReaderSource.setEventHandler { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
guard strongSelf.socket?.socketfd ?? -1 > 0 else {
|
||||
strongSelf.readerSource?.cancel()
|
||||
strongSelf.cleanup()
|
||||
return
|
||||
}
|
||||
guard !strongSelf.shouldShutdown else {
|
||||
strongSelf.readerSource?.cancel()
|
||||
strongSelf.cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
var length = 1 //initial value
|
||||
|
||||
do {
|
||||
if strongSelf.socket?.socketfd ?? -1 > 0 {
|
||||
var maxLength: Int = Int(strongSelf.readerSource?.data ?? 0)
|
||||
if (maxLength > strongSelf.maxReadLength) || (maxLength <= 0) {
|
||||
maxLength = strongSelf.maxReadLength
|
||||
}
|
||||
var readBuffer: UnsafeMutablePointer<Int8> = UnsafeMutablePointer<Int8>.allocate(capacity: maxLength)
|
||||
length = try strongSelf.socket?.socketRead(into: &readBuffer, maxLength:maxLength) ?? -1
|
||||
if length > 0 {
|
||||
self?.responseCompleted = false
|
||||
|
||||
let data = Data(bytes: readBuffer, count: length)
|
||||
let numberParsed = strongSelf.parser?.readStream(data:data) ?? 0
|
||||
|
||||
if numberParsed != data.count {
|
||||
print("Error: wrong number of bytes consumed by parser (\(numberParsed) instead of \(data.count)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("bad socket FD while reading")
|
||||
length = -1
|
||||
}
|
||||
} catch {
|
||||
print("ReaderSource Event Error: \(error)")
|
||||
self?.readerSource?.cancel()
|
||||
self?.errorOccurred = true
|
||||
self?.close()
|
||||
}
|
||||
if length == 0 {
|
||||
self?.readerSource?.cancel()
|
||||
}
|
||||
if length < 0 {
|
||||
self?.errorOccurred = true
|
||||
self?.readerSource?.cancel()
|
||||
self?.close()
|
||||
}
|
||||
}
|
||||
|
||||
tempReaderSource.setCancelHandler { [weak self] in
|
||||
self?.close() //close if we can
|
||||
}
|
||||
|
||||
self.readerSource = tempReaderSource
|
||||
self.readerSource?.resume()
|
||||
}
|
||||
|
||||
/// Called by the parser to give us data to send back out of the socket
|
||||
///
|
||||
/// - Parameter bytes: Data object to be queued to be written to the socket
|
||||
public func queueSocketWrite(_ bytes: Data, completion:@escaping (Result) -> Void) {
|
||||
self.socketWriterQueue.async { [weak self] in
|
||||
self?.write(bytes)
|
||||
completion(.ok)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write data to a socket. Should be called in an `async` block on the `socketWriterQueue`
|
||||
///
|
||||
/// - Parameter data: data to be written
|
||||
public func write(_ data: Data) {
|
||||
do {
|
||||
var written: Int = 0
|
||||
var offset = 0
|
||||
|
||||
while written < data.count && !errorOccurred {
|
||||
try data.withUnsafeBytes { (ptr: UnsafePointer<UInt8>) in
|
||||
let result = try socket?.socketWrite(from: ptr + offset, bufSize:
|
||||
data.count - offset) ?? -1
|
||||
if result < 0 {
|
||||
print("Received broken write socket indication")
|
||||
errorOccurred = true
|
||||
} else {
|
||||
written += result
|
||||
}
|
||||
}
|
||||
offset = data.count - written
|
||||
}
|
||||
if errorOccurred {
|
||||
close()
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
print("Received write socket error: \(error)")
|
||||
errorOccurred = true
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
-79
@@ -1,107 +1,124 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// BlueSocketSimpleServer.swift
|
||||
// SwiftServerHttp
|
||||
//
|
||||
// Created by Carl Brown on 5/2/17.
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
|
||||
import Socket
|
||||
|
||||
//import HeliumLogger
|
||||
|
||||
#if os(Linux)
|
||||
import Signals
|
||||
#endif
|
||||
|
||||
|
||||
// MARK: HTTPServer
|
||||
// MARK: Server
|
||||
|
||||
/// An HTTP server that listens for connections on a TCP socket and spawns Listeners to handle them.
|
||||
public class BlueSocketSimpleServer : CurrentConnectionCounting {
|
||||
|
||||
|
||||
/// Socket to listen on for connections
|
||||
private let serverSocket: Socket
|
||||
///:nodoc:
|
||||
public class PoCSocketSimpleServer: CurrentConnectionCounting {
|
||||
/// PoCSocket to listen on for connections
|
||||
private let serverSocket: PoCSocket = PoCSocket()
|
||||
|
||||
/// Collection of listeners of sockets. Used to kill connections on timeout or shutdown
|
||||
private var connectionListenerList = ConnectionListenerCollection()
|
||||
|
||||
|
||||
// Timer that cleans up idle sockets on expire
|
||||
private let pruneSocketTimer: DispatchSourceTimer
|
||||
|
||||
private let pruneSocketTimer: DispatchSourceTimer = DispatchSource.makeTimerSource(queue: DispatchQueue(label: "pruneSocketTimer"))
|
||||
|
||||
/// The port we're listening on. Used primarily to query a randomly assigned port during XCTests
|
||||
public var port: Int {
|
||||
return Int(serverSocket.listeningPort)
|
||||
}
|
||||
|
||||
|
||||
/// Tuning parameter to set the number of queues
|
||||
private var queueMax: Int
|
||||
|
||||
private var queueMax: Int = 4 //sensible default
|
||||
|
||||
/// Tuning parameter to set the number of sockets we can accept at one time
|
||||
private var acceptMax: Int
|
||||
|
||||
public init() {
|
||||
#if os(Linux)
|
||||
Signals.trap(signal: .pipe) {
|
||||
_ in
|
||||
print("Receiver closed socket, SIGPIPE ignored")
|
||||
private var acceptMax: Int = 8 //sensible default
|
||||
|
||||
///Used to stop `accept(2)`ing while shutdown in progress to avoid spurious logs
|
||||
private let _isShuttingDownLock = DispatchSemaphore(value: 1)
|
||||
private var _isShuttingDown: Bool = false
|
||||
var isShuttingDown: Bool {
|
||||
get {
|
||||
_isShuttingDownLock.wait()
|
||||
defer {
|
||||
_isShuttingDownLock.signal()
|
||||
}
|
||||
#endif
|
||||
|
||||
serverSocket = try! Socket.create()
|
||||
pruneSocketTimer = DispatchSource.makeTimerSource(queue: DispatchQueue(label: "pruneSocketTimer"))
|
||||
queueMax = 4 //sensible default
|
||||
acceptMax = 8 //sensible default
|
||||
return _isShuttingDown
|
||||
}
|
||||
set {
|
||||
_isShuttingDownLock.wait()
|
||||
defer {
|
||||
_isShuttingDownLock.signal()
|
||||
}
|
||||
_isShuttingDown = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Starts the server listening on a given port
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - port: TCP port. See listen(2)
|
||||
/// - webapp: Function that creates the HTTP Response from the HTTP Request
|
||||
/// - handler: Function that creates the HTTP Response from the HTTP Request
|
||||
/// - Throws: Error (usually a socket error) generated
|
||||
public func start(port: Int = 0, queueCount: Int = 0, acceptCount: Int = 0, webapp: @escaping WebApp) throws {
|
||||
public func start(port: Int = 0,
|
||||
queueCount: Int = 0,
|
||||
acceptCount: Int = 0,
|
||||
maxReadLength: Int = 1048576,
|
||||
keepAliveTimeout: Double = 5.0,
|
||||
handler: @escaping HTTPRequestHandler) throws {
|
||||
|
||||
// Don't let a signal generated by a broken socket kill the server
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
if queueCount > 0 {
|
||||
queueMax = queueCount
|
||||
}
|
||||
if acceptCount > 0 {
|
||||
acceptMax = acceptCount
|
||||
}
|
||||
try self.serverSocket.listen(on: port, maxBacklogSize: 100)
|
||||
|
||||
try self.serverSocket.bindAndListen(on: port)
|
||||
|
||||
pruneSocketTimer.setEventHandler { [weak self] in
|
||||
self?.connectionListenerList.prune()
|
||||
}
|
||||
pruneSocketTimer.scheduleRepeating(deadline: .now() + StreamingParser.keepAliveTimeout, interval: .seconds(Int(StreamingParser.keepAliveTimeout)))
|
||||
#if swift(>=4.0)
|
||||
pruneSocketTimer.schedule(deadline: .now() + keepAliveTimeout,
|
||||
repeating: .seconds(Int(keepAliveTimeout)))
|
||||
#else
|
||||
pruneSocketTimer.scheduleRepeating(deadline: .now() + keepAliveTimeout,
|
||||
interval: .seconds(Int(keepAliveTimeout)))
|
||||
#endif
|
||||
pruneSocketTimer.resume()
|
||||
|
||||
|
||||
var readQueues = [DispatchQueue]()
|
||||
var writeQueues = [DispatchQueue]()
|
||||
let acceptQueue = DispatchQueue(label: "Accept Queue", qos: .default, attributes: .concurrent)
|
||||
|
||||
|
||||
let acceptSemaphore = DispatchSemaphore.init(value: acceptMax)
|
||||
|
||||
for i in 0..<queueMax {
|
||||
readQueues.append(DispatchQueue(label: "Read Queue \(i)"))
|
||||
writeQueues.append(DispatchQueue(label: "Write Queue \(i)"))
|
||||
|
||||
for idx in 0..<queueMax {
|
||||
readQueues.append(DispatchQueue(label: "Read Queue \(idx)"))
|
||||
writeQueues.append(DispatchQueue(label: "Write Queue \(idx)"))
|
||||
}
|
||||
|
||||
print ("Started server on port \(self.serverSocket.listeningPort) with \(self.queueMax) Serial Queues of each type and \(self.acceptMax) accept sockets")
|
||||
|
||||
|
||||
print("Started server on port \(self.serverSocket.listeningPort) with \(self.queueMax) serial queues of each type and \(self.acceptMax) accept sockets")
|
||||
|
||||
var listenerCount = 0
|
||||
DispatchQueue.global().async {
|
||||
repeat {
|
||||
do {
|
||||
let clientSocket = try self.serverSocket.acceptClientConnection()
|
||||
let streamingParser = StreamingParser(webapp: webapp, connectionCounter: self)
|
||||
let acceptedClientSocket = try self.serverSocket.acceptClientConnection()
|
||||
guard let clientSocket = acceptedClientSocket else {
|
||||
if self.isShuttingDown {
|
||||
print("Received nil client socket - exiting accept loop")
|
||||
}
|
||||
break
|
||||
}
|
||||
let streamingParser = StreamingParser(handler: handler, connectionCounter: self, keepAliveTimeout: keepAliveTimeout)
|
||||
let readQueue = readQueues[listenerCount % self.queueMax]
|
||||
let writeQueue = writeQueues[listenerCount % self.queueMax]
|
||||
let listener = BlueSocketConnectionListener(socket:clientSocket, parser: streamingParser, readQueue:readQueue, writeQueue: writeQueue)
|
||||
let listener = PoCSocketConnectionListener(socket: clientSocket, parser: streamingParser, readQueue:readQueue, writeQueue: writeQueue, maxReadLength: maxReadLength)
|
||||
listenerCount += 1
|
||||
acceptSemaphore.wait()
|
||||
acceptQueue.async { [weak listener] in
|
||||
@@ -109,72 +126,71 @@ public class BlueSocketSimpleServer : CurrentConnectionCounting {
|
||||
acceptSemaphore.signal()
|
||||
}
|
||||
self.connectionListenerList.add(listener)
|
||||
|
||||
} catch let error {
|
||||
print("Error accepting client connection: \(error)")
|
||||
}
|
||||
} while self.serverSocket.isListening
|
||||
} while !self.isShuttingDown && self.serverSocket.isListening
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Stop the server and close the sockets
|
||||
public func stop() {
|
||||
isShuttingDown = true
|
||||
connectionListenerList.closeAll()
|
||||
serverSocket.close()
|
||||
serverSocket.shutdownAndClose()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Count the connections - can be used in XCTests
|
||||
public var connectionCount: Int {
|
||||
return connectionListenerList.count
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Collection of ConnectionListeners, wrapped with weak references, so the memory can be freed when the socket closes
|
||||
class ConnectionListenerCollection {
|
||||
|
||||
/// Weak wrapper class
|
||||
class WeakConnectionListener<T: AnyObject> {
|
||||
weak var value : T?
|
||||
weak var value: T?
|
||||
init (_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let lock = DispatchSemaphore(value: 1)
|
||||
|
||||
|
||||
/// Storage for weak connection listeners
|
||||
var storage = [WeakConnectionListener<BlueSocketConnectionListener>]()
|
||||
|
||||
|
||||
var storage = [WeakConnectionListener<PoCSocketConnectionListener>]()
|
||||
|
||||
/// Add a new connection to the collection
|
||||
///
|
||||
/// - Parameter listener: socket manager object
|
||||
func add(_ listener:BlueSocketConnectionListener) {
|
||||
func add(_ listener: PoCSocketConnectionListener) {
|
||||
lock.wait()
|
||||
storage.append(WeakConnectionListener(listener))
|
||||
lock.signal()
|
||||
}
|
||||
|
||||
|
||||
/// Used when shutting down the server to close all connections
|
||||
func closeAll() {
|
||||
lock.wait()
|
||||
storage.filter { nil != $0.value }.forEach { $0.value?.close() }
|
||||
lock.signal()
|
||||
}
|
||||
|
||||
|
||||
/// Close any idle sockets and remove any weak pointers to closed (and freed) sockets from the collection
|
||||
func prune() {
|
||||
lock.wait()
|
||||
storage.filter { nil != $0.value }.forEach { $0.value?.closeIfIdleSocket() }
|
||||
storage = storage.filter { nil != $0.value }.filter { $0.value?.isOpen ?? false}
|
||||
storage = storage.filter { nil != $0.value }.filter { $0.value?.isOpen ?? false }
|
||||
lock.signal()
|
||||
}
|
||||
|
||||
|
||||
/// Count of collections
|
||||
var count: Int {
|
||||
return storage.filter { nil != $0.value }.count
|
||||
lock.wait()
|
||||
let count = storage.filter { nil != $0.value }.count
|
||||
lock.signal()
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
//
|
||||
// BlueSocketConnectionListener.swift
|
||||
// SwiftServerHttp
|
||||
//
|
||||
// Created by Carl Brown on 5/2/17.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Socket
|
||||
|
||||
#if os(Linux)
|
||||
import Signals
|
||||
import Dispatch
|
||||
#endif
|
||||
|
||||
|
||||
/// The Interface between the StreamingParser class and IBM's BlueSocket wrapper around socket(2).
|
||||
/// You hopefully should be able to replace this with any network library/engine.
|
||||
public class BlueSocketConnectionListener: ParserConnecting {
|
||||
var socket: Socket?
|
||||
|
||||
///ivar for the thing that manages the CHTTP Parser
|
||||
var parser: StreamingParser?
|
||||
|
||||
///Save the socket file descriptor so we can loook at it for debugging purposes
|
||||
var socketFD: Int32
|
||||
|
||||
/// Queues for managing access to the socket without blocking the world
|
||||
weak var socketReaderQueue: DispatchQueue?
|
||||
weak var socketWriterQueue: DispatchQueue?
|
||||
|
||||
///Event handler for reading from the socket
|
||||
private var readerSource: DispatchSourceRead?
|
||||
|
||||
///Flag to track whether we're in the middle of a response or not (with lock)
|
||||
private let _responseCompletedLock = DispatchSemaphore(value: 1)
|
||||
private var _responseCompleted: Bool = false
|
||||
var responseCompleted: Bool {
|
||||
get {
|
||||
_responseCompletedLock.wait()
|
||||
defer {
|
||||
_responseCompletedLock.signal()
|
||||
}
|
||||
return _responseCompleted
|
||||
}
|
||||
set {
|
||||
_responseCompletedLock.wait()
|
||||
defer {
|
||||
_responseCompletedLock.signal()
|
||||
}
|
||||
_responseCompleted = newValue
|
||||
}
|
||||
}
|
||||
|
||||
///Flag to track whether we've received a socket error or not (with lock)
|
||||
private let _errorOccurredLock = DispatchSemaphore(value: 1)
|
||||
private var _errorOccurred: Bool = false
|
||||
var errorOccurred: Bool {
|
||||
get {
|
||||
_errorOccurredLock.wait()
|
||||
defer {
|
||||
_errorOccurredLock.signal()
|
||||
}
|
||||
return _errorOccurred
|
||||
}
|
||||
set {
|
||||
_errorOccurredLock.wait()
|
||||
defer {
|
||||
_errorOccurredLock.signal()
|
||||
}
|
||||
_errorOccurred = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// initializer
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - socket: Socket object from BlueSocket library wrapping a socket(2)
|
||||
/// - parser: Manager of the CHTTPParser library
|
||||
public init(socket: Socket, parser: StreamingParser, readQueue: DispatchQueue, writeQueue: DispatchQueue) {
|
||||
self.socket = socket
|
||||
socketFD = socket.socketfd
|
||||
socketReaderQueue = readQueue
|
||||
socketWriterQueue = writeQueue
|
||||
self.parser = parser
|
||||
parser.parserConnector = self
|
||||
}
|
||||
|
||||
|
||||
/// Check if socket is still open. Used to decide whether it should be closed/pruned after timeout
|
||||
public var isOpen: Bool {
|
||||
guard let socket = self.socket else {
|
||||
return false
|
||||
}
|
||||
return (socket.isActive || socket.isConnected)
|
||||
}
|
||||
|
||||
|
||||
/// Close the socket and free up memory unless we're in the middle of a request
|
||||
func close() {
|
||||
if !self.responseCompleted && !self.errorOccurred {
|
||||
return
|
||||
}
|
||||
if (self.socket?.socketfd ?? -1) > 0 {
|
||||
self.socket?.close()
|
||||
}
|
||||
|
||||
//In a perfect world, we wouldn't have to clean this all up explicitly,
|
||||
// but KDE/heaptrack informs us we're in far from a perfect world
|
||||
|
||||
if !(self.readerSource?.isCancelled ?? true) {
|
||||
self.readerSource?.cancel()
|
||||
}
|
||||
self.readerSource?.setEventHandler(handler: nil)
|
||||
self.readerSource?.setCancelHandler(handler: nil)
|
||||
|
||||
self.readerSource = nil
|
||||
self.socket = nil
|
||||
self.parser?.parserConnector = nil //allows for memory to be reclaimed
|
||||
self.parser = nil
|
||||
self.socketReaderQueue = nil
|
||||
self.socketWriterQueue = nil
|
||||
}
|
||||
|
||||
|
||||
/// Called by the parser to let us know that it's done with this socket
|
||||
public func closeWriter() {
|
||||
self.socketWriterQueue?.async { [weak self] in
|
||||
if (self?.readerSource?.isCancelled ?? true) {
|
||||
self?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the socket is idle, and if so, call close()
|
||||
func closeIfIdleSocket() {
|
||||
let now = Date().timeIntervalSinceReferenceDate
|
||||
if let keepAliveUntil = parser?.keepAliveUntil, now >= keepAliveUntil {
|
||||
print("Closing idle socket \(socketFD)")
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Called by the parser to let us know that a response has started being created
|
||||
public func responseBeginning() {
|
||||
self.socketWriterQueue?.async { [weak self] in
|
||||
self?.responseCompleted = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Called by the parser to let us know that a response is complete, and we can close after timeout
|
||||
public func responseComplete() {
|
||||
self.socketWriterQueue?.async { [weak self] in
|
||||
self?.responseCompleted = true
|
||||
if (self?.readerSource?.isCancelled ?? true) {
|
||||
self?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Starts reading from the socket and feeding that data to the parser
|
||||
public func process() {
|
||||
do {
|
||||
try! socket?.setBlocking(mode: true)
|
||||
|
||||
let tempReaderSource = DispatchSource.makeReadSource(fileDescriptor: socket?.socketfd ?? -1,
|
||||
queue: socketReaderQueue)
|
||||
|
||||
tempReaderSource.setEventHandler { [weak self] in
|
||||
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
guard strongSelf.socket?.socketfd ?? -1 > 0 else {
|
||||
self?.readerSource?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
var length = 1 //initial value
|
||||
do {
|
||||
repeat {
|
||||
if strongSelf.socket?.socketfd ?? -1 > 0 {
|
||||
let readBuffer:NSMutableData = NSMutableData()
|
||||
length = try strongSelf.socket?.read(into: readBuffer) ?? -1
|
||||
if length > 0 {
|
||||
self?.responseCompleted = false
|
||||
}
|
||||
let data = Data(bytes:readBuffer.bytes.assumingMemoryBound(to: Int8.self), count:readBuffer.length)
|
||||
|
||||
let numberParsed = strongSelf.parser?.readStream(data:data) ?? 0
|
||||
|
||||
if numberParsed != data.count {
|
||||
print("Error: wrong number of bytes consumed by parser (\(numberParsed) instead of \(data.count)")
|
||||
}
|
||||
} else {
|
||||
print("bad socket FD while reading")
|
||||
length = -1
|
||||
}
|
||||
|
||||
} while length > 0
|
||||
} catch {
|
||||
print("ReaderSource Event Error: \(error)")
|
||||
self?.readerSource?.cancel()
|
||||
self?.errorOccurred = true
|
||||
self?.close()
|
||||
}
|
||||
if (length == 0) {
|
||||
self?.readerSource?.cancel()
|
||||
}
|
||||
if (length < 0) {
|
||||
self?.errorOccurred = true
|
||||
self?.readerSource?.cancel()
|
||||
self?.close()
|
||||
}
|
||||
}
|
||||
|
||||
tempReaderSource.setCancelHandler { [ weak self] in
|
||||
self?.close() //close if we can
|
||||
}
|
||||
|
||||
self.readerSource = tempReaderSource
|
||||
self.readerSource?.resume()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Called by the parser to give us data to send back out of the socket
|
||||
///
|
||||
/// - Parameter bytes: Data object to be queued to be written to the socket
|
||||
public func queueSocketWrite(_ bytes: Data) {
|
||||
self.socketWriterQueue?.async { [ weak self ] in
|
||||
self?.write(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Write data to a socket. Should be called in an `async` block on the `socketWriterQueue`
|
||||
///
|
||||
/// - Parameter data: data to be written
|
||||
public func write(_ data:Data) {
|
||||
do {
|
||||
var written: Int = 0
|
||||
var offset = 0
|
||||
|
||||
while written < data.count && !errorOccurred {
|
||||
try data.withUnsafeBytes { (ptr: UnsafePointer<UInt8>) in
|
||||
let result = try socket?.write(from: ptr + offset, bufSize:
|
||||
data.count - offset) ?? -1
|
||||
if (result < 0) {
|
||||
print("Recived broken write socket indication")
|
||||
errorOccurred = true
|
||||
} else {
|
||||
written += result
|
||||
}
|
||||
}
|
||||
offset = data.count - written
|
||||
}
|
||||
if (errorOccurred) {
|
||||
close()
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
print("Recived write socket error: \(error)")
|
||||
errorOccurred = true
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
//
|
||||
// HTTPCommon.swift
|
||||
// SwiftServerHttp
|
||||
//
|
||||
// Created by Carl Brown on 4/24/17 based on
|
||||
// https://lists.swift.org/pipermail/swift-server-dev/Week-of-Mon-20170403/000422.html
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Version number of the HTTP Protocol
|
||||
public typealias HTTPVersion = (Int, Int)
|
||||
|
||||
/// Takes in a Request and an object to write to, and returns a function that handles reading the request body
|
||||
public typealias WebApp = (HTTPRequest, HTTPResponseWriter) -> HTTPBodyProcessing
|
||||
|
||||
/// Class protocol containing the WebApp func. Using a class protocol to allow weak references for ARC
|
||||
public protocol WebAppContaining: class {
|
||||
/// WebApp method
|
||||
func serve(req: HTTPRequest, res: HTTPResponseWriter ) -> HTTPBodyProcessing
|
||||
}
|
||||
|
||||
/// Headers structure.
|
||||
public struct HTTPHeaders {
|
||||
var storage: [String:[String]] /* lower cased keys */
|
||||
var original: [(String, String)] /* original casing */
|
||||
let description: String
|
||||
|
||||
public subscript(key: String) -> [String] {
|
||||
get {
|
||||
return storage[key.lowercased()] ?? []
|
||||
}
|
||||
mutating set {
|
||||
original = original.filter { $0.0 != key.lowercased() }
|
||||
storage[key.lowercased()]=nil
|
||||
for val in newValue {
|
||||
self.append(newHeader: (key, val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeIterator() -> IndexingIterator<Array<(String, String)>> {
|
||||
return original.makeIterator()
|
||||
}
|
||||
|
||||
public mutating func append(newHeader: (String, String)) {
|
||||
original.append(newHeader)
|
||||
let key = newHeader.0.lowercased()
|
||||
let val = newHeader.1
|
||||
|
||||
var existing = storage[key] ?? []
|
||||
existing.append(val)
|
||||
storage[key] = existing
|
||||
}
|
||||
|
||||
/// Create Header structure from an array of string pairs
|
||||
public init(_ headers: [(String, String)] = []) {
|
||||
original = headers
|
||||
description=""
|
||||
storage = [String:[String]]()
|
||||
makeIterator().forEach { (element: (String, String)) in
|
||||
let key = element.0.lowercased()
|
||||
let val = element.1
|
||||
|
||||
var existing = storage[key] ?? []
|
||||
existing.append(val)
|
||||
storage[key] = existing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Result<POSIXError, Void> {
|
||||
case success(())
|
||||
case failure(POSIXError)
|
||||
|
||||
// MARK: Constructors
|
||||
/// Constructs a success wrapping a `closure`.
|
||||
public init(completion: ()) {
|
||||
self = .success(completion)
|
||||
}
|
||||
|
||||
/// Constructs a failure wrapping an `POSIXError`.
|
||||
public init(error: POSIXError) {
|
||||
self = .failure(error)
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
//
|
||||
// HTTPRequest.swift
|
||||
// SwiftServerHttp
|
||||
//
|
||||
// Created by Carl Brown on 4/24/17 based on
|
||||
// https://lists.swift.org/pipermail/swift-server-dev/Week-of-Mon-20170403/000422.html
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dispatch
|
||||
|
||||
/// HTTP Request NOT INCLUDING THE BODY. This allows for streaming
|
||||
public struct HTTPRequest {
|
||||
public var method : HTTPMethod
|
||||
public var target : String /* e.g. "/foo/bar?buz=qux" */
|
||||
public var httpVersion : HTTPVersion
|
||||
public var headers : HTTPHeaders
|
||||
}
|
||||
|
||||
/// Method that takes a chunk of request body and is expected to write to the ResponseWriter
|
||||
public typealias HTTPBodyHandler = (HTTPBodyChunk, inout Bool) -> Void /* the Bool can be set to true when we don't want to process anything further */
|
||||
|
||||
/// Indicates whether the body is going to be processed or ignored
|
||||
public enum HTTPBodyProcessing {
|
||||
case discardBody /* if you're not interested in the body */
|
||||
case processBody(handler: HTTPBodyHandler)
|
||||
}
|
||||
|
||||
/// Part (or maybe all) of the incoming request body
|
||||
public enum HTTPBodyChunk {
|
||||
case chunk(data: DispatchData, finishedProcessing: () -> Void) /* a new bit of the HTTP request body has arrived, finishedProcessing() must be called when done with that chunk */
|
||||
case failed(error: /*HTTPParser*/ Error) /* error while streaming the HTTP request body, eg. connection closed */
|
||||
case trailer(key: String, value: String) /* trailer has arrived (this we actually haven't implemented yet) */
|
||||
case end /* body and trailers finished */
|
||||
}
|
||||
|
||||
/// HTTP Methods handled by http_parser.[ch] supports
|
||||
public enum HTTPMethod: String {
|
||||
// case custom(method: String)
|
||||
case UNKNOWN
|
||||
|
||||
/* everything that http_parser.[ch] supports */
|
||||
case DELETE
|
||||
case GET
|
||||
case HEAD
|
||||
case POST
|
||||
case PUT
|
||||
case CONNECT
|
||||
case OPTIONS
|
||||
case TRACE
|
||||
case COPY
|
||||
case LOCK
|
||||
case MKCOL
|
||||
case MOVE
|
||||
case PROPFIND
|
||||
case PROPPATCH
|
||||
case SEARCH
|
||||
case UNLOCK
|
||||
case BIND
|
||||
case REBIND
|
||||
case UNBIND
|
||||
case ACL
|
||||
case REPORT
|
||||
case MKACTIVITY
|
||||
case CHECKOUT
|
||||
case MERGE
|
||||
case MSEARCH
|
||||
case NOTIFY
|
||||
case SUBSCRIBE
|
||||
case UNSUBSCRIBE
|
||||
case PATCH
|
||||
case PURGE
|
||||
case MKCALENDAR
|
||||
case LINK
|
||||
case UNLINK
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
//
|
||||
// HTTPResponse.swift
|
||||
// SwiftServerHttp
|
||||
//
|
||||
// Created by Carl Brown on 4/24/17based on
|
||||
// https://lists.swift.org/pipermail/swift-server-dev/Week-of-Mon-20170403/000422.html
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dispatch
|
||||
|
||||
/// HTTP Response NOT INCLUDING THE BODY
|
||||
public struct HTTPResponse {
|
||||
public var httpVersion : HTTPVersion
|
||||
public var status: HTTPResponseStatus
|
||||
public var transferEncoding: HTTPTransferEncoding
|
||||
public var headers: HTTPHeaders
|
||||
|
||||
public init (httpVersion: HTTPVersion,
|
||||
status: HTTPResponseStatus,
|
||||
transferEncoding: HTTPTransferEncoding,
|
||||
headers: HTTPHeaders) {
|
||||
self.httpVersion = httpVersion
|
||||
self.status = status
|
||||
self.transferEncoding = transferEncoding
|
||||
self.headers = headers
|
||||
}
|
||||
}
|
||||
|
||||
/// Object that code writes the response and response body to.
|
||||
public protocol HTTPResponseWriter : class {
|
||||
func writeContinue(headers: HTTPHeaders?) /* to send an HTTP `100 Continue` */
|
||||
|
||||
func writeResponse(_ response: HTTPResponse)
|
||||
|
||||
func writeTrailer(key: String, value: String)
|
||||
|
||||
func writeBody(data: DispatchData, completion: @escaping (Result<POSIXError, ()>) -> Void)
|
||||
func writeBody(data: DispatchData) /* convenience */
|
||||
|
||||
func writeBody(data: Data, completion: @escaping (Result<POSIXError, ()>) -> Void)
|
||||
func writeBody(data: Data) /* convenience */
|
||||
|
||||
func done() /* convenience */
|
||||
func done(completion: @escaping (Result<POSIXError, ()>) -> Void)
|
||||
func abort()
|
||||
}
|
||||
|
||||
public enum HTTPTransferEncoding {
|
||||
case identity(contentLength: UInt)
|
||||
case chunked
|
||||
}
|
||||
|
||||
/// Response status (200 ok, 404 not found, etc)
|
||||
public enum HTTPResponseStatus: UInt16, RawRepresentable {
|
||||
/* The original spec used custom if you want to use a non-standard response code or
|
||||
have it available in a (UInt, String) pair from a higher-level web framework.
|
||||
|
||||
Can't do custom if we want rawRepresentable. TODO: Consider making these constants
|
||||
*/
|
||||
//case custom(code: UInt, reasonPhrase: String)
|
||||
|
||||
/* all the codes from http://www.iana.org/assignments/http-status-codes */
|
||||
case `continue` = 100
|
||||
case switchingProtocols = 101
|
||||
case processing = 102
|
||||
case ok = 200
|
||||
case created = 201
|
||||
case accepted = 202
|
||||
case nonAuthoritativeInformation = 203
|
||||
case noContent = 204
|
||||
case resetContent = 205
|
||||
case partialContent = 206
|
||||
case multiStatus = 207
|
||||
case alreadyReported = 208
|
||||
case imUsed = 226
|
||||
case multipleChoices = 300
|
||||
case movedPermanently = 301
|
||||
case found = 302
|
||||
case seeOther = 303
|
||||
case notModified = 304
|
||||
case useProxy = 305
|
||||
case temporaryRedirect = 307
|
||||
case permanentRedirect = 308
|
||||
case badRequest = 400
|
||||
case unauthorized = 401
|
||||
case paymentRequired = 402
|
||||
case forbidden = 403
|
||||
case notFound = 404
|
||||
case methodNotAllowed = 405
|
||||
case notAcceptable = 406
|
||||
case proxyAuthenticationRequired = 407
|
||||
case requestTimeout = 408
|
||||
case conflict = 409
|
||||
case gone = 410
|
||||
case lengthRequired = 411
|
||||
case preconditionFailed = 412
|
||||
case payloadTooLarge = 413
|
||||
case uriTooLong = 414
|
||||
case unsupportedMediaType = 415
|
||||
case rangeNotSatisfiable = 416
|
||||
case expectationFailed = 417
|
||||
case misdirectedRequest = 421
|
||||
case unprocessableEntity = 422
|
||||
case locked = 423
|
||||
case failedDependency = 424
|
||||
case upgradeRequired = 426
|
||||
case preconditionRequired = 428
|
||||
case tooManyRequests = 429
|
||||
case requestHeaderFieldsTooLarge = 431
|
||||
case unavailableForLegalReasons = 451
|
||||
case internalServerError = 500
|
||||
case notImplemented = 501
|
||||
case badGateway = 502
|
||||
case serviceUnavailable = 503
|
||||
case gatewayTimeout = 504
|
||||
case httpVersionNotSupported = 505
|
||||
case variantAlsoNegotiates = 506
|
||||
case insufficientStorage = 507
|
||||
case loopDetected = 508
|
||||
case notExtended = 510
|
||||
case networkAuthenticationRequired = 511
|
||||
}
|
||||
|
||||
extension HTTPResponseStatus {
|
||||
public var reasonPhrase: String {
|
||||
switch(self) {
|
||||
// Can't do custom if we want rawRepresentable. TODO: Consider making these constants
|
||||
// case .custom(_, let reasonPhrase):
|
||||
// return reasonPhrase
|
||||
case .`continue`:
|
||||
return "CONTINUE"
|
||||
default:
|
||||
return String(describing: self)
|
||||
}
|
||||
}
|
||||
|
||||
public var code: UInt16 {
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
public static func from(code: UInt16) -> HTTPResponseStatus? {
|
||||
return HTTPResponseStatus(rawValue: code)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// SimpleResponseCreator.swift
|
||||
// SwiftServerHttp
|
||||
//
|
||||
// Created by Carl Brown on 5/1/17.
|
||||
//
|
||||
//
|
||||
|
||||
/*
|
||||
|
||||
This file isn't part of the API per se, but it's the easiest way to get started- just supply a completion block.
|
||||
It's also really handy for building up `WebApp`s to use when writing tests.
|
||||
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Simple block-based wrapper to create a `WebApp`. Normally used during XCTests
|
||||
public class SimpleResponseCreator: WebAppContaining {
|
||||
|
||||
typealias SimpleHandlerBlock = (_ req: HTTPRequest, _ body: Data) -> (reponse: HTTPResponse, responseBody: Data)
|
||||
let completionHandler: SimpleHandlerBlock
|
||||
|
||||
public init(completionHandler:@escaping (_ req: HTTPRequest, _ body: Data) -> (reponse: HTTPResponse, responseBody: Data)) {
|
||||
self.completionHandler = completionHandler
|
||||
}
|
||||
|
||||
var buffer = Data()
|
||||
|
||||
public func serve(req: HTTPRequest, res: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
return .processBody { (chunk, stop) in
|
||||
switch chunk {
|
||||
case .chunk(let data, let finishedProcessing):
|
||||
if (data.count > 0) {
|
||||
self.buffer.append(Data(data))
|
||||
}
|
||||
finishedProcessing()
|
||||
case .end:
|
||||
let (response, body) = self.completionHandler(req, self.buffer)
|
||||
res.writeResponse(HTTPResponse(httpVersion: response.httpVersion,
|
||||
status: response.status,
|
||||
transferEncoding: .chunked,
|
||||
headers: response.headers))
|
||||
res.writeBody(data: body) { _ in
|
||||
res.done()
|
||||
}
|
||||
default:
|
||||
stop = true /* don't call us anymore */
|
||||
res.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,486 +0,0 @@
|
||||
//
|
||||
// StreamingParser.swift
|
||||
// SwiftServerHttp
|
||||
//
|
||||
// Created by Carl Brown on 5/4/17.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dispatch
|
||||
|
||||
import CHttpParser
|
||||
|
||||
|
||||
/// Class that wraps the CHTTPParser and calls the `WebApp` to get the response
|
||||
public class StreamingParser: HTTPResponseWriter {
|
||||
|
||||
let webapp : WebApp
|
||||
|
||||
/// Time to leave socket open waiting for next request to start
|
||||
public static let keepAliveTimeout: TimeInterval = 5
|
||||
|
||||
/// Flag to track if the client wants to send multiple requests on the same TCP connection
|
||||
var clientRequestedKeepAlive = false
|
||||
|
||||
|
||||
/// Tracks when socket should be closed. Needs to have a lock, since it's updated often
|
||||
private let _keepAliveUntilLock = DispatchSemaphore(value: 1)
|
||||
private var _keepAliveUntil: TimeInterval?
|
||||
var keepAliveUntil: TimeInterval? {
|
||||
get {
|
||||
_keepAliveUntilLock.wait()
|
||||
defer {
|
||||
_keepAliveUntilLock.signal()
|
||||
}
|
||||
return _keepAliveUntil
|
||||
}
|
||||
set {
|
||||
_keepAliveUntilLock.wait()
|
||||
defer {
|
||||
_keepAliveUntilLock.signal()
|
||||
}
|
||||
_keepAliveUntil = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Theoretical limit of how many open requests we can have. Used in Keep-Alive Header
|
||||
let maxRequests = 100
|
||||
|
||||
/// Optional delegate that can tell us how many connections are in-flight so we can set the Keep-Alive header
|
||||
/// to the correct number of available connections. If not present, the client will not be limited in number of
|
||||
/// connections that can be made simultaneously
|
||||
public weak var connectionCounter: CurrentConnectionCounting?
|
||||
|
||||
/// Holds the bytes that come from the CHTTPParser until we have enough of them to do something with it
|
||||
var parserBuffer: Data?
|
||||
|
||||
///HTTP Parser
|
||||
var httpParser = http_parser()
|
||||
var httpParserSettings = http_parser_settings()
|
||||
|
||||
/// Block that takes a chunk from the HTTPParser as input and writes to a Response as a result
|
||||
var httpBodyProcessingCallback: HTTPBodyProcessing?
|
||||
|
||||
//Note: we want this to be strong so it holds onto the connector until it's explicitly cleared
|
||||
/// Protocol that we use to send data (and status info) back to the Network layer
|
||||
public var parserConnector: ParserConnecting?
|
||||
|
||||
var lastCallBack = CallbackRecord.idle
|
||||
var lastHeaderName: String?
|
||||
var parsedHeaders = HTTPHeaders()
|
||||
var parsedHTTPMethod: HTTPMethod?
|
||||
var parsedHTTPVersion: HTTPVersion?
|
||||
var parsedURL: String?
|
||||
|
||||
/// Is the currently parsed request an upgrade request?
|
||||
public private(set) var upgradeRequested = false
|
||||
|
||||
/// Class that wraps the CHTTPParser and calls the `WebApp` to get the response
|
||||
///
|
||||
/// - Parameter webapp: function that is used to create the response
|
||||
public init(webapp: @escaping WebApp, connectionCounter: CurrentConnectionCounting? = nil) {
|
||||
self.webapp = webapp
|
||||
self.connectionCounter = connectionCounter
|
||||
|
||||
//Set up all the callbacks for the CHTTPParser library
|
||||
httpParserSettings.on_message_begin = {
|
||||
parser -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return Int32(0)
|
||||
}
|
||||
return listener.messageBegan()
|
||||
}
|
||||
|
||||
httpParserSettings.on_message_complete = {
|
||||
parser -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return Int32(0)
|
||||
}
|
||||
return listener.messageCompleted()
|
||||
}
|
||||
|
||||
httpParserSettings.on_headers_complete = {
|
||||
parser -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return Int32(0)
|
||||
}
|
||||
return listener.headersCompleted()
|
||||
}
|
||||
|
||||
httpParserSettings.on_header_field = {
|
||||
(parser, chunk, length) -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return Int32(0)
|
||||
}
|
||||
return listener.headerFieldReceived(data: chunk, length: length)
|
||||
}
|
||||
|
||||
httpParserSettings.on_header_value = {
|
||||
(parser, chunk, length) -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return Int32(0)
|
||||
}
|
||||
return listener.headerValueReceived(data: chunk, length: length)
|
||||
}
|
||||
|
||||
httpParserSettings.on_body = {
|
||||
(parser, chunk, length) -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return Int32(0)
|
||||
}
|
||||
return listener.bodyReceived(data: chunk, length: length)
|
||||
}
|
||||
|
||||
httpParserSettings.on_url = {
|
||||
(parser, chunk, length) -> Int32 in
|
||||
guard let listener = StreamingParser.getSelf(parser: parser) else {
|
||||
return Int32(0)
|
||||
}
|
||||
return listener.urlReceived(data: chunk, length: length)
|
||||
}
|
||||
http_parser_init(&httpParser, HTTP_REQUEST)
|
||||
|
||||
self.httpParser.data = Unmanaged.passUnretained(self).toOpaque()
|
||||
|
||||
}
|
||||
|
||||
/// Read a stream from the network, pass it to the parser and return number of bytes consumed
|
||||
///
|
||||
/// - Parameter data: data coming from network
|
||||
/// - Returns: number of bytes that we sent to the parser
|
||||
public func readStream(data:Data) -> Int {
|
||||
return data.withUnsafeBytes { (ptr) -> Int in
|
||||
return http_parser_execute(&self.httpParser, &self.httpParserSettings, ptr, data.count)
|
||||
}
|
||||
}
|
||||
|
||||
/// States to track where we are in parsing the HTTP Stream from the client
|
||||
enum CallbackRecord {
|
||||
case idle, messageBegan, messageCompleted, headersCompleted, headerFieldReceived, headerValueReceived, bodyReceived, urlReceived
|
||||
}
|
||||
|
||||
/// Process change of state as we get more and more parser callbacks
|
||||
///
|
||||
/// - Parameter currentCallBack: state we are entering, as specified by the CHTTPParser
|
||||
/// - Returns: Whether or not the state actually changed
|
||||
@discardableResult
|
||||
func processCurrentCallback(_ currentCallBack:CallbackRecord) -> Bool {
|
||||
if lastCallBack == currentCallBack {
|
||||
return false
|
||||
}
|
||||
switch lastCallBack {
|
||||
case .headerFieldReceived:
|
||||
if let parserBuffer = self.parserBuffer {
|
||||
self.lastHeaderName = String(data: parserBuffer, encoding: .utf8)
|
||||
self.parserBuffer=nil
|
||||
} else {
|
||||
print("Missing parserBuffer after \(lastCallBack)")
|
||||
}
|
||||
case .headerValueReceived:
|
||||
if let parserBuffer = self.parserBuffer, let lastHeaderName = self.lastHeaderName, let headerValue = String(data:parserBuffer, encoding: .utf8) {
|
||||
self.parsedHeaders.append(newHeader: (lastHeaderName, headerValue))
|
||||
self.lastHeaderName = nil
|
||||
self.parserBuffer=nil
|
||||
} else {
|
||||
print("Missing parserBuffer after \(lastCallBack)")
|
||||
}
|
||||
case .headersCompleted:
|
||||
let methodId = self.httpParser.method
|
||||
if let methodName = http_method_str(http_method(rawValue: methodId)) {
|
||||
self.parsedHTTPMethod = HTTPMethod(rawValue: String(validatingUTF8: methodName) ?? "GET")
|
||||
}
|
||||
self.parsedHTTPVersion = (Int(self.httpParser.http_major), Int(self.httpParser.http_minor))
|
||||
|
||||
self.parserBuffer=nil
|
||||
|
||||
if !upgradeRequested {
|
||||
self.httpBodyProcessingCallback = self.webapp(self.createRequest(), self)
|
||||
}
|
||||
case .urlReceived:
|
||||
if let parserBuffer = self.parserBuffer {
|
||||
//Under heaptrack, this may appear to leak via _CFGetTSDCreateIfNeeded,
|
||||
// apparently, that's because it triggers thread metadata to be created
|
||||
self.parsedURL = String(data:parserBuffer, encoding: .utf8)
|
||||
self.parserBuffer=nil
|
||||
} else {
|
||||
print("Missing parserBuffer after \(lastCallBack)")
|
||||
}
|
||||
case .idle:
|
||||
break
|
||||
case .messageBegan:
|
||||
break
|
||||
case .messageCompleted:
|
||||
break
|
||||
case .bodyReceived:
|
||||
break
|
||||
}
|
||||
lastCallBack = currentCallBack
|
||||
return true
|
||||
}
|
||||
|
||||
func messageBegan() -> Int32 {
|
||||
processCurrentCallback(.messageBegan)
|
||||
self.parserConnector?.responseBeginning()
|
||||
return 0
|
||||
}
|
||||
|
||||
func messageCompleted() -> Int32 {
|
||||
let didChangeState = processCurrentCallback(.messageCompleted)
|
||||
if let chunkHandler = self.httpBodyProcessingCallback, didChangeState {
|
||||
var stop=false
|
||||
switch chunkHandler {
|
||||
case .processBody(let handler):
|
||||
handler(.end, &stop)
|
||||
case .discardBody:
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func headersCompleted() -> Int32 {
|
||||
processCurrentCallback(.headersCompleted)
|
||||
//This needs to be set here and not messageCompleted if it's going to work here
|
||||
self.clientRequestedKeepAlive = (http_should_keep_alive(&httpParser) == 1)
|
||||
self.keepAliveUntil = Date(timeIntervalSinceNow: StreamingParser.keepAliveTimeout).timeIntervalSinceReferenceDate
|
||||
upgradeRequested = get_upgrade_value(&self.httpParser) == 1
|
||||
return 0
|
||||
}
|
||||
|
||||
func headerFieldReceived(data: UnsafePointer<Int8>?, length: Int) -> Int32 {
|
||||
processCurrentCallback(.headerFieldReceived)
|
||||
guard let data = data else { return 0 }
|
||||
data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in
|
||||
self.parserBuffer == nil ? self.parserBuffer = Data(bytes:data, count:length) : self.parserBuffer?.append(ptr, count:length)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func headerValueReceived(data: UnsafePointer<Int8>?, length: Int) -> Int32 {
|
||||
processCurrentCallback(.headerValueReceived)
|
||||
guard let data = data else { return 0 }
|
||||
data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in
|
||||
self.parserBuffer == nil ? self.parserBuffer = Data(bytes:data, count:length) : self.parserBuffer?.append(ptr, count:length)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func bodyReceived(data: UnsafePointer<Int8>?, length: Int) -> Int32 {
|
||||
processCurrentCallback(.bodyReceived)
|
||||
guard let data = data else { return 0 }
|
||||
data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in
|
||||
let buff = UnsafeBufferPointer<UInt8>(start: ptr, count: length)
|
||||
let chunk = DispatchData(bytes:buff)
|
||||
if let chunkHandler = self.httpBodyProcessingCallback {
|
||||
var stop=false
|
||||
var finished=false
|
||||
while !stop && !finished {
|
||||
switch chunkHandler {
|
||||
case .processBody(let handler):
|
||||
handler(.chunk(data: chunk, finishedProcessing: {
|
||||
finished=true
|
||||
}), &stop)
|
||||
case .discardBody:
|
||||
finished=true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func urlReceived(data: UnsafePointer<Int8>?, length: Int) -> Int32 {
|
||||
processCurrentCallback(.urlReceived)
|
||||
guard let data = data else { return 0 }
|
||||
data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in
|
||||
self.parserBuffer == nil ? self.parserBuffer = Data(bytes:data, count:length) : self.parserBuffer?.append(ptr, count:length)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
static func getSelf(parser: UnsafeMutablePointer<http_parser>?) -> StreamingParser? {
|
||||
guard let pointee = parser?.pointee.data else { return nil }
|
||||
return Unmanaged<StreamingParser>.fromOpaque(pointee).takeUnretainedValue()
|
||||
}
|
||||
|
||||
var headersWritten = false
|
||||
var isChunked = false
|
||||
|
||||
/// Create a `HTTPRequest` struct from the parsed information
|
||||
public func createRequest() -> HTTPRequest {
|
||||
return HTTPRequest(method: parsedHTTPMethod!, target: parsedURL!, httpVersion: parsedHTTPVersion!, headers: parsedHeaders)
|
||||
}
|
||||
|
||||
public func writeContinue(headers: HTTPHeaders?) /* to send an HTTP `100 Continue` */ {
|
||||
var status = "HTTP/1.1 \(HTTPResponseStatus.continue.code) \(HTTPResponseStatus.continue.reasonPhrase)\r\n"
|
||||
if let headers = headers {
|
||||
for (key, value) in headers.makeIterator() {
|
||||
status += "\(key): \(value)\r\n"
|
||||
}
|
||||
}
|
||||
status += "\r\n"
|
||||
|
||||
// TODO use requested encoding if specified
|
||||
if let data = status.data(using: .utf8) {
|
||||
self.parserConnector?.queueSocketWrite(data)
|
||||
} else {
|
||||
//TODO handle encoding error
|
||||
}
|
||||
}
|
||||
|
||||
public func writeResponse(_ response: HTTPResponse) {
|
||||
guard !headersWritten else {
|
||||
return
|
||||
}
|
||||
|
||||
var headers = "HTTP/1.1 \(response.status.code) \(response.status.reasonPhrase)\r\n"
|
||||
|
||||
switch(response.transferEncoding) {
|
||||
case .chunked:
|
||||
headers += "Transfer-Encoding: chunked\r\n"
|
||||
isChunked = true
|
||||
case .identity(let contentLength):
|
||||
headers += "Content-Length: \(contentLength)\r\n"
|
||||
}
|
||||
|
||||
for (key, value) in response.headers.makeIterator() {
|
||||
headers += "\(key): \(value)\r\n"
|
||||
}
|
||||
|
||||
let availableConnections = maxRequests - (self.connectionCounter?.connectionCount ?? 0)
|
||||
|
||||
if clientRequestedKeepAlive && (availableConnections > 0) {
|
||||
headers.append("Connection: Keep-Alive\r\n")
|
||||
headers.append("Keep-Alive: timeout=\(Int(StreamingParser.keepAliveTimeout)), max=\(availableConnections)\r\n")
|
||||
}
|
||||
else {
|
||||
headers.append("Connection: Close\r\n")
|
||||
}
|
||||
headers.append("\r\n")
|
||||
|
||||
// TODO use requested encoding if specified
|
||||
if let data = headers.data(using: .utf8) {
|
||||
self.parserConnector?.queueSocketWrite(data)
|
||||
headersWritten = true
|
||||
} else {
|
||||
//TODO handle encoding error
|
||||
}
|
||||
}
|
||||
|
||||
public func writeTrailer(key: String, value: String) {
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
public func writeBody(data: DispatchData, completion: @escaping (Result<POSIXError, ()>) -> Void) {
|
||||
writeBody(data: Data(data), completion: completion)
|
||||
}
|
||||
|
||||
|
||||
public func writeBody(data: DispatchData) /* convenience */ {
|
||||
writeBody(data: data) { _ in
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public func writeBody(data: Data, completion: @escaping (Result<POSIXError, ()>) -> Void) {
|
||||
guard headersWritten else {
|
||||
//TODO error or default headers?
|
||||
return
|
||||
}
|
||||
|
||||
guard data.count > 0 else {
|
||||
// TODO fix Result
|
||||
completion(Result(completion: ()))
|
||||
return
|
||||
}
|
||||
|
||||
var dataToWrite: Data!
|
||||
if isChunked {
|
||||
let chunkStart = (String(data.count, radix: 16) + "\r\n").data(using: .utf8)!
|
||||
dataToWrite = Data(chunkStart)
|
||||
dataToWrite.append(data)
|
||||
let chunkEnd = "\r\n".data(using: .utf8)!
|
||||
dataToWrite.append(chunkEnd)
|
||||
} else {
|
||||
dataToWrite = data
|
||||
}
|
||||
|
||||
self.parserConnector?.queueSocketWrite(dataToWrite)
|
||||
|
||||
completion(Result(completion: ()))
|
||||
}
|
||||
|
||||
public func writeBody(data: Data) /* convenience */ {
|
||||
writeBody(data: data) { _ in
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public func done(completion: @escaping (Result<POSIXError, ()>) -> Void) {
|
||||
if isChunked {
|
||||
let chunkTerminate = "0\r\n\r\n".data(using: .utf8)!
|
||||
self.parserConnector?.queueSocketWrite(chunkTerminate)
|
||||
}
|
||||
|
||||
self.parsedHTTPMethod = nil
|
||||
self.parsedURL=nil
|
||||
self.parsedHeaders = HTTPHeaders()
|
||||
self.lastHeaderName = nil
|
||||
self.parserBuffer = nil
|
||||
self.parsedHTTPMethod = nil
|
||||
self.parsedHTTPVersion = nil
|
||||
self.lastCallBack = .idle
|
||||
self.headersWritten = false
|
||||
self.httpBodyProcessingCallback = nil
|
||||
self.upgradeRequested = false
|
||||
|
||||
let closeAfter = {
|
||||
if self.clientRequestedKeepAlive {
|
||||
self.keepAliveUntil = Date(timeIntervalSinceNow:StreamingParser.keepAliveTimeout).timeIntervalSinceReferenceDate
|
||||
self.parserConnector?.responseComplete()
|
||||
} else {
|
||||
self.parserConnector?.closeWriter()
|
||||
}
|
||||
}
|
||||
|
||||
completion(Result(completion: closeAfter()))
|
||||
}
|
||||
|
||||
public func done() /* convenience */ {
|
||||
done() { _ in
|
||||
}
|
||||
}
|
||||
|
||||
public func abort() {
|
||||
fatalError("abort called, not sure what to do with it")
|
||||
}
|
||||
|
||||
deinit {
|
||||
httpParser.data = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Protocol implemented by the thing that sits in between us and the network layer
|
||||
public protocol ParserConnecting: class {
|
||||
|
||||
/// Send data to the network do be written to the client
|
||||
func queueSocketWrite(_ from: Data) -> Void
|
||||
|
||||
/// Let the network know that a response has started to avoid closing a connection during a slow write
|
||||
func responseBeginning() -> Void
|
||||
|
||||
/// Let the network know that a response is complete, so it can be closed after timeout
|
||||
func responseComplete() -> Void
|
||||
|
||||
/// Used to let the network know we're ready to close the connection
|
||||
func closeWriter() -> Void
|
||||
}
|
||||
|
||||
/// Delegate that can tell us how many connections are in-flight so we can set the Keep-Alive header
|
||||
/// to the correct number of available connections
|
||||
public protocol CurrentConnectionCounting: class {
|
||||
/// Current number of active connections
|
||||
var connectionCount: Int { get }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import HTTP
|
||||
|
||||
class HeadersTests: XCTestCase {
|
||||
func testHeaders() {
|
||||
var headers: HTTPHeaders = [
|
||||
.accept: "text/html",
|
||||
"Accept": "application/xhtml+xml",
|
||||
"Accept": "application/xml;q=0.9",
|
||||
"accept": "image/webp",
|
||||
.accept: "*/*;q=0.8",
|
||||
"Accept-Language": "ru-RU,ru;q=0.8",
|
||||
.acceptLanguage: "en-US;q=0.6",
|
||||
"accept-language": "en;q=0.4",
|
||||
"Content-Length": "200",
|
||||
"Set-Cookie": "test1=0; expires=Tue, 21 Jun 2016 16:26:50 GMT; path=/; domain=.my.mail.ru",
|
||||
"Set-Cookie": "test2=0; expires=Tue, 21 Jun 2016 16:26:50 GMT; path=/; domain=.my.mail.ru",
|
||||
]
|
||||
|
||||
XCTAssertEqual(headers["Content-Length"], "200")
|
||||
XCTAssertEqual(headers["content-length"], "200")
|
||||
XCTAssertEqual(headers[.contentLength], "200")
|
||||
XCTAssertEqual(headers[.accept], "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
|
||||
XCTAssertEqual(headers[.acceptLanguage], "ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4")
|
||||
XCTAssertEqual(headers[.setCookie], "test1=0; expires=Tue, 21 Jun 2016 16:26:50 GMT; path=/; domain=.my.mail.ru")
|
||||
|
||||
XCTAssertEqual(headers[valuesFor: "Content-Length"], ["200"])
|
||||
XCTAssertEqual(headers[valuesFor: "content-length"], ["200"])
|
||||
XCTAssertEqual(headers[valuesFor: .contentLength], ["200"])
|
||||
XCTAssertEqual(headers[valuesFor: .accept], ["text/html", "application/xhtml+xml", "application/xml;q=0.9", "image/webp", "*/*;q=0.8"])
|
||||
XCTAssertEqual(headers[valuesFor: .setCookie], [
|
||||
"test1=0; expires=Tue, 21 Jun 2016 16:26:50 GMT; path=/; domain=.my.mail.ru",
|
||||
"test2=0; expires=Tue, 21 Jun 2016 16:26:50 GMT; path=/; domain=.my.mail.ru",
|
||||
])
|
||||
|
||||
headers = HTTPHeaders()
|
||||
let initialCount = headers.makeIterator().reduce(0) { (last, _) -> Int in return last + 1 }
|
||||
XCTAssertEqual(0, initialCount)
|
||||
|
||||
headers.append(["Test-Header": "Test Value"])
|
||||
let nextCount = headers.makeIterator().reduce(0) { (last, _) -> Int in return last + 1 }
|
||||
XCTAssertEqual(1, nextCount)
|
||||
|
||||
let testHeaderValueArray = headers[valuesFor: "test-header"]
|
||||
XCTAssertNotNil(testHeaderValueArray)
|
||||
XCTAssertEqual(1, testHeaderValueArray.count)
|
||||
XCTAssertEqual("Test Value", testHeaderValueArray.first ?? "Not Found")
|
||||
|
||||
headers.append(["Test-header": "Test Value 2"])
|
||||
let testHeaderValueArray2 = headers[valuesFor: "test-header"]
|
||||
XCTAssertNotNil(testHeaderValueArray2)
|
||||
XCTAssertEqual(2, testHeaderValueArray2.count)
|
||||
XCTAssertEqual("Test Value", testHeaderValueArray2.first ?? "Not Found")
|
||||
let testHeaderValueArray2Remainder = testHeaderValueArray2.dropFirst()
|
||||
XCTAssertEqual("Test Value 2", testHeaderValueArray2Remainder.first ?? "Not Found")
|
||||
|
||||
//This should overwrites, since the subscript is documented to use lowercase keys
|
||||
headers[valuesFor: "TEST-HEADER"]=["Test Value 3"]
|
||||
let testHeaderValueArray3 = headers[valuesFor: "test-header"]
|
||||
XCTAssertNotNil(testHeaderValueArray3)
|
||||
XCTAssertEqual(1, testHeaderValueArray3.count)
|
||||
|
||||
//Overwrite
|
||||
headers[valuesFor: "TEST-HEADER"]=["Test Value 4a", "Test Value 4b"]
|
||||
let testHeaderValueArray4 = headers[valuesFor: "test-header"]
|
||||
XCTAssertNotNil(testHeaderValueArray4)
|
||||
XCTAssertEqual(2, testHeaderValueArray4.count)
|
||||
XCTAssertEqual("Test Value 4a", testHeaderValueArray4.first ?? "Not Found")
|
||||
let testHeaderValueArray4Remainder = testHeaderValueArray4.dropFirst()
|
||||
XCTAssertEqual("Test Value 4b", testHeaderValueArray4Remainder.first ?? "Not Found")
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("testHeaders", testHeaders),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HTTP
|
||||
|
||||
/// Simple `HTTPRequestHandler` that prints "Hello, World" as per K&R
|
||||
class AbortAndSendHelloHandler: HTTPRequestHandling {
|
||||
|
||||
var chunkCalledCount=0
|
||||
var chunkLength=0
|
||||
|
||||
func handle(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
//Assume the router gave us the right request - at least for now
|
||||
response.writeHeader(status: .ok, headers: [.transferEncoding: "chunked", "X-foo": "bar"])
|
||||
return .processBody { (chunk, stop) in
|
||||
switch chunk {
|
||||
case .chunk(let data, let finishedProcessing):
|
||||
stop = true
|
||||
self.chunkCalledCount += 1
|
||||
self.chunkLength += data.count
|
||||
finishedProcessing()
|
||||
case .end:
|
||||
response.writeBody("Hello, World!")
|
||||
response.done()
|
||||
default:
|
||||
stop = true /* don't call us anymore */
|
||||
response.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HTTP
|
||||
|
||||
/// Simple `HTTPRequestHandler` that just echoes back whatever input it gets
|
||||
class EchoHandler: HTTPRequestHandling {
|
||||
func handle(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
//Assume the router gave us the right request - at least for now
|
||||
response.writeHeader(status: .ok, headers: ["Transfer-Encoding": "chunked", "X-foo": "bar"])
|
||||
return .processBody { (chunk, stop) in
|
||||
switch chunk {
|
||||
case .chunk(let data, let finishedProcessing):
|
||||
response.writeBody(data) { _ in
|
||||
finishedProcessing()
|
||||
}
|
||||
case .end:
|
||||
response.done()
|
||||
default:
|
||||
stop = true /* don't call us anymore */
|
||||
response.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HTTP
|
||||
|
||||
/// Simple `HTTPRequestHandler` that prints "Hello, World" as per K&R
|
||||
class HelloWorldHandler: HTTPRequestHandling {
|
||||
func handle(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
//Assume the router gave us the right request - at least for now
|
||||
response.writeHeader(status: .ok, headers: [.transferEncoding: "chunked", "X-foo": "bar"])
|
||||
return .processBody { (chunk, stop) in
|
||||
switch chunk {
|
||||
case .chunk(_, let finishedProcessing):
|
||||
finishedProcessing()
|
||||
case .end:
|
||||
response.writeBody("Hello, World!")
|
||||
response.done()
|
||||
default:
|
||||
stop = true /* don't call us anymore */
|
||||
response.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HTTP
|
||||
|
||||
/// `HelloWorldRequestHandler` that sets the keep alive header for XCTest purposes
|
||||
class HelloWorldKeepAliveHandler: HTTPRequestHandling {
|
||||
func handle(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
//Assume the router gave us the right request - at least for now
|
||||
response.writeHeader(status: .ok, headers: [
|
||||
"Transfer-Encoding": "chunked",
|
||||
"Connection": "Keep-Alive",
|
||||
"Keep-Alive": "timeout=5, max=10",
|
||||
])
|
||||
return .processBody { (chunk, stop) in
|
||||
switch chunk {
|
||||
case .chunk(_, let finishedProcessing):
|
||||
finishedProcessing()
|
||||
case .end:
|
||||
response.writeBody("Hello, World!")
|
||||
response.done()
|
||||
default:
|
||||
stop = true /* don't call us anymore */
|
||||
response.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HTTP
|
||||
|
||||
/// Simple `HTTPRequestHandler` that returns 200: OK without a body
|
||||
class OkHandler: HTTPRequestHandling {
|
||||
func handle(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
//Assume the router gave us the right request - at least for now
|
||||
response.writeHeader(status: .ok, headers: ["Transfer-Encoding": "chunked", "X-foo": "bar"])
|
||||
return .discardBody
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
/*
|
||||
This file isn't part of the API per se, but it's the easiest way to get started - just supply a completion block.
|
||||
It's also really handy for building up `HTTPRequestHandler`s to use when writing tests.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import HTTP
|
||||
|
||||
/// Simple block-based wrapper to create a `HTTPRequestHandler`. Normally used during XCTests
|
||||
public class SimpleResponseCreator: HTTPRequestHandling {
|
||||
|
||||
public struct Response {
|
||||
public let status: HTTPResponseStatus
|
||||
public let headers: HTTPHeaders
|
||||
public let body: Data
|
||||
}
|
||||
|
||||
typealias SimpleHandlerBlock = (_ req: HTTPRequest, _ body: Data) -> Response
|
||||
let completionHandler: SimpleHandlerBlock
|
||||
|
||||
public init(completionHandler:@escaping (_ req: HTTPRequest, _ body: Data) -> Response) {
|
||||
self.completionHandler = completionHandler
|
||||
}
|
||||
|
||||
var buffer = Data()
|
||||
|
||||
public func handle(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
return .processBody { (chunk, stop) in
|
||||
switch chunk {
|
||||
case .chunk(let data, let finishedProcessing):
|
||||
if data.count > 0 {
|
||||
self.buffer.append(Data(data))
|
||||
}
|
||||
finishedProcessing()
|
||||
case .end:
|
||||
let responseResult = self.completionHandler(request, self.buffer)
|
||||
var headers = responseResult.headers
|
||||
headers.replace([.transferEncoding: "chunked"])
|
||||
response.writeHeader(status: responseResult.status, headers: headers)
|
||||
response.writeBody(responseResult.body) { _ in
|
||||
response.done()
|
||||
}
|
||||
default:
|
||||
stop = true /* don't call us anymore */
|
||||
response.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dispatch
|
||||
import HTTP
|
||||
|
||||
/// Acts as a fake/mock `HTTPServer` so we can write XCTests without having to worry about Sockets and such
|
||||
class TestResponseResolver: HTTPResponseWriter {
|
||||
let request: HTTPRequest
|
||||
let requestBody: DispatchData
|
||||
|
||||
var response: (status: HTTPResponseStatus, headers: HTTPHeaders)?
|
||||
var responseBody: HTTPResponseBody?
|
||||
|
||||
///Flag to track whether our handler has told us not to call it anymore
|
||||
private let _shouldStopProcessingBodyLock = DispatchSemaphore(value: 1)
|
||||
private var _shouldStopProcessingBody: Bool = false
|
||||
private var shouldStopProcessingBody: Bool {
|
||||
get {
|
||||
_shouldStopProcessingBodyLock.wait()
|
||||
defer {
|
||||
_shouldStopProcessingBodyLock.signal()
|
||||
}
|
||||
return _shouldStopProcessingBody
|
||||
}
|
||||
set {
|
||||
_shouldStopProcessingBodyLock.wait()
|
||||
defer {
|
||||
_shouldStopProcessingBodyLock.signal()
|
||||
}
|
||||
_shouldStopProcessingBody = newValue
|
||||
}
|
||||
}
|
||||
|
||||
init(request: HTTPRequest, requestBody: Data) {
|
||||
self.request = request
|
||||
self.requestBody = requestBody.withUnsafeBytes { (ptr: UnsafePointer<UInt8>) -> DispatchData in
|
||||
#if swift(>=4.0)
|
||||
return DispatchData(bytes: UnsafeRawBufferPointer(start: ptr, count: requestBody.count))
|
||||
#else
|
||||
return DispatchData(bytes: UnsafeBufferPointer<UInt8>(start: ptr, count: requestBody.count))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func resolveHandler(_ handler: HTTPRequestHandler) {
|
||||
let chunkHandler = handler(request, self)
|
||||
if shouldStopProcessingBody {
|
||||
return
|
||||
}
|
||||
switch chunkHandler {
|
||||
case .processBody(let handler):
|
||||
_shouldStopProcessingBodyLock.wait()
|
||||
handler(.chunk(data: self.requestBody, finishedProcessing: {self._shouldStopProcessingBodyLock.signal()}), &_shouldStopProcessingBody)
|
||||
var dummy = false
|
||||
handler(.end, &dummy)
|
||||
case .discardBody:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func writeHeader(status: HTTPResponseStatus, headers: HTTPHeaders, completion: @escaping (Result) -> Void) {
|
||||
self.response = (status: status, headers: headers)
|
||||
completion(.ok)
|
||||
}
|
||||
|
||||
func writeTrailer(_ trailers: HTTPHeaders, completion: @escaping (Result) -> Void) {
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
func writeBody(_ data: UnsafeHTTPResponseBody, completion: @escaping (Result) -> Void) {
|
||||
if let data = data as? HTTPResponseBody {
|
||||
self.responseBody = data
|
||||
} else {
|
||||
self.responseBody = data.withUnsafeBytes { Data($0) }
|
||||
}
|
||||
completion(.ok)
|
||||
}
|
||||
|
||||
func done(completion: @escaping (Result) -> Void) {
|
||||
completion(.ok)
|
||||
}
|
||||
func done() /* convenience */ {
|
||||
done { _ in
|
||||
}
|
||||
}
|
||||
|
||||
func abort() {
|
||||
fatalError("abort called, not sure what to do with it")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HTTP
|
||||
|
||||
/// Simple `HTTPRequestHandler` that prints "Hello, World" as per K&R
|
||||
class UnchunkedHelloWorldHandler: HTTPRequestHandling {
|
||||
func handle(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
//Assume the router gave us the right request - at least for now
|
||||
let responseString = "Hello, World!"
|
||||
response.writeHeader(status: .ok, headers: [.contentLength: "\(responseString.lengthOfBytes(using: .utf8))"])
|
||||
response.writeBody(responseString)
|
||||
response.done()
|
||||
return .discardBody
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import HTTP
|
||||
|
||||
class ResponseTests: XCTestCase {
|
||||
|
||||
func testOkay() {
|
||||
let okay = HTTPResponseStatus.ok
|
||||
XCTAssertEqual(200, okay.code)
|
||||
XCTAssertEqual("OK", okay.reasonPhrase)
|
||||
XCTAssertEqual("\(okay)", "200 OK")
|
||||
}
|
||||
|
||||
func testContinue() {
|
||||
XCTAssertEqual("Continue", HTTPResponseStatus.continue.reasonPhrase)
|
||||
}
|
||||
|
||||
func testNotFound() {
|
||||
XCTAssertEqual(HTTPResponseStatus.notFound, HTTPResponseStatus(code: 404))
|
||||
let notFound: HTTPResponseStatus = 404
|
||||
XCTAssertEqual(notFound, HTTPResponseStatus.notFound)
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("testOkay", testOkay),
|
||||
("testContinue", testContinue),
|
||||
("testNotFound", testNotFound),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import Dispatch
|
||||
|
||||
@testable import HTTP
|
||||
|
||||
class ServerTests: XCTestCase {
|
||||
func testResponseOK() {
|
||||
let request = HTTPRequest(method: .get, target: "/echo", httpVersion: HTTPVersion(major: 1, minor: 1), headers: ["X-foo": "bar"])
|
||||
let resolver = TestResponseResolver(request: request, requestBody: Data())
|
||||
resolver.resolveHandler(EchoHandler().handle)
|
||||
XCTAssertNotNil(resolver.response)
|
||||
XCTAssertNotNil(resolver.responseBody)
|
||||
XCTAssertEqual(HTTPResponseStatus.ok.code, resolver.response?.status.code ?? 0)
|
||||
}
|
||||
|
||||
func testEcho() {
|
||||
let testString="This is a test"
|
||||
let request = HTTPRequest(method: .post, target: "/echo", httpVersion: HTTPVersion(major: 1, minor: 1), headers: ["X-foo": "bar"])
|
||||
let resolver = TestResponseResolver(request: request, requestBody: testString.data(using: .utf8)!)
|
||||
resolver.resolveHandler(EchoHandler().handle)
|
||||
XCTAssertNotNil(resolver.response)
|
||||
XCTAssertNotNil(resolver.responseBody)
|
||||
XCTAssertEqual(HTTPResponseStatus.ok.code, resolver.response?.status.code ?? 0)
|
||||
XCTAssertEqual(testString, resolver.responseBody?.withUnsafeBytes { String(bytes: $0, encoding: .utf8) } ?? "Nil")
|
||||
}
|
||||
|
||||
func testHello() {
|
||||
let request = HTTPRequest(method: .get, target: "/helloworld", httpVersion: HTTPVersion(major: 1, minor: 1), headers: ["X-foo": "bar"])
|
||||
let resolver = TestResponseResolver(request: request, requestBody: Data())
|
||||
resolver.resolveHandler(HelloWorldHandler().handle)
|
||||
XCTAssertNotNil(resolver.response)
|
||||
XCTAssertNotNil(resolver.responseBody)
|
||||
XCTAssertEqual(HTTPResponseStatus.ok.code, resolver.response?.status.code ?? 0)
|
||||
XCTAssertEqual("Hello, World!", resolver.responseBody?.withUnsafeBytes { String(bytes: $0, encoding: .utf8) } ?? "Nil")
|
||||
}
|
||||
|
||||
func testSimpleHello() {
|
||||
let request = HTTPRequest(method: .get, target: "/helloworld", httpVersion: HTTPVersion(major: 1, minor: 1), headers: ["X-foo": "bar"])
|
||||
let resolver = TestResponseResolver(request: request, requestBody: Data())
|
||||
let simpleHelloWebApp = SimpleResponseCreator { (_, body) -> SimpleResponseCreator.Response in
|
||||
return SimpleResponseCreator.Response(
|
||||
status: .ok,
|
||||
headers: ["X-foo": "bar"],
|
||||
body: "Hello, World!".data(using: .utf8)!
|
||||
)
|
||||
}
|
||||
resolver.resolveHandler(simpleHelloWebApp.handle)
|
||||
XCTAssertNotNil(resolver.response)
|
||||
XCTAssertNotNil(resolver.responseBody)
|
||||
XCTAssertEqual(HTTPResponseStatus.ok.code, resolver.response?.status.code ?? 0)
|
||||
XCTAssertEqual("Hello, World!", resolver.responseBody?.withUnsafeBytes { String(bytes: $0, encoding: .utf8) } ?? "Nil")
|
||||
}
|
||||
|
||||
func testOkEndToEnd() {
|
||||
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
|
||||
|
||||
let server = HTTPServer()
|
||||
do {
|
||||
try server.start(port: 0, handler: OkHandler().handle)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
let dataTask = session.dataTask(with: url) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
receivedExpectation.fulfill()
|
||||
}
|
||||
dataTask.resume()
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
func testHelloEndToEnd() {
|
||||
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
|
||||
|
||||
let server = HTTPServer()
|
||||
do {
|
||||
try server.start(port: 0, handler: HelloWorldHandler().handle)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/helloworld")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
let dataTask = session.dataTask(with: url) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual("Hello, World!", String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
receivedExpectation.fulfill()
|
||||
}
|
||||
dataTask.resume()
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
func testSimpleHelloEndToEnd() {
|
||||
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
|
||||
let simpleHelloWebApp = SimpleResponseCreator { (_, body) -> SimpleResponseCreator.Response in
|
||||
return SimpleResponseCreator.Response(
|
||||
status: .ok,
|
||||
headers: ["X-foo": "bar"],
|
||||
body: "Hello, World!".data(using: .utf8)!
|
||||
)
|
||||
}
|
||||
|
||||
let server = HTTPServer()
|
||||
do {
|
||||
try server.start(port: 0, handler: simpleHelloWebApp.handle)
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/helloworld")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
let dataTask = session.dataTask(with: url) { (responseBody, rawResponse, error) in
|
||||
print("\(#function) dataTask returned")
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
let responseString = String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil"
|
||||
XCTAssertEqual("Hello, World!", responseString)
|
||||
print("\(#function) fulfilling expectation")
|
||||
receivedExpectation.fulfill()
|
||||
}
|
||||
dataTask.resume()
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
print("\(#function) stopping server")
|
||||
}
|
||||
|
||||
func testRequestEchoEndToEnd() {
|
||||
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
|
||||
let testString="This is a test"
|
||||
|
||||
let server = HTTPServer()
|
||||
do {
|
||||
try server.start(port: 0, handler: EchoHandler().handle)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/echo")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = testString.data(using: .utf8)
|
||||
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let dataTask = session.dataTask(with: request) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString, String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
receivedExpectation.fulfill()
|
||||
}
|
||||
dataTask.resume()
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
func testRequestKeepAliveEchoEndToEnd() {
|
||||
let receivedExpectation1 = self.expectation(description: "Received web response 1: \(#function)")
|
||||
let receivedExpectation2 = self.expectation(description: "Received web response 2: \(#function)")
|
||||
let receivedExpectation3 = self.expectation(description: "Received web response 3: \(#function)")
|
||||
let testString1="This is a test"
|
||||
let testString2="This is a test, too"
|
||||
let testString3="This is also a test"
|
||||
|
||||
let server = HTTPServer()
|
||||
do {
|
||||
try server.start(port: 0, handler: EchoHandler().handle)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/echo")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
var request1 = URLRequest(url: url)
|
||||
request1.httpMethod = "POST"
|
||||
request1.httpBody = testString1.data(using: .utf8)
|
||||
request1.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let dataTask1 = session.dataTask(with: request1) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
let headers = response?.allHeaderFields ?? ["": ""]
|
||||
let connectionHeader: String = headers["Connection"] as? String ?? ""
|
||||
XCTAssertEqual(connectionHeader, "Keep-Alive", "No Keep-Alive Connection")
|
||||
XCTAssertNotNil(responseBody, "No Response Body")
|
||||
XCTAssertEqual(server.connectionCount, 1)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString1, String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
var request2 = URLRequest(url: url)
|
||||
request2.httpMethod = "POST"
|
||||
request2.httpBody = testString2.data(using: .utf8)
|
||||
request2.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
let dataTask2 = session.dataTask(with: request2) { (responseBody2, rawResponse2, error2) in
|
||||
let response2 = rawResponse2 as? HTTPURLResponse
|
||||
XCTAssertNil(error2, "\(error2!.localizedDescription)")
|
||||
XCTAssertNotNil(response2)
|
||||
let headers = response2?.allHeaderFields ?? ["": ""]
|
||||
let connectionHeader: String = headers["Connection"] as? String ?? ""
|
||||
XCTAssertEqual(connectionHeader, "Keep-Alive", "No Keep-Alive Connection")
|
||||
XCTAssertEqual(server.connectionCount, 1)
|
||||
XCTAssertNotNil(responseBody2)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response2?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString2, String(data: responseBody2 ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
var request3 = URLRequest(url: url)
|
||||
request3.httpMethod = "POST"
|
||||
request3.httpBody = testString3.data(using: .utf8)
|
||||
request3.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
let dataTask3 = session.dataTask(with: request3) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
let headers = response?.allHeaderFields ?? ["": ""]
|
||||
let connectionHeader: String = headers["Connection"] as? String ?? ""
|
||||
XCTAssertEqual(connectionHeader, "Keep-Alive", "No Keep-Alive Connection")
|
||||
XCTAssertEqual(server.connectionCount, 1)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString3, String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
receivedExpectation3.fulfill()
|
||||
}
|
||||
dataTask3.resume()
|
||||
receivedExpectation2.fulfill()
|
||||
}
|
||||
dataTask2.resume()
|
||||
receivedExpectation1.fulfill()
|
||||
}
|
||||
dataTask1.resume()
|
||||
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
//server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
func testMultipleRequestWithoutKeepAliveEchoEndToEnd() {
|
||||
let receivedExpectation1 = self.expectation(description: "Received web response 1: \(#function)")
|
||||
let receivedExpectation2 = self.expectation(description: "Received web response 2: \(#function)")
|
||||
let receivedExpectation3 = self.expectation(description: "Received web response 3: \(#function)")
|
||||
let testString1="This is a test"
|
||||
let testString2="This is a test, too"
|
||||
let testString3="This is also a test"
|
||||
|
||||
let server = HTTPServer()
|
||||
do {
|
||||
try server.start(port: 0, handler: EchoHandler().handle)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url1 = URL(string: "http://localhost:\(server.port)/echo")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
var request1 = URLRequest(url: url1)
|
||||
request1.httpMethod = "POST"
|
||||
request1.httpBody = testString1.data(using: .utf8)
|
||||
request1.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let dataTask1 = session.dataTask(with: request1) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
let headers = response?.allHeaderFields ?? ["": ""]
|
||||
let connectionHeader: String = headers["Connection"] as? String ?? ""
|
||||
let keepAliveHeader = headers["Connection"]
|
||||
XCTAssertEqual(connectionHeader, "Keep-Alive", "No Keep-Alive Connection")
|
||||
XCTAssertNotNil(keepAliveHeader)
|
||||
XCTAssertNotNil(responseBody, "No Keep-Alive Header")
|
||||
XCTAssertEqual(server.connectionCount, 1)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString1, String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
let url2 = URL(string: "http://127.0.0.1:\(server.port)/echo")!
|
||||
var request2 = URLRequest(url: url2)
|
||||
request2.httpMethod = "POST"
|
||||
request2.httpBody = testString2.data(using: .utf8)
|
||||
request2.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
request2.setValue("close", forHTTPHeaderField: "Connection")
|
||||
let dataTask2 = session.dataTask(with: request2) { (responseBody2, rawResponse2, error2) in
|
||||
let response2 = rawResponse2 as? HTTPURLResponse
|
||||
XCTAssertNil(error2, "\(error2!.localizedDescription)")
|
||||
XCTAssertNotNil(response2)
|
||||
let headers = response2?.allHeaderFields ?? ["": ""]
|
||||
let connectionHeader: String = headers["Connection"] as? String ?? ""
|
||||
let keepAliveHeader = headers["Connection"]
|
||||
XCTAssertEqual(connectionHeader, "Keep-Alive", "No Keep-Alive Connection")
|
||||
XCTAssertNotNil(keepAliveHeader, "No Keep-Alive Header")
|
||||
XCTAssertEqual(server.connectionCount, 2)
|
||||
XCTAssertNotNil(responseBody2)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response2?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString2, String(data: responseBody2 ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
let url3 = URL(string: "http://0.0.0.0:\(server.port)/echo")!
|
||||
var request3 = URLRequest(url: url3)
|
||||
request3.httpMethod = "POST"
|
||||
request3.httpBody = testString3.data(using: .utf8)
|
||||
request3.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
request3.setValue("close", forHTTPHeaderField: "Connection")
|
||||
let dataTask3 = session.dataTask(with: request3) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
let headers = response?.allHeaderFields ?? ["": ""]
|
||||
let connectionHeader: String = headers["Connection"] as? String ?? ""
|
||||
let keepAliveHeader = headers["Connection"]
|
||||
XCTAssertEqual(connectionHeader, "Keep-Alive", "No Keep-Alive Connection")
|
||||
XCTAssertNotNil(keepAliveHeader, "No Keep-Alive Header")
|
||||
XCTAssertEqual(server.connectionCount, 3)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString3, String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
receivedExpectation3.fulfill()
|
||||
}
|
||||
dataTask3.resume()
|
||||
receivedExpectation2.fulfill()
|
||||
}
|
||||
dataTask2.resume()
|
||||
receivedExpectation1.fulfill()
|
||||
}
|
||||
dataTask1.resume()
|
||||
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
//server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testRequestLargeEchoEndToEnd() {
|
||||
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
|
||||
|
||||
//Use a small chunk size to make sure that we're testing multiple HTTPBodyHandler calls
|
||||
let chunkSize = 1024
|
||||
|
||||
// Get a file we know exists
|
||||
let executableURL = URL(fileURLWithPath: CommandLine.arguments[0])
|
||||
let testExecutableData: Data
|
||||
|
||||
do {
|
||||
testExecutableData = try Data(contentsOf: executableURL)
|
||||
} catch {
|
||||
XCTFail("Could not create Data from contents of \(executableURL)")
|
||||
return
|
||||
}
|
||||
|
||||
var testDataLong = testExecutableData + testExecutableData + testExecutableData + testExecutableData
|
||||
let length = testDataLong.count
|
||||
let keep = 16385
|
||||
let remove = length - keep
|
||||
if remove > 0 {
|
||||
testDataLong.removeLast(remove)
|
||||
}
|
||||
|
||||
let testData = Data(testDataLong)
|
||||
|
||||
let server = PoCSocketSimpleServer()
|
||||
do {
|
||||
try server.start(port: 0, maxReadLength: chunkSize, handler: EchoHandler().handle)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/echo")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = testData
|
||||
let dataTask = session.dataTask(with: request) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual(testData, responseBody ?? Data())
|
||||
receivedExpectation.fulfill()
|
||||
}
|
||||
dataTask.resume()
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
func testRequestLargePostHelloWorld() {
|
||||
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
|
||||
|
||||
//Use a small chunk size to make sure that we stop after one HTTPBodyHandler call
|
||||
let chunkSize = 1024
|
||||
|
||||
// Get a file we know exists
|
||||
let executableURL = URL(fileURLWithPath: CommandLine.arguments[0])
|
||||
let testExecutableData: Data
|
||||
|
||||
do {
|
||||
testExecutableData = try Data(contentsOf: executableURL)
|
||||
} catch {
|
||||
XCTFail("Could not create Data from contents of \(executableURL)")
|
||||
return
|
||||
}
|
||||
|
||||
//Make sure there's data there
|
||||
XCTAssertNotNil(testExecutableData)
|
||||
|
||||
let executableLength = testExecutableData.count
|
||||
|
||||
let server = PoCSocketSimpleServer()
|
||||
do {
|
||||
let testHandler = AbortAndSendHelloHandler()
|
||||
try server.start(port: 0, maxReadLength: chunkSize, handler: testHandler.handle)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/echo")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
let uploadTask = session.uploadTask(with: request, fromFile: executableURL) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual("Hello, World!", String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
XCTAssertEqual(Int(testHandler.chunkCalledCount), 1)
|
||||
XCTAssertLessThan(testHandler.chunkLength, executableLength, "Should have written less than the length of the file")
|
||||
XCTAssertLessThanOrEqual(Int(testHandler.chunkLength), chunkSize)
|
||||
receivedExpectation.fulfill()
|
||||
}
|
||||
uploadTask.resume()
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testExplicitCloseConnections() {
|
||||
let expectation = self.expectation(description: "0 Open Connection")
|
||||
let server = PoCSocketSimpleServer()
|
||||
let keepAliveTimeout = 0.1
|
||||
|
||||
do {
|
||||
try server.start(port: 0, keepAliveTimeout: keepAliveTimeout, handler: OkHandler().handle)
|
||||
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url1 = URL(string: "http://localhost:\(server.port)")!
|
||||
var request = URLRequest(url: url1)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("close", forHTTPHeaderField: "Connection")
|
||||
|
||||
let dataTask1 = session.dataTask(with: request) { (responseBody, rawResponse, error) in
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
#if os(Linux)
|
||||
XCTAssertEqual(server.connectionCount, 0)
|
||||
expectation.fulfill()
|
||||
|
||||
// Darwin's URLSession replaces the `Connection: close` header with `Connection: keep-alive`, so allow it to expire
|
||||
#else
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + keepAliveTimeout) {
|
||||
XCTAssertEqual(server.connectionCount, 0)
|
||||
expectation.fulfill()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
dataTask1.resume()
|
||||
|
||||
self.waitForExpectations(timeout: 30) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("testEcho", testEcho),
|
||||
("testHello", testHello),
|
||||
("testSimpleHello", testSimpleHello),
|
||||
("testResponseOK", testResponseOK),
|
||||
("testOkEndToEnd", testOkEndToEnd),
|
||||
("testHelloEndToEnd", testHelloEndToEnd),
|
||||
("testSimpleHelloEndToEnd", testSimpleHelloEndToEnd),
|
||||
("testRequestEchoEndToEnd", testRequestEchoEndToEnd),
|
||||
("testRequestKeepAliveEchoEndToEnd", testRequestKeepAliveEchoEndToEnd),
|
||||
("testRequestLargeEchoEndToEnd", testRequestLargeEchoEndToEnd),
|
||||
("testExplicitCloseConnections", testExplicitCloseConnections),
|
||||
("testRequestLargePostHelloWorld", testRequestLargePostHelloWorld),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import HTTP
|
||||
|
||||
class VersionTests: XCTestCase {
|
||||
let version10 = HTTPVersion(major: 1, minor: 0)
|
||||
let version11 = HTTPVersion(major: 1, minor: 1)
|
||||
let version20 = HTTPVersion(major: 2, minor: 0)
|
||||
|
||||
func testEquals() {
|
||||
XCTAssertEqual(version10, version10)
|
||||
XCTAssertEqual(version11, version11)
|
||||
XCTAssertEqual(version20, version20)
|
||||
|
||||
XCTAssertNotEqual(version10, version11)
|
||||
XCTAssertNotEqual(version11, version10)
|
||||
XCTAssertNotEqual(version20, version10)
|
||||
XCTAssertNotEqual(version20, version11)
|
||||
}
|
||||
|
||||
func testGreater() {
|
||||
XCTAssertGreaterThan(version11, version10)
|
||||
XCTAssertGreaterThan(version20, version10)
|
||||
XCTAssertGreaterThan(version20, version11)
|
||||
|
||||
XCTAssertGreaterThanOrEqual(version10, version10)
|
||||
XCTAssertGreaterThanOrEqual(version11, version11)
|
||||
XCTAssertGreaterThanOrEqual(version20, version20)
|
||||
|
||||
XCTAssertFalse(version10 > version11)
|
||||
XCTAssertFalse(version10 > version20)
|
||||
XCTAssertFalse(version11 > version20)
|
||||
|
||||
XCTAssertFalse(version10 >= version11)
|
||||
XCTAssertFalse(version10 >= version20)
|
||||
XCTAssertFalse(version11 >= version20)
|
||||
}
|
||||
|
||||
func testLess() {
|
||||
XCTAssertLessThan(version10, version11)
|
||||
XCTAssertLessThan(version10, version20)
|
||||
XCTAssertLessThan(version11, version20)
|
||||
|
||||
XCTAssertLessThanOrEqual(version10, version10)
|
||||
XCTAssertLessThanOrEqual(version11, version11)
|
||||
XCTAssertLessThanOrEqual(version20, version20)
|
||||
|
||||
XCTAssertFalse(version11 < version10)
|
||||
XCTAssertFalse(version20 < version10)
|
||||
XCTAssertFalse(version20 < version11)
|
||||
|
||||
XCTAssertFalse(version11 <= version10)
|
||||
XCTAssertFalse(version20 <= version10)
|
||||
XCTAssertFalse(version20 <= version11)
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("testEquals", testEquals),
|
||||
("testGreater", testGreater),
|
||||
("testLess", testLess),
|
||||
]
|
||||
}
|
||||
+14
-2
@@ -1,6 +1,18 @@
|
||||
// This source file is part of the Swift.org Server APIs open source project
|
||||
//
|
||||
// Copyright (c) 2017 Swift Server API project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See http://swift.org/LICENSE.txt for license information
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftServerHttpTests
|
||||
@testable import HTTPTests
|
||||
|
||||
XCTMain([
|
||||
testCase(SwiftServerHttpTests.allTests),
|
||||
// HTTPTests
|
||||
testCase(VersionTests.allTests),
|
||||
testCase(HeadersTests.allTests),
|
||||
testCase(ResponseTests.allTests),
|
||||
testCase(ServerTests.allTests),
|
||||
])
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import Foundation
|
||||
import SwiftServerHttp
|
||||
|
||||
|
||||
/// Simple `WebApp` that just echoes back whatever input it gets
|
||||
class EchoWebApp: WebAppContaining {
|
||||
func serve(req: HTTPRequest, res: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
//Assume the router gave us the right request - at least for now
|
||||
res.writeResponse(HTTPResponse(httpVersion: req.httpVersion,
|
||||
status: .ok,
|
||||
transferEncoding: .chunked,
|
||||
headers: HTTPHeaders([("X-foo", "bar")])))
|
||||
return .processBody { (chunk, stop) in
|
||||
switch chunk {
|
||||
case .chunk(let data, let finishedProcessing):
|
||||
res.writeBody(data: data) { _ in
|
||||
finishedProcessing()
|
||||
}
|
||||
case .end:
|
||||
res.done()
|
||||
default:
|
||||
stop = true /* don't call us anymore */
|
||||
res.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// HelloWorldKeepAliveWebApp.swift
|
||||
// SwiftServerHttp
|
||||
//
|
||||
// Created by Carl Brown on 5/12/17.
|
||||
//
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
import SwiftServerHttp
|
||||
|
||||
/// `HelloWorldWebApp` that sets the keep alive header for XCTest purposes
|
||||
class HelloWorldKeepAliveWebApp: WebAppContaining {
|
||||
func serve(req: HTTPRequest, res: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
//Assume the router gave us the right request - at least for now
|
||||
res.writeResponse(HTTPResponse(httpVersion: req.httpVersion,
|
||||
status: .ok,
|
||||
transferEncoding: .chunked,
|
||||
headers: HTTPHeaders([("Connection","Keep-Alive"),("Keep-Alive","timeout=5, max=10")])))
|
||||
return .processBody { (chunk, stop) in
|
||||
switch chunk {
|
||||
case .chunk(_, let finishedProcessing):
|
||||
finishedProcessing()
|
||||
case .end:
|
||||
res.writeBody(data: "Hello, World!".data(using: .utf8)!) { _ in }
|
||||
res.done()
|
||||
default:
|
||||
stop = true /* don't call us anymore */
|
||||
res.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
//
|
||||
// HelloWorldWebApp.swift
|
||||
// SwiftServerHttp
|
||||
//
|
||||
// Created by Carl Brown on 4/27/17.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftServerHttp
|
||||
|
||||
/// Simple `WebApp` that prints "Hello, World" as per K&R
|
||||
class HelloWorldWebApp: WebAppContaining {
|
||||
func serve(req: HTTPRequest, res: HTTPResponseWriter ) -> HTTPBodyProcessing {
|
||||
//Assume the router gave us the right request - at least for now
|
||||
res.writeResponse(HTTPResponse(httpVersion: req.httpVersion,
|
||||
status: .ok,
|
||||
transferEncoding: .chunked,
|
||||
headers: HTTPHeaders([("X-foo", "bar")])))
|
||||
return .processBody { (chunk, stop) in
|
||||
switch chunk {
|
||||
case .chunk(_, let finishedProcessing):
|
||||
finishedProcessing()
|
||||
case .end:
|
||||
res.writeBody(data: "Hello, World!".data(using: .utf8)!) { _ in }
|
||||
res.done()
|
||||
default:
|
||||
stop = true /* don't call us anymore */
|
||||
res.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
//
|
||||
// TestResponseResolver.swift
|
||||
// SwiftServerHttp
|
||||
//
|
||||
// Created by Carl Brown on 4/24/17.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dispatch
|
||||
import SwiftServerHttp
|
||||
|
||||
/// Acts as a fake/mock `HTTPServer` so we can write XCTests without having to worry about Sockets and such
|
||||
class TestResponseResolver: HTTPResponseWriter {
|
||||
let request: HTTPRequest
|
||||
let requestBody: DispatchData
|
||||
|
||||
var response: HTTPResponse?
|
||||
var responseBody: Data?
|
||||
|
||||
|
||||
init(request: HTTPRequest, requestBody: Data) {
|
||||
self.request = request
|
||||
self.requestBody = requestBody.withUnsafeBytes { (ptr: UnsafePointer<UInt8>) -> DispatchData in
|
||||
DispatchData(bytes: UnsafeBufferPointer<UInt8>(start: ptr, count: requestBody.count))
|
||||
}
|
||||
}
|
||||
|
||||
func resolveHandler(_ handler:WebApp) {
|
||||
let chunkHandler = handler(request, self)
|
||||
var stop=false
|
||||
var finished=false
|
||||
while !stop && !finished {
|
||||
switch chunkHandler {
|
||||
case .processBody(let handler):
|
||||
handler(.chunk(data: self.requestBody, finishedProcessing: {
|
||||
finished=true
|
||||
}), &stop)
|
||||
handler(.end, &stop)
|
||||
case .discardBody:
|
||||
finished=true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeContinue(headers: HTTPHeaders?) /* to send an HTTP `100 Continue` */ {
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
func writeResponse(_ response: HTTPResponse) {
|
||||
self.response=response
|
||||
}
|
||||
|
||||
func writeTrailer(key: String, value: String) {
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
func writeBody(data: DispatchData, completion: @escaping (Result<POSIXError, ()>) -> Void) {
|
||||
self.responseBody = Data(data)
|
||||
completion(Result(completion: ()))
|
||||
}
|
||||
func writeBody(data: DispatchData) /* convenience */ {
|
||||
writeBody(data: data) { _ in
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func writeBody(data: Data, completion: @escaping (Result<POSIXError, ()>) -> Void) {
|
||||
self.responseBody = data
|
||||
completion(Result(completion: ()))
|
||||
}
|
||||
|
||||
func writeBody(data: Data) /* convenience */ {
|
||||
writeBody(data: data) { _ in
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func done(completion: @escaping (Result<POSIXError, ()>) -> Void) {
|
||||
completion(Result(completion: ()))
|
||||
}
|
||||
func done() /* convenience */ {
|
||||
done() { _ in
|
||||
}
|
||||
}
|
||||
|
||||
func abort() {
|
||||
fatalError("abort called, not sure what to do with it")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
import XCTest
|
||||
|
||||
@testable import SwiftServerHttp
|
||||
|
||||
class SwiftServerHttpTests: XCTestCase {
|
||||
func testResponseOK() {
|
||||
let request = HTTPRequest(method: .GET, target:"/echo", httpVersion: (1, 1), headers: HTTPHeaders([("X-foo", "bar")]))
|
||||
let resolver = TestResponseResolver(request: request, requestBody: Data())
|
||||
resolver.resolveHandler(EchoWebApp().serve)
|
||||
XCTAssertNotNil(resolver.response)
|
||||
XCTAssertNotNil(resolver.responseBody)
|
||||
XCTAssertEqual(HTTPResponseStatus.ok.code, resolver.response?.status.code ?? 0)
|
||||
}
|
||||
|
||||
func testEcho() {
|
||||
let testString="This is a test"
|
||||
let request = HTTPRequest(method: .POST, target:"/echo", httpVersion: (1, 1), headers: HTTPHeaders([("X-foo", "bar")]))
|
||||
let resolver = TestResponseResolver(request: request, requestBody: testString.data(using: .utf8)!)
|
||||
resolver.resolveHandler(EchoWebApp().serve)
|
||||
XCTAssertNotNil(resolver.response)
|
||||
XCTAssertNotNil(resolver.responseBody)
|
||||
XCTAssertEqual(HTTPResponseStatus.ok.code, resolver.response?.status.code ?? 0)
|
||||
XCTAssertEqual(testString, String(data: resolver.responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
}
|
||||
|
||||
func testHello() {
|
||||
let request = HTTPRequest(method: .GET, target:"/helloworld", httpVersion: (1, 1), headers: HTTPHeaders([("X-foo", "bar")]))
|
||||
let resolver = TestResponseResolver(request: request, requestBody: Data())
|
||||
resolver.resolveHandler(HelloWorldWebApp().serve)
|
||||
XCTAssertNotNil(resolver.response)
|
||||
XCTAssertNotNil(resolver.responseBody)
|
||||
XCTAssertEqual(HTTPResponseStatus.ok.code, resolver.response?.status.code ?? 0)
|
||||
XCTAssertEqual("Hello, World!", String(data: resolver.responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
}
|
||||
|
||||
func testHeaders() {
|
||||
var headers = HTTPHeaders()
|
||||
let initialCount = headers.makeIterator().reduce(0) { (last, element) -> Int in return last + 1 }
|
||||
XCTAssertEqual(0, initialCount)
|
||||
|
||||
headers.append(newHeader: ("Test-Header","Test Value"))
|
||||
let nextCount = headers.makeIterator().reduce(0) { (last, element) -> Int in return last + 1 }
|
||||
XCTAssertEqual(1, nextCount)
|
||||
|
||||
let testHeaderValueArray = headers["test-header"]
|
||||
XCTAssertNotNil(testHeaderValueArray)
|
||||
XCTAssertEqual(1,testHeaderValueArray.count)
|
||||
XCTAssertEqual("Test Value",testHeaderValueArray.first ?? "Not Found")
|
||||
|
||||
headers.append(newHeader: ("Test-header","Test Value 2"))
|
||||
let testHeaderValueArray2 = headers["test-header"]
|
||||
XCTAssertNotNil(testHeaderValueArray2)
|
||||
XCTAssertEqual(2,testHeaderValueArray2.count)
|
||||
XCTAssertEqual("Test Value",testHeaderValueArray2.first ?? "Not Found")
|
||||
let testHeaderValueArray2Remainder = testHeaderValueArray2.dropFirst()
|
||||
XCTAssertEqual("Test Value 2",testHeaderValueArray2Remainder.first ?? "Not Found")
|
||||
|
||||
//This should overwrites, since the subscript is documented to use lowercase keys
|
||||
headers["TEST-HEADER"]=["Test Value 3"]
|
||||
let testHeaderValueArray3 = headers["test-header"]
|
||||
XCTAssertNotNil(testHeaderValueArray3)
|
||||
XCTAssertEqual(1,testHeaderValueArray3.count)
|
||||
|
||||
//Overwrite
|
||||
headers["TEST-HEADER"]=["Test Value 4a","Test Value 4b"]
|
||||
let testHeaderValueArray4 = headers["test-header"]
|
||||
XCTAssertNotNil(testHeaderValueArray4)
|
||||
XCTAssertEqual(2,testHeaderValueArray4.count)
|
||||
XCTAssertEqual("Test Value 4a",testHeaderValueArray4.first ?? "Not Found")
|
||||
let testHeaderValueArray4Remainder = testHeaderValueArray4.dropFirst()
|
||||
XCTAssertEqual("Test Value 4b",testHeaderValueArray4Remainder.first ?? "Not Found")
|
||||
|
||||
}
|
||||
|
||||
func testResponseCodes() {
|
||||
let okay = HTTPResponseStatus.ok
|
||||
XCTAssertEqual(200,okay.code)
|
||||
XCTAssertEqual("ok",okay.reasonPhrase)
|
||||
XCTAssertEqual("CONTINUE",HTTPResponseStatus.continue.reasonPhrase)
|
||||
XCTAssertEqual(HTTPResponseStatus.notFound, HTTPResponseStatus.from(code: 404))
|
||||
}
|
||||
|
||||
func testSimpleHello() {
|
||||
let request = HTTPRequest(method: .GET, target:"/helloworld", httpVersion: (1, 1), headers: HTTPHeaders([("X-foo", "bar")]))
|
||||
let resolver = TestResponseResolver(request: request, requestBody: Data())
|
||||
let simpleHelloWebApp = SimpleResponseCreator { (request, body) -> (reponse: HTTPResponse, responseBody: Data) in
|
||||
return (HTTPResponse(httpVersion: request.httpVersion,
|
||||
status: .ok,
|
||||
transferEncoding: .chunked,
|
||||
headers: HTTPHeaders([("X-foo", "bar")])),
|
||||
"Hello, World!".data(using: .utf8)!)
|
||||
|
||||
}
|
||||
resolver.resolveHandler(simpleHelloWebApp.serve)
|
||||
XCTAssertNotNil(resolver.response)
|
||||
XCTAssertNotNil(resolver.responseBody)
|
||||
XCTAssertEqual(HTTPResponseStatus.ok.code, resolver.response?.status.code ?? 0)
|
||||
XCTAssertEqual("Hello, World!", String(data: resolver.responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
}
|
||||
|
||||
func testHelloEndToEnd() {
|
||||
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
|
||||
|
||||
let server = BlueSocketSimpleServer()
|
||||
do {
|
||||
try server.start(port: 0, webapp: HelloWorldWebApp().serve)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/helloworld")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
let dataTask = session.dataTask(with: url) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual("Hello, World!", String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
receivedExpectation.fulfill()
|
||||
}
|
||||
dataTask.resume()
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
func testSimpleHelloEndToEnd() {
|
||||
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
|
||||
let simpleHelloWebApp = SimpleResponseCreator { (request, body) -> (reponse: HTTPResponse, responseBody: Data) in
|
||||
return (HTTPResponse(httpVersion: request.httpVersion,
|
||||
status: .ok,
|
||||
transferEncoding: .chunked,
|
||||
headers: HTTPHeaders([("X-foo", "bar")])),
|
||||
"Hello, World!".data(using: .utf8)!)
|
||||
|
||||
}
|
||||
|
||||
let server = BlueSocketSimpleServer()
|
||||
do {
|
||||
try server.start(port: 0, webapp: simpleHelloWebApp.serve)
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/helloworld")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
let dataTask = session.dataTask(with: url) { (responseBody, rawResponse, error) in
|
||||
print("\(#function) dataTask returned")
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
let responseString = String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil"
|
||||
XCTAssertEqual("Hello, World!", responseString)
|
||||
print("\(#function) fulfilling expectation")
|
||||
receivedExpectation.fulfill()
|
||||
}
|
||||
dataTask.resume()
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
print("\(#function) stopping server")
|
||||
}
|
||||
|
||||
|
||||
func testRequestEchoEndToEnd() {
|
||||
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
|
||||
let testString="This is a test"
|
||||
|
||||
let server = BlueSocketSimpleServer()
|
||||
do {
|
||||
try server.start(port: 0, webapp: EchoWebApp().serve)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/echo")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = testString.data(using: .utf8)
|
||||
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let dataTask = session.dataTask(with: request) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString, String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
receivedExpectation.fulfill()
|
||||
}
|
||||
dataTask.resume()
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
func testRequestKeepAliveEchoEndToEnd() {
|
||||
let receivedExpectation1 = self.expectation(description: "Received web response 1: \(#function)")
|
||||
let receivedExpectation2 = self.expectation(description: "Received web response 2: \(#function)")
|
||||
let receivedExpectation3 = self.expectation(description: "Received web response 3: \(#function)")
|
||||
let testString1="This is a test"
|
||||
let testString2="This is a test, too"
|
||||
let testString3="This is also a test"
|
||||
|
||||
let server = BlueSocketSimpleServer()
|
||||
do {
|
||||
try server.start(port: 0, webapp: EchoWebApp().serve)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/echo")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
var request1 = URLRequest(url: url)
|
||||
request1.httpMethod = "POST"
|
||||
request1.httpBody = testString1.data(using: .utf8)
|
||||
request1.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let dataTask1 = session.dataTask(with: request1) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
let headers = response?.allHeaderFields ?? ["":""]
|
||||
let connectionHeader: String = headers["Connection"] as? String ?? ""
|
||||
let keepAliveHeader = headers["Keep-Alive"]
|
||||
XCTAssertEqual(connectionHeader,"Keep-Alive","No Keep-Alive Connection")
|
||||
XCTAssertNotNil(keepAliveHeader)
|
||||
XCTAssertNotNil(responseBody,"No Keep-Alive Header")
|
||||
XCTAssertEqual(server.connectionCount, 1)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString1, String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
var request2 = URLRequest(url: url)
|
||||
request2.httpMethod = "POST"
|
||||
request2.httpBody = testString2.data(using: .utf8)
|
||||
request2.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
let dataTask2 = session.dataTask(with: request2) { (responseBody2, rawResponse2, error2) in
|
||||
let response2 = rawResponse2 as? HTTPURLResponse
|
||||
XCTAssertNil(error2, "\(error2!.localizedDescription)")
|
||||
XCTAssertNotNil(response2)
|
||||
let headers = response2?.allHeaderFields ?? ["":""]
|
||||
let connectionHeader: String = headers["Connection"] as? String ?? ""
|
||||
let keepAliveHeader = headers["Keep-Alive"]
|
||||
XCTAssertEqual(connectionHeader,"Keep-Alive","No Keep-Alive Connection")
|
||||
XCTAssertNotNil(keepAliveHeader,"No Keep-Alive Header")
|
||||
XCTAssertEqual(server.connectionCount, 1)
|
||||
XCTAssertNotNil(responseBody2)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response2?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString2, String(data: responseBody2 ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
var request3 = URLRequest(url: url)
|
||||
request3.httpMethod = "POST"
|
||||
request3.httpBody = testString3.data(using: .utf8)
|
||||
request3.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
||||
let dataTask3 = session.dataTask(with: request3) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
let headers = response?.allHeaderFields ?? ["":""]
|
||||
let connectionHeader: String = headers["Connection"] as? String ?? ""
|
||||
let keepAliveHeader = headers["Keep-Alive"]
|
||||
XCTAssertEqual(connectionHeader,"Keep-Alive","No Keep-Alive Connection")
|
||||
XCTAssertNotNil(keepAliveHeader,"No Keep-Alive Header")
|
||||
XCTAssertEqual(server.connectionCount, 1)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual(testString3, String(data: responseBody ?? Data(), encoding: .utf8) ?? "Nil")
|
||||
receivedExpectation3.fulfill()
|
||||
}
|
||||
dataTask3.resume()
|
||||
receivedExpectation2.fulfill()
|
||||
}
|
||||
dataTask2.resume()
|
||||
receivedExpectation1.fulfill()
|
||||
}
|
||||
dataTask1.resume()
|
||||
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
//server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testRequestLargeEchoEndToEnd() {
|
||||
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
|
||||
//Get a file we know exists
|
||||
//let currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
||||
let executableUrl = URL(fileURLWithPath: CommandLine.arguments[0])
|
||||
|
||||
let testExecutableData = try! Data(contentsOf: executableUrl)
|
||||
|
||||
var testDataLong = testExecutableData + testExecutableData + testExecutableData + testExecutableData
|
||||
let length = testDataLong.count
|
||||
let keep = 16385
|
||||
let remove = length - keep
|
||||
if (remove > 0) {
|
||||
testDataLong.removeLast(remove)
|
||||
}
|
||||
|
||||
let testData = Data(testDataLong)
|
||||
|
||||
let server = BlueSocketSimpleServer()
|
||||
do {
|
||||
try server.start(port: 0, webapp: EchoWebApp().serve)
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default)
|
||||
let url = URL(string: "http://localhost:\(server.port)/echo")!
|
||||
print("Test \(#function) on port \(server.port)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = testData
|
||||
let dataTask = session.dataTask(with: request) { (responseBody, rawResponse, error) in
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
XCTAssertNil(error, "\(error!.localizedDescription)")
|
||||
XCTAssertNotNil(response)
|
||||
XCTAssertNotNil(responseBody)
|
||||
XCTAssertEqual(Int(HTTPResponseStatus.ok.code), response?.statusCode ?? 0)
|
||||
XCTAssertEqual(testData, responseBody ?? Data())
|
||||
receivedExpectation.fulfill()
|
||||
}
|
||||
dataTask.resume()
|
||||
self.waitForExpectations(timeout: 10) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
server.stop()
|
||||
} catch {
|
||||
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
|
||||
}
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("testEcho", testEcho),
|
||||
("testHello", testHello),
|
||||
("testHeaders", testHeaders),
|
||||
("testSimpleHello", testSimpleHello),
|
||||
("testResponseOK", testResponseOK),
|
||||
("testResponseCodes", testResponseCodes),
|
||||
("testHelloEndToEnd", testHelloEndToEnd),
|
||||
("testSimpleHelloEndToEnd", testSimpleHelloEndToEnd),
|
||||
("testRequestEchoEndToEnd", testRequestEchoEndToEnd),
|
||||
("testRequestKeepAliveEchoEndToEnd", testRequestKeepAliveEchoEndToEnd),
|
||||
("testRequestLargeEchoEndToEnd", testRequestLargeEchoEndToEnd),
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user