Add .swiftlint.yml and clean up project (#33)
This commit is contained in:
committed by
Chris Bailey
parent
0a09bcac28
commit
6c0317d239
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user