Add .swiftlint.yml and clean up project (#33)

This commit is contained in:
Ian Partridge
2017-08-24 18:15:50 +01:00
committed by Chris Bailey
parent 0a09bcac28
commit 6c0317d239
17 changed files with 409 additions and 384 deletions
+36
View File
@@ -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
@@ -7,33 +7,31 @@
//
import Foundation
import Socket
#if os(Linux)
import Signals
import Dispatch
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
@@ -53,7 +51,7 @@ public class BlueSocketConnectionListener: ParserConnecting {
_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
@@ -73,8 +71,7 @@ public class BlueSocketConnectionListener: ParserConnecting {
_errorOccurred = newValue
}
}
/// initializer
///
/// - Parameters:
@@ -88,8 +85,7 @@ public class BlueSocketConnectionListener: ParserConnecting {
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 {
@@ -97,8 +93,7 @@ public class BlueSocketConnectionListener: ParserConnecting {
}
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 {
@@ -107,7 +102,7 @@ public class BlueSocketConnectionListener: ParserConnecting {
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
@@ -116,7 +111,7 @@ public class BlueSocketConnectionListener: ParserConnecting {
}
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
@@ -124,17 +119,16 @@ public class BlueSocketConnectionListener: ParserConnecting {
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) {
if self?.readerSource?.isCancelled ?? true {
self?.close()
}
}
}
/// Check if the socket is idle, and if so, call close()
func closeIfIdleSocket() {
let now = Date().timeIntervalSinceReferenceDate
@@ -143,117 +137,112 @@ public class BlueSocketConnectionListener: ParserConnecting {
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) {
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()
try socket?.setBlocking(mode: true)
} catch {
fatalError("Failed to set socket as non-blocking")
}
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()
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.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) {
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")
if result < 0 {
print("Received broken write socket indication")
errorOccurred = true
} else {
written += result
@@ -261,12 +250,12 @@ public class BlueSocketConnectionListener: ParserConnecting {
}
offset = data.count - written
}
if (errorOccurred) {
if errorOccurred {
close()
return
}
} catch {
print("Recived write socket error: \(error)")
print("Received write socket error: \(error)")
errorOccurred = true
close()
}
@@ -8,64 +8,64 @@
import Dispatch
import Foundation
import Socket
//import HeliumLogger
#if os(Linux)
import Signals
import Signals
#endif
import Socket
// MARK: Server
/// An HTTP server that listens for connections on a TCP socket and spawns Listeners to handle them.
public class BlueSocketSimpleServer : CurrentConnectionCounting {
public class BlueSocketSimpleServer: CurrentConnectionCounting {
/// Socket to listen on for connections
private let serverSocket: Socket
/// 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
/// 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
/// 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
Signals.trap(signal: .pipe) { _ in
print("Receiver closed socket, SIGPIPE ignored")
}
#endif
serverSocket = try! Socket.create()
do {
serverSocket = try Socket.create()
} catch {
// TODO: handle this properly
fatalError("Failed to create socket")
}
pruneSocketTimer = DispatchSource.makeTimerSource(queue: DispatchQueue(label: "pruneSocketTimer"))
queueMax = 4 //sensible default
acceptMax = 8 //sensible default
}
/// Starts the server listening on a given port
///
/// - Parameters:
/// - port: TCP port. See listen(2)
/// - 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, handler: @escaping HTTPRequestHandler) throws {
public func start(port: Int = 0,
queueCount: Int = 0,
acceptCount: Int = 0,
handler: @escaping HTTPRequestHandler) throws {
if queueCount > 0 {
queueMax = queueCount
}
@@ -73,26 +73,27 @@ public class BlueSocketSimpleServer : CurrentConnectionCounting {
acceptMax = acceptCount
}
try self.serverSocket.listen(on: port, maxBacklogSize: 100)
pruneSocketTimer.setEventHandler { [weak self] in
self?.connectionListenerList.prune()
}
pruneSocketTimer.scheduleRepeating(deadline: .now() + StreamingParser.keepAliveTimeout, interval: .seconds(Int(StreamingParser.keepAliveTimeout)))
pruneSocketTimer.scheduleRepeating(deadline: .now() + StreamingParser.keepAliveTimeout,
interval: .seconds(Int(StreamingParser.keepAliveTimeout)))
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 {
@@ -101,7 +102,7 @@ public class BlueSocketSimpleServer : CurrentConnectionCounting {
let streamingParser = StreamingParser(handler: handler, connectionCounter: self)
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 = BlueSocketConnectionListener(socket: clientSocket, parser: streamingParser, readQueue:readQueue, writeQueue: writeQueue)
listenerCount += 1
acceptSemaphore.wait()
acceptQueue.async { [weak listener] in
@@ -109,77 +110,69 @@ public class BlueSocketSimpleServer : CurrentConnectionCounting {
acceptSemaphore.signal()
}
self.connectionListenerList.add(listener)
} catch let error {
print("Error accepting client connection: \(error)")
}
} while self.serverSocket.isListening
}
}
/// Stop the server and close the sockets
public func stop() {
connectionListenerList.closeAll()
serverSocket.close()
}
/// 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>]()
/// Add a new connection to the collection
///
/// - Parameter listener: socket manager object
func add(_ listener:BlueSocketConnectionListener) {
func add(_ listener: BlueSocketConnectionListener) {
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 {
lock.wait()
let c = storage.filter { nil != $0.value }.count
let count = storage.filter { nil != $0.value }.count
lock.signal()
return c
return count
}
}
+5 -6
View File
@@ -58,7 +58,7 @@ extension HTTPHeaders : ExpressibleByDictionaryLiteral {
extension HTTPHeaders {
// Used instead of HTTPHeaders to save CPU on dictionary construction
/// :nodoc:
public struct Literal : ExpressibleByDictionaryLiteral {
public struct Literal: ExpressibleByDictionaryLiteral {
let fields: [(name: Name, value: String)]
public init(dictionaryLiteral: (Name, String)...) {
@@ -102,7 +102,7 @@ extension HTTPHeaders : Sequence {
}
}
struct StorageIterator : IteratorProtocol {
struct StorageIterator: IteratorProtocol {
var headers: DictionaryIterator<Name, [String]>
var header: (name: Name, values: IndexingIterator<[String]>)?
@@ -126,9 +126,8 @@ extension HTTPHeaders : Sequence {
/// HTTPHeaders structure.
extension HTTPHeaders {
/// Type used for the name of a HTTP header in the `HTTPHeaders` storage.
public struct Name : Hashable, ExpressibleByStringLiteral, CustomStringConvertible {
public struct Name: Hashable, ExpressibleByStringLiteral, CustomStringConvertible {
let original: String
let lowercased: String
public let hashValue: Int
@@ -156,7 +155,7 @@ extension HTTPHeaders {
public var description: String {
return original
}
/// :nodoc:
public static func == (lhs: Name, rhs: Name) -> Bool {
return lhs.lowercased == rhs.lowercased
@@ -164,7 +163,7 @@ extension HTTPHeaders {
// 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.
+5 -6
View File
@@ -10,7 +10,7 @@
public struct HTTPMethod {
/// HTTP method
public let method: String
/// Creates an HTTP method
public init(_ method: String) {
self.method = method.uppercased()
@@ -88,16 +88,15 @@ extension HTTPMethod {
}
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
@@ -109,12 +108,12 @@ extension HTTPMethod : ExpressibleByStringLiteral {
public init(stringLiteral: String) {
self.init(stringLiteral)
}
/// :nodoc:
public init(unicodeScalarLiteral: String) {
self.init(unicodeScalarLiteral)
}
/// :nodoc:
public init(extendedGraphemeClusterLiteral: String) {
self.init(extendedGraphemeClusterLiteral)
+1 -1
View File
@@ -6,8 +6,8 @@
// See http://swift.org/LICENSE.txt for license information
//
import Foundation
import Dispatch
import Foundation
/// A structure representing the headers from a HTTP request, without the body of the request.
public struct HTTPRequest {
+21 -21
View File
@@ -12,7 +12,7 @@ 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
public var httpVersion: HTTPVersion
/// HTTP response status
public var status: HTTPResponseStatus
/// HTTP response headers
@@ -21,41 +21,40 @@ public struct HTTPResponse {
/// HTTPResponseWriter provides functions to create an HTTP response
public protocol HTTPResponseWriter : class {
/// writeHeader: Writer function to create the headers for an HTTP response
/// 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)
/// writeTrailer: Writer function to write a trailer header as part of the HTTP response
/// 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)
/// writeBody: Writer function to write data to the body of the HTTP response
/// 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)
/// done: Writer function to complete the HTTP response
/// 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 funtion to write the headers for an HTTP response without a completion handler
/// 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 funtion to write a HTTP response with no headers or completion handler
/// 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: [:])
@@ -95,7 +94,7 @@ public struct HTTPResponseStatus: Equatable, CustomStringConvertible, Expressibl
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
@@ -107,7 +106,7 @@ public struct HTTPResponseStatus: Equatable, CustomStringConvertible, Expressibl
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)
@@ -226,6 +225,7 @@ public struct HTTPResponseStatus: Equatable, CustomStringConvertible, Expressibl
/// 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"
@@ -289,7 +289,7 @@ public struct HTTPResponseStatus: Equatable, CustomStringConvertible, Expressibl
default: return "http_\(code)"
}
}
/// :nodoc:
public var description: String {
return "\(code) \(reasonPhrase)"
@@ -349,7 +349,7 @@ public struct HTTPResponseStatus: Equatable, CustomStringConvertible, Expressibl
}
/// :nodoc:
public static func ==(lhs: HTTPResponseStatus, rhs: HTTPResponseStatus) -> Bool {
public static func == (lhs: HTTPResponseStatus, rhs: HTTPResponseStatus) -> Bool {
return lhs.code == rhs.code
}
}
@@ -360,30 +360,30 @@ public protocol UnsafeHTTPResponseBody {
}
/// :nodoc:
extension UnsafeRawBufferPointer : UnsafeHTTPResponseBody {
extension UnsafeRawBufferPointer: UnsafeHTTPResponseBody {
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
return try body(self)
}
}
/// :nodoc:
public protocol HTTPResponseBody : UnsafeHTTPResponseBody {}
public protocol HTTPResponseBody: UnsafeHTTPResponseBody {}
extension Data : HTTPResponseBody {
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 {
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 {
extension String: HTTPResponseBody {
/// :nodoc:
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
return try ContiguousArray(utf8).withUnsafeBytes(body)
+2 -2
View File
@@ -25,7 +25,7 @@ public protocol HTTPServing : class {
/// A basic HTTP server. Currently this is implemented using the BlueSocket
/// 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 {
public class HTTPServer: HTTPServing {
private let server = BlueSocketSimpleServer()
public init() {
@@ -34,7 +34,7 @@ public class HTTPServer : HTTPServing {
public func start(port: Int = 0, handler: @escaping HTTPRequestHandler) throws {
try server.start(handler: handler)
}
public func stop() {
server.stop()
}
+116 -102
View File
@@ -6,25 +6,22 @@
// See http://swift.org/LICENSE.txt for license information
//
import CHTTPParser
import Foundation
import Dispatch
import CHTTPParser
/// 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 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?
@@ -47,7 +44,7 @@ public class StreamingParser: HTTPResponseWriter {
/// 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
@@ -59,14 +56,14 @@ public class StreamingParser: HTTPResponseWriter {
/// 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()
@@ -76,85 +73,82 @@ public class StreamingParser: HTTPResponseWriter {
/// 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) {
self.handle = handler
self.connectionCounter = connectionCounter
//Set up all the callbacks for the CHTTPParser library
httpParserSettings.on_message_begin = {
parser -> Int32 in
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
httpParserSettings.on_message_complete = { parser -> Int32 in
guard let listener = StreamingParser.getSelf(parser: parser) else {
return Int32(0)
return 0
}
return listener.messageCompleted()
}
httpParserSettings.on_headers_complete = {
parser -> Int32 in
httpParserSettings.on_headers_complete = { parser -> Int32 in
guard let listener = StreamingParser.getSelf(parser: parser) else {
return Int32(0)
return 0
}
let methodId = parser?.pointee.method
let methodName = String(validatingUTF8:http_method_str(http_method(rawValue: methodId ?? 0))) ?? "GET"
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 keepAlive = http_should_keep_alive(parser) == 1
let upgradeRequested = get_upgrade_value(parser) == 1
return listener.headersCompleted(methodName: methodName, majorVersion: major, minorVersion: minor, keepAlive: keepAlive, upgrade: upgradeRequested)
return listener.headersCompleted(methodName: methodName,
majorVersion: major,
minorVersion: minor,
keepAlive: keepAlive,
upgrade: upgradeRequested)
}
httpParserSettings.on_header_field = {
(parser, chunk, length) -> Int32 in
httpParserSettings.on_header_field = { (parser, chunk, length) -> Int32 in
guard let listener = StreamingParser.getSelf(parser: parser) else {
return Int32(0)
return 0
}
return listener.headerFieldReceived(data: chunk, length: length)
}
httpParserSettings.on_header_value = {
(parser, chunk, length) -> Int32 in
httpParserSettings.on_header_value = { (parser, chunk, length) -> Int32 in
guard let listener = StreamingParser.getSelf(parser: parser) else {
return Int32(0)
return 0
}
return listener.headerValueReceived(data: chunk, length: length)
}
httpParserSettings.on_body = {
(parser, chunk, length) -> Int32 in
httpParserSettings.on_body = { (parser, chunk, length) -> Int32 in
guard let listener = StreamingParser.getSelf(parser: parser) else {
return Int32(0)
return 0
}
return listener.bodyReceived(data: chunk, length: length)
}
httpParserSettings.on_url = {
(parser, chunk, length) -> Int32 in
httpParserSettings.on_url = { (parser, chunk, length) -> Int32 in
guard let listener = StreamingParser.getSelf(parser: parser) else {
return Int32(0)
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
@@ -164,18 +158,18 @@ public class StreamingParser: HTTPResponseWriter {
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 {
func processCurrentCallback(_ currentCallBack: CallbackRecord) -> Bool {
if lastCallBack == currentCallBack {
return false
}
@@ -183,21 +177,23 @@ public class StreamingParser: HTTPResponseWriter {
case .headerFieldReceived:
if let parserBuffer = self.parserBuffer {
self.lastHeaderName = String(data: parserBuffer, encoding: .utf8)
self.parserBuffer=nil
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) {
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
self.parserBuffer = nil
} else {
print("Missing parserBuffer after \(lastCallBack)")
}
case .headersCompleted:
self.parserBuffer=nil
self.parserBuffer = nil
if !upgradeRequested {
self.httpBodyProcessingCallback = self.handle(self.createRequest(), self)
}
@@ -206,7 +202,7 @@ public class StreamingParser: HTTPResponseWriter {
//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
self.parserBuffer = nil
} else {
print("Missing parserBuffer after \(lastCallBack)")
}
@@ -222,17 +218,17 @@ public class StreamingParser: HTTPResponseWriter {
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
var stop = false
switch chunkHandler {
case .processBody(let handler):
handler(.end, &stop)
@@ -242,8 +238,12 @@ public class StreamingParser: HTTPResponseWriter {
}
return 0
}
func headersCompleted(methodName:String, majorVersion: Int, minorVersion:Int, keepAlive: Bool, upgrade:Bool) -> Int32 {
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)
@@ -254,25 +254,33 @@ public class StreamingParser: HTTPResponseWriter {
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
self.parserBuffer == nil ? self.parserBuffer = Data(bytes:data, count:length) : self.parserBuffer?.append(ptr, count:length)
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
self.parserBuffer == nil ? self.parserBuffer = Data(bytes:data, count:length) : self.parserBuffer?.append(ptr, count:length)
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 }
@@ -280,51 +288,58 @@ public class StreamingParser: HTTPResponseWriter {
let buff = UnsafeBufferPointer<UInt8>(start: ptr, count: length)
let chunk = DispatchData(bytes:buff)
if let chunkHandler = self.httpBodyProcessingCallback {
var stop=false
var finished=false
var stop = false
var finished = false
while !stop && !finished {
switch chunkHandler {
case .processBody(let handler):
handler(.chunk(data: chunk, finishedProcessing: {
finished=true
finished = true
}), &stop)
case .discardBody:
finished=true
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)
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)
return HTTPRequest(method: parsedHTTPMethod!,
target: parsedURL!,
httpVersion: parsedHTTPVersion!,
headers: parsedHeaders)
}
public func writeHeader(status: HTTPResponseStatus, headers: HTTPHeaders, completion: @escaping (Result) -> Void) {
// TODO call completion()
guard !headersWritten else {
return
}
var header = "HTTP/1.1 \(status.code) \(status.reasonPhrase)\r\n"
let isContinue = status == .continue
@@ -333,13 +348,13 @@ public class StreamingParser: HTTPResponseWriter {
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) {
@@ -384,22 +399,22 @@ public class StreamingParser: HTTPResponseWriter {
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 {
@@ -410,25 +425,25 @@ public class StreamingParser: HTTPResponseWriter {
dataToWrite.append(chunkEnd)
return dataToWrite
}
} else if let d = data as? Data {
dataToWrite = d
} else if let data = data as? Data {
dataToWrite = data
} else {
dataToWrite = data.withUnsafeBytes { Data($0) }
}
self.parserConnector?.queueSocketWrite(dataToWrite)
completion(.ok)
}
public func done(completion: @escaping (Result) -> Void) {
if isChunked {
let chunkTerminate = "0\r\n\r\n".data(using: .utf8)!
self.parserConnector?.queueSocketWrite(chunkTerminate)
}
self.parsedHTTPMethod = nil
self.parsedURL=nil
self.parsedURL = nil
self.parsedHeaders = HTTPHeaders()
self.lastHeaderName = nil
self.parserBuffer = nil
@@ -438,16 +453,16 @@ public class StreamingParser: HTTPResponseWriter {
self.headersWritten = false
self.httpBodyProcessingCallback = nil
self.upgradeRequested = false
let closeAfter = {
if self.clientRequestedKeepAlive {
self.keepAliveUntil = Date(timeIntervalSinceNow:StreamingParser.keepAliveTimeout).timeIntervalSinceReferenceDate
self.keepAliveUntil = Date(timeIntervalSinceNow: StreamingParser.keepAliveTimeout).timeIntervalSinceReferenceDate
self.parserConnector?.responseComplete()
} else {
self.parserConnector?.closeWriter()
}
}
// FIXME I do not understand what code written here before was meant to do
// If it was about delayed closure invocation then it couldn't work either
// Here is the equivalent code
@@ -458,7 +473,7 @@ public class StreamingParser: HTTPResponseWriter {
public func abort() {
fatalError("abort called, not sure what to do with it")
}
deinit {
httpParser.data = nil
}
@@ -468,18 +483,17 @@ public class StreamingParser: HTTPResponseWriter {
/// 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) -> Void
func queueSocketWrite(_ from: Data)
/// Let the network know that a response has started to avoid closing a connection during a slow write
func responseBeginning() -> Void
func responseBeginning()
/// Let the network know that a response is complete, so it can be closed after timeout
func responseComplete() -> Void
func responseComplete()
/// Used to let the network know we're ready to close the connection
func closeWriter() -> Void
func closeWriter()
}
/// Delegate that can tell us how many connections are in-flight so we can set the Keep-Alive header
+1 -1
View File
@@ -30,7 +30,7 @@ extension HTTPVersion : Hashable {
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
+13 -13
View File
@@ -43,42 +43,42 @@ class HeadersTests: XCTestCase {
])
headers = HTTPHeaders()
let initialCount = headers.makeIterator().reduce(0) { (last, element) -> Int in return last + 1 }
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, element) -> Int in return last + 1 }
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")
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")
XCTAssertEqual(2, testHeaderValueArray2.count)
XCTAssertEqual("Test Value", testHeaderValueArray2.first ?? "Not Found")
let testHeaderValueArray2Remainder = testHeaderValueArray2.dropFirst()
XCTAssertEqual("Test Value 2",testHeaderValueArray2Remainder.first ?? "Not Found")
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)
XCTAssertEqual(1, testHeaderValueArray3.count)
//Overwrite
headers[valuesFor: "TEST-HEADER"]=["Test Value 4a","Test Value 4b"]
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")
XCTAssertEqual(2, testHeaderValueArray4.count)
XCTAssertEqual("Test Value 4a", testHeaderValueArray4.first ?? "Not Found")
let testHeaderValueArray4Remainder = testHeaderValueArray4.dropFirst()
XCTAssertEqual("Test Value 4b",testHeaderValueArray4Remainder.first ?? "Not Found")
XCTAssertEqual("Test Value 4b", testHeaderValueArray4Remainder.first ?? "Not Found")
}
static var allTests = [
("testHeaders", testHeaders),
]
@@ -9,7 +9,6 @@
import Foundation
import HTTP
/// Simple `HTTPRequestHandler` that just echoes back whatever input it gets
class EchoHandler: HTTPRequestHandling {
func handle(request: HTTPRequest, response: HTTPResponseWriter ) -> HTTPBodyProcessing {
@@ -7,10 +7,8 @@
//
/*
This file isn't part of the API per se, but it's the easiest way to get started- just supply a completion block.
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
@@ -24,21 +22,21 @@ public class SimpleResponseCreator: HTTPRequestHandling {
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) {
if data.count > 0 {
self.buffer.append(Data(data))
}
finishedProcessing()
@@ -14,44 +14,43 @@ import HTTP
class TestResponseResolver: HTTPResponseWriter {
let request: HTTPRequest
let requestBody: DispatchData
var response: (status: HTTPResponseStatus, headers: HTTPHeaders)?
var responseBody: HTTPResponseBody?
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: HTTPRequestHandler) {
let chunkHandler = handler(request, self)
var stop=false
var finished=false
var stop = false
var finished = false
while !stop && !finished {
switch chunkHandler {
case .processBody(let handler):
handler(.chunk(data: self.requestBody, finishedProcessing: {
finished=true
finished = true
}), &stop)
handler(.end, &stop)
case .discardBody:
finished=true
finished = true
}
}
}
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
@@ -60,17 +59,16 @@ class TestResponseResolver: HTTPResponseWriter {
}
completion(.ok)
}
func done(completion: @escaping (Result) -> Void) {
completion(.ok)
}
func done() /* convenience */ {
done() { _ in
done { _ in
}
}
func abort() {
fatalError("abort called, not sure what to do with it")
}
}
+3 -3
View File
@@ -14,13 +14,13 @@ class ResponseTests: XCTestCase {
func testOkay() {
let okay = HTTPResponseStatus.ok
XCTAssertEqual(200,okay.code)
XCTAssertEqual("OK",okay.reasonPhrase)
XCTAssertEqual(200, okay.code)
XCTAssertEqual("OK", okay.reasonPhrase)
XCTAssertEqual("\(okay)", "200 OK")
}
func testContinue() {
XCTAssertEqual("Continue",HTTPResponseStatus.continue.reasonPhrase)
XCTAssertEqual("Continue", HTTPResponseStatus.continue.reasonPhrase)
}
func testNotFound() {
+42 -40
View File
@@ -12,7 +12,7 @@ import XCTest
class ServerTests: XCTestCase {
func testResponseOK() {
let request = HTTPRequest(method: .get, target:"/echo", httpVersion: HTTPVersion(major: 1, minor: 1), headers: ["X-foo": "bar"])
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)
@@ -22,7 +22,7 @@ class ServerTests: XCTestCase {
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 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)
@@ -30,9 +30,9 @@ class ServerTests: XCTestCase {
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 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)
@@ -40,17 +40,16 @@ class ServerTests: XCTestCase {
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 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 { (request, body) -> SimpleResponseCreator.Response in
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)
@@ -58,10 +57,10 @@ class ServerTests: XCTestCase {
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)
@@ -90,7 +89,7 @@ class ServerTests: XCTestCase {
func testHelloEndToEnd() {
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
let server = HTTPServer()
do {
try server.start(port: 0, handler: HelloWorldHandler().handle)
@@ -117,16 +116,15 @@ class ServerTests: XCTestCase {
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) -> SimpleResponseCreator.Response in
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()
@@ -135,7 +133,7 @@ class ServerTests: XCTestCase {
} 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)")
@@ -161,7 +159,6 @@ class ServerTests: XCTestCase {
print("\(#function) stopping server")
}
func testRequestEchoEndToEnd() {
let receivedExpectation = self.expectation(description: "Received web response \(#function)")
let testString="This is a test"
@@ -176,7 +173,7 @@ class ServerTests: XCTestCase {
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)")
@@ -197,7 +194,7 @@ class ServerTests: XCTestCase {
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)")
@@ -205,7 +202,7 @@ class ServerTests: XCTestCase {
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)
@@ -216,17 +213,17 @@ class ServerTests: XCTestCase {
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 headers = response?.allHeaderFields ?? ["": ""]
let connectionHeader: String = headers["Connection"] as? String ?? ""
let keepAliveHeader = headers["Keep-Alive"]
XCTAssertEqual(connectionHeader,"Keep-Alive","No Keep-Alive Connection")
XCTAssertEqual(connectionHeader, "Keep-Alive", "No Keep-Alive Connection")
XCTAssertNotNil(keepAliveHeader)
XCTAssertNotNil(responseBody,"No Keep-Alive Header")
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")
@@ -238,11 +235,11 @@ class ServerTests: XCTestCase {
let response2 = rawResponse2 as? HTTPURLResponse
XCTAssertNil(error2, "\(error2!.localizedDescription)")
XCTAssertNotNil(response2)
let headers = response2?.allHeaderFields ?? ["":""]
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(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)
@@ -255,11 +252,11 @@ class ServerTests: XCTestCase {
let response = rawResponse as? HTTPURLResponse
XCTAssertNil(error, "\(error!.localizedDescription)")
XCTAssertNotNil(response)
let headers = response?.allHeaderFields ?? ["":""]
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(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)
@@ -285,25 +282,30 @@ class ServerTests: XCTestCase {
}
}
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)
// 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) {
if remove > 0 {
testDataLong.removeLast(remove)
}
let testData = Data(testDataLong)
let server = HTTPServer()
do {
try server.start(port: 0, handler: EchoHandler().handle)
@@ -333,7 +335,7 @@ class ServerTests: XCTestCase {
XCTFail("Error listening on port \(0): \(error). Use server.failed(callback:) to handle")
}
}
static var allTests = [
("testEcho", testEcho),
("testHello", testHello),
+9 -11
View File
@@ -11,27 +11,26 @@ 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)
@@ -39,14 +38,13 @@ class VersionTests: XCTestCase {
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)
@@ -54,16 +52,16 @@ class VersionTests: XCTestCase {
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),