From 6a14e218018ed9257bc894e1d89cf12dca9c8e9c Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 7 Nov 2023 09:25:20 +0000 Subject: [PATCH] Moving stuff about, Get TLS working --- Package.swift | 135 ++-- .../Error/HTTPErrorResponse.swift | 13 +- .../Request/HTTPRequest.swift | 0 .../HummingbirdCore/Request/RequestBody.swift | 115 +--- .../Response/HTTPResponse.swift | 0 .../Response/ResponseBody.swift | 151 +---- .../Server/ChannelSetup.swift | 0 .../Server/HTTP/HTTP1Channel.swift | 2 +- .../Server/HTTP/HTTPChannelSetup.swift | 0 .../HTTPSendableResponseChannelHandler.swift | 10 +- .../Server/HTTPUserEventHandler.swift | 18 +- .../Server/Server.swift | 0 .../Server/ServerConfiguration.swift | 0 .../Request/RequestBody.swift | 61 -- .../Response/ResponseBody.swift | 55 -- .../RequestBodyStreamer+async.swift | 0 .../AsyncAwaitSupport/Sendable.swift | 0 .../Error/HTTPError.swift | 0 .../Error/HTTPErrorResponse.swift | 13 +- .../HTTPResponder.swift | 0 .../Request/ByteBufferStreamer.swift | 0 .../Request/Request.swift | 0 .../Request/RequestBody.swift | 114 ++++ .../Response/Response.swift | 0 .../Response/ResponseBody.swift | 154 +++++ .../Server/BindAddress.swift | 0 .../Server/ChannelInitializer.swift | 0 .../Server/HTTPServer+Configuration.swift | 0 .../Server/HTTPServer+ServiceLifecycle.swift | 0 .../Server/HTTPServer.swift | 0 .../Server/HTTPServerHandler.swift | 0 .../Server/TSTLSOptions.swift | 0 .../HummingbirdTLS/ChannelInitializer.swift | 50 -- Sources/HummingbirdTLS/TLSChannelSetup.swift | 68 ++ .../HummingbirdCoreAsyncTests/CoreTests.swift | 308 --------- .../HummingbirdCoreAsyncTests/TestUtils.swift | 126 ---- Tests/HummingbirdCoreOldTests/CoreTests.swift | 624 ++++++++++++++++++ .../StreamerTests.swift | 0 Tests/HummingbirdCoreOldTests/TLSTests.swift | 66 ++ Tests/HummingbirdCoreOldTests/TSTests.swift | 88 +++ Tests/HummingbirdCoreOldTests/TestUtils.swift | 65 ++ Tests/HummingbirdCoreTests/CoreTests.swift | 566 ++++------------ Tests/HummingbirdCoreTests/TLSTests.swift | 12 +- Tests/HummingbirdCoreTests/TSTests.swift | 58 +- Tests/HummingbirdCoreTests/TestUtils.swift | 123 +++- 45 files changed, 1580 insertions(+), 1415 deletions(-) rename Sources/{HummingbirdCoreAsync => HummingbirdCore}/Request/HTTPRequest.swift (100%) rename Sources/{HummingbirdCoreAsync => HummingbirdCore}/Response/HTTPResponse.swift (100%) rename Sources/{HummingbirdCoreAsync => HummingbirdCore}/Server/ChannelSetup.swift (100%) rename Sources/{HummingbirdCoreAsync => HummingbirdCore}/Server/HTTP/HTTP1Channel.swift (94%) rename Sources/{HummingbirdCoreAsync => HummingbirdCore}/Server/HTTP/HTTPChannelSetup.swift (100%) rename Sources/{HummingbirdCoreAsync => HummingbirdCore}/Server/HTTPSendableResponseChannelHandler.swift (77%) rename Sources/{HummingbirdCoreAsync => HummingbirdCore}/Server/HTTPUserEventHandler.swift (82%) rename Sources/{HummingbirdCoreAsync => HummingbirdCore}/Server/Server.swift (100%) rename Sources/{HummingbirdCoreAsync => HummingbirdCore}/Server/ServerConfiguration.swift (100%) delete mode 100644 Sources/HummingbirdCoreAsync/Request/RequestBody.swift delete mode 100644 Sources/HummingbirdCoreAsync/Response/ResponseBody.swift rename Sources/{HummingbirdCore => HummingbirdCoreOld}/AsyncAwaitSupport/RequestBodyStreamer+async.swift (100%) rename Sources/{HummingbirdCore => HummingbirdCoreOld}/AsyncAwaitSupport/Sendable.swift (100%) rename Sources/{HummingbirdCoreAsync => HummingbirdCoreOld}/Error/HTTPError.swift (100%) rename Sources/{HummingbirdCoreAsync => HummingbirdCoreOld}/Error/HTTPErrorResponse.swift (71%) rename Sources/{HummingbirdCore => HummingbirdCoreOld}/HTTPResponder.swift (100%) rename Sources/{HummingbirdCore => HummingbirdCoreOld}/Request/ByteBufferStreamer.swift (100%) rename Sources/{HummingbirdCore => HummingbirdCoreOld}/Request/Request.swift (100%) create mode 100644 Sources/HummingbirdCoreOld/Request/RequestBody.swift rename Sources/{HummingbirdCore => HummingbirdCoreOld}/Response/Response.swift (100%) create mode 100644 Sources/HummingbirdCoreOld/Response/ResponseBody.swift rename Sources/{HummingbirdCoreAsync => HummingbirdCoreOld}/Server/BindAddress.swift (100%) rename Sources/{HummingbirdCore => HummingbirdCoreOld}/Server/ChannelInitializer.swift (100%) rename Sources/{HummingbirdCore => HummingbirdCoreOld}/Server/HTTPServer+Configuration.swift (100%) rename Sources/{HummingbirdCore => HummingbirdCoreOld}/Server/HTTPServer+ServiceLifecycle.swift (100%) rename Sources/{HummingbirdCore => HummingbirdCoreOld}/Server/HTTPServer.swift (100%) rename Sources/{HummingbirdCore => HummingbirdCoreOld}/Server/HTTPServerHandler.swift (100%) rename Sources/{HummingbirdCoreAsync => HummingbirdCoreOld}/Server/TSTLSOptions.swift (100%) delete mode 100644 Sources/HummingbirdTLS/ChannelInitializer.swift create mode 100644 Sources/HummingbirdTLS/TLSChannelSetup.swift delete mode 100644 Tests/HummingbirdCoreAsyncTests/CoreTests.swift delete mode 100644 Tests/HummingbirdCoreAsyncTests/TestUtils.swift create mode 100644 Tests/HummingbirdCoreOldTests/CoreTests.swift rename Tests/{HummingbirdCoreTests => HummingbirdCoreOldTests}/StreamerTests.swift (100%) create mode 100644 Tests/HummingbirdCoreOldTests/TLSTests.swift create mode 100644 Tests/HummingbirdCoreOldTests/TSTests.swift create mode 100644 Tests/HummingbirdCoreOldTests/TestUtils.swift diff --git a/Package.swift b/Package.swift index 2fa9d2262..cff070399 100644 --- a/Package.swift +++ b/Package.swift @@ -7,11 +7,11 @@ let package = Package( name: "hummingbird", platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17)], products: [ - .library(name: "Hummingbird", targets: ["Hummingbird"]), - .library(name: "HummingbirdFoundation", targets: ["HummingbirdFoundation"]), - .library(name: "HummingbirdJobs", targets: ["HummingbirdJobs"]), - .library(name: "HummingbirdXCT", targets: ["HummingbirdXCT"]), - .executable(name: "PerformanceTest", targets: ["PerformanceTest"]), + // .library(name: "Hummingbird", targets: ["Hummingbird"]), + // .library(name: "HummingbirdFoundation", targets: ["HummingbirdFoundation"]), + // .library(name: "HummingbirdJobs", targets: ["HummingbirdJobs"]), + // .library(name: "HummingbirdXCT", targets: ["HummingbirdXCT"]), + // .executable(name: "PerformanceTest", targets: ["PerformanceTest"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-async-algorithms.git", from: "0.1.0"), @@ -26,46 +26,37 @@ let package = Package( .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"), ], targets: [ - .target(name: "Hummingbird", dependencies: [ - .byName(name: "HummingbirdCore"), - .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), - .product(name: "Logging", package: "swift-log"), - .product(name: "Metrics", package: "swift-metrics"), - .product(name: "Tracing", package: "swift-distributed-tracing"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "NIOHTTP1", package: "swift-nio"), - ]), - .target(name: "HummingbirdFoundation", dependencies: [ - .byName(name: "Hummingbird"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), - ]), - .target(name: "HummingbirdJobs", dependencies: [ - .byName(name: "Hummingbird"), - .product(name: "Logging", package: "swift-log"), - ]), - .target(name: "HummingbirdXCT", dependencies: [ - .byName(name: "Hummingbird"), - .byName(name: "HummingbirdCoreXCT"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOEmbedded", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "NIOHTTP1", package: "swift-nio"), - ]), + /* .target(name: "Hummingbird", dependencies: [ + .byName(name: "HummingbirdCore"), + .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), + .product(name: "Logging", package: "swift-log"), + .product(name: "Metrics", package: "swift-metrics"), + .product(name: "Tracing", package: "swift-distributed-tracing"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "NIOHTTP1", package: "swift-nio"), + ]), + .target(name: "HummingbirdFoundation", dependencies: [ + .byName(name: "Hummingbird"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + ]), + .target(name: "HummingbirdJobs", dependencies: [ + .byName(name: "Hummingbird"), + .product(name: "Logging", package: "swift-log"), + ]), + .target(name: "HummingbirdXCT", dependencies: [ + .byName(name: "Hummingbird"), + .byName(name: "HummingbirdCoreXCT"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOEmbedded", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "NIOHTTP1", package: "swift-nio"), + ]),*/ .target(name: "HummingbirdCore", dependencies: [ - .product(name: "Logging", package: "swift-log"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), - .product(name: "NIOExtras", package: "swift-nio-extras"), - .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), - .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), - ]), - .target(name: "HummingbirdCoreAsync", dependencies: [ + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "Logging", package: "swift-log"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), @@ -82,36 +73,36 @@ let package = Package( .product(name: "NIOPosix", package: "swift-nio"), .product(name: "NIOSSL", package: "swift-nio-ssl"), ]), - .target(name: "HummingbirdHTTP2", dependencies: [ - .byName(name: "HummingbirdCore"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOHTTP2", package: "swift-nio-http2"), - .product(name: "NIOSSL", package: "swift-nio-ssl"), - ]), + /* .target(name: "HummingbirdHTTP2", dependencies: [ + .byName(name: "HummingbirdCore"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOHTTP2", package: "swift-nio-http2"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), + ]),*/ .target(name: "HummingbirdTLS", dependencies: [ .byName(name: "HummingbirdCore"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOSSL", package: "swift-nio-ssl"), ]), - .executableTarget(name: "PerformanceTest", dependencies: [ - .byName(name: "Hummingbird"), - .byName(name: "HummingbirdFoundation"), - .product(name: "NIOPosix", package: "swift-nio"), - ]), + /* .executableTarget(name: "PerformanceTest", dependencies: [ + .byName(name: "Hummingbird"), + .byName(name: "HummingbirdFoundation"), + .product(name: "NIOPosix", package: "swift-nio"), + ]),*/ // test targets - .testTarget(name: "HummingbirdTests", dependencies: [ - .byName(name: "Hummingbird"), - .byName(name: "HummingbirdFoundation"), - .byName(name: "HummingbirdXCT"), - ]), - .testTarget(name: "HummingbirdFoundationTests", dependencies: [ - .byName(name: "HummingbirdFoundation"), - .byName(name: "HummingbirdXCT"), - ]), - .testTarget(name: "HummingbirdJobsTests", dependencies: [ - .byName(name: "HummingbirdJobs"), - .byName(name: "HummingbirdXCT"), - ]), + /* .testTarget(name: "HummingbirdTests", dependencies: [ + .byName(name: "Hummingbird"), + .byName(name: "HummingbirdFoundation"), + .byName(name: "HummingbirdXCT"), + ]), + .testTarget(name: "HummingbirdFoundationTests", dependencies: [ + .byName(name: "HummingbirdFoundation"), + .byName(name: "HummingbirdXCT"), + ]), + .testTarget(name: "HummingbirdJobsTests", dependencies: [ + .byName(name: "HummingbirdJobs"), + .byName(name: "HummingbirdXCT"), + ]),*/ .testTarget( name: "HummingbirdCoreTests", dependencies: @@ -123,13 +114,5 @@ let package = Package( ], resources: [.process("Certificates")] ), - .testTarget( - name: "HummingbirdCoreAsyncTests", - dependencies: - [ - .byName(name: "HummingbirdCoreAsync"), - .byName(name: "HummingbirdCoreXCT"), - ] - ), ] ) diff --git a/Sources/HummingbirdCore/Error/HTTPErrorResponse.swift b/Sources/HummingbirdCore/Error/HTTPErrorResponse.swift index eb9532391..5ae6b4d09 100644 --- a/Sources/HummingbirdCore/Error/HTTPErrorResponse.swift +++ b/Sources/HummingbirdCore/Error/HTTPErrorResponse.swift @@ -32,18 +32,13 @@ extension HBHTTPResponseError { /// Generate response from error /// - Parameter allocator: Byte buffer allocator used to allocate message body /// - Returns: Response - public func response(version: HTTPVersion, allocator: ByteBufferAllocator) -> HBHTTPResponse { - var headers: HTTPHeaders = self.headers - + public func response(allocator: ByteBufferAllocator) -> HBHTTPResponse { let body: HBResponseBody if let buffer = self.body(allocator: allocator) { - body = .byteBuffer(buffer) - headers.replaceOrAdd(name: "content-length", value: String(describing: buffer.readableBytes)) + body = .init(byteBuffer: buffer) } else { - body = .empty - headers.replaceOrAdd(name: "content-length", value: "0") + body = .init() } - let responseHead = HTTPResponseHead(version: version, status: self.status, headers: headers) - return .init(head: responseHead, body: body) + return .init(status: status, headers: headers, body: body) } } diff --git a/Sources/HummingbirdCoreAsync/Request/HTTPRequest.swift b/Sources/HummingbirdCore/Request/HTTPRequest.swift similarity index 100% rename from Sources/HummingbirdCoreAsync/Request/HTTPRequest.swift rename to Sources/HummingbirdCore/Request/HTTPRequest.swift diff --git a/Sources/HummingbirdCore/Request/RequestBody.swift b/Sources/HummingbirdCore/Request/RequestBody.swift index eaae073c8..6a906faf0 100644 --- a/Sources/HummingbirdCore/Request/RequestBody.swift +++ b/Sources/HummingbirdCore/Request/RequestBody.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2021 the Hummingbird authors +// Copyright (c) 2023 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,103 +12,50 @@ // //===----------------------------------------------------------------------===// +import AsyncAlgorithms import NIOCore +import NIOHTTP1 -/// Request Body. Either a ByteBuffer or a ByteBuffer streamer -public enum HBRequestBody: Sendable { - /// Static ByteBuffer - case byteBuffer(ByteBuffer?) - /// ByteBuffer streamer - case stream(HBByteBufferStreamer) +/// A type that represents an HTTP request body. +public struct HBRequestBody: Sendable, AsyncSequence { + public typealias Element = ByteBuffer - /// Return as ByteBuffer - public var buffer: ByteBuffer? { - switch self { - case .byteBuffer(let buffer): - return buffer - default: - preconditionFailure("Cannot get buffer on streaming RequestBody") - } - } + public struct AsyncIterator: AsyncIteratorProtocol { + public typealias Element = ByteBuffer + + fileprivate var underlyingIterator: AsyncThrowingChannel.AsyncIterator - /// Return as streamer if it is a streamer - public var stream: HBStreamerProtocol? { - switch self { - case .stream(let streamer): - return streamer - case .byteBuffer(let buffer): - guard let buffer = buffer else { - return nil - } - return HBStaticStreamer(buffer) + public mutating func next() async throws -> ByteBuffer? { + try await self.underlyingIterator.next() } } - /// Provide body as a single ByteBuffer - /// - Parameter eventLoop: EventLoop to use - /// - Returns: EventLoopFuture that will be fulfilled with ByteBuffer. If no body is include then return `nil` - @available(*, deprecated, message: "Use the version of `consumeBody` which sets a maximum size for the resultant ByteBuffer") - public func consumeBody(on eventLoop: EventLoop) -> EventLoopFuture { - switch self { - case .byteBuffer(let buffer): - return eventLoop.makeSucceededFuture(buffer) - case .stream(let streamer): - return streamer.collate(maxSize: .max).hop(to: eventLoop) - } + /// HBRequestBody is internally represented by AsyncThrowingChannel + private var channel: AsyncThrowingChannel + + /// Creates a new HTTP request body + public init() { + self.channel = .init() } - /// Provide body as a single ByteBuffer - /// - Parameters - /// - maxSize: Maximum size of ByteBuffer to generate - /// - eventLoop: EventLoop to use - /// - Returns: EventLoopFuture that will be fulfilled with ByteBuffer. If no body is include then return `nil` - public func consumeBody(maxSize: Int, on eventLoop: EventLoop) -> EventLoopFuture { - switch self { - case .byteBuffer(let buffer): - return eventLoop.makeSucceededFuture(buffer) - case .stream(let streamer): - return streamer.collate(maxSize: maxSize).hop(to: eventLoop) - } + public func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(underlyingIterator: self.channel.makeAsyncIterator()) } } -extension HBRequestBody: CustomStringConvertible { - public var description: String { - let maxOutput = 256 - switch self { - case .byteBuffer(let buffer): - guard var buffer2 = buffer else { return "empty" } - if let string = buffer2.readString(length: min(maxOutput, buffer2.readableBytes)), - string.allSatisfy(\.isASCII) - { - if buffer2.readableBytes > 0 { - return "\"\(string)...\"" - } else { - return "\"\(string)\"" - } - } else { - return "\(buffer!.readableBytes) bytes" - } +extension HBRequestBody { + /// push a single ByteBuffer to the HTTP request body stream + func send(_ buffer: ByteBuffer) async { + await self.channel.send(buffer) + } - case .stream: - return "byte stream" - } + /// pass error to HTTP request body + func fail(_ error: Error) { + self.channel.fail(error) } -} -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension HBRequestBody { - /// Provide body as a single ByteBuffer - /// - Parameters - /// - maxSize: Maximum size of ByteBuffer to generate - /// - eventLoop: EventLoop to use - /// - Returns: EventLoopFuture that will be fulfilled with ByteBuffer. If no body is include then return `nil` - public func consumeBody(maxSize: Int) async throws -> ByteBuffer? { - switch self { - case .byteBuffer(let buffer): - return buffer - case .stream(let streamer): - return try await streamer.collate(maxSize: maxSize).get() - } + /// Finish HTTP request body stream + func finish() { + self.channel.finish() } } diff --git a/Sources/HummingbirdCoreAsync/Response/HTTPResponse.swift b/Sources/HummingbirdCore/Response/HTTPResponse.swift similarity index 100% rename from Sources/HummingbirdCoreAsync/Response/HTTPResponse.swift rename to Sources/HummingbirdCore/Response/HTTPResponse.swift diff --git a/Sources/HummingbirdCore/Response/ResponseBody.swift b/Sources/HummingbirdCore/Response/ResponseBody.swift index 166a90977..3fe6fd0ff 100644 --- a/Sources/HummingbirdCore/Response/ResponseBody.swift +++ b/Sources/HummingbirdCore/Response/ResponseBody.swift @@ -14,141 +14,42 @@ import NIOCore -/// Function returning streamed byte buffer output -public typealias HBStreamCallback = @Sendable (EventLoop) -> EventLoopFuture - -/// Response body. Can be a single ByteBuffer, a stream of ByteBuffers or empty -public enum HBResponseBody: Sendable { - /// Body stored as a single ByteBuffer - case byteBuffer(ByteBuffer) - /// Streamer object supplying byte buffers - case stream(HBResponseBodyStreamer) - /// Empty body - case empty - - /// Construct a `HBResponseBody` from a closure supplying `ByteBuffer`'s. - /// - /// This function should supply `.byteBuffer(ByteBuffer)` until there is no more data, at which - /// point is should return `'end`. - /// - /// - Parameter closure: Closure called whenever a new ByteBuffer is needed - public static func stream(_ streamer: HBStreamerProtocol) -> Self { - .stream(ResponseByteBufferStreamer(streamer: streamer)) - } - - /// Construct a `HBResponseBody` from a closure supplying `ByteBuffer`'s. - /// - /// This function should supply `.byteBuffer(ByteBuffer)` until there is no more data, at which - /// point is should return `'end`. - /// - /// - Parameter closure: Closure called whenever a new ByteBuffer is needed - public static func streamCallback(_ closure: @escaping HBStreamCallback) -> Self { - .stream(ResponseBodyStreamerCallback(closure: closure)) - } +public protocol HBResponseBodyWriter { + func write(_ buffer: ByteBuffer) async throws } -extension HBResponseBody: CustomStringConvertible { - public var description: String { - let maxOutput = 256 - switch self { - case .empty: - return "empty" - - case .byteBuffer(let buffer): - var buffer2 = buffer - if let string = buffer2.readString(length: min(maxOutput, buffer2.readableBytes)), - string.allSatisfy(\.isASCII) - { - if buffer2.readableBytes > 0 { - return "\"\(string)...\"" - } else { - return "\"\(string)\"" - } - } else { - return "\(buffer.readableBytes) bytes" - } +/// Response body +public struct HBResponseBody: Sendable { + let write: @Sendable (any HBResponseBodyWriter) async throws -> Void + let contentLength: Int? - case .stream: - return "byte stream" - } + /// Initialise HBResponseBody with closure writing body contents + /// - Parameters: + /// - contentLength: Optional length of body + /// - write: closure provided with `writer` type that can be used to write to response body + public init(contentLength: Int? = nil, _ write: @Sendable @escaping (any HBResponseBodyWriter) async throws -> Void) { + self.write = write + self.contentLength = contentLength } -} -/// Object supplying ByteBuffers for a response body -public protocol HBResponseBodyStreamer: Sendable { - func read(on eventLoop: EventLoop) -> EventLoopFuture -} - -extension HBResponseBodyStreamer { - /// Call closure for every ByteBuffer streamed - /// - Returns: When everything has been streamed - @preconcurrency - func write(on eventLoop: EventLoop, _ writeCallback: @escaping @Sendable (ByteBuffer) -> Void) -> EventLoopFuture { - let promise = eventLoop.makePromise(of: Void.self) - @Sendable func _stream() { - self.read(on: eventLoop).whenComplete { result in - switch result { - case .success(.byteBuffer(let buffer)): - writeCallback(buffer) - _stream() - case .success(.end): - promise.succeed(()) - case .failure(let error): - promise.fail(error) - } - } - } - _stream() - return promise.futureResult - } -} - -/// Response body that you can feed ByteBuffers -struct ResponseByteBufferStreamer: HBResponseBodyStreamer { - let streamer: HBStreamerProtocol - - /// Read ByteBuffer from streamer. - /// - /// This is used internally when serializing the response body - /// - Parameter eventLoop: EventLoop everything runs on - /// - Returns: Streamer output (ByteBuffer or end of stream) - func read(on eventLoop: EventLoop) -> EventLoopFuture { - return self.streamer.consume(on: eventLoop) + /// Initialise empty HBResponseBody + public init() { + self.init(contentLength: 0) { _ in } } -} - -struct ResponseBodyStreamerCallback: HBResponseBodyStreamer { - /// Closure called whenever a new ByteBuffer is needed - let closure: HBStreamCallback - - /// Read ByteBuffer from streamer. - /// - /// This is used internally when serializing the response body - /// - Parameter eventLoop: EventLoop everything runs on - /// - Returns: Streamer output (ByteBuffer or end of stream) - func read(on eventLoop: EventLoop) -> EventLoopFuture { - return self.closure(eventLoop) - } -} - -/// Response body streamer which uses an AsyncSequence as its input. -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public final class AsyncSequenceResponseBodyStreamer: HBResponseBodyStreamer where ByteBufferSequence.Element == ByteBuffer { - var iterator: ByteBufferSequence.AsyncIterator - public init(_ asyncSequence: ByteBufferSequence) { - self.iterator = asyncSequence.makeAsyncIterator() + /// Initialise HBResponseBody that contains a single ByteBuffer + /// - Parameter byteBuffer: ByteBuffer to write + public init(byteBuffer: ByteBuffer) { + self.init(contentLength: byteBuffer.readableBytes) { writer in try await writer.write(byteBuffer) } } - public func read(on eventLoop: EventLoop) -> EventLoopFuture { - let promise = eventLoop.makePromise(of: HBStreamerOutput.self) - promise.completeWithTask { - if let buffer = try await self.iterator.next() { - return .byteBuffer(buffer) - } else { - return .end + /// Initialise HBResponseBody with an AsyncSequence of ByteBuffers + /// - Parameter asyncSequence: ByteBuffer AsyncSequence + public init(asyncSequence: BufferSequence) where BufferSequence.Element == ByteBuffer { + self.init { writer in + for try await buffer in asyncSequence { + try await writer.write(buffer) } } - return promise.futureResult } } diff --git a/Sources/HummingbirdCoreAsync/Server/ChannelSetup.swift b/Sources/HummingbirdCore/Server/ChannelSetup.swift similarity index 100% rename from Sources/HummingbirdCoreAsync/Server/ChannelSetup.swift rename to Sources/HummingbirdCore/Server/ChannelSetup.swift diff --git a/Sources/HummingbirdCoreAsync/Server/HTTP/HTTP1Channel.swift b/Sources/HummingbirdCore/Server/HTTP/HTTP1Channel.swift similarity index 94% rename from Sources/HummingbirdCoreAsync/Server/HTTP/HTTP1Channel.swift rename to Sources/HummingbirdCore/Server/HTTP/HTTP1Channel.swift index 185e5830c..ad4725abc 100644 --- a/Sources/HummingbirdCoreAsync/Server/HTTP/HTTP1Channel.swift +++ b/Sources/HummingbirdCore/Server/HTTP/HTTP1Channel.swift @@ -22,7 +22,7 @@ public struct HTTP1Channel: HTTPChannelSetup { public init( additionalChannelHandlers: @autoclosure @escaping @Sendable () -> [any RemovableChannelHandler] = [], - _ responder: @escaping @Sendable (HBHTTPRequest, Channel) async throws -> HBHTTPResponse + responder: @escaping @Sendable (HBHTTPRequest, Channel) async throws -> HBHTTPResponse ) { self.additionalChannelHandlers = additionalChannelHandlers self.responder = responder diff --git a/Sources/HummingbirdCoreAsync/Server/HTTP/HTTPChannelSetup.swift b/Sources/HummingbirdCore/Server/HTTP/HTTPChannelSetup.swift similarity index 100% rename from Sources/HummingbirdCoreAsync/Server/HTTP/HTTPChannelSetup.swift rename to Sources/HummingbirdCore/Server/HTTP/HTTPChannelSetup.swift diff --git a/Sources/HummingbirdCoreAsync/Server/HTTPSendableResponseChannelHandler.swift b/Sources/HummingbirdCore/Server/HTTPSendableResponseChannelHandler.swift similarity index 77% rename from Sources/HummingbirdCoreAsync/Server/HTTPSendableResponseChannelHandler.swift rename to Sources/HummingbirdCore/Server/HTTPSendableResponseChannelHandler.swift index 0f0b44f71..b1e074f7d 100644 --- a/Sources/HummingbirdCoreAsync/Server/HTTPSendableResponseChannelHandler.swift +++ b/Sources/HummingbirdCore/Server/HTTPSendableResponseChannelHandler.swift @@ -19,11 +19,13 @@ import NIOHTTP1 public typealias SendableHTTPServerResponsePart = HTTPPart /// Channel to convert HTTPServerResponsePart to the Sendable type HBHTTPServerResponsePart -final class HBHTTPSendableResponseChannelHandler: ChannelOutboundHandler, RemovableChannelHandler { - typealias OutboundIn = SendableHTTPServerResponsePart - typealias OutboundOut = HTTPServerResponsePart +public final class HBHTTPSendableResponseChannelHandler: ChannelOutboundHandler, RemovableChannelHandler { + public typealias OutboundIn = SendableHTTPServerResponsePart + public typealias OutboundOut = HTTPServerResponsePart - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + public init() {} + + public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { let part = unwrapOutboundIn(data) switch part { case .head(let head): diff --git a/Sources/HummingbirdCoreAsync/Server/HTTPUserEventHandler.swift b/Sources/HummingbirdCore/Server/HTTPUserEventHandler.swift similarity index 82% rename from Sources/HummingbirdCoreAsync/Server/HTTPUserEventHandler.swift rename to Sources/HummingbirdCore/Server/HTTPUserEventHandler.swift index e96ce58b1..0faebe505 100644 --- a/Sources/HummingbirdCoreAsync/Server/HTTPUserEventHandler.swift +++ b/Sources/HummingbirdCore/Server/HTTPUserEventHandler.swift @@ -16,22 +16,22 @@ import Logging import NIOCore import NIOHTTP1 -class HBHTTPUserEventHandler: ChannelDuplexHandler, RemovableChannelHandler { - typealias InboundIn = HTTPServerRequestPart - typealias InboundOut = HTTPServerRequestPart - typealias OutboundIn = HTTPServerResponsePart - typealias OutboundOut = HTTPServerResponsePart +public class HBHTTPUserEventHandler: ChannelDuplexHandler, RemovableChannelHandler { + public typealias InboundIn = HTTPServerRequestPart + public typealias InboundOut = HTTPServerRequestPart + public typealias OutboundIn = HTTPServerResponsePart + public typealias OutboundOut = HTTPServerResponsePart var closeAfterResponseWritten: Bool = false var requestsBeingRead: Int = 0 var requestsInProgress: Int = 0 let logger: Logger - init(logger: Logger) { + public init(logger: Logger) { self.logger = logger } - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { let part = unwrapOutboundIn(data) if case .end = part { self.requestsInProgress -= 1 @@ -45,7 +45,7 @@ class HBHTTPUserEventHandler: ChannelDuplexHandler, RemovableChannelHandler { } } - func channelRead(context: ChannelHandlerContext, data: NIOAny) { + public func channelRead(context: ChannelHandlerContext, data: NIOAny) { let part = self.unwrapInboundIn(data) switch part { case .head: @@ -62,7 +62,7 @@ class HBHTTPUserEventHandler: ChannelDuplexHandler, RemovableChannelHandler { context.fireChannelRead(data) } - func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { + public func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { switch event { case is ChannelShouldQuiesceEvent: // we received a quiesce event. If we have any requests in progress we should diff --git a/Sources/HummingbirdCoreAsync/Server/Server.swift b/Sources/HummingbirdCore/Server/Server.swift similarity index 100% rename from Sources/HummingbirdCoreAsync/Server/Server.swift rename to Sources/HummingbirdCore/Server/Server.swift diff --git a/Sources/HummingbirdCoreAsync/Server/ServerConfiguration.swift b/Sources/HummingbirdCore/Server/ServerConfiguration.swift similarity index 100% rename from Sources/HummingbirdCoreAsync/Server/ServerConfiguration.swift rename to Sources/HummingbirdCore/Server/ServerConfiguration.swift diff --git a/Sources/HummingbirdCoreAsync/Request/RequestBody.swift b/Sources/HummingbirdCoreAsync/Request/RequestBody.swift deleted file mode 100644 index 6a906faf0..000000000 --- a/Sources/HummingbirdCoreAsync/Request/RequestBody.swift +++ /dev/null @@ -1,61 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Hummingbird server framework project -// -// Copyright (c) 2023 the Hummingbird authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AsyncAlgorithms -import NIOCore -import NIOHTTP1 - -/// A type that represents an HTTP request body. -public struct HBRequestBody: Sendable, AsyncSequence { - public typealias Element = ByteBuffer - - public struct AsyncIterator: AsyncIteratorProtocol { - public typealias Element = ByteBuffer - - fileprivate var underlyingIterator: AsyncThrowingChannel.AsyncIterator - - public mutating func next() async throws -> ByteBuffer? { - try await self.underlyingIterator.next() - } - } - - /// HBRequestBody is internally represented by AsyncThrowingChannel - private var channel: AsyncThrowingChannel - - /// Creates a new HTTP request body - public init() { - self.channel = .init() - } - - public func makeAsyncIterator() -> AsyncIterator { - AsyncIterator(underlyingIterator: self.channel.makeAsyncIterator()) - } -} - -extension HBRequestBody { - /// push a single ByteBuffer to the HTTP request body stream - func send(_ buffer: ByteBuffer) async { - await self.channel.send(buffer) - } - - /// pass error to HTTP request body - func fail(_ error: Error) { - self.channel.fail(error) - } - - /// Finish HTTP request body stream - func finish() { - self.channel.finish() - } -} diff --git a/Sources/HummingbirdCoreAsync/Response/ResponseBody.swift b/Sources/HummingbirdCoreAsync/Response/ResponseBody.swift deleted file mode 100644 index 3fe6fd0ff..000000000 --- a/Sources/HummingbirdCoreAsync/Response/ResponseBody.swift +++ /dev/null @@ -1,55 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Hummingbird server framework project -// -// Copyright (c) 2021-2021 the Hummingbird authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIOCore - -public protocol HBResponseBodyWriter { - func write(_ buffer: ByteBuffer) async throws -} - -/// Response body -public struct HBResponseBody: Sendable { - let write: @Sendable (any HBResponseBodyWriter) async throws -> Void - let contentLength: Int? - - /// Initialise HBResponseBody with closure writing body contents - /// - Parameters: - /// - contentLength: Optional length of body - /// - write: closure provided with `writer` type that can be used to write to response body - public init(contentLength: Int? = nil, _ write: @Sendable @escaping (any HBResponseBodyWriter) async throws -> Void) { - self.write = write - self.contentLength = contentLength - } - - /// Initialise empty HBResponseBody - public init() { - self.init(contentLength: 0) { _ in } - } - - /// Initialise HBResponseBody that contains a single ByteBuffer - /// - Parameter byteBuffer: ByteBuffer to write - public init(byteBuffer: ByteBuffer) { - self.init(contentLength: byteBuffer.readableBytes) { writer in try await writer.write(byteBuffer) } - } - - /// Initialise HBResponseBody with an AsyncSequence of ByteBuffers - /// - Parameter asyncSequence: ByteBuffer AsyncSequence - public init(asyncSequence: BufferSequence) where BufferSequence.Element == ByteBuffer { - self.init { writer in - for try await buffer in asyncSequence { - try await writer.write(buffer) - } - } - } -} diff --git a/Sources/HummingbirdCore/AsyncAwaitSupport/RequestBodyStreamer+async.swift b/Sources/HummingbirdCoreOld/AsyncAwaitSupport/RequestBodyStreamer+async.swift similarity index 100% rename from Sources/HummingbirdCore/AsyncAwaitSupport/RequestBodyStreamer+async.swift rename to Sources/HummingbirdCoreOld/AsyncAwaitSupport/RequestBodyStreamer+async.swift diff --git a/Sources/HummingbirdCore/AsyncAwaitSupport/Sendable.swift b/Sources/HummingbirdCoreOld/AsyncAwaitSupport/Sendable.swift similarity index 100% rename from Sources/HummingbirdCore/AsyncAwaitSupport/Sendable.swift rename to Sources/HummingbirdCoreOld/AsyncAwaitSupport/Sendable.swift diff --git a/Sources/HummingbirdCoreAsync/Error/HTTPError.swift b/Sources/HummingbirdCoreOld/Error/HTTPError.swift similarity index 100% rename from Sources/HummingbirdCoreAsync/Error/HTTPError.swift rename to Sources/HummingbirdCoreOld/Error/HTTPError.swift diff --git a/Sources/HummingbirdCoreAsync/Error/HTTPErrorResponse.swift b/Sources/HummingbirdCoreOld/Error/HTTPErrorResponse.swift similarity index 71% rename from Sources/HummingbirdCoreAsync/Error/HTTPErrorResponse.swift rename to Sources/HummingbirdCoreOld/Error/HTTPErrorResponse.swift index 5ae6b4d09..eb9532391 100644 --- a/Sources/HummingbirdCoreAsync/Error/HTTPErrorResponse.swift +++ b/Sources/HummingbirdCoreOld/Error/HTTPErrorResponse.swift @@ -32,13 +32,18 @@ extension HBHTTPResponseError { /// Generate response from error /// - Parameter allocator: Byte buffer allocator used to allocate message body /// - Returns: Response - public func response(allocator: ByteBufferAllocator) -> HBHTTPResponse { + public func response(version: HTTPVersion, allocator: ByteBufferAllocator) -> HBHTTPResponse { + var headers: HTTPHeaders = self.headers + let body: HBResponseBody if let buffer = self.body(allocator: allocator) { - body = .init(byteBuffer: buffer) + body = .byteBuffer(buffer) + headers.replaceOrAdd(name: "content-length", value: String(describing: buffer.readableBytes)) } else { - body = .init() + body = .empty + headers.replaceOrAdd(name: "content-length", value: "0") } - return .init(status: status, headers: headers, body: body) + let responseHead = HTTPResponseHead(version: version, status: self.status, headers: headers) + return .init(head: responseHead, body: body) } } diff --git a/Sources/HummingbirdCore/HTTPResponder.swift b/Sources/HummingbirdCoreOld/HTTPResponder.swift similarity index 100% rename from Sources/HummingbirdCore/HTTPResponder.swift rename to Sources/HummingbirdCoreOld/HTTPResponder.swift diff --git a/Sources/HummingbirdCore/Request/ByteBufferStreamer.swift b/Sources/HummingbirdCoreOld/Request/ByteBufferStreamer.swift similarity index 100% rename from Sources/HummingbirdCore/Request/ByteBufferStreamer.swift rename to Sources/HummingbirdCoreOld/Request/ByteBufferStreamer.swift diff --git a/Sources/HummingbirdCore/Request/Request.swift b/Sources/HummingbirdCoreOld/Request/Request.swift similarity index 100% rename from Sources/HummingbirdCore/Request/Request.swift rename to Sources/HummingbirdCoreOld/Request/Request.swift diff --git a/Sources/HummingbirdCoreOld/Request/RequestBody.swift b/Sources/HummingbirdCoreOld/Request/RequestBody.swift new file mode 100644 index 000000000..eaae073c8 --- /dev/null +++ b/Sources/HummingbirdCoreOld/Request/RequestBody.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2021 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +/// Request Body. Either a ByteBuffer or a ByteBuffer streamer +public enum HBRequestBody: Sendable { + /// Static ByteBuffer + case byteBuffer(ByteBuffer?) + /// ByteBuffer streamer + case stream(HBByteBufferStreamer) + + /// Return as ByteBuffer + public var buffer: ByteBuffer? { + switch self { + case .byteBuffer(let buffer): + return buffer + default: + preconditionFailure("Cannot get buffer on streaming RequestBody") + } + } + + /// Return as streamer if it is a streamer + public var stream: HBStreamerProtocol? { + switch self { + case .stream(let streamer): + return streamer + case .byteBuffer(let buffer): + guard let buffer = buffer else { + return nil + } + return HBStaticStreamer(buffer) + } + } + + /// Provide body as a single ByteBuffer + /// - Parameter eventLoop: EventLoop to use + /// - Returns: EventLoopFuture that will be fulfilled with ByteBuffer. If no body is include then return `nil` + @available(*, deprecated, message: "Use the version of `consumeBody` which sets a maximum size for the resultant ByteBuffer") + public func consumeBody(on eventLoop: EventLoop) -> EventLoopFuture { + switch self { + case .byteBuffer(let buffer): + return eventLoop.makeSucceededFuture(buffer) + case .stream(let streamer): + return streamer.collate(maxSize: .max).hop(to: eventLoop) + } + } + + /// Provide body as a single ByteBuffer + /// - Parameters + /// - maxSize: Maximum size of ByteBuffer to generate + /// - eventLoop: EventLoop to use + /// - Returns: EventLoopFuture that will be fulfilled with ByteBuffer. If no body is include then return `nil` + public func consumeBody(maxSize: Int, on eventLoop: EventLoop) -> EventLoopFuture { + switch self { + case .byteBuffer(let buffer): + return eventLoop.makeSucceededFuture(buffer) + case .stream(let streamer): + return streamer.collate(maxSize: maxSize).hop(to: eventLoop) + } + } +} + +extension HBRequestBody: CustomStringConvertible { + public var description: String { + let maxOutput = 256 + switch self { + case .byteBuffer(let buffer): + guard var buffer2 = buffer else { return "empty" } + if let string = buffer2.readString(length: min(maxOutput, buffer2.readableBytes)), + string.allSatisfy(\.isASCII) + { + if buffer2.readableBytes > 0 { + return "\"\(string)...\"" + } else { + return "\"\(string)\"" + } + } else { + return "\(buffer!.readableBytes) bytes" + } + + case .stream: + return "byte stream" + } + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension HBRequestBody { + /// Provide body as a single ByteBuffer + /// - Parameters + /// - maxSize: Maximum size of ByteBuffer to generate + /// - eventLoop: EventLoop to use + /// - Returns: EventLoopFuture that will be fulfilled with ByteBuffer. If no body is include then return `nil` + public func consumeBody(maxSize: Int) async throws -> ByteBuffer? { + switch self { + case .byteBuffer(let buffer): + return buffer + case .stream(let streamer): + return try await streamer.collate(maxSize: maxSize).get() + } + } +} diff --git a/Sources/HummingbirdCore/Response/Response.swift b/Sources/HummingbirdCoreOld/Response/Response.swift similarity index 100% rename from Sources/HummingbirdCore/Response/Response.swift rename to Sources/HummingbirdCoreOld/Response/Response.swift diff --git a/Sources/HummingbirdCoreOld/Response/ResponseBody.swift b/Sources/HummingbirdCoreOld/Response/ResponseBody.swift new file mode 100644 index 000000000..166a90977 --- /dev/null +++ b/Sources/HummingbirdCoreOld/Response/ResponseBody.swift @@ -0,0 +1,154 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2021 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +/// Function returning streamed byte buffer output +public typealias HBStreamCallback = @Sendable (EventLoop) -> EventLoopFuture + +/// Response body. Can be a single ByteBuffer, a stream of ByteBuffers or empty +public enum HBResponseBody: Sendable { + /// Body stored as a single ByteBuffer + case byteBuffer(ByteBuffer) + /// Streamer object supplying byte buffers + case stream(HBResponseBodyStreamer) + /// Empty body + case empty + + /// Construct a `HBResponseBody` from a closure supplying `ByteBuffer`'s. + /// + /// This function should supply `.byteBuffer(ByteBuffer)` until there is no more data, at which + /// point is should return `'end`. + /// + /// - Parameter closure: Closure called whenever a new ByteBuffer is needed + public static func stream(_ streamer: HBStreamerProtocol) -> Self { + .stream(ResponseByteBufferStreamer(streamer: streamer)) + } + + /// Construct a `HBResponseBody` from a closure supplying `ByteBuffer`'s. + /// + /// This function should supply `.byteBuffer(ByteBuffer)` until there is no more data, at which + /// point is should return `'end`. + /// + /// - Parameter closure: Closure called whenever a new ByteBuffer is needed + public static func streamCallback(_ closure: @escaping HBStreamCallback) -> Self { + .stream(ResponseBodyStreamerCallback(closure: closure)) + } +} + +extension HBResponseBody: CustomStringConvertible { + public var description: String { + let maxOutput = 256 + switch self { + case .empty: + return "empty" + + case .byteBuffer(let buffer): + var buffer2 = buffer + if let string = buffer2.readString(length: min(maxOutput, buffer2.readableBytes)), + string.allSatisfy(\.isASCII) + { + if buffer2.readableBytes > 0 { + return "\"\(string)...\"" + } else { + return "\"\(string)\"" + } + } else { + return "\(buffer.readableBytes) bytes" + } + + case .stream: + return "byte stream" + } + } +} + +/// Object supplying ByteBuffers for a response body +public protocol HBResponseBodyStreamer: Sendable { + func read(on eventLoop: EventLoop) -> EventLoopFuture +} + +extension HBResponseBodyStreamer { + /// Call closure for every ByteBuffer streamed + /// - Returns: When everything has been streamed + @preconcurrency + func write(on eventLoop: EventLoop, _ writeCallback: @escaping @Sendable (ByteBuffer) -> Void) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: Void.self) + @Sendable func _stream() { + self.read(on: eventLoop).whenComplete { result in + switch result { + case .success(.byteBuffer(let buffer)): + writeCallback(buffer) + _stream() + case .success(.end): + promise.succeed(()) + case .failure(let error): + promise.fail(error) + } + } + } + _stream() + return promise.futureResult + } +} + +/// Response body that you can feed ByteBuffers +struct ResponseByteBufferStreamer: HBResponseBodyStreamer { + let streamer: HBStreamerProtocol + + /// Read ByteBuffer from streamer. + /// + /// This is used internally when serializing the response body + /// - Parameter eventLoop: EventLoop everything runs on + /// - Returns: Streamer output (ByteBuffer or end of stream) + func read(on eventLoop: EventLoop) -> EventLoopFuture { + return self.streamer.consume(on: eventLoop) + } +} + +struct ResponseBodyStreamerCallback: HBResponseBodyStreamer { + /// Closure called whenever a new ByteBuffer is needed + let closure: HBStreamCallback + + /// Read ByteBuffer from streamer. + /// + /// This is used internally when serializing the response body + /// - Parameter eventLoop: EventLoop everything runs on + /// - Returns: Streamer output (ByteBuffer or end of stream) + func read(on eventLoop: EventLoop) -> EventLoopFuture { + return self.closure(eventLoop) + } +} + +/// Response body streamer which uses an AsyncSequence as its input. +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public final class AsyncSequenceResponseBodyStreamer: HBResponseBodyStreamer where ByteBufferSequence.Element == ByteBuffer { + var iterator: ByteBufferSequence.AsyncIterator + + public init(_ asyncSequence: ByteBufferSequence) { + self.iterator = asyncSequence.makeAsyncIterator() + } + + public func read(on eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: HBStreamerOutput.self) + promise.completeWithTask { + if let buffer = try await self.iterator.next() { + return .byteBuffer(buffer) + } else { + return .end + } + } + return promise.futureResult + } +} diff --git a/Sources/HummingbirdCoreAsync/Server/BindAddress.swift b/Sources/HummingbirdCoreOld/Server/BindAddress.swift similarity index 100% rename from Sources/HummingbirdCoreAsync/Server/BindAddress.swift rename to Sources/HummingbirdCoreOld/Server/BindAddress.swift diff --git a/Sources/HummingbirdCore/Server/ChannelInitializer.swift b/Sources/HummingbirdCoreOld/Server/ChannelInitializer.swift similarity index 100% rename from Sources/HummingbirdCore/Server/ChannelInitializer.swift rename to Sources/HummingbirdCoreOld/Server/ChannelInitializer.swift diff --git a/Sources/HummingbirdCore/Server/HTTPServer+Configuration.swift b/Sources/HummingbirdCoreOld/Server/HTTPServer+Configuration.swift similarity index 100% rename from Sources/HummingbirdCore/Server/HTTPServer+Configuration.swift rename to Sources/HummingbirdCoreOld/Server/HTTPServer+Configuration.swift diff --git a/Sources/HummingbirdCore/Server/HTTPServer+ServiceLifecycle.swift b/Sources/HummingbirdCoreOld/Server/HTTPServer+ServiceLifecycle.swift similarity index 100% rename from Sources/HummingbirdCore/Server/HTTPServer+ServiceLifecycle.swift rename to Sources/HummingbirdCoreOld/Server/HTTPServer+ServiceLifecycle.swift diff --git a/Sources/HummingbirdCore/Server/HTTPServer.swift b/Sources/HummingbirdCoreOld/Server/HTTPServer.swift similarity index 100% rename from Sources/HummingbirdCore/Server/HTTPServer.swift rename to Sources/HummingbirdCoreOld/Server/HTTPServer.swift diff --git a/Sources/HummingbirdCore/Server/HTTPServerHandler.swift b/Sources/HummingbirdCoreOld/Server/HTTPServerHandler.swift similarity index 100% rename from Sources/HummingbirdCore/Server/HTTPServerHandler.swift rename to Sources/HummingbirdCoreOld/Server/HTTPServerHandler.swift diff --git a/Sources/HummingbirdCoreAsync/Server/TSTLSOptions.swift b/Sources/HummingbirdCoreOld/Server/TSTLSOptions.swift similarity index 100% rename from Sources/HummingbirdCoreAsync/Server/TSTLSOptions.swift rename to Sources/HummingbirdCoreOld/Server/TSTLSOptions.swift diff --git a/Sources/HummingbirdTLS/ChannelInitializer.swift b/Sources/HummingbirdTLS/ChannelInitializer.swift deleted file mode 100644 index bae03bf91..000000000 --- a/Sources/HummingbirdTLS/ChannelInitializer.swift +++ /dev/null @@ -1,50 +0,0 @@ -import HummingbirdCore -import NIOCore -import NIOHTTP1 -import NIOSSL - -/// Setup child channel for HTTP1 with TLS -public struct HTTP1WithTLSChannel: HBChannelInitializer { - public init(tlsConfiguration: TLSConfiguration, upgraders: [HTTPServerProtocolUpgrader] = []) throws { - var tlsConfiguration = tlsConfiguration - tlsConfiguration.applicationProtocols.append("http/1.1") - self.sslContext = try NIOSSLContext(configuration: tlsConfiguration) - self.upgraders = upgraders - } - - /// Initialize HTTP1 channel - /// - Parameters: - /// - channel: channel - /// - childHandlers: Channel handlers to add - /// - configuration: server configuration - public func initialize(channel: Channel, childHandlers: [RemovableChannelHandler], configuration: HBHTTPServer.Configuration) -> EventLoopFuture { - var serverUpgrade: NIOHTTPServerUpgradeConfiguration? - if self.upgraders.count > 0 { - let loopBoundChildHandlers = NIOLoopBound(childHandlers, eventLoop: channel.eventLoop) - serverUpgrade = (self.upgraders, { channel in - // remove HTTP handlers after upgrade - loopBoundChildHandlers.value.forEach { - _ = channel.pipeline.removeHandler($0) - } - }) - } - return channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHandler(NIOSSLServerHandler(context: self.sslContext)) - try channel.pipeline.syncOperations.configureHTTPServerPipeline( - withPipeliningAssistance: configuration.withPipeliningAssistance, - withServerUpgrade: serverUpgrade, - withErrorHandling: true - ) - try channel.pipeline.syncOperations.addHandlers(childHandlers) - } - } - - /// Add protocol upgrader to channel initializer - /// - Parameter upgrader: HTTP server protocol upgrader to add - public mutating func addProtocolUpgrader(_ upgrader: HTTPServerProtocolUpgrader) { - self.upgraders.append(upgrader) - } - - let sslContext: NIOSSLContext - var upgraders: [HTTPServerProtocolUpgrader] -} diff --git a/Sources/HummingbirdTLS/TLSChannelSetup.swift b/Sources/HummingbirdTLS/TLSChannelSetup.swift new file mode 100644 index 000000000..318673e67 --- /dev/null +++ b/Sources/HummingbirdTLS/TLSChannelSetup.swift @@ -0,0 +1,68 @@ +import HummingbirdCore +import Logging +import NIOCore +import NIOHTTP1 +import NIOSSL + +/// Setup child channel for HTTP1 with TLS +public struct HTTP1WithTLSChannel: HTTPChannelSetup { + public typealias In = HTTPServerRequestPart + public typealias Out = SendableHTTPServerResponsePart + + public init( + tlsConfiguration: TLSConfiguration, + additionalChannelHandlers: @autoclosure @escaping @Sendable () -> [any RemovableChannelHandler] = [], + responder: @escaping @Sendable (HBHTTPRequest, Channel) async throws -> HBHTTPResponse + ) throws { + var tlsConfiguration = tlsConfiguration + tlsConfiguration.applicationProtocols.append("http/1.1") + self.sslContext = try NIOSSLContext(configuration: tlsConfiguration) + self.additionalChannelHandlers = additionalChannelHandlers + self.responder = responder + } + + public func initialize(channel: Channel, configuration: HBServerConfiguration, logger: Logger) -> EventLoopFuture { + let childChannelHandlers: [RemovableChannelHandler] = self.additionalChannelHandlers() + [ + HBHTTPUserEventHandler(logger: logger), + HBHTTPSendableResponseChannelHandler(), + ] + return channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.addHandler(NIOSSLServerHandler(context: self.sslContext)) + try channel.pipeline.syncOperations.configureHTTPServerPipeline( + withPipeliningAssistance: configuration.withPipeliningAssistance, + withErrorHandling: true + ) + try channel.pipeline.syncOperations.addHandlers(childChannelHandlers) + } + } + + public let responder: @Sendable (HBHTTPRequest, Channel) async throws -> HBHTTPResponse + let sslContext: NIOSSLContext + let additionalChannelHandlers: @Sendable () -> [any RemovableChannelHandler] +} + +/* public struct HTTP1WithTLSChannel: HBChannelSetup { + public init(tlsConfiguration: TLSConfiguration) throws { + var tlsConfiguration = tlsConfiguration + tlsConfiguration.applicationProtocols.append("http/1.1") + self.sslContext = try NIOSSLContext(configuration: tlsConfiguration) + } + + /// Initialize HTTP1 channel + /// - Parameters: + /// - channel: channel + /// - childHandlers: Channel handlers to add + /// - configuration: server configuration + public func initialize(channel: Channel, childHandlers: [RemovableChannelHandler], configuration: HBHTTPServer.Configuration) -> EventLoopFuture { + return channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.addHandler(NIOSSLServerHandler(context: self.sslContext)) + try channel.pipeline.syncOperations.configureHTTPServerPipeline( + withPipeliningAssistance: configuration.withPipeliningAssistance, + withErrorHandling: true + ) + try channel.pipeline.syncOperations.addHandlers(childHandlers) + } + } + + let sslContext: NIOSSLContext + } */ diff --git a/Tests/HummingbirdCoreAsyncTests/CoreTests.swift b/Tests/HummingbirdCoreAsyncTests/CoreTests.swift deleted file mode 100644 index f2d6d78b1..000000000 --- a/Tests/HummingbirdCoreAsyncTests/CoreTests.swift +++ /dev/null @@ -1,308 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Hummingbird server framework project -// -// Copyright (c) 2021-2021 the Hummingbird authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AsyncAlgorithms -import Atomics -import HummingbirdCoreAsync -@testable import HummingbirdCoreXCT -import Logging -import NIOCore -import NIOEmbedded -import NIOHTTP1 -import NIOPosix -import NIOTransportServices -import ServiceLifecycle -import XCTest - -class HummingBirdCoreAsyncTests: XCTestCase { - static var eventLoopGroup: EventLoopGroup! - - override class func setUp() { - #if os(iOS) - self.eventLoopGroup = NIOTSEventLoopGroup() - #else - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - #endif - } - - override class func tearDown() { - XCTAssertNoThrow(try self.eventLoopGroup.syncShutdownGracefully()) - } - - func randomBuffer(size: Int) -> ByteBuffer { - var data = [UInt8](repeating: 0, count: size) - data = data.map { _ in UInt8.random(in: 0...255) } - return ByteBufferAllocator().buffer(bytes: data) - } - - func testConnect() async throws { - var logger = Logger(label: "HB") - logger.logLevel = .trace - try await testServer( - childChannelSetup: HTTP1Channel(helloResponder), - configuration: .init(address: .hostname(port: 0)), - eventLoopGroup: Self.eventLoopGroup, - logger: logger - ) { client in - let response = try await client.get("/") - var body = try XCTUnwrap(response.body) - XCTAssertEqual(body.readString(length: body.readableBytes), "Hello") - } - } - - func testError() async throws { - try await testServer( - childChannelSetup: HTTP1Channel { _, _ in throw HBHTTPError(.unauthorized) }, - configuration: .init(address: .hostname(port: 0)), - eventLoopGroup: Self.eventLoopGroup, - logger: Logger(label: "HB") - ) { client in - let response = try await client.get("/") - XCTAssertEqual(response.status, .unauthorized) - XCTAssertEqual(response.headers["content-length"].first, "0") - } - } - - func testConsumeBody() async throws { - try await testServer( - childChannelSetup: HTTP1Channel { request, _ in - let buffer = try await request.body.collect(upTo: .max) - return HBHTTPResponse(status: .ok, body: .init(byteBuffer: buffer)) - }, - configuration: .init(address: .hostname(port: 0)), - eventLoopGroup: Self.eventLoopGroup, - logger: Logger(label: "HB") - ) { client in - let buffer = self.randomBuffer(size: 1_140_000) - let response = try await client.post("/", body: buffer) - let body = try XCTUnwrap(response.body) - XCTAssertEqual(body, buffer) - } - } - - func testWriteBody() async throws { - try await testServer( - childChannelSetup: HTTP1Channel { _, _ in - let buffer = self.randomBuffer(size: 1_140_000) - return HBHTTPResponse(status: .ok, body: .init(byteBuffer: buffer)) - }, - configuration: .init(address: .hostname(port: 0)), - eventLoopGroup: Self.eventLoopGroup, - logger: Logger(label: "HB") - ) { client in - let response = try await client.get("/") - let body = try XCTUnwrap(response.body) - XCTAssertEqual(body.readableBytes, 1_140_000) - } - } - - func testStreamBody() async throws { - try await testServer( - childChannelSetup: HTTP1Channel { request, _ in - return HBHTTPResponse(status: .ok, body: .init(asyncSequence: request.body)) - }, - configuration: .init(address: .hostname(port: 0)), - eventLoopGroup: Self.eventLoopGroup, - logger: Logger(label: "HB") - ) { client in - let buffer = self.randomBuffer(size: 1_140_000) - let response = try await client.post("/", body: buffer) - let body = try XCTUnwrap(response.body) - XCTAssertEqual(body, buffer) - } - } - - func testStreamBodyWriteSlow() async throws { - try await testServer( - childChannelSetup: HTTP1Channel { request, _ in - return HBHTTPResponse(status: .ok, body: .init(asyncSequence: request.body.delayed())) - }, - configuration: .init(address: .hostname(port: 0)), - eventLoopGroup: Self.eventLoopGroup, - logger: Logger(label: "HB") - ) { client in - let buffer = self.randomBuffer(size: 1_140_000) - let response = try await client.post("/", body: buffer) - let body = try XCTUnwrap(response.body) - XCTAssertEqual(body, buffer) - } - } - - func testStreamBodySlowStream() async throws { - /// channel handler that delays the sending of data - class SlowInputChannelHandler: ChannelOutboundHandler, RemovableChannelHandler { - public typealias OutboundIn = Never - public typealias OutboundOut = HTTPServerResponsePart - - func read(context: ChannelHandlerContext) { - let loopBoundContext = NIOLoopBound(context, eventLoop: context.eventLoop) - context.eventLoop.scheduleTask(in: .milliseconds(Int64.random(in: 5..<50))) { - loopBoundContext.value.read() - } - } - } - try await testServer( - childChannelSetup: HTTP1Channel(additionalChannelHandlers: [SlowInputChannelHandler()]) { request, _ in - return HBHTTPResponse(status: .ok, body: .init(asyncSequence: request.body.delayed())) - }, - configuration: .init(address: .hostname(port: 0)), - eventLoopGroup: Self.eventLoopGroup, - logger: Logger(label: "HB") - ) { client in - let buffer = self.randomBuffer(size: 1_140_000) - let response = try await client.post("/", body: buffer) - let body = try XCTUnwrap(response.body) - XCTAssertEqual(body, buffer) - } - } - - func testChannelHandlerErrorPropagation() async throws { - class CreateErrorHandler: ChannelInboundHandler, RemovableChannelHandler { - typealias InboundIn = HTTPServerRequestPart - - var seen: Bool = false - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - if case .body = self.unwrapInboundIn(data) { - context.fireErrorCaught(HBHTTPError(.insufficientStorage)) - } - context.fireChannelRead(data) - } - } - try await testServer( - childChannelSetup: HTTP1Channel(additionalChannelHandlers: [CreateErrorHandler()]) { request, _ in - _ = try await request.body.collect(upTo: .max) - return HBHTTPResponse(status: .ok) - }, - configuration: .init(address: .hostname(port: 0)), - eventLoopGroup: Self.eventLoopGroup, - logger: Logger(label: "HB") - ) { client in - let buffer = self.randomBuffer(size: 32) - let response = try await client.post("/", body: buffer) - XCTAssertEqual(response.status, .insufficientStorage) - } - } - - func testDropRequestBody() async throws { - try await testServer( - childChannelSetup: HTTP1Channel { _, _ in - // ignore request body - return HBHTTPResponse(status: .accepted) - }, - configuration: .init(address: .hostname(port: 0)), - eventLoopGroup: Self.eventLoopGroup, - logger: Logger(label: "HB") - ) { client in - let buffer = self.randomBuffer(size: 16384) - let response = try await client.post("/", body: buffer) - XCTAssertEqual(response.status, .accepted) - let response2 = try await client.post("/", body: buffer) - XCTAssertEqual(response2.status, .accepted) - } - } - - /// test server closes connection if "connection" header is set to "close" - func testConnectionClose() async throws { - try await testServer( - childChannelSetup: HTTP1Channel(helloResponder), - configuration: .init(address: .hostname(port: 0)), - eventLoopGroup: Self.eventLoopGroup, - logger: Logger(label: "HB") - ) { client in - try await withTimeout(.seconds(5)) { - _ = try await client.get("/", headers: ["connection": "close"]) - let channel = try await client.channelPromise.futureResult.get() - try await channel.closeFuture.get() - } - } - } - - /* - func testReadIdleHandler() async throws { - /// Channel Handler for serializing request header and data - final class HTTPServerIncompleteRequest: ChannelInboundHandler, RemovableChannelHandler { - typealias InboundIn = HTTPServerRequestPart - typealias InboundOut = HTTPServerRequestPart - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let part = self.unwrapInboundIn(data) - switch part { - case .end: - break - default: - context.fireChannelRead(data) - } - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0)), - responder: HelloResponder(), - additionalChannelHandlers: [HTTPServerIncompleteRequest(), IdleStateHandler(readTimeout: .seconds(1))], - logger: Logger(label: "HB") - ) - try await testServer(server) { client in - try await withTimeout(.seconds(5)) { - do { - _ = try await client.get("/", headers: ["connection": "keep-alive"]) - XCTFail("Should not get here") - } catch HBXCTClient.Error.connectionClosing { - } catch { - XCTFail("Unexpected error: \(error)") - } - } - } - } - - func testWriteIdleTimeout() async throws { - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0)), - responder: HelloResponder(), - additionalChannelHandlers: [IdleStateHandler(writeTimeout: .seconds(1))], - logger: Logger(label: "HB") - ) - try await testServer(server) { client in - try await withTimeout(.seconds(5)) { - _ = try await client.get("/", headers: ["connection": "keep-alive"]) - let channel = try await client.channelPromise.futureResult.get() - try await channel.closeFuture.get() - } - } - }*/ -} - -struct DelayAsyncSequence: AsyncSequence { - typealias Element = CoreSequence.Element - struct AsyncIterator: AsyncIteratorProtocol { - var iterator: CoreSequence.AsyncIterator - - mutating func next() async throws -> Element? { - try await Task.sleep(for: .milliseconds(Int.random(in: 10..<100))) - return try await self.iterator.next() - } - } - - let seq: CoreSequence - - func makeAsyncIterator() -> AsyncIterator { - .init(iterator: self.seq.makeAsyncIterator()) - } -} - -extension AsyncSequence { - func delayed() -> DelayAsyncSequence { - return .init(seq: self) - } -} diff --git a/Tests/HummingbirdCoreAsyncTests/TestUtils.swift b/Tests/HummingbirdCoreAsyncTests/TestUtils.swift deleted file mode 100644 index 630347bf2..000000000 --- a/Tests/HummingbirdCoreAsyncTests/TestUtils.swift +++ /dev/null @@ -1,126 +0,0 @@ -import HummingbirdCoreAsync -import HummingbirdCoreXCT -import Logging -import NIOCore -import NIOHTTP1 -import NIOSSL -import ServiceLifecycle -import XCTest - -public enum TestErrors: Error { - case timeout -} - -/// Basic responder that just returns "Hello" in body -@Sendable public func helloResponder(to request: HBHTTPRequest, channel: Channel) async -> HBHTTPResponse { - let responseBody = channel.allocator.buffer(string: "Hello") - return HBHTTPResponse(status: .ok, body: .init(byteBuffer: responseBody)) -} - -/// Helper function for test a server -/// -/// Creates test client, runs test function abd ensures everything is -/// shutdown correctly -public func testServer( - childChannelSetup: ChannelSetup, - configuration: HBServerConfiguration, - eventLoopGroup: EventLoopGroup, - logger: Logger, - clientConfiguration: HBXCTClient.Configuration = .init(), - _ test: @escaping @Sendable (HBXCTClient) async throws -> Void -) async throws { - try await withThrowingTaskGroup(of: Void.self) { group in - let promise = Promise() - let server = HBServer( - childChannelSetup: childChannelSetup, - configuration: configuration, - onServerRunning: { await promise.complete($0.localAddress!.port!) }, - eventLoopGroup: eventLoopGroup, - logger: logger - ) - let serviceGroup = ServiceGroup( - configuration: .init( - services: [server], - gracefulShutdownSignals: [.sigterm, .sigint], - logger: logger - ) - ) - group.addTask { - try await serviceGroup.run() - } - let client = await HBXCTClient( - host: "localhost", - port: promise.wait(), - configuration: clientConfiguration, - eventLoopGroupProvider: .createNew - ) - group.addTask { - client.connect() - try await test(client) - } - var iterator = group.makeAsyncIterator() - do { - try await iterator.next() - } catch {} - await serviceGroup.triggerGracefulShutdown() - try await client.shutdown() - } -} - -/// Run process with a timeout -/// - Parameters: -/// - timeout: Amount of time before timeout error is thrown -/// - process: Process to run -public func withTimeout(_ timeout: TimeAmount, _ process: @escaping @Sendable () async throws -> Void) async throws { - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await Task.sleep(nanoseconds: numericCast(timeout.nanoseconds)) - throw TestErrors.timeout - } - group.addTask { - try await process() - } - try await group.next() - group.cancelAll() - } -} - -/// Promise type. -actor Promise { - enum State { - case blocked([CheckedContinuation]) - case unblocked(Value) - } - - var state: State - - init() { - self.state = .blocked([]) - } - - /// wait from promise to be completed - func wait() async -> Value { - switch self.state { - case .blocked(var continuations): - return await withCheckedContinuation { cont in - continuations.append(cont) - self.state = .blocked(continuations) - } - case .unblocked(let value): - return value - } - } - - /// complete promise with value - func complete(_ value: Value) { - switch self.state { - case .blocked(let continuations): - for cont in continuations { - cont.resume(returning: value) - } - self.state = .unblocked(value) - case .unblocked: - break - } - } -} diff --git a/Tests/HummingbirdCoreOldTests/CoreTests.swift b/Tests/HummingbirdCoreOldTests/CoreTests.swift new file mode 100644 index 000000000..9ea751559 --- /dev/null +++ b/Tests/HummingbirdCoreOldTests/CoreTests.swift @@ -0,0 +1,624 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2021 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AsyncAlgorithms +import Atomics +import HummingbirdCore +@testable import HummingbirdCoreXCT +import Logging +import NIOCore +import NIOEmbedded +import NIOHTTP1 +import NIOPosix +import NIOTransportServices +import ServiceLifecycle +import XCTest + +class HummingBirdCoreTests: XCTestCase { + static var eventLoopGroup: EventLoopGroup! + + override class func setUp() { + #if os(iOS) + self.eventLoopGroup = NIOTSEventLoopGroup() + #else + self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + #endif + } + + override class func tearDown() { + XCTAssertNoThrow(try self.eventLoopGroup.syncShutdownGracefully()) + } + + func randomBuffer(size: Int) -> ByteBuffer { + var data = [UInt8](repeating: 0, count: size) + data = data.map { _ in UInt8.random(in: 0...255) } + return ByteBufferAllocator().buffer(bytes: data) + } + + func testConnect() async throws { + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: HelloResponder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let response = try await client.get("/") + var body = try XCTUnwrap(response.body) + XCTAssertEqual(body.readString(length: body.readableBytes), "Hello") + } + } + + func testError() async throws { + struct ErrorResponder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + throw HBHTTPError(.unauthorized) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: ErrorResponder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let response = try await client.get("/") + XCTAssertEqual(response.status, .unauthorized) + XCTAssertEqual(response.headers["content-length"].first, "0") + } + } + + func testConsumeBody() async throws { + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + let buffer = try await request.body.consumeBody(maxSize: .max) + guard let buffer = buffer else { + throw HBHTTPError(.badRequest) + } + return HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .ok), + body: .byteBuffer(buffer) + ) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), + responder: Responder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 1_140_000) + let response = try await client.post("/", body: buffer) + let body = try XCTUnwrap(response.body) + XCTAssertEqual(body, buffer) + } + } + + func testConsumeAllBody() async throws { + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + var size = 0 + for try await buffer in request.body.stream!.sequence { + size += buffer.readableBytes + } + return HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .ok), + body: .byteBuffer(channel.allocator.buffer(integer: size)) + ) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), + responder: Responder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 450_000) + let response = try await client.post("/", body: buffer) + var body = try XCTUnwrap(response.body) + XCTAssertEqual(body.readInteger(), buffer.readableBytes) + } + } + + func testConsumeBodyInTask() async throws { + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + do { + let buffer = try await request.body.consumeBody(maxSize: .max) + guard let buffer = buffer else { + throw HBHTTPError(.badRequest) + } + let response = HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .ok), + body: .byteBuffer(buffer) + ) + return response + } + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), + responder: Responder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 1_140_000) + let response = try await client.post("/", body: buffer) + let body = try XCTUnwrap(response.body) + XCTAssertEqual(body, buffer) + } + } + + func testStreamBody() async throws { + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + let eventLoop = channel.eventLoop + let body: HBResponseBody = .streamCallback { _ in + return request.body.stream!.consume(on: eventLoop).map { output in + switch output { + case .byteBuffer(let buffer): + return .byteBuffer(buffer) + case .end: + return .end + } + } + } + let response = HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .ok), + body: body + ) + return response + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), + responder: Responder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 1_140_000) + let response = try await client.post("/", body: buffer) + let body = try XCTUnwrap(response.body) + XCTAssertEqual(body, buffer) + } + } + + func testStreamBody2() async throws { + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + let streamer = AsyncSequenceResponseBodyStreamer(request.body.stream!.sequence) + return HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .ok), + body: .stream(streamer) + ) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), + responder: Responder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 1_140_000) + let response = try await client.post("/", body: buffer) + let body = try XCTUnwrap(response.body) + XCTAssertEqual(body, buffer) + } + } + + func testStreamBodySlowProcess() async throws { + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + let streamer = AsyncSequenceResponseBodyStreamer(request.body.stream!.sequence.delayed()) + return HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .ok), + body: .stream(streamer) + ) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), + responder: Responder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 1_140_000) + let response = try await client.post("/", body: buffer) + let body = try XCTUnwrap(response.body) + XCTAssertEqual(body, buffer) + } + } + + func testStreamBodySlowStream() async throws { + /// channel handler that delays the sending of data + class SlowInputChannelHandler: ChannelOutboundHandler, RemovableChannelHandler { + public typealias OutboundIn = Never + public typealias OutboundOut = HTTPServerResponsePart + + func read(context: ChannelHandlerContext) { + let loopBoundContext = NIOLoopBound(context, eventLoop: context.eventLoop) + context.eventLoop.scheduleTask(in: .milliseconds(Int64.random(in: 25..<200))) { + loopBoundContext.value.read() + } + } + } + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + let streamer = AsyncSequenceResponseBodyStreamer(request.body.stream!.sequence) + return HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .ok), + body: .stream(streamer) + ) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: Responder(), + additionalChannelHandlers: [SlowInputChannelHandler()], + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 1_140_000) + let response = try await client.post("/", body: buffer) + let body = try XCTUnwrap(response.body) + XCTAssertEqual(body, buffer) + } + } + + func testChannelHandlerErrorPropagation() async throws { + class CreateErrorHandler: ChannelInboundHandler, RemovableChannelHandler { + typealias InboundIn = HTTPServerRequestPart + + var seen: Bool = false + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + if case .body = self.unwrapInboundIn(data) { + context.fireErrorCaught(HBHTTPError(.insufficientStorage)) + } + context.fireChannelRead(data) + } + } + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + return HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .accepted), + body: .empty + ) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: Responder(), + additionalChannelHandlers: [CreateErrorHandler()], + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 32) + let response = try await client.post("/", body: buffer) + XCTAssertEqual(response.status, .insufficientStorage) + } + } + + func testStreamedRequestDrop() async throws { + /// Embedded channels pass all the data down immediately. This is not a real world situation so this handler + /// can be used to fake TCP/IP data packets coming in arbitrary sizes (well at least for the HTTP body) + class BreakupHTTPBodyChannelHandler: ChannelInboundHandler, RemovableChannelHandler { + typealias InboundIn = HTTPServerRequestPart + typealias InboundOut = HTTPServerRequestPart + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let part = unwrapInboundIn(data) + + switch part { + case .head, .end: + context.fireChannelRead(data) + case .body(var buffer): + while buffer.readableBytes > 0 { + let size = min(Int.random(in: 16...8192), buffer.readableBytes) + let slice = buffer.readSlice(length: size)! + context.fireChannelRead(self.wrapInboundOut(.body(slice))) + } + } + } + } + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + XCTAssertNotNil(request.body.stream) + return HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .accepted), + body: .empty + ) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: Responder(), + additionalChannelHandlers: [BreakupHTTPBodyChannelHandler()], + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 16384) + let response = try await client.post("/", body: buffer) + XCTAssertEqual(response.status, .accepted) + } + } + + func testMaxStreamedUploadSize() async throws { + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + guard let buffer = try await request.body.consumeBody(maxSize: .max) else { + throw HBHTTPError(.badRequest) + } + return HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .ok), + body: .byteBuffer(buffer) + ) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0), maxUploadSize: 64 * 1024), + responder: Responder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 320_000) + let response = try await client.post("/", body: buffer) + XCTAssertEqual(response.status, .payloadTooLarge) + } + } + + func testMaxUploadSize() async throws { + struct Responder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + guard let buffer = try await request.body.consumeBody(maxSize: 64 * 1024) else { + throw HBHTTPError(.badRequest) + } + return HBHTTPResponse( + head: .init(version: .init(major: 1, minor: 1), status: .ok), + body: .byteBuffer(buffer) + ) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0), maxUploadSize: 64 * 1024), + responder: Responder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let buffer = self.randomBuffer(size: 320_000) + let response = try await client.post("/", body: buffer) + XCTAssertEqual(response.status, .payloadTooLarge) + } + } + + /// test a request is finished with before the next one starts to be processed + func testHTTPPipelining() async throws { + struct WaitResponder: HBHTTPResponder { + func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + guard let wait = request.head.headers["wait"].first.map({ Int64($0) }) ?? nil else { + throw HBHTTPError(.badRequest) + } + try await Task.sleep(for: .milliseconds(wait)) + let responseHead = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok) + let responseBody = channel.allocator.buffer(string: "\(wait)") + return HBHTTPResponse(head: responseHead, body: .byteBuffer(responseBody)) + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0), maxUploadSize: 64 * 1024), + responder: WaitResponder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + try await withThrowingTaskGroup(of: Void.self) { group in + let waitTimes: [Int] = (0..<16).map { _ in Int.random(in: 0..<50) } + for time in waitTimes { + group.addTask { + let headers: HTTPHeaders = ["wait": String(describing: time), "connection": "keep-alive"] + let response = try await client.get("/", headers: headers) + XCTAssertEqual(response.body.map { String(buffer: $0) }, "\(time)") + } + } + try await group.waitForAll() + } + } + } + + /// test server closes connection if "connection" header is set to "close" + func testConnectionClose() async throws { + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: HelloResponder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + try await withTimeout(.seconds(5)) { + _ = try await client.get("/", headers: ["connection": "close"]) + let channel = try await client.channelPromise.futureResult.get() + try await channel.closeFuture.get() + } + } + } + + func testBodyDescription() { + XCTAssertEqual(HBRequestBody.byteBuffer(nil).description, "empty") + XCTAssertEqual(HBRequestBody.byteBuffer(self.randomBuffer(size: 64)).description, "64 bytes") + XCTAssertEqual(HBRequestBody.byteBuffer(.init(string: "Test String")).description, "\"Test String\"") + } + + func testReadIdleHandler() async throws { + /// Channel Handler for serializing request header and data + final class HTTPServerIncompleteRequest: ChannelInboundHandler, RemovableChannelHandler { + typealias InboundIn = HTTPServerRequestPart + typealias InboundOut = HTTPServerRequestPart + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let part = self.unwrapInboundIn(data) + switch part { + case .end: + break + default: + context.fireChannelRead(data) + } + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: HelloResponder(), + additionalChannelHandlers: [HTTPServerIncompleteRequest(), IdleStateHandler(readTimeout: .seconds(1))], + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + try await withTimeout(.seconds(5)) { + do { + _ = try await client.get("/", headers: ["connection": "keep-alive"]) + XCTFail("Should not get here") + } catch HBXCTClient.Error.connectionClosing { + } catch { + XCTFail("Unexpected error: \(error)") + } + } + } + } + + func testWriteIdleTimeout() async throws { + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: HelloResponder(), + additionalChannelHandlers: [IdleStateHandler(writeTimeout: .seconds(1))], + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + try await withTimeout(.seconds(5)) { + _ = try await client.get("/", headers: ["connection": "keep-alive"]) + let channel = try await client.channelPromise.futureResult.get() + try await channel.closeFuture.get() + } + } + } + + func testServerAsService() async throws { + actor Promise { + enum State { + case blocked([CheckedContinuation]) + case unblocked(Value) + } + + var state: State + + init() { + self.state = .blocked([]) + } + + func wait() async -> Value { + switch self.state { + case .blocked(var continuations): + return await withCheckedContinuation { cont in + continuations.append(cont) + self.state = .blocked(continuations) + } + case .unblocked(let value): + return value + } + } + + func complete(_ value: Value) { + switch self.state { + case .blocked(let continuations): + for cont in continuations { + cont.resume(returning: value) + } + self.state = .unblocked(value) + case .unblocked: + self.state = .unblocked(value) + } + } + } + + let promise = Promise() + var logger = Logger(label: "HB") + logger.logLevel = .trace + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: HelloResponder(), + onServerRunning: { channel in await promise.complete(channel.localAddress!.port!) }, + logger: logger + ) + try await withThrowingTaskGroup(of: Void.self) { group in + let serviceGroup = await ServiceGroup( + configuration: .init( + services: [server], + gracefulShutdownSignals: [.sigterm, .sigint], + logger: server.logger + ) + ) + group.addTask { + try await serviceGroup.run() + } + let port = await promise.wait() + let client = HBXCTClient( + host: "localhost", + port: port, + configuration: .init(timeout: .seconds(2)), + eventLoopGroupProvider: .createNew + ) + client.connect() + group.addTask { + _ = try await client.get("/") + } + try await group.next() + await serviceGroup.triggerGracefulShutdown() + try await client.shutdown() + } + } +} + +struct DelayAsyncSequence: AsyncSequence { + typealias Element = CoreSequence.Element + struct AsyncIterator: AsyncIteratorProtocol { + var iterator: CoreSequence.AsyncIterator + + mutating func next() async throws -> Element? { + try await Task.sleep(for: .milliseconds(Int.random(in: 10..<100))) + return try await self.iterator.next() + } + } + + let seq: CoreSequence + + func makeAsyncIterator() -> AsyncIterator { + .init(iterator: self.seq.makeAsyncIterator()) + } +} + +extension AsyncSequence { + func delayed() -> DelayAsyncSequence { + return .init(seq: self) + } +} diff --git a/Tests/HummingbirdCoreTests/StreamerTests.swift b/Tests/HummingbirdCoreOldTests/StreamerTests.swift similarity index 100% rename from Tests/HummingbirdCoreTests/StreamerTests.swift rename to Tests/HummingbirdCoreOldTests/StreamerTests.swift diff --git a/Tests/HummingbirdCoreOldTests/TLSTests.swift b/Tests/HummingbirdCoreOldTests/TLSTests.swift new file mode 100644 index 000000000..38bed6500 --- /dev/null +++ b/Tests/HummingbirdCoreOldTests/TLSTests.swift @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2021 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import HummingbirdCore +import HummingbirdCoreXCT +import HummingbirdTLS +import Logging +import NIOCore +import NIOHTTP1 +import NIOPosix +import NIOSSL +import NIOTransportServices +import XCTest + +class HummingBirdTLSTests: XCTestCase { + func testConnect() async throws { + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2) + defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } + let server = try HBHTTPServer( + group: eventLoopGroup, + configuration: .init(address: .hostname(port: 0), serverName: testServerName), + responder: HelloResponder(), + childChannelInitializer: HTTP1WithTLSChannel(tlsConfiguration: self.getServerTLSConfiguration()), + logger: Logger(label: "HB") + ) + try await testServer( + server, + clientConfiguration: .init(tlsConfiguration: self.getClientTLSConfiguration(), serverName: testServerName) + ) { client in + let response = try await client.get("/") + var body = try XCTUnwrap(response.body) + XCTAssertEqual(body.readString(length: body.readableBytes), "Hello") + } + } + + func getServerTLSConfiguration() throws -> TLSConfiguration { + let caCertificate = try NIOSSLCertificate(bytes: [UInt8](caCertificateData.utf8), format: .pem) + let certificate = try NIOSSLCertificate(bytes: [UInt8](serverCertificateData.utf8), format: .pem) + let privateKey = try NIOSSLPrivateKey(bytes: [UInt8](serverPrivateKeyData.utf8), format: .pem) + var tlsConfig = TLSConfiguration.makeServerConfiguration(certificateChain: [.certificate(certificate)], privateKey: .privateKey(privateKey)) + tlsConfig.trustRoots = .certificates([caCertificate]) + return tlsConfig + } + + func getClientTLSConfiguration() throws -> TLSConfiguration { + let caCertificate = try NIOSSLCertificate(bytes: [UInt8](caCertificateData.utf8), format: .pem) + let certificate = try NIOSSLCertificate(bytes: [UInt8](clientCertificateData.utf8), format: .pem) + let privateKey = try NIOSSLPrivateKey(bytes: [UInt8](clientPrivateKeyData.utf8), format: .pem) + var tlsConfig = TLSConfiguration.makeClientConfiguration() + tlsConfig.trustRoots = .certificates([caCertificate]) + tlsConfig.certificateChain = [.certificate(certificate)] + tlsConfig.privateKey = .privateKey(privateKey) + return tlsConfig + } +} diff --git a/Tests/HummingbirdCoreOldTests/TSTests.swift b/Tests/HummingbirdCoreOldTests/TSTests.swift new file mode 100644 index 000000000..ba6e1af4d --- /dev/null +++ b/Tests/HummingbirdCoreOldTests/TSTests.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2021 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(Network) + +import HummingbirdCore +import HummingbirdCoreXCT +import Logging +import Network +import NIOCore +import NIOHTTP1 +import NIOSSL +import NIOTransportServices +import XCTest + +class TransportServicesTests: XCTestCase { + func randomBuffer(size: Int) -> ByteBuffer { + var data = [UInt8](repeating: 0, count: size) + data = data.map { _ in UInt8.random(in: 0...255) } + return ByteBufferAllocator().buffer(bytes: data) + } + + func testConnect() async throws { + let eventLoopGroup = NIOTSEventLoopGroup() + let server = HBHTTPServer( + group: eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: HelloResponder(), + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + let response = try await client.get("/") + var body = try XCTUnwrap(response.body) + XCTAssertEqual(body.readString(length: body.readableBytes), "Hello") + } + } + + func testTLS() async throws { + let eventLoopGroup = NIOTSEventLoopGroup() + let p12Path = Bundle.module.path(forResource: "server", ofType: "p12")! + let tlsOptions = try XCTUnwrap(TSTLSOptions.options( + serverIdentity: .p12(filename: p12Path, password: "MyPassword") + )) + let configuration = HBHTTPServer.Configuration( + address: .hostname(port: 0), + serverName: testServerName, + tlsOptions: tlsOptions + ) + let server = HBHTTPServer( + group: eventLoopGroup, + configuration: configuration, + responder: HelloResponder(), + logger: Logger(label: "HB") + ) + try await testServer( + server, + clientConfiguration: .init(tlsConfiguration: self.getClientTLSConfiguration(), serverName: testServerName) + ) { client in + let response = try await client.get("/") + var body = try XCTUnwrap(response.body) + XCTAssertEqual(body.readString(length: body.readableBytes), "Hello") + } + } + + func getClientTLSConfiguration() throws -> TLSConfiguration { + let caCertificate = try NIOSSLCertificate(bytes: [UInt8](caCertificateData.utf8), format: .pem) + let certificate = try NIOSSLCertificate(bytes: [UInt8](clientCertificateData.utf8), format: .pem) + let privateKey = try NIOSSLPrivateKey(bytes: [UInt8](clientPrivateKeyData.utf8), format: .pem) + var tlsConfig = TLSConfiguration.makeClientConfiguration() + tlsConfig.trustRoots = .certificates([caCertificate]) + tlsConfig.certificateChain = [.certificate(certificate)] + tlsConfig.privateKey = .privateKey(privateKey) + return tlsConfig + } +} + +#endif diff --git a/Tests/HummingbirdCoreOldTests/TestUtils.swift b/Tests/HummingbirdCoreOldTests/TestUtils.swift new file mode 100644 index 000000000..df59c6302 --- /dev/null +++ b/Tests/HummingbirdCoreOldTests/TestUtils.swift @@ -0,0 +1,65 @@ +import HummingbirdCore +import HummingbirdCoreXCT +import NIOCore +import NIOHTTP1 +import NIOSSL +import XCTest + +public enum TestErrors: Error { + case timeout +} + +/// Basic responder that just returns "Hello" in body +public struct HelloResponder: HBHTTPResponder { + public func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { + let responseHead = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok) + let responseBody = channel.allocator.buffer(string: "Hello") + return HBHTTPResponse(head: responseHead, body: .byteBuffer(responseBody)) + } +} + +/// Helper function for test a server +/// +/// Creates test client, runs test function abd ensures everything is +/// shutdown correctly +public func testServer( + _ server: HBHTTPServer, + clientConfiguration: HBXCTClient.Configuration = .init(), + _ test: (HBXCTClient) async throws -> Void +) async throws { + try await server.start() + let client = await HBXCTClient( + host: "localhost", + port: server.port!, + configuration: clientConfiguration, + eventLoopGroupProvider: .createNew + ) + client.connect() + do { + try await test(client) + } catch { + try await client.shutdown() + try await server.shutdownGracefully() + throw error + } + try await client.shutdown() + try await server.shutdownGracefully() +} + +/// Run process with a timeout +/// - Parameters: +/// - timeout: Amount of time before timeout error is thrown +/// - process: Process to run +public func withTimeout(_ timeout: TimeAmount, _ process: @escaping @Sendable () async throws -> Void) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await Task.sleep(nanoseconds: numericCast(timeout.nanoseconds)) + throw TestErrors.timeout + } + group.addTask { + try await process() + } + try await group.next() + group.cancelAll() + } +} diff --git a/Tests/HummingbirdCoreTests/CoreTests.swift b/Tests/HummingbirdCoreTests/CoreTests.swift index 9ea751559..9cbd864ba 100644 --- a/Tests/HummingbirdCoreTests/CoreTests.swift +++ b/Tests/HummingbirdCoreTests/CoreTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2021 the Hummingbird authors +// Copyright (c) 2023 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -47,13 +47,12 @@ class HummingBirdCoreTests: XCTestCase { } func testConnect() async throws { - let server = HBHTTPServer( - group: Self.eventLoopGroup, + try await testServer( + childChannelSetup: HTTP1Channel(responder: helloResponder), configuration: .init(address: .hostname(port: 0)), - responder: HelloResponder(), + eventLoopGroup: Self.eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in + ) { client in let response = try await client.get("/") var body = try XCTUnwrap(response.body) XCTAssertEqual(body.readString(length: body.readableBytes), "Hello") @@ -61,18 +60,12 @@ class HummingBirdCoreTests: XCTestCase { } func testError() async throws { - struct ErrorResponder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - throw HBHTTPError(.unauthorized) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, + try await testServer( + childChannelSetup: HTTP1Channel { _, _ in throw HBHTTPError(.unauthorized) }, configuration: .init(address: .hostname(port: 0)), - responder: ErrorResponder(), + eventLoopGroup: Self.eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in + ) { client in let response = try await client.get("/") XCTAssertEqual(response.status, .unauthorized) XCTAssertEqual(response.headers["content-length"].first, "0") @@ -80,25 +73,15 @@ class HummingBirdCoreTests: XCTestCase { } func testConsumeBody() async throws { - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - let buffer = try await request.body.consumeBody(maxSize: .max) - guard let buffer = buffer else { - throw HBHTTPError(.badRequest) - } - return HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .ok), - body: .byteBuffer(buffer) - ) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), - responder: Responder(), + try await testServer( + childChannelSetup: HTTP1Channel { request, _ in + let buffer = try await request.body.collect(upTo: .max) + return HBHTTPResponse(status: .ok, body: .init(byteBuffer: buffer)) + }, + configuration: .init(address: .hostname(port: 0)), + eventLoopGroup: Self.eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in + ) { client in let buffer = self.randomBuffer(size: 1_140_000) let response = try await client.post("/", body: buffer) let body = try XCTUnwrap(response.body) @@ -106,115 +89,31 @@ class HummingBirdCoreTests: XCTestCase { } } - func testConsumeAllBody() async throws { - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - var size = 0 - for try await buffer in request.body.stream!.sequence { - size += buffer.readableBytes - } - return HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .ok), - body: .byteBuffer(channel.allocator.buffer(integer: size)) - ) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), - responder: Responder(), - logger: Logger(label: "HB") - ) - try await testServer(server) { client in - let buffer = self.randomBuffer(size: 450_000) - let response = try await client.post("/", body: buffer) - var body = try XCTUnwrap(response.body) - XCTAssertEqual(body.readInteger(), buffer.readableBytes) - } - } - - func testConsumeBodyInTask() async throws { - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - do { - let buffer = try await request.body.consumeBody(maxSize: .max) - guard let buffer = buffer else { - throw HBHTTPError(.badRequest) - } - let response = HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .ok), - body: .byteBuffer(buffer) - ) - return response - } - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), - responder: Responder(), + func testWriteBody() async throws { + try await testServer( + childChannelSetup: HTTP1Channel { _, _ in + let buffer = self.randomBuffer(size: 1_140_000) + return HBHTTPResponse(status: .ok, body: .init(byteBuffer: buffer)) + }, + configuration: .init(address: .hostname(port: 0)), + eventLoopGroup: Self.eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in - let buffer = self.randomBuffer(size: 1_140_000) - let response = try await client.post("/", body: buffer) + ) { client in + let response = try await client.get("/") let body = try XCTUnwrap(response.body) - XCTAssertEqual(body, buffer) + XCTAssertEqual(body.readableBytes, 1_140_000) } } func testStreamBody() async throws { - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - let eventLoop = channel.eventLoop - let body: HBResponseBody = .streamCallback { _ in - return request.body.stream!.consume(on: eventLoop).map { output in - switch output { - case .byteBuffer(let buffer): - return .byteBuffer(buffer) - case .end: - return .end - } - } - } - let response = HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .ok), - body: body - ) - return response - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), - responder: Responder(), - logger: Logger(label: "HB") - ) - try await testServer(server) { client in - let buffer = self.randomBuffer(size: 1_140_000) - let response = try await client.post("/", body: buffer) - let body = try XCTUnwrap(response.body) - XCTAssertEqual(body, buffer) - } - } - - func testStreamBody2() async throws { - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - let streamer = AsyncSequenceResponseBodyStreamer(request.body.stream!.sequence) - return HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .ok), - body: .stream(streamer) - ) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), - responder: Responder(), + try await testServer( + childChannelSetup: HTTP1Channel { request, _ in + return HBHTTPResponse(status: .ok, body: .init(asyncSequence: request.body)) + }, + configuration: .init(address: .hostname(port: 0)), + eventLoopGroup: Self.eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in + ) { client in let buffer = self.randomBuffer(size: 1_140_000) let response = try await client.post("/", body: buffer) let body = try XCTUnwrap(response.body) @@ -222,23 +121,15 @@ class HummingBirdCoreTests: XCTestCase { } } - func testStreamBodySlowProcess() async throws { - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - let streamer = AsyncSequenceResponseBodyStreamer(request.body.stream!.sequence.delayed()) - return HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .ok), - body: .stream(streamer) - ) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0), maxStreamingBufferSize: 256 * 1024), - responder: Responder(), + func testStreamBodyWriteSlow() async throws { + try await testServer( + childChannelSetup: HTTP1Channel { request, _ in + return HBHTTPResponse(status: .ok, body: .init(asyncSequence: request.body.delayed())) + }, + configuration: .init(address: .hostname(port: 0)), + eventLoopGroup: Self.eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in + ) { client in let buffer = self.randomBuffer(size: 1_140_000) let response = try await client.post("/", body: buffer) let body = try XCTUnwrap(response.body) @@ -254,28 +145,19 @@ class HummingBirdCoreTests: XCTestCase { func read(context: ChannelHandlerContext) { let loopBoundContext = NIOLoopBound(context, eventLoop: context.eventLoop) - context.eventLoop.scheduleTask(in: .milliseconds(Int64.random(in: 25..<200))) { + context.eventLoop.scheduleTask(in: .milliseconds(Int64.random(in: 5..<50))) { loopBoundContext.value.read() } } } - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - let streamer = AsyncSequenceResponseBodyStreamer(request.body.stream!.sequence) - return HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .ok), - body: .stream(streamer) - ) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, + try await testServer( + childChannelSetup: HTTP1Channel(additionalChannelHandlers: [SlowInputChannelHandler()]) { request, _ in + return HBHTTPResponse(status: .ok, body: .init(asyncSequence: request.body.delayed())) + }, configuration: .init(address: .hostname(port: 0)), - responder: Responder(), - additionalChannelHandlers: [SlowInputChannelHandler()], + eventLoopGroup: Self.eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in + ) { client in let buffer = self.randomBuffer(size: 1_140_000) let response = try await client.post("/", body: buffer) let body = try XCTUnwrap(response.body) @@ -295,166 +177,47 @@ class HummingBirdCoreTests: XCTestCase { context.fireChannelRead(data) } } - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - return HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .accepted), - body: .empty - ) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, + try await testServer( + childChannelSetup: HTTP1Channel(additionalChannelHandlers: [CreateErrorHandler()]) { request, _ in + _ = try await request.body.collect(upTo: .max) + return HBHTTPResponse(status: .ok) + }, configuration: .init(address: .hostname(port: 0)), - responder: Responder(), - additionalChannelHandlers: [CreateErrorHandler()], + eventLoopGroup: Self.eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in + ) { client in let buffer = self.randomBuffer(size: 32) let response = try await client.post("/", body: buffer) XCTAssertEqual(response.status, .insufficientStorage) } } - func testStreamedRequestDrop() async throws { - /// Embedded channels pass all the data down immediately. This is not a real world situation so this handler - /// can be used to fake TCP/IP data packets coming in arbitrary sizes (well at least for the HTTP body) - class BreakupHTTPBodyChannelHandler: ChannelInboundHandler, RemovableChannelHandler { - typealias InboundIn = HTTPServerRequestPart - typealias InboundOut = HTTPServerRequestPart - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let part = unwrapInboundIn(data) - - switch part { - case .head, .end: - context.fireChannelRead(data) - case .body(var buffer): - while buffer.readableBytes > 0 { - let size = min(Int.random(in: 16...8192), buffer.readableBytes) - let slice = buffer.readSlice(length: size)! - context.fireChannelRead(self.wrapInboundOut(.body(slice))) - } - } - } - } - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - XCTAssertNotNil(request.body.stream) - return HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .accepted), - body: .empty - ) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, + func testDropRequestBody() async throws { + try await testServer( + childChannelSetup: HTTP1Channel { _, _ in + // ignore request body + return HBHTTPResponse(status: .accepted) + }, configuration: .init(address: .hostname(port: 0)), - responder: Responder(), - additionalChannelHandlers: [BreakupHTTPBodyChannelHandler()], + eventLoopGroup: Self.eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in + ) { client in let buffer = self.randomBuffer(size: 16384) let response = try await client.post("/", body: buffer) XCTAssertEqual(response.status, .accepted) - } - } - - func testMaxStreamedUploadSize() async throws { - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - guard let buffer = try await request.body.consumeBody(maxSize: .max) else { - throw HBHTTPError(.badRequest) - } - return HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .ok), - body: .byteBuffer(buffer) - ) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0), maxUploadSize: 64 * 1024), - responder: Responder(), - logger: Logger(label: "HB") - ) - try await testServer(server) { client in - let buffer = self.randomBuffer(size: 320_000) - let response = try await client.post("/", body: buffer) - XCTAssertEqual(response.status, .payloadTooLarge) - } - } - - func testMaxUploadSize() async throws { - struct Responder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - guard let buffer = try await request.body.consumeBody(maxSize: 64 * 1024) else { - throw HBHTTPError(.badRequest) - } - return HBHTTPResponse( - head: .init(version: .init(major: 1, minor: 1), status: .ok), - body: .byteBuffer(buffer) - ) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0), maxUploadSize: 64 * 1024), - responder: Responder(), - logger: Logger(label: "HB") - ) - try await testServer(server) { client in - let buffer = self.randomBuffer(size: 320_000) - let response = try await client.post("/", body: buffer) - XCTAssertEqual(response.status, .payloadTooLarge) - } - } - - /// test a request is finished with before the next one starts to be processed - func testHTTPPipelining() async throws { - struct WaitResponder: HBHTTPResponder { - func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - guard let wait = request.head.headers["wait"].first.map({ Int64($0) }) ?? nil else { - throw HBHTTPError(.badRequest) - } - try await Task.sleep(for: .milliseconds(wait)) - let responseHead = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok) - let responseBody = channel.allocator.buffer(string: "\(wait)") - return HBHTTPResponse(head: responseHead, body: .byteBuffer(responseBody)) - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0), maxUploadSize: 64 * 1024), - responder: WaitResponder(), - logger: Logger(label: "HB") - ) - try await testServer(server) { client in - try await withThrowingTaskGroup(of: Void.self) { group in - let waitTimes: [Int] = (0..<16).map { _ in Int.random(in: 0..<50) } - for time in waitTimes { - group.addTask { - let headers: HTTPHeaders = ["wait": String(describing: time), "connection": "keep-alive"] - let response = try await client.get("/", headers: headers) - XCTAssertEqual(response.body.map { String(buffer: $0) }, "\(time)") - } - } - try await group.waitForAll() - } + let response2 = try await client.post("/", body: buffer) + XCTAssertEqual(response2.status, .accepted) } } /// test server closes connection if "connection" header is set to "close" func testConnectionClose() async throws { - let server = HBHTTPServer( - group: Self.eventLoopGroup, + try await testServer( + childChannelSetup: HTTP1Channel(responder: helloResponder), configuration: .init(address: .hostname(port: 0)), - responder: HelloResponder(), + eventLoopGroup: Self.eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in + ) { client in try await withTimeout(.seconds(5)) { _ = try await client.get("/", headers: ["connection": "close"]) let channel = try await client.channelPromise.futureResult.get() @@ -463,140 +226,59 @@ class HummingBirdCoreTests: XCTestCase { } } - func testBodyDescription() { - XCTAssertEqual(HBRequestBody.byteBuffer(nil).description, "empty") - XCTAssertEqual(HBRequestBody.byteBuffer(self.randomBuffer(size: 64)).description, "64 bytes") - XCTAssertEqual(HBRequestBody.byteBuffer(.init(string: "Test String")).description, "\"Test String\"") - } - - func testReadIdleHandler() async throws { - /// Channel Handler for serializing request header and data - final class HTTPServerIncompleteRequest: ChannelInboundHandler, RemovableChannelHandler { - typealias InboundIn = HTTPServerRequestPart - typealias InboundOut = HTTPServerRequestPart - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let part = self.unwrapInboundIn(data) - switch part { - case .end: - break - default: - context.fireChannelRead(data) - } - } - } - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0)), - responder: HelloResponder(), - additionalChannelHandlers: [HTTPServerIncompleteRequest(), IdleStateHandler(readTimeout: .seconds(1))], - logger: Logger(label: "HB") - ) - try await testServer(server) { client in - try await withTimeout(.seconds(5)) { - do { - _ = try await client.get("/", headers: ["connection": "keep-alive"]) - XCTFail("Should not get here") - } catch HBXCTClient.Error.connectionClosing { - } catch { - XCTFail("Unexpected error: \(error)") - } - } - } - } - - func testWriteIdleTimeout() async throws { - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0)), - responder: HelloResponder(), - additionalChannelHandlers: [IdleStateHandler(writeTimeout: .seconds(1))], - logger: Logger(label: "HB") - ) - try await testServer(server) { client in - try await withTimeout(.seconds(5)) { - _ = try await client.get("/", headers: ["connection": "keep-alive"]) - let channel = try await client.channelPromise.futureResult.get() - try await channel.closeFuture.get() - } - } - } - - func testServerAsService() async throws { - actor Promise { - enum State { - case blocked([CheckedContinuation]) - case unblocked(Value) - } - - var state: State - - init() { - self.state = .blocked([]) - } - - func wait() async -> Value { - switch self.state { - case .blocked(var continuations): - return await withCheckedContinuation { cont in - continuations.append(cont) - self.state = .blocked(continuations) - } - case .unblocked(let value): - return value - } - } - - func complete(_ value: Value) { - switch self.state { - case .blocked(let continuations): - for cont in continuations { - cont.resume(returning: value) - } - self.state = .unblocked(value) - case .unblocked: - self.state = .unblocked(value) - } - } - } - - let promise = Promise() - var logger = Logger(label: "HB") - logger.logLevel = .trace - let server = HBHTTPServer( - group: Self.eventLoopGroup, - configuration: .init(address: .hostname(port: 0)), - responder: HelloResponder(), - onServerRunning: { channel in await promise.complete(channel.localAddress!.port!) }, - logger: logger - ) - try await withThrowingTaskGroup(of: Void.self) { group in - let serviceGroup = await ServiceGroup( - configuration: .init( - services: [server], - gracefulShutdownSignals: [.sigterm, .sigint], - logger: server.logger - ) - ) - group.addTask { - try await serviceGroup.run() - } - let port = await promise.wait() - let client = HBXCTClient( - host: "localhost", - port: port, - configuration: .init(timeout: .seconds(2)), - eventLoopGroupProvider: .createNew - ) - client.connect() - group.addTask { - _ = try await client.get("/") - } - try await group.next() - await serviceGroup.triggerGracefulShutdown() - try await client.shutdown() - } - } + /* + func testReadIdleHandler() async throws { + /// Channel Handler for serializing request header and data + final class HTTPServerIncompleteRequest: ChannelInboundHandler, RemovableChannelHandler { + typealias InboundIn = HTTPServerRequestPart + typealias InboundOut = HTTPServerRequestPart + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let part = self.unwrapInboundIn(data) + switch part { + case .end: + break + default: + context.fireChannelRead(data) + } + } + } + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: HelloResponder(), + additionalChannelHandlers: [HTTPServerIncompleteRequest(), IdleStateHandler(readTimeout: .seconds(1))], + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + try await withTimeout(.seconds(5)) { + do { + _ = try await client.get("/", headers: ["connection": "keep-alive"]) + XCTFail("Should not get here") + } catch HBXCTClient.Error.connectionClosing { + } catch { + XCTFail("Unexpected error: \(error)") + } + } + } + } + + func testWriteIdleTimeout() async throws { + let server = HBHTTPServer( + group: Self.eventLoopGroup, + configuration: .init(address: .hostname(port: 0)), + responder: HelloResponder(), + additionalChannelHandlers: [IdleStateHandler(writeTimeout: .seconds(1))], + logger: Logger(label: "HB") + ) + try await testServer(server) { client in + try await withTimeout(.seconds(5)) { + _ = try await client.get("/", headers: ["connection": "keep-alive"]) + let channel = try await client.channelPromise.futureResult.get() + try await channel.closeFuture.get() + } + } + }*/ } struct DelayAsyncSequence: AsyncSequence { diff --git a/Tests/HummingbirdCoreTests/TLSTests.swift b/Tests/HummingbirdCoreTests/TLSTests.swift index 38bed6500..46cd917e4 100644 --- a/Tests/HummingbirdCoreTests/TLSTests.swift +++ b/Tests/HummingbirdCoreTests/TLSTests.swift @@ -27,15 +27,11 @@ class HummingBirdTLSTests: XCTestCase { func testConnect() async throws { let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2) defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - let server = try HBHTTPServer( - group: eventLoopGroup, - configuration: .init(address: .hostname(port: 0), serverName: testServerName), - responder: HelloResponder(), - childChannelInitializer: HTTP1WithTLSChannel(tlsConfiguration: self.getServerTLSConfiguration()), - logger: Logger(label: "HB") - ) try await testServer( - server, + childChannelSetup: HTTP1WithTLSChannel(tlsConfiguration: self.getServerTLSConfiguration(), responder: helloResponder), + configuration: .init(address: .hostname(port: 0), serverName: testServerName), + eventLoopGroup: eventLoopGroup, + logger: Logger(label: "HB"), clientConfiguration: .init(tlsConfiguration: self.getClientTLSConfiguration(), serverName: testServerName) ) { client in let response = try await client.get("/") diff --git a/Tests/HummingbirdCoreTests/TSTests.swift b/Tests/HummingbirdCoreTests/TSTests.swift index ba6e1af4d..e324d5cae 100644 --- a/Tests/HummingbirdCoreTests/TSTests.swift +++ b/Tests/HummingbirdCoreTests/TSTests.swift @@ -33,13 +33,13 @@ class TransportServicesTests: XCTestCase { func testConnect() async throws { let eventLoopGroup = NIOTSEventLoopGroup() - let server = HBHTTPServer( - group: eventLoopGroup, + defer { try? eventLoopGroup.syncShutdownGracefully() } + try await testServer( + childChannelSetup: HTTP1Channel(responder: helloResponder), configuration: .init(address: .hostname(port: 0)), - responder: HelloResponder(), + eventLoopGroup: eventLoopGroup, logger: Logger(label: "HB") - ) - try await testServer(server) { client in + ) { client in let response = try await client.get("/") var body = try XCTUnwrap(response.body) XCTAssertEqual(body.readString(length: body.readableBytes), "Hello") @@ -47,30 +47,30 @@ class TransportServicesTests: XCTestCase { } func testTLS() async throws { - let eventLoopGroup = NIOTSEventLoopGroup() - let p12Path = Bundle.module.path(forResource: "server", ofType: "p12")! - let tlsOptions = try XCTUnwrap(TSTLSOptions.options( - serverIdentity: .p12(filename: p12Path, password: "MyPassword") - )) - let configuration = HBHTTPServer.Configuration( - address: .hostname(port: 0), - serverName: testServerName, - tlsOptions: tlsOptions - ) - let server = HBHTTPServer( - group: eventLoopGroup, - configuration: configuration, - responder: HelloResponder(), - logger: Logger(label: "HB") - ) - try await testServer( - server, - clientConfiguration: .init(tlsConfiguration: self.getClientTLSConfiguration(), serverName: testServerName) - ) { client in - let response = try await client.get("/") - var body = try XCTUnwrap(response.body) - XCTAssertEqual(body.readString(length: body.readableBytes), "Hello") - } + /* let eventLoopGroup = NIOTSEventLoopGroup() + let p12Path = Bundle.module.path(forResource: "server", ofType: "p12")! + let tlsOptions = try XCTUnwrap(TSTLSOptions.options( + serverIdentity: .p12(filename: p12Path, password: "MyPassword") + )) + let configuration = HBHTTPServer.Configuration( + address: .hostname(port: 0), + serverName: testServerName, + tlsOptions: tlsOptions + ) + let server = HBHTTPServer( + group: eventLoopGroup, + configuration: configuration, + responder: HelloResponder(), + logger: Logger(label: "HB") + ) + try await testServer( + server, + clientConfiguration: .init(tlsConfiguration: self.getClientTLSConfiguration(), serverName: testServerName) + ) { client in + let response = try await client.get("/") + var body = try XCTUnwrap(response.body) + XCTAssertEqual(body.readString(length: body.readableBytes), "Hello") + }*/ } func getClientTLSConfiguration() throws -> TLSConfiguration { diff --git a/Tests/HummingbirdCoreTests/TestUtils.swift b/Tests/HummingbirdCoreTests/TestUtils.swift index df59c6302..5d9371c1e 100644 --- a/Tests/HummingbirdCoreTests/TestUtils.swift +++ b/Tests/HummingbirdCoreTests/TestUtils.swift @@ -1,8 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2023 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import HummingbirdCore import HummingbirdCoreXCT +import Logging import NIOCore import NIOHTTP1 import NIOSSL +import ServiceLifecycle import XCTest public enum TestErrors: Error { @@ -10,40 +26,59 @@ public enum TestErrors: Error { } /// Basic responder that just returns "Hello" in body -public struct HelloResponder: HBHTTPResponder { - public func respond(to request: HBHTTPRequest, channel: Channel) async throws -> HBHTTPResponse { - let responseHead = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok) - let responseBody = channel.allocator.buffer(string: "Hello") - return HBHTTPResponse(head: responseHead, body: .byteBuffer(responseBody)) - } +@Sendable public func helloResponder(to request: HBHTTPRequest, channel: Channel) async -> HBHTTPResponse { + let responseBody = channel.allocator.buffer(string: "Hello") + return HBHTTPResponse(status: .ok, body: .init(byteBuffer: responseBody)) } /// Helper function for test a server /// /// Creates test client, runs test function abd ensures everything is /// shutdown correctly -public func testServer( - _ server: HBHTTPServer, +public func testServer( + childChannelSetup: ChannelSetup, + configuration: HBServerConfiguration, + eventLoopGroup: EventLoopGroup, + logger: Logger, clientConfiguration: HBXCTClient.Configuration = .init(), - _ test: (HBXCTClient) async throws -> Void + _ test: @escaping @Sendable (HBXCTClient) async throws -> Void ) async throws { - try await server.start() - let client = await HBXCTClient( - host: "localhost", - port: server.port!, - configuration: clientConfiguration, - eventLoopGroupProvider: .createNew - ) - client.connect() - do { - try await test(client) - } catch { + try await withThrowingTaskGroup(of: Void.self) { group in + let promise = Promise() + let server = HBServer( + childChannelSetup: childChannelSetup, + configuration: configuration, + onServerRunning: { await promise.complete($0.localAddress!.port!) }, + eventLoopGroup: eventLoopGroup, + logger: logger + ) + let serviceGroup = ServiceGroup( + configuration: .init( + services: [server], + gracefulShutdownSignals: [.sigterm, .sigint], + logger: logger + ) + ) + group.addTask { + try await serviceGroup.run() + } + let client = await HBXCTClient( + host: "localhost", + port: promise.wait(), + configuration: clientConfiguration, + eventLoopGroupProvider: .createNew + ) + group.addTask { + client.connect() + try await test(client) + } + var iterator = group.makeAsyncIterator() + do { + try await iterator.next() + } catch {} + await serviceGroup.triggerGracefulShutdown() try await client.shutdown() - try await server.shutdownGracefully() - throw error } - try await client.shutdown() - try await server.shutdownGracefully() } /// Run process with a timeout @@ -63,3 +98,43 @@ public func withTimeout(_ timeout: TimeAmount, _ process: @escaping @Sendable () group.cancelAll() } } + +/// Promise type. +actor Promise { + enum State { + case blocked([CheckedContinuation]) + case unblocked(Value) + } + + var state: State + + init() { + self.state = .blocked([]) + } + + /// wait from promise to be completed + func wait() async -> Value { + switch self.state { + case .blocked(var continuations): + return await withCheckedContinuation { cont in + continuations.append(cont) + self.state = .blocked(continuations) + } + case .unblocked(let value): + return value + } + } + + /// complete promise with value + func complete(_ value: Value) { + switch self.state { + case .blocked(let continuations): + for cont in continuations { + cont.resume(returning: value) + } + self.state = .unblocked(value) + case .unblocked: + break + } + } +}