diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7d0cb72..3580dea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,12 +14,8 @@ jobs: name: Unit tests uses: apple/swift-nio/.github/workflows/unit_tests.yml@main with: - linux_nightly_next_enabled: true + linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - windows_6_0_enabled: false - windows_6_1_enabled: false - windows_nightly_next_enabled: false - windows_nightly_main_enabled: false macos-tests: name: macOS tests @@ -30,20 +26,12 @@ jobs: swift_6_1_enabled: false swift_6_2_enabled: false swift_6_3_enabled: false - macos_xcode_build_enabled: false - ios_xcode_build_enabled: false - watchos_xcode_build_enabled: false - tvos_xcode_build_enabled: false release-builds: name: Release builds uses: apple/swift-nio/.github/workflows/release_builds.yml@main with: - linux_5_10_enabled: false linux_6_0_enabled: false linux_6_1_enabled: false - linux_nightly_next_enabled: true - windows_6_0_enabled: false - windows_6_1_enabled: false - windows_nightly_next_enabled: false - windows_nightly_main_enabled: false + linux_6_2_enabled: false + linux_6_3_enabled: false diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cc281e6..a4f9315 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -20,10 +20,8 @@ jobs: name: Unit tests uses: apple/swift-nio/.github/workflows/unit_tests.yml@main with: - linux_nightly_next_enabled: true + linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - windows_nightly_next_enabled: false - windows_nightly_main_enabled: false macos-tests: name: macOS tests @@ -34,20 +32,12 @@ jobs: swift_6_1_enabled: false swift_6_2_enabled: false swift_6_3_enabled: false - macos_xcode_build_enabled: false - ios_xcode_build_enabled: false - watchos_xcode_build_enabled: false - tvos_xcode_build_enabled: false release-builds: name: Release builds uses: apple/swift-nio/.github/workflows/release_builds.yml@main with: - linux_5_10_enabled: false linux_6_0_enabled: false linux_6_1_enabled: false - linux_nightly_next_enabled: true - windows_6_0_enabled: false - windows_6_1_enabled: false - windows_nightly_next_enabled: false - windows_nightly_main_enabled: false + linux_6_2_enabled: false + linux_6_3_enabled: false diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..05a08f7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Submodules/swift-http-server"] + path = Submodules/swift-http-server + url = https://github.com/swift-server/swift-http-server.git diff --git a/.licenseignore b/.licenseignore index 9b9434a..d22ed6a 100644 --- a/.licenseignore +++ b/.licenseignore @@ -14,3 +14,4 @@ docker/* .editorconfig Scripts/* **.html +Submodules/ diff --git a/Package.swift b/Package.swift index 358a5de..a23950a 100644 --- a/Package.swift +++ b/Package.swift @@ -45,7 +45,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-http-types.git", from: "1.6.0"), .package(url: "https://github.com/apple/swift-certificates.git", from: "1.19.1"), .package(url: "https://github.com/apple/swift-log.git", from: "1.13.2"), - .package(url: "https://github.com/apple/swift-nio.git", from: "2.100.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.101.3"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.37.0"), .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.34.1"), .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.44.0"), @@ -116,11 +116,9 @@ let package = Package( "HTTPClient", // These dependencies are needed by the `swift-http-server` that // we borrowed. - .product(name: "AsyncStreaming", package: "swift-async-algorithms"), .product(name: "DequeModule", package: "swift-collections"), .product(name: "BasicContainers", package: "swift-collections"), .product(name: "X509", package: "swift-certificates"), - .product(name: "HTTPTypes", package: "swift-http-types"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), .product(name: "NIOHTTP1", package: "swift-nio"), @@ -135,6 +133,7 @@ let package = Package( package: "swift-configuration", condition: .when(traits: ["Configuration"]) ), + .product(name: "NIOExtras", package: "swift-nio-extras"), ], swiftSettings: extraSettings ), diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/HTTPRequestContext.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/HTTPRequestContext.swift deleted file mode 100644 index 9613ac8..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/HTTPRequestContext.swift +++ /dev/null @@ -1,19 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -public import HTTPAPIs - -@available(anyAppleOS 26.0, *) -public struct HTTPRequestContext: HTTPServerCapability.RequestContext { - public init() {} -} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPResponseSender.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPResponseSender.swift deleted file mode 100644 index 2b36616..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPResponseSender.swift +++ /dev/null @@ -1,141 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -public import AsyncStreaming -import BasicContainers -public import ContainersPreview -public import HTTPAPIs -public import HTTPTypes -import NIOCore -import NIOHTTPTypes -import Synchronization - -/// A NIO-backed HTTP response sender used by the test server. -/// -/// ``NIOHTTPResponseSender`` writes the response head, streams the body, and concludes with -/// optional trailing fields, all to a NIO async channel outbound writer. It also supports -/// sending informational (1xx) responses before the final response. -@available(anyAppleOS 26.0, *) -public struct NIOHTTPResponseSender: HTTPResponseSender, ~Copyable { - /// A writer for HTTP response body chunks that implements the ``CallerAsyncWriter`` protocol. - public struct ResponseBodyWriter: CallerAsyncWriter { - public typealias WriteElement = UInt8 - public typealias WriteFailure = any Error - public typealias FinalElement = HTTPFields? - - private var writer: NIOAsyncChannelOutboundWriter - private var writerState: WriterState - private var byteBuffer: ByteBuffer - - init( - writer: NIOAsyncChannelOutboundWriter, - writerState: WriterState - ) { - self.writer = writer - self.writerState = writerState - self.byteBuffer = ByteBuffer() - self.byteBuffer.reserveCapacity(1 << 14) - } - - public mutating func write & ~Copyable>( - buffer: inout Buffer - ) async throws(WriteFailure) where Buffer.Element: ~Copyable { - guard buffer.count > 0 else { return } - self.byteBuffer.clear() - var consumer = buffer.consumeAll() - // `while !done { ... }` instead of `while true { ... break }` to - // dodge a SIL ownership-verifier crash on the nightly main - // toolchain (https://github.com/swiftlang/swift/issues/89639). - var done = false - while !done { - let span = consumer.drainNext() - if span.isEmpty { - done = true - } else { - self.byteBuffer.writeBytes(span.span.bytes) - } - } - try await self.writer.write(.body(self.byteBuffer)) - } - - public consuming func finish & ~Copyable>( - buffer: inout Buffer, - finalElement: consuming HTTPFields? - ) async throws(WriteFailure) where Buffer.Element: ~Copyable { - if buffer.count > 0 { - self.byteBuffer.clear() - var consumer = buffer.consumeAll() - // See note in `write(buffer:)`. - var done = false - while !done { - let span = consumer.drainNext() - if span.isEmpty { - done = true - } else { - self.byteBuffer.writeBytes(span.span.bytes) - } - } - try await self.writer.write(.body(self.byteBuffer)) - } - try await self.writer.write(.end(finalElement)) - self.writerState.wrapped.withLock { $0.finishedWriting = true } - } - } - - public final class WriterState: Sendable { - struct Wrapped { - var finishedWriting: Bool = false - } - - let wrapped: Mutex - - public init() { - self.wrapped = .init(.init()) - } - } - - public typealias Writer = ResponseBodyWriter - - private var writer: NIOAsyncChannelOutboundWriter - private var writerState: WriterState - - init( - writer: NIOAsyncChannelOutboundWriter, - writerState: WriterState - ) { - self.writer = writer - self.writerState = writerState - } - - public func sendInformational(_ response: HTTPResponse) async throws { - precondition(response.status.kind == .informational) - try await self.writer.write(.head(response)) - } - - public consuming func send(_ response: HTTPResponse) async throws -> ResponseBodyWriter { - precondition(response.status.kind != .informational) - // TODO: This is a temporary fix that informs clients that this server does not support - // keep-alive. This server should be updated to eventually support keep-alive. - var response = response - response.headerFields[.connection] = "close" - try await self.writer.write(.head(response)) - - return ResponseBodyWriter(writer: self.writer, writerState: self.writerState) - } -} - -@available(*, unavailable) -extension NIOHTTPResponseSender: Sendable {} - -@available(*, unavailable) -extension NIOHTTPResponseSender.ResponseBodyWriter: Sendable {} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+ConnectionContext.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+ConnectionContext.swift deleted file mode 100644 index 8a31e57..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+ConnectionContext.swift +++ /dev/null @@ -1,44 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIOCore -import NIOSSL -public import X509 - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServer { - /// Connection-specific information available during request handling. - /// - /// Provides access to data such as the peer's validated certificate chain. - public struct ConnectionContext: Sendable { - var peerCertificateChainFuture: EventLoopFuture? - - init(_ peerCertificateChainFuture: EventLoopFuture? = nil) { - self.peerCertificateChainFuture = peerCertificateChainFuture - } - - /// The peer's validated certificate chain. - /// - /// This returns `nil` if a custom verification callback was not set - /// when configuring mTLS in the server configuration, or if the custom verification callback did not return the - /// derived validated chain. - public var peerCertificateChain: X509.ValidatedCertificateChain? { - get async throws { - if let certs = try await self.peerCertificateChainFuture?.get() { - return .init(uncheckedCertificateChain: try certs.map { try Certificate($0) }) - } - return nil - } - } - } -} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+HTTP1_1.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+HTTP1_1.swift deleted file mode 100644 index f15f5d0..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+HTTP1_1.swift +++ /dev/null @@ -1,89 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import HTTPAPIs -import NIOCore -import NIOEmbedded -import NIOHTTP1 -import NIOHTTPTypes -import NIOHTTPTypesHTTP1 -import NIOPosix - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServer { - func serveInsecureHTTP1_1( - bindTarget: NIOHTTPServerConfiguration.BindTarget, - handler: some HTTPServerRequestHandler, - asyncChannelConfiguration: NIOAsyncChannel.Configuration - ) async throws { - let serverChannel = try await self.setupHTTP1_1ServerChannel( - bindTarget: bindTarget, - asyncChannelConfiguration: asyncChannelConfiguration - ) - - try await _serveInsecureHTTP1_1(serverChannel: serverChannel, handler: handler) - } - - private func setupHTTP1_1ServerChannel( - bindTarget: NIOHTTPServerConfiguration.BindTarget, - asyncChannelConfiguration: NIOAsyncChannel.Configuration - ) async throws -> NIOAsyncChannel, Never> { - switch bindTarget.backing { - case .hostAndPort(let host, let port): - let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup) - .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) - .bind(host: host, port: port) { channel in - self.setupHTTP1_1ConnectionChildChannel( - channel: channel, - asyncChannelConfiguration: asyncChannelConfiguration - ) - } - - try self.addressBound(serverChannel.channel.localAddress) - - return serverChannel - } - } - - func setupHTTP1_1ConnectionChildChannel( - channel: any Channel, - asyncChannelConfiguration: NIOAsyncChannel.Configuration - ) -> EventLoopFuture> { - channel.pipeline.configureHTTPServerPipeline().flatMapThrowing { - try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: false)) - - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: asyncChannelConfiguration - ) - } - } - - func _serveInsecureHTTP1_1( - serverChannel: NIOAsyncChannel, Never>, - handler: some HTTPServerRequestHandler - ) async throws { - try await withThrowingDiscardingTaskGroup { group in - try await serverChannel.executeThenClose { inbound in - for try await http1Channel in inbound { - group.addTask { - try await self.handleRequestChannel( - channel: http1Channel, - handler: handler - ) - } - } - } - } - } -} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+ListeningAddress.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+ListeningAddress.swift deleted file mode 100644 index 2be65ef..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+ListeningAddress.swift +++ /dev/null @@ -1,143 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIOConcurrencyHelpers -import NIOCore -import NIOPosix - -enum ListeningAddressError: CustomStringConvertible, Error { - case addressOrPortNotAvailable - case unsupportedAddressType - case serverClosed - - var description: String { - switch self { - case .addressOrPortNotAvailable: - return "Unable to retrieve the bound address or port from the underlying socket" - case .unsupportedAddressType: - return "Unsupported address type: only IPv4 and IPv6 are supported" - case .serverClosed: - return """ - There is no listening address bound for this server: there may have been an error which caused the server to close, or it may have shut down. - """ - } - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServer { - func addressBound(_ address: NIOCore.SocketAddress?) throws { - switch self.listeningAddressState.withLockedValue({ $0.addressBound(address) }) { - case .succeedPromise(let promise, let boundAddress): - promise.succeed(boundAddress) - case .failPromise(let promise, let error): - promise.fail(error) - } - } - - /// The address the server is listening from. - /// - /// It is an `async` property because it will only return once the address has been successfully bound. - /// - /// - Throws: An error will be thrown if the address could not be bound or is not bound any longer because the - /// server isn't listening anymore. - public var listeningAddress: SocketAddress { - get async throws { - try await self.listeningAddressState - .withLockedValue { try $0.listeningAddressFuture } - .get() - } - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServer { - enum State { - case idle(EventLoopPromise) - case listening(EventLoopFuture) - case closedOrInvalidAddress(ListeningAddressError) - - var listeningAddressFuture: EventLoopFuture { - get throws { - switch self { - case .idle(let eventLoopPromise): - return eventLoopPromise.futureResult - case .listening(let eventLoopFuture): - return eventLoopFuture - case .closedOrInvalidAddress(let error): - throw error - } - } - } - - enum OnBound { - case succeedPromise(_ promise: EventLoopPromise, address: SocketAddress) - case failPromise(_ promise: EventLoopPromise, error: ListeningAddressError) - } - - mutating func addressBound(_ address: NIOCore.SocketAddress?) -> OnBound { - switch self { - case .idle(let listeningAddressPromise): - do { - let socketAddress = try SocketAddress(address) - self = .listening(listeningAddressPromise.futureResult) - return .succeedPromise(listeningAddressPromise, address: socketAddress) - } catch { - self = .closedOrInvalidAddress(error) - return .failPromise(listeningAddressPromise, error: error) - } - - case .listening, .closedOrInvalidAddress: - fatalError("Invalid state: addressBound should only be called once and when in idle state") - } - } - - enum OnClose { - case failPromise(_ promise: EventLoopPromise, error: ListeningAddressError) - case doNothing - } - - mutating func close() -> OnClose { - switch self { - case .idle(let listeningAddressPromise): - self = .closedOrInvalidAddress(.serverClosed) - return .failPromise(listeningAddressPromise, error: .serverClosed) - - case .listening: - self = .closedOrInvalidAddress(.serverClosed) - return .doNothing - - case .closedOrInvalidAddress: - return .doNothing - } - } - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServer.SocketAddress { - fileprivate init(_ address: NIOCore.SocketAddress?) throws(ListeningAddressError) { - guard let address, let port = address.port else { - throw ListeningAddressError.addressOrPortNotAvailable - } - - switch address { - case .v4(let ipv4Address): - self.init(base: .ipv4(.init(host: ipv4Address.host, port: port))) - case .v6(let ipv6Address): - self.init(base: .ipv6(.init(host: ipv6Address.host, port: port))) - case .unixDomainSocket: - throw ListeningAddressError.unsupportedAddressType - } - } -} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+SecureUpgrade.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+SecureUpgrade.swift deleted file mode 100644 index 61e867c..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+SecureUpgrade.swift +++ /dev/null @@ -1,211 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import HTTPAPIs -import Logging -import NIOCore -import NIOEmbedded -import NIOHTTP2 -import NIOHTTPTypes -import NIOHTTPTypesHTTP1 -import NIOHTTPTypesHTTP2 -import NIOPosix -import NIOSSL -import X509 - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServer { - func serveSecureUpgrade( - bindTarget: NIOHTTPServerConfiguration.BindTarget, - tlsConfiguration: TLSConfiguration, - handler: some HTTPServerRequestHandler, - asyncChannelConfiguration: NIOAsyncChannel.Configuration, - http2Configuration: NIOHTTP2Handler.Configuration, - verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? = nil - ) async throws { - let serverChannel = try await self.setupSecureUpgradeServerChannel( - bindTarget: bindTarget, - tlsConfiguration: tlsConfiguration, - asyncChannelConfiguration: asyncChannelConfiguration, - http2Configuration: http2Configuration, - verificationCallback: verificationCallback - ) - - try await self._serveSecureUpgrade(serverChannel: serverChannel, handler: handler) - } - - typealias NegotiatedChannel = NIONegotiatedHTTPVersion< - NIOAsyncChannel, - (any Channel, NIOHTTP2Handler.AsyncStreamMultiplexer>) - > - - private func setupSecureUpgradeServerChannel( - bindTarget: NIOHTTPServerConfiguration.BindTarget, - tlsConfiguration: TLSConfiguration, - asyncChannelConfiguration: NIOAsyncChannel.Configuration, - http2Configuration: NIOHTTP2Handler.Configuration, - verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? - ) async throws -> NIOAsyncChannel, Never> { - switch bindTarget.backing { - case .hostAndPort(let host, let port): - let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup) - .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) - .bind(host: host, port: port) { channel in - self.setupSecureUpgradeConnectionChildChannel( - channel: channel, - tlsConfiguration: tlsConfiguration, - asyncChannelConfiguration: asyncChannelConfiguration, - http2Configuration: http2Configuration, - verificationCallback: verificationCallback - ) - } - - try self.addressBound(serverChannel.channel.localAddress) - - return serverChannel - } - } - - func setupSecureUpgradeConnectionChildChannel( - channel: any Channel, - tlsConfiguration: TLSConfiguration, - asyncChannelConfiguration: NIOAsyncChannel.Configuration, - http2Configuration: NIOHTTP2Handler.Configuration, - verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? - ) -> EventLoopFuture> { - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHandler( - self.makeSSLServerHandler(tlsConfiguration, verificationCallback) - ) - }.flatMap { - channel.configureAsyncHTTPServerPipeline( - http2Configuration: http2Configuration, - http1ConnectionInitializer: { channel in - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: true)) - - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: asyncChannelConfiguration - ) - } - }, - http2ConnectionInitializer: { channel in channel.eventLoop.makeCompletedFuture(.success(channel)) }, - http2StreamInitializer: { channel in - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations - .addHandler( - HTTP2FramePayloadToHTTPServerCodec() - ) - - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: asyncChannelConfiguration - ) - } - } - ) - } - } - - func _serveSecureUpgrade( - serverChannel: NIOAsyncChannel, Never>, - handler: some HTTPServerRequestHandler - ) async throws { - try await withThrowingDiscardingTaskGroup { group in - try await serverChannel.executeThenClose { inbound in - for try await upgradeResult in inbound { - group.addTask { - do { - try await withThrowingDiscardingTaskGroup { connectionGroup in - switch try await upgradeResult.get() { - case .http1_1(let http1Channel): - let chainFuture = http1Channel.channel.nioSSL_peerValidatedCertificateChain() - Self.$connectionContext.withValue(ConnectionContext(chainFuture)) { - connectionGroup.addTask { - try await self.handleRequestChannel( - channel: http1Channel, - handler: handler - ) - } - } - case .http2((let http2Connection, let http2Multiplexer)): - do { - let chainFuture = http2Connection.nioSSL_peerValidatedCertificateChain() - try await Self.$connectionContext.withValue(ConnectionContext(chainFuture)) { - for try await http2StreamChannel in http2Multiplexer.inbound { - connectionGroup.addTask { - try await self.handleRequestChannel( - channel: http2StreamChannel, - handler: handler - ) - } - } - } - } catch { - self.logger.debug("HTTP2 connection closed: \(error)") - } - } - } - } catch { - self.logger.debug("Negotiating ALPN failed: \(error)") - } - } - } - } - } - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServer { - func makeSSLServerHandler( - _ tlsConfiguration: TLSConfiguration, - _ customVerificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? - ) throws -> NIOSSLServerHandler { - guard let customVerificationCallback else { - return try NIOSSLServerHandler(context: .init(configuration: tlsConfiguration)) - } - return try NIOSSLServerHandler( - context: .init(configuration: tlsConfiguration), - customVerificationCallbackWithMetadata: { certificates, promise in - promise.completeWithTask { - // Convert input [NIOSSLCertificate] to [X509.Certificate] - let x509Certs = try certificates.map { try Certificate($0) } - - let callbackResult = try await customVerificationCallback(x509Certs) - - switch callbackResult { - case .certificateVerified(let verificationMetadata): - guard let peerChain = verificationMetadata.validatedCertificateChain else { - return .certificateVerified(.init(nil)) - } - - // Convert the result into [NIOSSLCertificate] - let nioSSLCerts = try peerChain.map { try NIOSSLCertificate($0) } - return .certificateVerified(.init(.init(nioSSLCerts))) - - case .failed(let error): - self.logger.error( - "Custom certificate verification failed", - metadata: [ - "failure-reason": .string(error.reason) - ] - ) - return .failed - } - } - } - ) - } -} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+SwiftConfiguration.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+SwiftConfiguration.swift deleted file mode 100644 index bd12298..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+SwiftConfiguration.swift +++ /dev/null @@ -1,345 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if Configuration -public import Configuration -import NIOCertificateReloading -import SwiftASN1 -public import X509 - -enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible { - case customVerificationCallbackProvidedWhenNotUsingMTLS - - var description: String { - switch self { - case .customVerificationCallbackProvidedWhenNotUsingMTLS: - "Invalid configuration. A custom certificate verification callback was provided despite the server not being configured for mTLS." - } - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServerConfiguration { - /// Initialize the server configuration from a config reader. - /// - /// ## Configuration keys: - /// - /// ``NIOHTTPServerConfiguration`` is comprised of four types. Provide configuration for each type under the - /// specified key: - /// - ``BindTarget`` - Provide under key `"bindTarget"` (keys listed in ``BindTarget/init(config:)``). - /// - ``TransportSecurity`` - Provide under key `"transportSecurity"` (keys listed in - /// ``TransportSecurity/init(config:customCertificateVerificationCallback:)``). - /// - ``BackPressureStrategy`` - Provide under key `"backpressureStrategy"` (keys listed in - /// ``BackPressureStrategy/init(config:)``). - /// - ``HTTP2`` - Provide under key `"http2"` (keys listed in ``HTTP2/init(config:)``). - /// - /// - Parameters: - /// - config: The configuration reader to read configuration values from. - /// - customCertificateVerificationCallback: An optional client certificate verification callback to use when - /// mTLS is configured (i.e., when `"transportSecurity.security"` is `"mTLS"` or `"reloadingMTLS"`). If provided - /// when mTLS is *not* configured, this initializer throws - /// ``NIOHTTPServerConfigurationError/customVerificationCallbackProvidedWhenNotUsingMTLS``. If set to `nil` when - /// mTLS *is* configured, the default client certificate verification logic of the underlying SSL implementation - /// is used. - public init( - config: ConfigReader, - customCertificateVerificationCallback: ( - @Sendable ([Certificate]) async throws -> CertificateVerificationResult - )? = nil - ) throws { - let snapshot = config.snapshot() - - self.init( - bindTarget: try .init(config: snapshot.scoped(to: "bindTarget")), - transportSecurity: try .init( - config: snapshot.scoped(to: "transportSecurity"), - customCertificateVerificationCallback: customCertificateVerificationCallback - ), - backpressureStrategy: .init(config: snapshot.scoped(to: "backpressureStrategy")), - http2: .init(config: snapshot.scoped(to: "http2")) - ) - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServerConfiguration.BindTarget { - /// Initialize a bind target configuration from a config reader. - /// - /// ## Configuration keys: - /// - `host` (string, required): The hostname or IP address the server will bind to (e.g., "localhost", "0.0.0.0"). - /// - `port` (int, required): The port number the server will listen on (e.g., 8080, 443). - /// - /// - Parameter config: The configuration reader. - public init(config: ConfigSnapshotReader) throws { - self.init( - backing: .hostAndPort( - host: try config.requiredString(forKey: "host"), - port: try config.requiredInt(forKey: "port") - ) - ) - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServerConfiguration.TransportSecurity { - /// Initialize a transport security configuration from a config reader. - /// - /// ## Configuration keys: - /// - `security` (string, required): The transport security for the server (permitted values: `"plaintext"`, - /// `"tls"`, `"reloadingTLS"`, `"mTLS"`, `"reloadingMTLS"`). - /// - /// ### Configuration keys for `"tls"`: - /// - `certificateChainPEMString` (string, required): PEM-formatted certificate chain content. - /// - `privateKeyPEMString` (string, required, secret): PEM-formatted private key content. - /// - /// ### Configuration keys for `"reloadingTLS"`: - /// - `refreshInterval` (int, optional, default: 30): The interval (in seconds) at which the certificate chain and - /// private key will be reloaded. - /// - `certificateChainPEMPath` (string, required): Path to the certificate chain PEM file. - /// - `privateKeyPEMPath` (string, required): Path to the private key PEM file. - /// - /// ### Configuration keys for `"mTLS"`: - /// - `certificateChainPEMString` (string, required): PEM-formatted certificate chain content. - /// - `privateKeyPEMString` (string, required, secret): PEM-formatted private key content. - /// - `trustRoots` (string array, optional, default: system trust roots): The root certificates to trust when - /// verifying client certificates. - /// - `certificateVerificationMode` (string, required): The client certificate validation behavior (permitted - /// values: "optionalVerification" or "noHostnameVerification"). - /// - /// ### Configuration keys for `"reloadingMTLS"`: - /// - `refreshInterval` (int, optional, default: 30): The interval (in seconds) at which the certificate chain and - /// private key will be reloaded. - /// - `certificateChainPEMPath` (string, required): Path to the certificate chain PEM file. - /// - `privateKeyPEMPath` (string, required): Path to the private key PEM file. - /// - `trustRoots` (string array, optional, default: system trust roots): The root certificates to trust when - /// verifying client certificates. - /// - `certificateVerificationMode` (string, required): The client certificate validation behavior (permitted - /// values: "optionalVerification" or "noHostnameVerification"). - /// - /// - Parameters: - /// - config: The configuration reader. - /// - customCertificateVerificationCallback: An optional client certificate verification callback to use when - /// mTLS is configured (i.e., when `"transportSecurity.security"` is `"mTLS"` or `"reloadingMTLS"`). If provided - /// when mTLS is *not* configured, this initializer throws - /// ``NIOHTTPServerConfigurationError/customVerificationCallbackProvidedWhenNotUsingMTLS``. If set to `nil` when - /// mTLS *is* configured, the default client certificate verification logic of the underlying SSL implementation - /// is used. - public init( - config: ConfigSnapshotReader, - customCertificateVerificationCallback: ( - @Sendable ([Certificate]) async throws -> CertificateVerificationResult - )? = nil - ) throws { - let security = try config.requiredString(forKey: "security", as: TransportSecurityKind.self) - - // A custom verification callback can only be used when the server is configured for mTLS. - if customCertificateVerificationCallback != nil, !security.isMTLS() { - throw NIOHTTPServerConfigurationError.customVerificationCallbackProvidedWhenNotUsingMTLS - } - - switch security { - case .plaintext: - self = .plaintext - - case .tls: - self = try .tls(config: config) - - case .reloadingTLS: - self = try .reloadingTLS(config: config) - - case .mTLS: - self = try .mTLS( - config: config, - customCertificateVerificationCallback: customCertificateVerificationCallback - ) - - case .reloadingMTLS: - self = try .reloadingMTLS( - config: config, - customCertificateVerificationCallback: customCertificateVerificationCallback - ) - } - } - - private static func tls(config: ConfigSnapshotReader) throws -> Self { - let certificateChainPEMString = try config.requiredString(forKey: "certificateChainPEMString") - let privateKeyPEMString = try config.requiredString(forKey: "privateKeyPEMString", isSecret: true) - - return Self.tls( - certificateChain: try PEMDocument.parseMultiple(pemString: certificateChainPEMString) - .map { try Certificate(pemEncoded: $0.pemString) }, - privateKey: try .init(pemEncoded: privateKeyPEMString) - ) - } - - private static func reloadingTLS(config: ConfigSnapshotReader) throws -> Self { - let refreshInterval = config.int(forKey: "refreshInterval", default: 30) - let certificateChainPEMPath = try config.requiredString(forKey: "certificateChainPEMPath") - let privateKeyPEMPath = try config.requiredString(forKey: "privateKeyPEMPath") - - return try Self.tls( - certificateReloader: TimedCertificateReloader( - refreshInterval: .seconds(refreshInterval), - certificateSource: .init(location: .file(path: certificateChainPEMPath), format: .pem), - privateKeySource: .init(location: .file(path: privateKeyPEMPath), format: .pem) - ) - ) - } - - private static func mTLS( - config: ConfigSnapshotReader, - customCertificateVerificationCallback: ( - @Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult - )? = nil - ) throws -> Self { - let certificateChainPEMString = try config.requiredString(forKey: "certificateChainPEMString") - let privateKeyPEMString = try config.requiredString(forKey: "privateKeyPEMString", isSecret: true) - let trustRoots = config.stringArray(forKey: "trustRoots") - let verificationMode = try config.requiredString( - forKey: "certificateVerificationMode", - as: VerificationMode.self - ) - - return Self.mTLS( - certificateChain: try PEMDocument.parseMultiple(pemString: certificateChainPEMString) - .map { try Certificate(pemEncoded: $0.pemString) }, - privateKey: try .init(pemEncoded: privateKeyPEMString), - trustRoots: try trustRoots?.map { try Certificate(pemEncoded: $0) }, - certificateVerification: .init(verificationMode), - customCertificateVerificationCallback: customCertificateVerificationCallback - ) - } - - private static func reloadingMTLS( - config: ConfigSnapshotReader, - customCertificateVerificationCallback: ( - @Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult - )? = nil - ) throws -> Self { - let refreshInterval = config.int(forKey: "refreshInterval", default: 30) - let certificateChainPEMPath = try config.requiredString(forKey: "certificateChainPEMPath") - let privateKeyPEMPath = try config.requiredString(forKey: "privateKeyPEMPath") - let trustRoots = config.stringArray(forKey: "trustRoots") - let verificationMode = try config.requiredString( - forKey: "certificateVerificationMode", - as: VerificationMode.self - ) - - return try Self.mTLS( - certificateReloader: TimedCertificateReloader( - refreshInterval: .seconds(refreshInterval), - certificateSource: .init(location: .file(path: certificateChainPEMPath), format: .pem), - privateKeySource: .init(location: .file(path: privateKeyPEMPath), format: .pem) - ), - trustRoots: try trustRoots?.map { try Certificate(pemEncoded: $0) }, - certificateVerification: .init(verificationMode), - customCertificateVerificationCallback: customCertificateVerificationCallback - ) - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServerConfiguration.BackPressureStrategy { - /// Initialize the backpressure strategy configuration from a config reader. - /// - /// ## Configuration keys: - /// - `low` (int, optional, default: 2): The threshold below which the consumer will ask the producer to produce - /// more elements. - /// - `high` (int, optional, default: 10): The threshold above which the producer will stop producing elements. - /// - /// - Parameter config: The configuration reader. - public init(config: ConfigSnapshotReader) { - self.init( - backing: .watermark( - low: config.int( - forKey: "low", - default: NIOHTTPServerConfiguration.BackPressureStrategy.defaultWatermarkLow - ), - high: config.int( - forKey: "high", - default: NIOHTTPServerConfiguration.BackPressureStrategy.defaultWatermarkHigh - ) - ) - ) - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServerConfiguration.HTTP2 { - /// Initialize a HTTP/2 configuration from a config reader. - /// - /// ## Configuration keys: - /// - `maxFrameSize` (int, optional, default: 2^14): The maximum frame size to be used in an HTTP/2 connection. - /// - `targetWindowSize` (int, optional, default: 2^16 - 1): The target window size to be used in an HTTP/2 - /// connection. - /// - `maxConcurrentStreams` (int, optional, default: 100): The maximum number of concurrent streams in an HTTP/2 - /// connection. - /// - /// - Parameter config: The configuration reader. - public init(config: ConfigSnapshotReader) { - self.init( - maxFrameSize: config.int( - forKey: "maxFrameSize", - default: NIOHTTPServerConfiguration.HTTP2.defaultMaxFrameSize - ), - targetWindowSize: config.int( - forKey: "targetWindowSize", - default: NIOHTTPServerConfiguration.HTTP2.defaultTargetWindowSize - ), - /// The default value, ``NIOHTTPServerConfiguration.HTTP2.DEFAULT_TARGET_WINDOW_SIZE``, is `nil`. However, - /// we can only specify a non-nil `default` argument to `config.int(...)`. But `config.int(...)` already - /// defaults to `nil` if it can't find the `"maxConcurrentStreams"` key, so that works for us. - maxConcurrentStreams: config.int(forKey: "maxConcurrentStreams") - ) - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServerConfiguration.TransportSecurity { - fileprivate enum TransportSecurityKind: String { - case plaintext - case tls - case reloadingTLS - case mTLS - case reloadingMTLS - - func isMTLS() -> Bool { - switch self { - case .mTLS, .reloadingMTLS: - return true - - default: - return false - } - } - } - - /// A wrapper over ``CertificateVerificationMode``. - fileprivate enum VerificationMode: String { - case optionalVerification - case noHostnameVerification - } -} - -@available(anyAppleOS 26.0, *) -extension CertificateVerificationMode { - fileprivate init(_ mode: NIOHTTPServerConfiguration.TransportSecurity.VerificationMode) { - switch mode { - case .optionalVerification: - self.init(mode: .optionalVerification) - case .noHostnameVerification: - self.init(mode: .noHostnameVerification) - } - } -} -#endif // Configuration diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer.swift deleted file mode 100644 index e6cef90..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer.swift +++ /dev/null @@ -1,366 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -public import HTTPAPIs -import HTTPTypes -public import Logging -import NIOCertificateReloading -import NIOConcurrencyHelpers -import NIOCore -import NIOHTTP1 -import NIOHTTP2 -import NIOHTTPTypes -import NIOHTTPTypesHTTP1 -import NIOHTTPTypesHTTP2 -import NIOPosix -import NIOSSL -import SwiftASN1 -import Synchronization -import X509 - -/// A generic HTTP server that can handle incoming HTTP requests. -/// -/// The `Server` class provides a high-level interface for creating HTTP servers with support for: -/// - TLS/SSL encryption -/// - Custom request handlers -/// - Configurable binding targets -/// - Async/await request processing -/// - Bi-directional streaming support -/// - Request and response trailers -/// -/// ## Usage -/// -/// ```swift -/// let configuration = NIOHTTPServerConfiguration( -/// bindTarget: .hostAndPort(host: "localhost", port: 8080), -/// tlsConfiguration: .insecure() -/// ) -/// -/// try await Server.serve( -/// logger: logger, -/// configuration: configuration -/// ) { request, _, reader, responseSender in -/// // Echo the request body back as the response body -/// let writer = try await responseSender.send(.init(status: .ok)) -/// try await reader.pipe(into: writer) -/// } -/// ``` -@available(anyAppleOS 26.0, *) -public struct NIOHTTPServer: HTTPServer { - public typealias RequestContext = HTTPRequestContext - public typealias Reader = NIORequestBodyReader - public typealias ResponseSender = NIOHTTPResponseSender - - let logger: Logger - private let configuration: NIOHTTPServerConfiguration - - var listeningAddressState: NIOLockedValueBox - - /// Task-local storage for connection-specific information accessible from request handlers. - /// - /// - SeeAlso: ``ConnectionContext``. - @TaskLocal public static var connectionContext = ConnectionContext() - - /// Create a new ``HTTPServer`` implemented over `SwiftNIO`. - /// - Parameters: - /// - logger: A logger instance for recording server events and debugging information. - /// - configuration: The server configuration including bind target and TLS settings. - public init( - logger: Logger, - configuration: NIOHTTPServerConfiguration, - ) { - self.logger = logger - self.configuration = configuration - - // TODO: If we allow users to pass in an event loop, use that instead of the singleton MTELG. - let eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup - self.listeningAddressState = .init(.idle(eventLoopGroup.any().makePromise())) - } - - /// Starts an HTTP server with the specified request handler. - /// - /// This method creates and runs an HTTP server that processes incoming requests using the provided - /// ``HTTPServerRequestHandler`` implementation. The server binds to the specified configuration and - /// handles each connection concurrently using Swift's structured concurrency. - /// - /// - Parameters: - /// - logger: A logger instance for recording server events and debugging information. - /// - configuration: The server configuration including bind target and TLS settings. - /// - handler: A ``HTTPServerRequestHandler`` implementation that processes incoming HTTP requests. The handler - /// receives each request along with a body reader and response sender function. - /// - /// ## Example - /// - /// ```swift - /// struct EchoHandler: HTTPServerRequestHandler { - /// func handle( - /// request: HTTPRequest, - /// requestContext: consuming HTTPRequestContext, - /// reader: consuming sending NIORequestBodyReader, - /// responseSender: consuming sending NIOHTTPResponseSender - /// ) async throws { - /// let writer = try await responseSender.send(.init(status: .ok)) - /// try await reader.pipe(into: writer) - /// } - /// } - /// - /// let configuration = HTTPServerConfiguration( - /// bindTarget: .hostAndPort(host: "localhost", port: 8080), - /// tlsConfiguration: .insecure() - /// ) - /// - /// try await Server.serve( - /// logger: logger, - /// configuration: configuration, - /// handler: EchoHandler() - /// ) - /// ``` - public func serve(handler: some HTTPServerRequestHandler) async throws { - defer { - switch self.listeningAddressState.withLockedValue({ $0.close() }) { - case .failPromise(let promise, let error): - promise.fail(error) - case .doNothing: - () - } - } - - let asyncChannelConfiguration: NIOAsyncChannel.Configuration - switch self.configuration.backpressureStrategy.backing { - case .watermark(let low, let high): - asyncChannelConfiguration = .init( - backPressureStrategy: .init(lowWatermark: low, highWatermark: high), - isOutboundHalfClosureEnabled: true - ) - } - - switch self.configuration.transportSecurity.backing { - case .plaintext: - try await self.serveInsecureHTTP1_1( - bindTarget: self.configuration.bindTarget, - handler: handler, - asyncChannelConfiguration: asyncChannelConfiguration - ) - - case .tls(let certificateChain, let privateKey): - let http2Config = NIOHTTP2Handler.Configuration( - httpServerHTTP2Configuration: self.configuration.http2 - ) - - let certificateChain = try certificateChain.map { try NIOSSLCertificateSource($0) } - let privateKey = try NIOSSLPrivateKeySource(privateKey) - - var tlsConfiguration: TLSConfiguration = .makeServerConfiguration( - certificateChain: certificateChain, - privateKey: privateKey - ) - tlsConfiguration.applicationProtocols = ["h2", "http/1.1"] - - try await self.serveSecureUpgrade( - bindTarget: self.configuration.bindTarget, - tlsConfiguration: tlsConfiguration, - handler: handler, - asyncChannelConfiguration: asyncChannelConfiguration, - http2Configuration: http2Config - ) - - case .reloadingTLS(let certificateReloader): - let http2Config = NIOHTTP2Handler.Configuration( - httpServerHTTP2Configuration: configuration.http2 - ) - - var tlsConfiguration: TLSConfiguration = try .makeServerConfiguration( - certificateReloader: certificateReloader - ) - tlsConfiguration.applicationProtocols = ["h2", "http/1.1"] - - try await self.serveSecureUpgrade( - bindTarget: self.configuration.bindTarget, - tlsConfiguration: tlsConfiguration, - handler: handler, - asyncChannelConfiguration: asyncChannelConfiguration, - http2Configuration: http2Config - ) - - case .mTLS(let certificateChain, let privateKey, let trustRoots, let verificationMode, let verificationCallback): - let http2Config = NIOHTTP2Handler.Configuration( - httpServerHTTP2Configuration: configuration.http2 - ) - - let certificateChain = try certificateChain.map { try NIOSSLCertificateSource($0) } - let privateKey = try NIOSSLPrivateKeySource(privateKey) - let nioTrustRoots = try NIOSSLTrustRoots(treatingNilAsSystemTrustRoots: trustRoots) - - var tlsConfiguration: TLSConfiguration = .makeServerConfigurationWithMTLS( - certificateChain: certificateChain, - privateKey: privateKey, - trustRoots: nioTrustRoots - ) - tlsConfiguration.certificateVerification = .init(verificationMode) - tlsConfiguration.applicationProtocols = ["h2", "http/1.1"] - - try await self.serveSecureUpgrade( - bindTarget: self.configuration.bindTarget, - tlsConfiguration: tlsConfiguration, - handler: handler, - asyncChannelConfiguration: asyncChannelConfiguration, - http2Configuration: http2Config, - verificationCallback: verificationCallback - ) - - case .reloadingMTLS(let certificateReloader, let trustRoots, let verificationMode, let verificationCallback): - let http2Config = NIOHTTP2Handler.Configuration( - httpServerHTTP2Configuration: configuration.http2 - ) - - let nioTrustRoots = try NIOSSLTrustRoots(treatingNilAsSystemTrustRoots: trustRoots) - - var tlsConfiguration: TLSConfiguration = try .makeServerConfigurationWithMTLS( - certificateReloader: certificateReloader, - trustRoots: nioTrustRoots - ) - tlsConfiguration.certificateVerification = .init(verificationMode) - tlsConfiguration.applicationProtocols = ["h2", "http/1.1"] - - try await self.serveSecureUpgrade( - bindTarget: self.configuration.bindTarget, - tlsConfiguration: tlsConfiguration, - handler: handler, - asyncChannelConfiguration: asyncChannelConfiguration, - http2Configuration: http2Config, - verificationCallback: verificationCallback - ) - } - } - - func handleRequestChannel( - channel: NIOAsyncChannel, - handler: some HTTPServerRequestHandler - ) async throws { - do { - try await channel - .executeThenClose { inbound, outbound in - var iterator = inbound.makeAsyncIterator() - - let httpRequest: HTTPRequest - switch try await iterator.next() { - case .head(let request): - httpRequest = request - case .body: - self.logger.debug("Unexpectedly received body on connection. Closing now") - outbound.finish() - return - case .end: - self.logger.debug("Unexpectedly received end on connection. Closing now") - outbound.finish() - return - case .none: - self.logger.trace("No more requests parts on connection") - return - } - - let readerState = NIORequestBodyReader.ReaderState() - let writerState = NIOHTTPResponseSender.WriterState() - - do { - try await handler.handle( - request: httpRequest, - requestContext: HTTPRequestContext(), - reader: NIORequestBodyReader( - iterator: iterator, - readerState: readerState - ), - responseSender: NIOHTTPResponseSender( - writer: outbound, - writerState: writerState - ) - ) - } catch { - logger.error("Error thrown while handling connection: \(error)") - if !readerState.wrapped.withLock({ $0.finishedReading }) { - logger.error("Did not finish reading but error thrown.") - // TODO: if h2 reset stream; if h1 try draining request? - } - if !writerState.wrapped.withLock({ $0.finishedWriting }) { - logger.error("Did not write response but error thrown.") - // TODO: we need to do something, possibly just close the connection or - // reset the stream with the appropriate error. - } - throw error - } - - // TODO: handle other state scenarios. - // For example, if we're using h2 and we didn't finish reading but we wrote back - // a response, we should send a RST_STREAM with NO_ERROR set. - // If we finished reading but we didn't write back a response, then RST_STREAM - // is also likely appropriate but unclear about the error. - // For h1, we should close the connection. - - // Finish the outbound and wait on the close future to make sure all pending - // writes are actually written. - outbound.finish() - try await channel.channel.closeFuture.get() - } - } catch { - self.logger.debug("Error thrown while handling connection: \(error)") - // TODO: We need to send a response head here potentially - throw error - } - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTP2Handler.Configuration { - init(httpServerHTTP2Configuration http2Config: NIOHTTPServerConfiguration.HTTP2) { - let clampedTargetWindowSize = Self.clampTargetWindowSize(http2Config.targetWindowSize) - let clampedMaxFrameSize = Self.clampMaxFrameSize(http2Config.maxFrameSize) - - var http2HandlerConnectionConfiguration = NIOHTTP2Handler.ConnectionConfiguration() - var http2HandlerHTTP2Settings = HTTP2Settings([ - HTTP2Setting(parameter: .initialWindowSize, value: clampedTargetWindowSize), - HTTP2Setting(parameter: .maxFrameSize, value: clampedMaxFrameSize), - ]) - if let maxConcurrentStreams = http2Config.maxConcurrentStreams { - http2HandlerHTTP2Settings.append( - HTTP2Setting(parameter: .maxConcurrentStreams, value: maxConcurrentStreams) - ) - } - http2HandlerConnectionConfiguration.initialSettings = http2HandlerHTTP2Settings - - var http2HandlerStreamConfiguration = NIOHTTP2Handler.StreamConfiguration() - http2HandlerStreamConfiguration.targetWindowSize = clampedTargetWindowSize - - self = NIOHTTP2Handler.Configuration( - connection: http2HandlerConnectionConfiguration, - stream: http2HandlerStreamConfiguration - ) - } - - /// Window size which mustn't exceed `2^31 - 1` (RFC 9113 § 6.5.2). - private static func clampTargetWindowSize(_ targetWindowSize: Int) -> Int { - min(targetWindowSize, Int(Int32.max)) - } - - /// Max frame size must be in the range `2^14 ..< 2^24` (RFC 9113 § 4.2). - private static func clampMaxFrameSize(_ maxFrameSize: Int) -> Int { - let clampedMaxFrameSize: Int - if maxFrameSize >= (1 << 24) { - clampedMaxFrameSize = (1 << 24) - 1 - } else if maxFrameSize < (1 << 14) { - clampedMaxFrameSize = (1 << 14) - } else { - clampedMaxFrameSize = maxFrameSize - } - return clampedMaxFrameSize - } -} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServerConfiguration.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServerConfiguration.swift deleted file mode 100644 index f272450..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServerConfiguration.swift +++ /dev/null @@ -1,390 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -public import NIOCertificateReloading -import NIOCore -import NIOSSL -public import X509 - -/// Configuration settings for ``NIOHTTPServer``. -/// -/// This structure contains all the necessary configuration options for setting up -/// and running ``NIOHTTPServer``, including network binding and TLS settings. -@available(anyAppleOS 26.0, *) -public struct NIOHTTPServerConfiguration: Sendable { - /// Specifies where the server should bind and listen for incoming connections. - /// - /// Currently supports binding to a specific host and port combination. - /// Additional binding targets may be added in the future. - public struct BindTarget: Sendable { - enum Backing { - case hostAndPort(host: String, port: Int) - } - - let backing: Backing - - /// Creates a bind target for a specific host and port. - /// - /// - Parameters: - /// - host: The hostname or IP address to bind to (e.g., "localhost", "0.0.0.0") - /// - port: The port number to listen on (e.g., 8080, 443) - /// - Returns: A configured `BindTarget` instance - /// - /// ## Example - /// ```swift - /// let target = BindTarget.hostAndPort(host: "localhost", port: 8080) - /// ``` - public static func hostAndPort(host: String, port: Int) -> Self { - Self(backing: .hostAndPort(host: host, port: port)) - } - } - - /// Configuration for transport security settings. - /// - /// Provides options for running the server with or without TLS encryption. - /// When using TLS, you must either provide a certificate chain and private key, or a `CertificateReloader`. - public struct TransportSecurity: Sendable { - enum Backing { - case plaintext - case tls( - certificateChain: [Certificate], - privateKey: Certificate.PrivateKey - ) - case reloadingTLS(certificateReloader: any CertificateReloader) - case mTLS( - certificateChain: [Certificate], - privateKey: Certificate.PrivateKey, - trustRoots: [Certificate]?, - certificateVerification: CertificateVerificationMode = .noHostnameVerification, - customCertificateVerificationCallback: ( - @Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult - )? = nil - ) - case reloadingMTLS( - certificateReloader: any CertificateReloader, - trustRoots: [Certificate]?, - certificateVerification: CertificateVerificationMode = .noHostnameVerification, - customCertificateVerificationCallback: ( - @Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult - )? = nil - ) - } - - let backing: Backing - - /// Configures the server for plaintext HTTP without TLS encryption. - public static let plaintext: Self = Self(backing: .plaintext) - - /// Configures the server for TLS with the provided certificate chain and private key. - /// - Parameters: - /// - certificateChain: The certificate chain to present during negotiation. - /// - privateKey: The private key corresponding to the leaf certificate in `certificateChain`. - public static func tls( - certificateChain: [Certificate], - privateKey: Certificate.PrivateKey - ) -> Self { - Self( - backing: .tls( - certificateChain: certificateChain, - privateKey: privateKey - ) - ) - } - - /// Configures the server for TLS with automatic certificate reloading. - /// - Parameters: - /// - certificateReloader: The certificate reloader instance. - public static func tls(certificateReloader: any CertificateReloader) throws -> Self { - Self(backing: .reloadingTLS(certificateReloader: certificateReloader)) - } - - /// Configures the server for mTLS with support for customizing client certificate verification logic. - /// - /// - Parameters: - /// - certificateChain: The certificate chain to present during negotiation. - /// - privateKey: The private key corresponding to the leaf certificate in `certificateChain`. - /// - trustRoots: The root certificates to trust when verifying client certificates. - /// - certificateVerification: Configures the client certificate validation behaviour. Defaults to - /// ``CertificateVerificationMode/noHostnameVerification``. - /// - customCertificateVerificationCallback: If specified, this callback *overrides* the default NIOSSL client - /// certificate verification logic. The callback receives the certificates presented by the peer. Within the - /// callback, you must validate these certificates against your trust roots and derive a validated chain of - /// trust per [RFC 4158](https://datatracker.ietf.org/doc/html/rfc4158). Return - /// ``CertificateVerificationResult/certificateVerified(_:)`` from the callback if verification succeeds, - /// optionally including the validated certificate chain you derived. Returning the validated certificate - /// chain allows ``NIOHTTPServer`` to provide access to it in the request handler through - /// ``NIOHTTPServer/ConnectionContext/peerCertificateChain``, accessed via the task-local - /// ``NIOHTTPServer/connectionContext`` property. Otherwise, return - /// ``CertificateVerificationResult/failed(_:)`` if verification fails. - /// - /// - Warning: If `customCertificateVerificationCallback` is set, it will **override** NIOSSL's default - /// certificate verification logic. - public static func mTLS( - certificateChain: [Certificate], - privateKey: Certificate.PrivateKey, - trustRoots: [Certificate]?, - certificateVerification: CertificateVerificationMode = .noHostnameVerification, - customCertificateVerificationCallback: ( - @Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult - )? = nil - ) -> Self { - Self( - backing: .mTLS( - certificateChain: certificateChain, - privateKey: privateKey, - trustRoots: trustRoots, - certificateVerification: certificateVerification, - customCertificateVerificationCallback: customCertificateVerificationCallback - ) - ) - } - - /// Configures the server for mTLS with automatic certificate reloading and support for customizing client - /// certificate verification logic. - /// - /// - Parameters: - /// - certificateReloader: The certificate reloader instance. - /// - trustRoots: The root certificates to trust when verifying client certificates. - /// - certificateVerification: Configures the client certificate validation behaviour. Defaults to - /// ``CertificateVerification/noHostnameVerification``. - /// - customCertificateVerificationCallback: If specified, this callback *overrides* the default NIOSSL client - /// certificate verification logic. The callback receives the certificates presented by the peer. Within the - /// callback, you must validate these certificates against your trust roots and derive a validated chain of - /// trust per [RFC 4158](https://datatracker.ietf.org/doc/html/rfc4158). Return - /// ``CertificateVerificationResult/certificateVerified(_:)`` from the callback if verification succeeds, - /// optionally including the validated certificate chain you derived. Returning the validated certificate - /// chain allows ``NIOHTTPServer`` to provide access to it in the request handler through - /// ``NIOHTTPServer/ConnectionContext/peerCertificateChain``, accessed via the task-local - /// ``NIOHTTPServer/connectionContext`` property. Otherwise, return - /// ``CertificateVerificationResult/failed(_:)`` if verification fails. - /// - /// - Warning: If `customCertificateVerificationCallback` is set, it will **override** NIOSSL's default - /// certificate verification logic. - public static func mTLS( - certificateReloader: any CertificateReloader, - trustRoots: [Certificate]?, - certificateVerification: CertificateVerificationMode = .noHostnameVerification, - customCertificateVerificationCallback: ( - @Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult - )? = nil - ) throws -> Self { - Self( - backing: .reloadingMTLS( - certificateReloader: certificateReloader, - trustRoots: trustRoots, - certificateVerification: certificateVerification, - customCertificateVerificationCallback: customCertificateVerificationCallback - ) - ) - } - } - - /// HTTP/2 specific configuration. - public struct HTTP2: Sendable, Hashable { - /// The maximum frame size to be used in an HTTP/2 connection. - public var maxFrameSize: Int - - /// The target window size for this connection. - /// - /// - Note: This will also be set as the initial window size for the connection. - public var targetWindowSize: Int - - /// The number of concurrent streams on the HTTP/2 connection. - public var maxConcurrentStreams: Int? - - public init( - maxFrameSize: Int, - targetWindowSize: Int, - maxConcurrentStreams: Int? - ) { - self.maxFrameSize = maxFrameSize - self.targetWindowSize = targetWindowSize - self.maxConcurrentStreams = maxConcurrentStreams - } - - @inlinable - static var defaultMaxFrameSize: Int { 1 << 14 } - - @inlinable - static var defaultTargetWindowSize: Int { (1 << 16) - 1 } - - @inlinable - static var defaultMaxConcurrentStreams: Int? { nil } - - /// Default values. - /// - /// The max frame size defaults to 2^14, the target window size defaults to 2^16-1, and - /// the max concurrent streams default to infinite. - public static var defaults: Self { - Self( - maxFrameSize: Self.defaultMaxFrameSize, - targetWindowSize: Self.defaultTargetWindowSize, - maxConcurrentStreams: Self.defaultMaxConcurrentStreams - ) - } - } - - /// Configuration for the backpressure strategy to use when reading requests and writing back responses. - public struct BackPressureStrategy: Sendable { - enum Backing { - case watermark(low: Int, high: Int) - } - - internal let backing: Backing - - init(backing: Backing) { - self.backing = backing - } - - /// A low/high watermark will be applied when reading requests and writing responses. - /// - Parameters: - /// - low: The threshold below which the consumer will ask the producer to produce more elements. - /// - high: The threshold above which the producer will stop producing elements. - /// - Returns: A low/high watermark strategy with the configured thresholds. - public static func watermark(low: Int, high: Int) -> Self { - .init(backing: .watermark(low: low, high: high)) - } - - @inlinable - static var defaultWatermarkLow: Int { 2 } - - @inlinable - static var defaultWatermarkHigh: Int { 10 } - - /// Default values. - /// - /// The watermark low value defaults to 2, and the watermark high value default to 10. - public static var defaults: Self { - Self.init( - backing: .watermark( - low: Self.defaultWatermarkLow, - high: Self.defaultWatermarkHigh - ) - ) - } - } - - /// Network binding configuration. - public var bindTarget: BindTarget - - /// TLS configuration for the server. - public var transportSecurity: TransportSecurity - - /// Backpressure strategy to use in the server. - public var backpressureStrategy: BackPressureStrategy - - /// Backpressure strategy to use in the server. - public var http2: HTTP2 - - /// Create a new configuration. - /// - Parameters: - /// - bindTarget: A ``BindTarget``. - /// - transportSecurity: A ``TransportSecurity``. Defaults to ``TransportSecurity/plaintext``. - /// - backpressureStrategy: A ``BackPressureStrategy``. - /// Defaults to ``BackPressureStrategy/watermark(low:high:)`` with a low watermark of 2 and a high of 10. - /// - http2: A ``HTTP2``. Defaults to ``HTTP2/defaults``. - public init( - bindTarget: BindTarget, - transportSecurity: TransportSecurity = .plaintext, - backpressureStrategy: BackPressureStrategy = .defaults, - http2: HTTP2 = .defaults - ) { - self.bindTarget = bindTarget - self.transportSecurity = transportSecurity - self.backpressureStrategy = backpressureStrategy - self.http2 = http2 - } -} - -/// Represents the outcome of certificate verification. -/// -/// Indicates whether certificate verification succeeded or failed, and provides associated metadata when verification -/// is successful. -@available(anyAppleOS 26.0, *) -public enum CertificateVerificationResult: Sendable, Hashable { - /// Metadata resulting from successful certificate verification. - public struct VerificationMetadata: Sendable, Hashable { - /// A container for the validated certificate chain: an array of certificates forming a verified and ordered - /// chain of trust, starting from the peer's leaf certificate to a trusted root certificate. - public var validatedCertificateChain: X509.ValidatedCertificateChain? - - /// Creates an instance with the peer's *validated* certificate chain. - /// - /// - Parameter validatedCertificateChain: An optional *validated* certificate chain. If provided, it must - /// **only** contain the **validated** chain of trust that was built and verified from the certificates - /// presented by the peer. - public init(_ validatedCertificateChain: X509.ValidatedCertificateChain?) { - self.validatedCertificateChain = validatedCertificateChain - } - } - - /// An error representing certificate verification failure. - public struct VerificationError: Swift.Error, Hashable { - public let reason: String - - /// Creates a verification error with the reason why verification failed. - /// - Parameter reason: The reason of why certificate verification failed. - public init(reason: String) { - self.reason = reason - } - } - - /// Certificate verification succeeded. - /// - /// The associated metadata contains information captured during verification. - case certificateVerified(VerificationMetadata) - - /// Certificate verification failed. - case failed(VerificationError) -} - -/// Represents the certificate verification behaviour. -public struct CertificateVerificationMode: Sendable { - enum VerificationMode { - case optionalVerification - case noHostnameVerification - } - - let mode: VerificationMode - - /// Allows peers to connect without presenting any certificates. - /// - /// However, if the peer *does* present - /// certificates, they are validated like normal (exactly like with ``noHostnameVerification``), and the TLS - /// handshake will fail if verification fails. - /// - /// - Warning: With this mode, a peer can successfully connect even without presenting any certificates. As such, - /// this mode must be used with great caution. - public static var optionalVerification: Self { - Self(mode: .optionalVerification) - } - - /// Validates the certificates presented by the peer but skips hostname verification as it cannot succeed in - /// a server context. - public static var noHostnameVerification: Self { - Self(mode: .noHostnameVerification) - } -} - -@available(anyAppleOS 26.0, *) -extension NIOSSL.CertificateVerification { - /// Maps ``CertificateVerificationMode`` to the NIOSSL representation. - init(_ verificationMode: CertificateVerificationMode) { - switch verificationMode.mode { - case .noHostnameVerification: - self = .noHostnameVerification - case .optionalVerification: - self = .optionalVerification - } - } -} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIORequestBodyReader.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIORequestBodyReader.swift deleted file mode 100644 index ac231b5..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIORequestBodyReader.swift +++ /dev/null @@ -1,99 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -public import AsyncStreaming -public import BasicContainers -import HTTPAPIs -public import HTTPTypes -import NIOCore -import NIOHTTPTypes -import Synchronization - -/// A NIO-backed HTTP request body reader used by the test server. -@available(anyAppleOS 26.0, *) -public struct NIORequestBodyReader: AsyncReader, ~Copyable { - public typealias ReadElement = UInt8 - public typealias ReadFailure = any Error - public typealias Buffer = UniqueArray - public typealias FinalElement = HTTPFields? - - public final class ReaderState: Sendable { - struct Wrapped { - var trailers: HTTPFields? = nil - var finishedReading: Bool = false - } - - let wrapped: Mutex - - public init() { - self.wrapped = .init(.init()) - } - } - - private var state: ReaderState - private var iterator: NIOAsyncChannelInboundStream.AsyncIterator - - init( - iterator: consuming sending NIOAsyncChannelInboundStream.AsyncIterator, - readerState: ReaderState - ) { - self.iterator = iterator - self.state = readerState - } - - public mutating func read( - body: nonisolated(nonsending) (inout UniqueArray, consuming HTTPFields??) async throws(Failure) -> Return - ) async throws(EitherError) -> Return { - var buffer = UniqueArray() - var finalElement: HTTPFields?? = nil - - let alreadyFinished = self.state.wrapped.withLock { $0.finishedReading } - if !alreadyFinished { - let requestPart: HTTPRequestPart? - do { - requestPart = try await self.iterator.next(isolation: #isolation) - } catch { - throw .first(error) - } - - switch requestPart { - case .head: - fatalError() - case .body(let element): - buffer.reserveCapacity(element.readableBytes) - unsafe element.withUnsafeReadableBytes { rawBufferPtr in - let usbptr = unsafe rawBufferPtr.assumingMemoryBound(to: UInt8.self) - unsafe buffer.append(copying: usbptr) - } - case .end(let t): - self.state.wrapped.withLock { state in - state.trailers = t - state.finishedReading = true - } - finalElement = .some(t) - case .none: - self.state.wrapped.withLock { $0.finishedReading = true } - finalElement = .some(nil) - } - } - - do { - return try await body(&buffer, finalElement) - } catch { - throw .second(error) - } - } -} - -@available(*, unavailable) -extension NIORequestBodyReader: Sendable {} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOSSL+X509.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOSSL+X509.swift deleted file mode 100644 index a66e248..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOSSL+X509.swift +++ /dev/null @@ -1,70 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIOSSL -import SwiftASN1 -import X509 - -/// Some convenience helpers for converting between NIOSSL and X509 certificate and private key types. - -// MARK: X509 to NIOSSL - -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, visionOS 1.0, *) -extension NIOSSLCertificate { - convenience init(_ certificate: Certificate) throws { - var serializer = DER.Serializer() - try certificate.serialize(into: &serializer) - try self.init(bytes: serializer.serializedBytes, format: .der) - } -} - -@available(macOS 11.0, iOS 14, tvOS 14, watchOS 7, macCatalyst 14, visionOS 1.0, *) -extension NIOSSLPrivateKey { - convenience init(_ privateKey: Certificate.PrivateKey) throws { - try self.init(bytes: try privateKey.serializeAsPEM().derBytes, format: .der) - } -} - -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, visionOS 1.0, *) -extension NIOSSLCertificateSource { - init(_ certificate: Certificate) throws { - self = .certificate(try NIOSSLCertificate(certificate)) - } -} - -@available(macOS 11.0, iOS 14, tvOS 14, watchOS 7, macCatalyst 14, visionOS 1.0, *) -extension NIOSSLPrivateKeySource { - init(_ privateKey: Certificate.PrivateKey) throws { - self = .privateKey(try NIOSSLPrivateKey(privateKey)) - } -} - -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, visionOS 1.0, *) -extension NIOSSLTrustRoots { - init(treatingNilAsSystemTrustRoots certificates: [Certificate]?) throws { - if let certificates { - self = .certificates(try certificates.map { try NIOSSLCertificate($0) }) - } else { - self = .default - } - } -} - -// MARK: NIOSSL to X509 - -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, visionOS 1.0, *) -extension Certificate { - init(_ certificate: NIOSSLCertificate) throws { - try self.init(derEncoded: certificate.toDERBytes()) - } -} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/RequestResponseMiddlewareBox.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/RequestResponseMiddlewareBox.swift deleted file mode 100644 index 850dcfa..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/RequestResponseMiddlewareBox.swift +++ /dev/null @@ -1,70 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -public import AsyncStreaming -public import HTTPAPIs -public import HTTPTypes - -/// This type holds the values passed to the ``HTTPServerRequestHandler`` when handling a request. -/// -/// It is necessary to box them together so that they can be used with `Middlewares`, as this will be the `Middleware.Input`. -@available(anyAppleOS 26.0, *) -public struct RequestResponseMiddlewareBox< - RequestContext: HTTPServerCapability.RequestContext & ~Copyable, - Reader: AsyncReader & ~Copyable, - ResponseSender: HTTPResponseSender & ~Copyable ->: ~Copyable -where - Reader.ReadElement == UInt8, - Reader.FinalElement == HTTPFields?, - ResponseSender.Writer: ~Copyable -{ - private let request: HTTPRequest - private let requestContext: RequestContext - private let reader: Reader - private let responseSender: ResponseSender - - /// Create a new ``RequestResponseMiddlewareBox``. - public init( - request: HTTPRequest, - requestContext: consuming RequestContext, - reader: consuming Reader, - responseSender: consuming ResponseSender - ) { - self.request = request - self.requestContext = requestContext - self.reader = reader - self.responseSender = responseSender - } - - /// Provides a closure exposing the request, reader, and response sender contained in this box. - public consuming func withContents( - _ handler: - nonisolated(nonsending) ( - HTTPRequest, - consuming RequestContext, - consuming Reader, - consuming ResponseSender - ) async throws -> T - ) async throws -> T { - try await handler( - self.request, - self.requestContext, - self.reader, - self.responseSender - ) - } -} - -@available(*, unavailable) -extension RequestResponseMiddlewareBox: Sendable {} diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/SocketAddress.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/SocketAddress.swift deleted file mode 100644 index ccd23b8..0000000 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/SocketAddress.swift +++ /dev/null @@ -1,110 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServer { - /// Represents an IPv4 address. - public struct IPv4: Hashable, Sendable { - /// The resolved host address. - public var host: String - /// The port to connect to. - public var port: Int - - /// Creates a new IPv4 address. - /// - /// - Parameters: - /// - host: Resolved host address. - /// - port: Port to connect to. - public init(host: String, port: Int) { - self.host = host - self.port = port - } - } - - /// Represents an IPv6 address. - public struct IPv6: Hashable, Sendable { - /// The resolved host address. - public var host: String - /// The port to connect to. - public var port: Int - - /// Creates a new IPv6 address. - /// - /// - Parameters: - /// - host: Resolved host address. - /// - port: Port to connect to. - public init(host: String, port: Int) { - self.host = host - self.port = port - } - } - - /// An address to which a socket may connect or bind to. - public struct SocketAddress: Hashable, Sendable { - enum Base: Hashable, Sendable { - case ipv4(IPv4) - case ipv6(IPv6) - } - - let base: Base - - /// Creates an IPv4 socket address. - public static func ipv4(host: String, port: Int) -> Self { - Self(base: .ipv4(.init(host: host, port: port))) - } - - /// Creates an IPv6 socket address. - public static func ipv6(host: String, port: Int) -> Self { - Self(base: .ipv6(.init(host: host, port: port))) - } - - /// Returns the address as an IPv4 address, if possible. - public var ipv4: IPv4? { - guard case .ipv4(let address) = self.base else { - return nil - } - - return address - } - - /// Returns the address as an IPv6 address, if possible. - public var ipv6: IPv6? { - guard case .ipv6(let address) = self.base else { - return nil - } - - return address - } - - /// The ``SocketAddress``'s host. - public var host: String { - switch self.base { - case .ipv4(let ipv4): - return ipv4.host - case .ipv6(let ipv6): - return ipv6.host - } - } - - /// The ``SocketAddress``'s port. - public var port: Int { - switch self.base { - case .ipv4(let ipv4): - return ipv4.port - - case .ipv6(let ipv6): - return ipv6.port - } - } - } -} diff --git a/Sources/HTTPClientConformance/NIOHTTPServer b/Sources/HTTPClientConformance/NIOHTTPServer new file mode 120000 index 0000000..c57c77a --- /dev/null +++ b/Sources/HTTPClientConformance/NIOHTTPServer @@ -0,0 +1 @@ +../../Submodules/swift-http-server/Sources/NIOHTTPServer \ No newline at end of file diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/RawHTTPServer.swift b/Sources/HTTPClientConformance/RawHTTPServer.swift similarity index 100% rename from Sources/HTTPClientConformance/HTTPServerForTesting/RawHTTPServer.swift rename to Sources/HTTPClientConformance/RawHTTPServer.swift diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift b/Sources/HTTPClientConformance/TestHTTPServer.swift similarity index 82% rename from Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift rename to Sources/HTTPClientConformance/TestHTTPServer.swift index 1456d4e..5f9c977 100644 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift +++ b/Sources/HTTPClientConformance/TestHTTPServer.swift @@ -11,12 +11,8 @@ // //===----------------------------------------------------------------------===// -import AsyncStreaming import BasicContainers -import ContainersPreview import Foundation -import HTTPAPIs -import HTTPTypes import Logging import Synchronization @@ -48,11 +44,16 @@ struct JSONHTTPRequest: Codable { public func withTestHTTPServer(perform: (Int) async throws -> Void) async throws { try await withThrowingTaskGroup { let logger = Logger(label: "TestHTTPServer") - let server = NIOHTTPServer(logger: logger, configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 0))) + let configuration = try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http1_1], + transportSecurity: .plaintext + ) + let server = NIOHTTPServer(logger: logger, configuration: configuration) $0.addTask { try await serve(server: server) } - let port = try await server.listeningAddress.port + let port = try await server.listeningAddresses[0].port print("Test HTTP Server: \(port)") try await perform(port) $0.cancelAll() @@ -98,6 +99,8 @@ func serve(server: NIOHTTPServer) async throws { capacity: 17, copying: "No path specified".utf8 ) + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) try await responseSender.sendAndFinish(HTTPResponse(status: .internalServerError), buffer: &body, trailer: nil) return } @@ -108,6 +111,8 @@ func serve(server: NIOHTTPServer) async throws { capacity: 17, copying: "Malformed path".utf8 ) + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) try await responseSender.sendAndFinish(HTTPResponse(status: .internalServerError), buffer: &body, trailer: nil) return } @@ -152,6 +157,8 @@ func serve(server: NIOHTTPServer) async throws { var arrayResponseData = UniqueArray(copying: responseData) try await responseSender.sendAndFinish(HTTPResponse(status: .ok), buffer: &arrayResponseData, trailer: nil) case "/head_with_cl": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) if request.method != .head { try await responseSender.sendAndFinish(HTTPResponse(status: .methodNotAllowed)) break @@ -167,8 +174,12 @@ func serve(server: NIOHTTPServer) async throws { ) ) case "/200": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) try await responseSender.sendAndFinish(HTTPResponse(status: .ok)) case "/gzip": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) // If the client didn't say that they supported this encoding, // then fallback to no encoding. let acceptEncoding = request.headerFields[.acceptEncoding] @@ -214,6 +225,8 @@ func serve(server: NIOHTTPServer) async throws { try await responseSender.sendAndFinish(HTTPResponse(status: .ok, headerFields: headers), buffer: &bytes, trailer: nil) case "/deflate": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) // If the client didn't say that they supported this encoding, // then fallback to no encoding. let acceptEncoding = request.headerFields[.acceptEncoding] @@ -234,6 +247,8 @@ func serve(server: NIOHTTPServer) async throws { try await responseSender .sendAndFinish(HTTPResponse(status: .ok, headerFields: headers), buffer: &bytes, trailer: nil) case "/brotli": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) // If the client didn't say that they supported this encoding, // then fallback to no encoding. let acceptEncoding = request.headerFields[.acceptEncoding] @@ -253,6 +268,8 @@ func serve(server: NIOHTTPServer) async throws { try await responseSender.sendAndFinish(HTTPResponse(status: .ok, headerFields: headers), buffer: &bytes, trailer: nil) case "/header_multivalue": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) try await responseSender.sendAndFinish( HTTPResponse( status: .ok, @@ -263,37 +280,53 @@ func serve(server: NIOHTTPServer) async throws { ) ) case "/identity": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) // This will always write out the body with no encoding. // Used to check that a client can handle fallback to no encoding. var body = UniqueArray(copying: "TEST\n".utf8) try await responseSender.sendAndFinish(HTTPResponse(status: .ok), buffer: &body, trailer: nil) case "/redirect_ping": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) // Infinite redirection as a result of arriving here try await responseSender.sendAndFinish( HTTPResponse(status: .movedPermanently, headerFields: [.location: "/redirect_pong"]) ) case "/redirect_pong": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) // Infinite redirection as a result of arriving here try await responseSender.sendAndFinish( HTTPResponse(status: .movedPermanently, headerFields: [.location: "/redirect_ping"]) ) case "/301": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) // Redirect to /request try await responseSender.sendAndFinish( HTTPResponse(status: .movedPermanently, headerFields: [.location: "/request"]) ) case "/308": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) // Redirect to /request try await responseSender.sendAndFinish( HTTPResponse(status: .permanentRedirect, headerFields: [.location: "/request"]) ) case "/404": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) try await responseSender.sendAndFinish(HTTPResponse(status: .notFound)) case "/999": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) try await responseSender.sendAndFinish(HTTPResponse(status: 999)) case "/echo": // Bad method if request.method != .post { + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) var body = UniqueArray(copying: "Incorrect method".utf8) try await responseSender.sendAndFinish( HTTPResponse(status: .methodNotAllowed), @@ -330,6 +363,8 @@ func serve(server: NIOHTTPServer) async throws { try await writer.finish(trailer: nil) case "/stall": do { + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) // Wait for an hour (effectively never giving an answer) try await Task.sleep(for: .seconds(60 * 60)) assertionFailure("Not expected to complete hour-long wait") @@ -338,6 +373,8 @@ func serve(server: NIOHTTPServer) async throws { } case "/stall_body": do { + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) var writer = try await responseSender.send(.init(status: .ok)) var buffer = UniqueArray(copying: [UInt8](repeating: UInt8(ascii: "A"), count: 1000)) try await writer.write(buffer: &buffer) @@ -352,6 +389,8 @@ func serve(server: NIOHTTPServer) async throws { // It is okay for the client to give up on the connection due to the stall. } case "/1mb_body": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) var body = UniqueArray(copying: String(repeating: "A", count: 1_000_000).data(using: .ascii)!) do { try await responseSender.sendAndFinish(.init(status: .ok), buffer: &body, trailer: nil) @@ -363,6 +402,8 @@ func serve(server: NIOHTTPServer) async throws { // acceptable outcome here. } case "/cookie": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) let cookie = UUID().uuidString try await responseSender.sendAndFinish( .init( @@ -373,6 +414,8 @@ func serve(server: NIOHTTPServer) async throws { ) ) case "/etag": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) let clientETag = request.headerFields[.ifNoneMatch] let (serverETag, isNotModified) = eTag.next(clientETag: clientETag) if isNotModified { @@ -403,6 +446,8 @@ func serve(server: NIOHTTPServer) async throws { ) } case "/trailers": + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) // Send a response with custom trailers, fused with the body in a single finish call. let writer = try await responseSender.send(.init(status: .ok)) var buffer = UniqueArray(copying: "Response body".utf8) @@ -415,6 +460,8 @@ func serve(server: NIOHTTPServer) async throws { ] ) default: + var bodyBuffer = UniqueArray(minimumCapacity: 1) + _ = try await requestReader.collect(into: &bodyBuffer) var body = UniqueArray(copying: "Unknown path".utf8) try await responseSender.sendAndFinish(HTTPResponse(status: .internalServerError), buffer: &body, trailer: nil) } diff --git a/Submodules/swift-http-server b/Submodules/swift-http-server new file mode 160000 index 0000000..0dc060b --- /dev/null +++ b/Submodules/swift-http-server @@ -0,0 +1 @@ +Subproject commit 0dc060b94856e54be07cc28e488738a2e3e78e49