diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift index c2e7d14..3d9f931 100644 --- a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift @@ -228,33 +228,24 @@ extension NIOQUIC.KeyExchangeGroup { @available(anyAppleOS 26.0, *) extension NIOQUIC.AuthenticationConfiguration { - init(_ transportSecurity: NIOHTTPServerConfiguration.TransportSecurity) throws { - switch transportSecurity.backing { - case .plaintext: - throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity - - case .mTLS: - throw NIOHTTPServerConfigurationError.mTLSNotCurrentlySupportedOverHTTP3 - - case .tls(let tlsCredentials): - switch tlsCredentials.backing { - case .x509(let x509Credentials): - switch x509Credentials.backing { - case .serialized(.file(let certificateChain, let privateKey, format: .pem)): - self = .x509Certificates(certificateChainFilePath: certificateChain, privateKeyFilePath: privateKey) - - case .certificates, .reloading, .serialized(.file(_, _, .der)), .serialized(.bytes): - throw NIOHTTPServerConfigurationError.onlyPEMFileCredentialsCurrentlySupportedOverHTTP3 - } - - case .rawPublicKey(let rawPublicKeyCredentials): - switch rawPublicKeyCredentials.backing { - case .file(let publicKey, let privateKey, .der): - self = .rawPublicKeys(publicKeyFilePath: publicKey, privateKeyFilePath: privateKey) - - case .file(_, _, .pem): - throw NIOHTTPServerConfigurationError.pemRawPublicKeysNotCurrentlySupported - } + init(_ tlsCredentials: NIOHTTPServerConfiguration.TransportSecurity.TLSCredentials) throws { + switch tlsCredentials.backing { + case .x509(let x509Credentials): + switch x509Credentials.backing { + case .serialized(.file(let certificateChain, let privateKey, format: .pem)): + self = .x509Certificates(certificateChainFilePath: certificateChain, privateKeyFilePath: privateKey) + + case .certificates, .reloading, .serialized(.file(_, _, .der)), .serialized(.bytes): + throw NIOHTTPServerConfigurationError.onlyPEMFileX509CredentialsCurrentlySupportedOverHTTP3 + } + + case .rawPublicKey(let rawPublicKeyCredentials): + switch rawPublicKeyCredentials.backing { + case .file(let publicKey, let privateKey, .der): + self = .rawPublicKeys(publicKeyFilePath: publicKey, privateKeyFilePath: privateKey) + + case .file(_, _, .pem): + throw NIOHTTPServerConfigurationError.pemRawPublicKeysNotCurrentlySupported } } } @@ -296,34 +287,27 @@ extension NIOQUIC.Authenticator { /// Returns `nil` for raw public key credentials, because NIOQUIC reads the public/private key paths directly from /// `QUICConfiguration.authenticationConfiguration` (no `Authenticator` instance is required in that case). /// - /// - Parameter transportSecurity: The server's transport security configuration. + /// - Parameter tlsCredentials: The server's TLS credentials. /// /// - Throws: - /// - ``NIOHTTPServerConfigurationError/incompatibleTransportSecurity`` if `transportSecurity` is `.plaintext`. - /// - ``NIOHTTPServerConfigurationError/http3RequiresPEMFileCertificates`` if the X.509 credentials are not - /// provided as a PEM-encoded certificate chain and private key on disk. + /// - ``NIOHTTPServerConfigurationError/onlyPEMFileCredentialsCurrentlySupportedOverHTTP3`` if X.509 credentials + /// are not provided as a PEM-encoded certificate chain and private key on disk. /// - An underlying error from `Authenticator`'s initializer if the certificate chain or private key cannot be /// loaded. - convenience init?(_ transportSecurity: NIOHTTPServerConfiguration.TransportSecurity) throws { - switch transportSecurity.backing { - case .plaintext: - throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity - - case .tls(let tlsCredentials), .mTLS(let tlsCredentials, _): - switch tlsCredentials.backing { - case .rawPublicKey: - // Public/private key paths are read directly from `QUICConfiguration.authenticationConfiguration`, so - // we return `nil` here. - return nil - - case .x509(let x509Credentials): - switch x509Credentials.backing { - case .reloading, .serialized(.bytes), .serialized(.file(_, _, .der)), .certificates: - throw NIOHTTPServerConfigurationError.onlyPEMFileCredentialsCurrentlySupportedOverHTTP3 - - case .serialized(.file(let certificateChain, let privateKey, .pem)): - try self.init(certificateFilePath: certificateChain, privateKeyFilePath: privateKey) - } + convenience init?(_ tlsCredentials: NIOHTTPServerConfiguration.TransportSecurity.TLSCredentials) throws { + switch tlsCredentials.backing { + case .rawPublicKey: + // Public/private key paths are read directly from `QUICConfiguration.authenticationConfiguration`, so we + // return `nil` here. + return nil + + case .x509(let x509Credentials): + switch x509Credentials.backing { + case .reloading, .serialized(.bytes), .serialized(.file(_, _, .der)), .certificates: + throw NIOHTTPServerConfigurationError.onlyPEMFileX509CredentialsCurrentlySupportedOverHTTP3 + + case .serialized(.file(let certificateChain, let privateKey, .pem)): + try self.init(certificateFilePath: certificateChain, privateKeyFilePath: privateKey) } } } diff --git a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration+Validation.swift b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration+Validation.swift new file mode 100644 index 0000000..6ae2780 --- /dev/null +++ b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration+Validation.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOSSL + +#if HTTP3 +import NIOQUIC +#endif + +@available(anyAppleOS 26.0, *) +extension NIOHTTPServerConfiguration { + /// Validates the compatibility of the `supportedHTTPVersions` and `transportSecurity` configurations, and stores + /// the TLS resources required to set up the server channels. + mutating func validateTransportConfiguration() throws { + #if HTTP3 + (self.quicAuthenticationConfiguration, self.quicAuthenticator) = try self.makeQUICAuthentication() + #endif + + self.sslContext = try self.makeSSLContext() + } + + /// Creates the `NIOSSLContext` used by the secure upgrade channel(s), or `nil` if the configuration does not + /// specify a secure upgrade channel. + private func makeSSLContext() throws -> NIOSSLContext? { + #if HTTP3 + if self.supportedHTTPVersions.http3ConfigIfSupported != nil, self.supportedHTTPVersions.count == 1 { + // Only HTTP/3 was specified. As such, `NIOSSLContext` is not needed because a secure upgrade channel won't + // be set up. We can just return `nil` here. + return nil + } + #endif + + switch self.transportSecurity.backing { + case .plaintext: + // Only HTTP/1.1 can be served over plaintext. To serve HTTP/2, `transportSecurity` must be set to `.tls` or + // `.mTLS`. + guard self.supportedHTTPVersions == [.http1_1] else { + throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity + } + return nil + + case .tls, .mTLS: + return try .makeServerContext( + transportSecurity: self.transportSecurity, + alpnIdentifiers: self.supportedHTTPVersions.alpnIdentifiers + ) + } + } + + #if HTTP3 + /// Creates the QUIC authentication resources used by the HTTP/3 channel(s). + /// + /// Both are `nil` if HTTP/3 is not among ``supportedHTTPVersions``. + private func makeQUICAuthentication() throws -> ( + configuration: NIOQUIC.AuthenticationConfiguration?, + authenticator: NIOQUIC.Authenticator? + ) { + guard self.supportedHTTPVersions.http3ConfigIfSupported != nil else { return (nil, nil) } + + switch self.transportSecurity.backing { + case .plaintext: + // Only HTTP/1.1 can be served over plaintext. To serve HTTP/3, `transportSecurity` must be set to `.tls`. + throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity + + case .tls(let tlsCredentials): + // We unfortunately need to pass forward both an `AuthenticationConfiguration` and an `Authenticator`: + // + // - RPK credentials are read from `AuthenticationConfiguration`; + // - X509 certificates (in-memory or PEM files on disk) are read from `Authenticator`. + // + // The problem is that `QUICConfiguration` requires the `AuthenticationConfiguration` argument, *even* + // when the TLS credentials are X509 certificates. Moreover, `AuthenticationConfiguration` can only be + // created with *PEM-file backed X509 credentials* (or RPKs), *even though* `Authenticator` supports + // `swift-certificates` objects as the source. + return (configuration: try .init(tlsCredentials), authenticator: try .init(tlsCredentials)) + + case .mTLS: + throw NIOHTTPServerConfigurationError.mTLSNotCurrentlySupportedOverHTTP3 + } + } + #endif // HTTP3 +} diff --git a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift index 3a74de3..33c39f3 100644 --- a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift +++ b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift @@ -16,6 +16,10 @@ import NIOCore import NIOSSL public import X509 +#if HTTP3 +import NIOQUIC +#endif + /// Configuration settings for ``NIOHTTPServer``. /// /// This structure contains all the necessary configuration options for setting up @@ -285,21 +289,65 @@ public struct NIOHTTPServerConfiguration: Sendable { } /// Network binding configuration specifying all addresses where the server should listen. - public var bindTargets: [BindTarget] + /// + /// - Precondition: Must not be empty. + public var bindTargets: [BindTarget] { + didSet { + if self.bindTargets.isEmpty { + preconditionFailure(NIOHTTPServerConfigurationError.noBindTargetsSpecified.description) + } + } + } /// TLS configuration for the server. - public var transportSecurity: TransportSecurity + /// + /// - Precondition: Must be compatible with ``supportedHTTPVersions``: + /// - `transportSecurity == .mTLS` is not supported when `supportedHTTPVersions` contains `.http3`. + /// - Raw Public Key credentials are only supported when `supportedHTTPVersions == [.http3]`. + /// - When `supportedHTTPVersions` contains `.http2` and `.http3`, TLS credentials must be provided as PEM files + /// on disk. Other credential sources are not supported. + /// - `transportSecurity` can only be set to `.plaintext` when `supportedHTTPVersions == [.http1_1]`. + public var transportSecurity: TransportSecurity { + didSet { + do { + try self.validateTransportConfiguration() + } catch { + preconditionFailure("\(error)") + } + } + } /// The HTTP protocol versions the server advertises and accepts connections for. - public var supportedHTTPVersions: Set + /// + /// - Precondition: Must not be empty, and must be compatible with ``transportSecurity``: + /// - `transportSecurity == .mTLS` is not supported when `supportedHTTPVersions` contains `.http3`. + /// - Raw Public Key credentials are only supported when `supportedHTTPVersions == [.http3]`. + /// - When `supportedHTTPVersions` contains `.http2` and `.http3`, TLS credentials must be provided as PEM files + /// on disk. Other credential sources are not supported. + /// - `transportSecurity` can only be set to `.plaintext` when `supportedHTTPVersions == [.http1_1]`. + public var supportedHTTPVersions: Set { + didSet { + if self.supportedHTTPVersions.isEmpty { + preconditionFailure(NIOHTTPServerConfigurationError.noSupportedHTTPVersionsSpecified.description) + } + + do { + try self.validateTransportConfiguration() + } catch { + preconditionFailure("\(error)") + } + } + } /// Backpressure strategy to use in the server. public var backpressureStrategy: BackPressureStrategy /// The maximum number of concurrent connections the server will accept. /// - /// When this limit is reached, the server stops accepting new connections - /// until existing ones close. `nil` means unlimited (the default). + /// When this limit is reached, the server stops accepting new connections until existing ones close. `nil` means + /// unlimited (the default). + /// + /// - Note: Connection limits are not currently supported over HTTP/3. /// /// - Precondition: Must be greater than 0 if non-`nil`. public var maxConnections: Int? { @@ -313,6 +361,24 @@ public struct NIOHTTPServerConfiguration: Sendable { /// Configuration for connection timeouts. public var connectionTimeouts: ConnectionTimeouts + /// The `NIOSSLContext` used by the secure upgrade channel(s), derived when the configuration is validated. + /// + /// `nil` when the configuration doesn't call for a secure upgrade channel, i.e. plaintext HTTP/1.1 or HTTP/3 only. + var sslContext: NIOSSLContext? + + #if HTTP3 + /// The QUIC authentication configuration used by the HTTP/3 channel(s). + /// + /// `nil` when HTTP/3 is not among ``supportedHTTPVersions``. + var quicAuthenticationConfiguration: NIOQUIC.AuthenticationConfiguration? + + /// The QUIC authenticator used by the HTTP/3 channel(s), derived when the configuration is validated. + /// + /// `nil` when HTTP/3 is not among ``supportedHTTPVersions``, and also when the TLS credentials are raw public keys; + /// NIOQUIC reads those directly from ``quicAuthenticationConfiguration``. + var quicAuthenticator: NIOQUIC.Authenticator? + #endif + /// Create a new configuration with multiple bind targets. /// /// Other configuration properties (``backpressureStrategy``, ``maxConnections``, @@ -332,14 +398,6 @@ public struct NIOHTTPServerConfiguration: Sendable { throw NIOHTTPServerConfigurationError.noBindTargetsSpecified } - // If `transportSecurity`` is set to `.plaintext`, the server can only support HTTP/1.1. - // To support HTTP/2, `transportSecurity` must be set to `.tls` or `.mTLS`. - if case .plaintext = transportSecurity.backing { - guard supportedHTTPVersions == [.http1_1] else { - throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity - } - } - if supportedHTTPVersions.isEmpty { throw NIOHTTPServerConfigurationError.noSupportedHTTPVersionsSpecified } @@ -350,6 +408,9 @@ public struct NIOHTTPServerConfiguration: Sendable { self.backpressureStrategy = .defaults self.maxConnections = nil self.connectionTimeouts = .defaults + + // Validate the compatibility of `supportedHTTPVersions` and `transportSecurity`. + try self.validateTransportConfiguration() } /// Create a new configuration with a single bind target. @@ -507,17 +568,31 @@ extension NIOHTTPServerConfiguration { /// The HTTP/2 protocol version. /// /// - Parameter config: The configuration to use for HTTP/2. - public static func http2(config: HTTP2) -> Self { + public static func http2(config: HTTP2 = .defaults) -> Self { Self(version: .http2(config: config)) } + /// The HTTP/2 protocol version with default configuration values. + /// + /// - Note: Use ``http2(config:)`` to specify custom configuration values. + public static var http2: Self { + .http2(config: .defaults) + } + #if HTTP3 /// The HTTP/3 protocol version. /// /// - Parameter config: The configuration to use for HTTP/3. - public static func http3(config: HTTP3) -> Self { + public static func http3(config: HTTP3 = .defaults) -> Self { Self(version: .http3(config: config)) } + + /// The HTTP/3 protocol version with default configuration values. + /// + /// - Note: Use ``http3(config:)`` to specify custom configuration values. + public static var http3: Self { + .http3(config: .defaults) + } #endif /// Two values are equal if they represent the same protocol version, regardless of any differences in HTTP/2 diff --git a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift index 086e93a..855d442 100644 --- a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift +++ b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift @@ -17,7 +17,7 @@ enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible { case noSupportedHTTPVersionsSpecified case incompatibleTransportSecurity case noBindTargetsSpecified - case onlyPEMFileCredentialsCurrentlySupportedOverHTTP3 + case onlyPEMFileX509CredentialsCurrentlySupportedOverHTTP3 case rawPublicKeyTLSCredentialsNotCurrentlySupportedOverHTTP1OrHTTP2 case pemRawPublicKeysNotCurrentlySupported // swift-nio-quic doesn't currently support mTLS. See https://github.com/apple/swift-nio-quic/issues/5. @@ -34,7 +34,7 @@ enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible { case .noBindTargetsSpecified: "Invalid configuration: at least one bind target must be specified." - case .onlyPEMFileCredentialsCurrentlySupportedOverHTTP3: + case .onlyPEMFileX509CredentialsCurrentlySupportedOverHTTP3: "Invalid configuration: only PEM-file X.509 credentials are supported over HTTP/3. DER-encoded, in-memory, reloading, and PEM/DER bytes credential sources are not currently supported." case .rawPublicKeyTLSCredentialsNotCurrentlySupportedOverHTTP1OrHTTP2: diff --git a/Sources/NIOHTTPServer/Configuration/TransportSecurity+TLSCredentials.swift b/Sources/NIOHTTPServer/Configuration/TransportSecurity+TLSCredentials.swift index 131a929..31b3c7a 100644 --- a/Sources/NIOHTTPServer/Configuration/TransportSecurity+TLSCredentials.swift +++ b/Sources/NIOHTTPServer/Configuration/TransportSecurity+TLSCredentials.swift @@ -61,7 +61,7 @@ extension NIOHTTPServerConfiguration.TransportSecurity { /// /// The credentials can be provided in any of the following ways: /// - As in-memory `X509.Certificate` and `X509.Certificate.PrivateKey` objects (``certificates(chain:privateKey:)``); - /// - From files (``pemFile(certificateChain:privateKey:)``, ``derFile(certificate:privateKey:)``) or bytes + /// - From files (``pemFile(certificateChainPath:privateKeyPath:)``, ``derFile(certificatePath:privateKeyPath:)``) or bytes /// (``pemBytes(certificateChain:privateKey:)``, ``derBytes(certificate:privateKey:)``), or; /// - Through a `CertificateReloader` instance that periodically reloads the credentials (``reloading(_:)``). public struct X509Credentials: Sendable { diff --git a/Sources/NIOHTTPServer/NIOHTTPServer+HTTP3.swift b/Sources/NIOHTTPServer/NIOHTTPServer+HTTP3.swift index edff7e2..558a7ec 100644 --- a/Sources/NIOHTTPServer/NIOHTTPServer+HTTP3.swift +++ b/Sources/NIOHTTPServer/NIOHTTPServer+HTTP3.swift @@ -106,7 +106,9 @@ extension NIOHTTPServer { /// alongside the associated HTTP/3 connection multiplexer. func setupHTTP3ServerChannels( bindTargets: [NIOHTTPServerConfiguration.BindTarget], - http3Configuration: NIOHTTPServerConfiguration.HTTP3 + http3Configuration: NIOHTTPServerConfiguration.HTTP3, + authenticationConfiguration: NIOQUIC.AuthenticationConfiguration, + authenticator: NIOQUIC.Authenticator? ) async throws -> [( quicChannel: any Channel, connectionMultiplexer: HTTP3ServerConnectionMultiplexer< @@ -114,11 +116,6 @@ extension NIOHTTPServer { NIOQUIC.QUICStreamCreator > )] { - let quicConfiguration = try NIOQUIC.QUICConfiguration.init( - http3Configuration.quicConfiguration, - authenticationConfiguration: .init(self.configuration.transportSecurity) - ) - let bootstrap = DatagramBootstrap(group: .singletonMultiThreadedEventLoopGroup) .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) @@ -138,8 +135,9 @@ extension NIOHTTPServer { channel.eventLoop.makeCompletedFuture { try self.setupQUICChannel( channel: channel, - quicConfiguration: quicConfiguration, - http3Configuration: http3Configuration + http3Configuration: http3Configuration, + authenticationConfiguration: authenticationConfiguration, + authenticator: authenticator ) } } @@ -162,8 +160,9 @@ extension NIOHTTPServer { /// multiplexer. func setupQUICChannel( channel: any Channel, - quicConfiguration: NIOQUIC.QUICConfiguration, - http3Configuration: NIOHTTPServerConfiguration.HTTP3 + http3Configuration: NIOHTTPServerConfiguration.HTTP3, + authenticationConfiguration: NIOQUIC.AuthenticationConfiguration, + authenticator: NIOQUIC.Authenticator? ) throws -> ( quicChannel: any Channel, connectionMultiplexer: HTTP3ServerConnectionMultiplexer< @@ -177,10 +176,13 @@ extension NIOHTTPServer { let quicHandler = QUICHandler( channel: channel, - quicConfiguration: quicConfiguration, + quicConfiguration: .init( + http3Configuration.quicConfiguration, + authenticationConfiguration: authenticationConfiguration + ), // TODO: mTLS is not yet supported by NIOQUIC so we don't specify a value for `asyncVerifier`. asyncVerifier: nil, - authenticator: try .init(self.configuration.transportSecurity), + authenticator: authenticator, logger: self.logger, inboundConnectionInitializer: { connectionChannel, streamCreator in connectionChannel.eventLoop.makeCompletedFuture { diff --git a/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift b/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift index db88208..eb88fd7 100644 --- a/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift +++ b/Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift @@ -214,7 +214,7 @@ extension NIOHTTPServer { func setupSecureUpgradeServerChannels( bindTargets: [NIOHTTPServerConfiguration.BindTarget], - supportedHTTPVersions: Set, + http2Configuration: NIOHTTPServerConfiguration.HTTP2?, sslContext: NIOSSLContext ) async throws -> [(NIOAsyncChannel, Never>, ServerQuiescingHelper)] { let bootstrap = ServerBootstrap(group: self.eventLoopGroup) @@ -242,7 +242,7 @@ extension NIOHTTPServer { }.bind(host: host, port: port) { channel in self.setupSecureUpgradeConnectionChildChannel( channel: channel, - supportedHTTPVersions: supportedHTTPVersions, + http2Configuration: http2Configuration, sslContext: sslContext ) } @@ -313,31 +313,19 @@ extension NIOHTTPServer { func setupSecureUpgradeConnectionChildChannel( channel: any Channel, - supportedHTTPVersions: Set, + http2Configuration: NIOHTTPServerConfiguration.HTTP2?, sslContext: NIOSSLContext ) -> EventLoopFuture> { channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHandler( - self.makeSSLServerHandler( - sslContext, - self.configuration.transportSecurity.customVerificationCallback - ) + let sslHandler = self.makeSSLServerHandler( + sslContext, + self.configuration.transportSecurity.customVerificationCallback ) - }.flatMap { - channel.eventLoop.makeCompletedFuture { - let alpnHandler = self.makeALPNHandler( - channel: channel, - http2Config: supportedHTTPVersions.http2ConfigIfSupported - ) + let alpnHandler = self.makeALPNHandler(channel: channel, http2Config: http2Configuration) - do { - try channel.pipeline.syncOperations.addHandler(alpnHandler) - } catch { - return channel.eventLoop.makeFailedFuture(error) - } + try channel.pipeline.syncOperations.addHandlers([sslHandler, alpnHandler]) - return alpnHandler.protocolNegotiationResult - } + return alpnHandler.protocolNegotiationResult } } diff --git a/Sources/NIOHTTPServer/NIOHTTPServer.swift b/Sources/NIOHTTPServer/NIOHTTPServer.swift index 5b86a2f..c1335c4 100644 --- a/Sources/NIOHTTPServer/NIOHTTPServer.swift +++ b/Sources/NIOHTTPServer/NIOHTTPServer.swift @@ -205,23 +205,18 @@ public struct NIOHTTPServer: HTTPServer { /// Creates and returns server channels based on the configured transport security. func makeServerChannels() async throws -> [ServerChannel] { - // If transport security is `plaintext`, we can only create an HTTP/1.1 channel. - if case .plaintext = self.configuration.transportSecurity.backing { - let http1Channels = try await self.setupHTTP1_1ServerChannels(bindTargets: self.configuration.bindTargets) - try self.addressesBound(http1Channels.map { (channel, _) in channel.channel.localAddress }) - return http1Channels.map { (channel, quiescingHelper) in - .plaintextHTTP1_1(channel: channel, quiescingHelper: quiescingHelper) - } - } - var serverChannels = [ServerChannel]() var secureUpgradeBindTargets = self.configuration.bindTargets #if HTTP3 - if let http3Config = self.configuration.supportedHTTPVersions.http3ConfigIfSupported { + if let http3Configuration = self.configuration.supportedHTTPVersions.http3ConfigIfSupported, + let authenticationConfiguration = self.configuration.quicAuthenticationConfiguration + { let http3Channels = try await self.setupHTTP3ServerChannels( bindTargets: self.configuration.bindTargets, - http3Configuration: http3Config + http3Configuration: http3Configuration, + authenticationConfiguration: authenticationConfiguration, + authenticator: self.configuration.quicAuthenticator ) serverChannels.append( contentsOf: http3Channels.map { (quicChannel, mux) in @@ -229,7 +224,7 @@ public struct NIOHTTPServer: HTTPServer { } ) - guard self.configuration.supportedHTTPVersions.count > 1 else { + if self.configuration.sslContext == nil { // `supportedHTTPVersions == [.http3]` here. We therefore just return HTTP/3 channel(s). try self.addressesBound(http3Channels.map { (channel, _) in channel.localAddress }) return serverChannels @@ -242,13 +237,17 @@ public struct NIOHTTPServer: HTTPServer { } #endif // HTTP3 + guard let sslContext = self.configuration.sslContext else { + // Set up plaintext HTTP/1.1 channel(s). + let http1Channels = try await self.setupHTTP1_1ServerChannels(bindTargets: secureUpgradeBindTargets) + try self.addressesBound(http1Channels.map { (channel, _) in channel.channel.localAddress }) + return http1Channels.map { .plaintextHTTP1_1(channel: $0, quiescingHelper: $1) } + } + let secureUpgradeChannels = try await self.setupSecureUpgradeServerChannels( bindTargets: secureUpgradeBindTargets, - supportedHTTPVersions: self.configuration.supportedHTTPVersions, - sslContext: .makeServerContext( - transportSecurity: self.configuration.transportSecurity, - alpnIdentifiers: self.configuration.supportedHTTPVersions.alpnIdentifiers - ) + http2Configuration: self.configuration.supportedHTTPVersions.http2ConfigIfSupported, + sslContext: sslContext ) try self.addressesBound(secureUpgradeChannels.map { (channel, _) in channel.channel.localAddress }) diff --git a/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift b/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift index 0d01747..65a3a28 100644 --- a/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift +++ b/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift @@ -56,34 +56,13 @@ struct HTTP3ConfigurationTests { @Suite struct AuthenticationConfigurationTests { - @Test("Plaintext transport security is rejected") - @available(anyAppleOS 26.0, *) - func plaintextRejected() { - #expect(throws: NIOHTTPServerConfigurationError.incompatibleTransportSecurity) { - _ = try NIOQUIC.AuthenticationConfiguration(.plaintext) - } - } - - @Test("mTLS transport security is rejected") - @available(anyAppleOS 26.0, *) - func mTLSRejected() { - #expect(throws: NIOHTTPServerConfigurationError.mTLSNotCurrentlySupportedOverHTTP3) { - _ = try NIOQUIC.AuthenticationConfiguration( - .mTLS( - credentials: .x509(.pemFile(certificateChainPath: "/cert.pem", privateKeyPath: "/key.pem")), - trustConfiguration: .init(.pemFile(trustRootsPath: "/roots.pem")) - ) - ) - } - } - @Test("In-memory TLS credentials are rejected") @available(anyAppleOS 26.0, *) func inMemoryCredentialsRejected() throws { let chain = try TestCA.makeSelfSignedChain() - #expect(throws: NIOHTTPServerConfigurationError.onlyPEMFileCredentialsCurrentlySupportedOverHTTP3) { + #expect(throws: NIOHTTPServerConfigurationError.onlyPEMFileX509CredentialsCurrentlySupportedOverHTTP3) { _ = try NIOQUIC.AuthenticationConfiguration( - .tls(credentials: .x509(.certificates(chain: chain.chain, privateKey: chain.privateKey))) + .x509(.certificates(chain: chain.chain, privateKey: chain.privateKey)) ) } } @@ -93,7 +72,7 @@ struct HTTP3ConfigurationTests { func pemFileCredentialsAccepted() { #expect(throws: Never.self) { _ = try NIOQUIC.AuthenticationConfiguration( - .tls(credentials: .x509(.pemFile(certificateChainPath: "/cert.pem", privateKeyPath: "/key.pem"))) + .x509(.pemFile(certificateChainPath: "/cert.pem", privateKeyPath: "/key.pem")) ) } } diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift index 8a280c5..5509333 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServer+ServiceLifecycleTests.swift @@ -379,7 +379,7 @@ struct NIOHTTPServiceLifecycleTests { ) async throws { // Configure two listeners. We want to test whether graceful shutdown works independently on each listener. let (serverConfiguration, trustRootsPEMPath) = try TestHelpers.makeSecureUpgradeServerConfiguration( - supportedHTTPVersions: [.http1_1, .http2(config: .defaults)], + supportedHTTPVersions: [.http1_1, .http2], concurrentListeners: 2 ) let server = NIOHTTPServer(logger: self.serverLogger, configuration: serverConfiguration) diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerConfigurationTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerConfigurationTests.swift new file mode 100644 index 0000000..71484bf --- /dev/null +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerConfigurationTests.swift @@ -0,0 +1,377 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import NIOCertificateReloading +import NIOSSL +import Testing +import X509 + +@testable import NIOHTTPServer + +@Suite +struct NIOHTTPServerConfigurationTests { + @Suite + struct BindTarget { + @available(anyAppleOS 26.0, *) + @Test("Empty bindTargets throws error") + func emptyBindTargetsThrows() throws { + #expect(throws: NIOHTTPServerConfigurationError.noBindTargetsSpecified) { + try NIOHTTPServerConfiguration( + bindTargets: [], + supportedHTTPVersions: [.http1_1], + transportSecurity: .plaintext + ) + } + } + } + + @Suite + struct SupportedHTTPVersions { + @available(anyAppleOS 26.0, *) + @Test("Empty supportedHTTPVersions throws error") + func emptySupportedHTTPVersionsThrows() { + #expect(throws: NIOHTTPServerConfigurationError.noSupportedHTTPVersionsSpecified) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [], + transportSecurity: .plaintext + ) + } + } + + @available(anyAppleOS 26.0, *) + @Test("transport: plaintext, versions: {HTTP/1.1} -> valid") + func plaintextTransportAndHTTP1_1IsValid() { + #expect(throws: Never.self) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http1_1], + transportSecurity: .plaintext + ) + } + } + + #if HTTP3 + @available(anyAppleOS 26.0, *) + @Test( + "transport: plaintext, versions: HTTP/2 and/or HTTP/3 -> invalid", + arguments: [ + [NIOHTTPServerConfiguration.HTTPVersion.http2], + [.http3], + [.http2, .http3], + // Even when HTTP/1.1 is specified, the presence of HTTP/2 and/or HTTP/3 should make the config invalid. + [.http1_1, .http2], + [.http1_1, .http3], + [.http1_1, .http2, .http3], + ] + ) + func plaintextNotSupportedForHTTP2OrHTTP3(supportedHTTPVersions: Set) { + #expect(throws: NIOHTTPServerConfigurationError.incompatibleTransportSecurity) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: supportedHTTPVersions, + transportSecurity: .plaintext + ) + } + } + #endif // HTTP3 + } + + @Suite + struct TransportSecurity { + @available(anyAppleOS 26.0, *) + @Test( + "All X.509 credential sources produce a valid configuration", + arguments: TestX509CredentialSource.allCases + ) + func x509CredentialSourceProducesValidConfiguration(source: TestX509CredentialSource) throws { + let chain = try TestCA.makeSelfSignedChain() + let credentials = try source.makeCredentials(from: chain) + + #expect(throws: Never.self) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http1_1, .http2], + transportSecurity: .tls(credentials: .x509(credentials)) + ) + } + } + + @available(anyAppleOS 26.0, *) + @Test("All mTLS trust root sources produce a valid configuration", arguments: MTLSTrustSource.allCases) + func mTLSTrustRootSourceProducesValidConfiguration(source: MTLSTrustSource) throws { + let chain = try TestCA.makeSelfSignedChain() + let trustConfiguration = try source.makeTrustConfiguration(from: chain) + + #expect(throws: Never.self) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http1_1, .http2], + transportSecurity: .mTLS( + credentials: .x509(.certificates(chain: chain.chain, privateKey: chain.privateKey)), + trustConfiguration: .init(trustConfiguration) + ) + ) + } + } + + @available(anyAppleOS 26.0, *) + @Test( + "A non-existent X.509 certificate file path is rejected", + arguments: [ + NIOHTTPServerConfiguration.TransportSecurity.X509Credentials.pemFile( + certificateChainPath: "/does/not/exist.pem", + privateKeyPath: "/does/not/exist.key" + ), + .derFile(certificatePath: "/does/not/exist.der", privateKeyPath: "/does/not/exist.der"), + ] + ) + func nonExistentX509FilePathRejected( + credentials: NIOHTTPServerConfiguration.TransportSecurity.X509Credentials + ) { + #expect(throws: Error.self) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http1_1, .http2], + transportSecurity: .tls(credentials: .x509(credentials)) + ) + } + } + + @available(anyAppleOS 26.0, *) + @Test( + "Malformed X.509 credential bytes are rejected", + arguments: [ + NIOHTTPServerConfiguration.TransportSecurity.X509Credentials.pemBytes( + certificateChain: Array("not a valid PEM document".utf8), + privateKey: Array("not a valid PEM document".utf8) + ), + .derBytes(certificate: [0x00, 0x01, 0x02], privateKey: [0x03, 0x04, 0x05]), + ] + ) + func malformedX509BytesRejected( + credentials: NIOHTTPServerConfiguration.TransportSecurity.X509Credentials + ) throws { + #expect(throws: NIOSSLError.failedToLoadCertificate) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http1_1, .http2], + transportSecurity: .tls(credentials: .x509(credentials)) + ) + } + } + + #if HTTP3 + @available(anyAppleOS 26.0, *) + @Test("PEM-file X.509 credentials over HTTP/3 produces a valid configuration") + func pemFileX509ProducesValidHTTP3Configuration() throws { + let chain = try TestCA.makeSelfSignedChain() + let (leafPath, _, keyPath) = try chain.writeToDisk() + + #expect(throws: Never.self) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http3], + transportSecurity: .tls( + credentials: .x509(.pemFile(certificateChainPath: leafPath, privateKeyPath: keyPath)) + ) + ) + } + } + + @available(anyAppleOS 26.0, *) + @Test( + "Non-PEM-file X.509 credentials are rejected over HTTP/3", + arguments: [TestX509CredentialSource.inMemory, .reloading, .pemBytes, .derFile, .derBytes] + ) + func nonPEMFileX509RejectedOverHTTP3(source: TestX509CredentialSource) throws { + let chain = try TestCA.makeSelfSignedChain() + let credentials = try source.makeCredentials(from: chain) + + #expect(throws: NIOHTTPServerConfigurationError.onlyPEMFileX509CredentialsCurrentlySupportedOverHTTP3) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http3], + transportSecurity: .tls(credentials: .x509(credentials)) + ) + } + } + + @available(anyAppleOS 26.0, *) + @Test("DER-file RPK credentials produces a valid TLS configuration") + func derFileRPKProducesValidConfiguration() throws { + let chain = try TestCA.makeSelfSignedChain() + + #expect(throws: Never.self) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http3], + transportSecurity: .tls(credentials: .rawPublicKey(.makeTestCredentials(from: chain))) + ) + } + } + + @available(anyAppleOS 26.0, *) + @Test("Raw public key credentials are rejected over HTTP/1.1 and HTTP/2") + func rawPublicKeyRejectedOverHTTP1AndHTTP2() throws { + #expect( + throws: NIOHTTPServerConfigurationError.rawPublicKeyTLSCredentialsNotCurrentlySupportedOverHTTP1OrHTTP2 + ) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http1_1, .http2], + transportSecurity: .tls( + credentials: .rawPublicKey(.derFile(publicKeyPath: "public.der", privateKeyPath: "private.der")) + ) + ) + } + } + + @available(anyAppleOS 26.0, *) + @Test("mTLS is rejected over HTTP/3") + func mTLSRejectedOverHTTP3() throws { + let chain = try TestCA.makeSelfSignedChain() + let (leafPath, _, keyPath) = try chain.writeToDisk() + + #expect(throws: NIOHTTPServerConfigurationError.mTLSNotCurrentlySupportedOverHTTP3) { + try NIOHTTPServerConfiguration( + bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), + supportedHTTPVersions: [.http3], + transportSecurity: .mTLS( + credentials: .x509(.pemFile(certificateChainPath: leafPath, privateKeyPath: keyPath)), + trustConfiguration: .init(.systemDefaults) + ) + ) + } + } + #endif // HTTP3 + } +} + +@available(anyAppleOS 26.0, *) +enum TestX509CredentialSource: Sendable, CaseIterable { + case inMemory + case reloading + case pemFile + case pemBytes + case derFile + case derBytes + + /// Builds ``X509Credentials`` from `chain`. + func makeCredentials( + from chain: ChainPrivateKeyPair + ) throws -> NIOHTTPServerConfiguration.TransportSecurity.X509Credentials { + switch self { + case .inMemory: + return .certificates(chain: chain.chain, privateKey: chain.privateKey) + + case .reloading: + let (leafPath, _, keyPath) = try chain.writeToDisk() + + let reloader = try TimedCertificateReloader.makeReloaderValidatingSources( + configuration: .init( + refreshInterval: .seconds(60), + certificateSource: .init(location: .file(path: leafPath), format: .pem), + privateKeySource: .init(location: .file(path: keyPath), format: .pem) + ) + ) + + return .reloading(reloader) + + case .pemFile: + let (leafPath, _, keyPath) = try chain.writeToDisk() + return .pemFile(certificateChainPath: leafPath, privateKeyPath: keyPath) + + case .pemBytes: + return .pemBytes( + certificateChain: Array(try chain.chainPEMString.utf8), + privateKey: Array(try chain.privateKey.serializeAsPEM().pemString.utf8) + ) + + case .derFile: + let (leafPath, _, keyPath) = try chain.writeToDisk(encoding: .der) + return .derFile(certificatePath: leafPath, privateKeyPath: keyPath) + + case .derBytes: + return .derBytes( + certificate: try chain.leaf.serializeAsPEM().derBytes, + privateKey: try chain.privateKey.serializeAsPEM().derBytes + ) + } + } +} + +#if HTTP3 +@available(anyAppleOS 26.0, *) +extension NIOHTTPServerConfiguration.TransportSecurity.RawPublicKeyCredentials { + /// Builds ``RawPublicKeyCredentials`` from `chain`. + static func makeTestCredentials( + from chain: ChainPrivateKeyPair + ) throws -> Self { + let publicKey = chain.leaf.publicKey + let privateKey = chain.privateKey + + let uuid = UUID().uuidString + let publicKeyPath = FileManager.default.temporaryDirectory.appendingPathComponent("leaf-\(uuid)") + let privateKeyPath = FileManager.default.temporaryDirectory.appendingPathComponent("key-\(uuid)") + + try Data(try publicKey.serializeAsPEM().derBytes).write(to: publicKeyPath) + try Data(try privateKey.serializeAsPEM().derBytes).write(to: privateKeyPath) + + return .derFile(publicKeyPath: publicKeyPath.path, privateKeyPath: privateKeyPath.path) + } +} +#endif // HTTP3 + +@available(anyAppleOS 26.0, *) +enum MTLSTrustSource: Sendable, CaseIterable { + case systemDefaults + case inMemory + case pemFile + case pemBytes + case derFile + case derBytes + case customCallback + + /// Builds ``MTLSTrustConfiguration`` from `chain`. + func makeTrustConfiguration( + from chain: ChainPrivateKeyPair + ) throws -> NIOHTTPServerConfiguration.TransportSecurity.MTLSTrustConfiguration.TrustSource { + switch self { + case .systemDefaults: + return .systemDefaults + + case .inMemory: + return .certificates(trustRoots: [chain.ca]) + + case .pemFile: + let (_, caPath, _) = try chain.writeToDisk() + return .pemFile(trustRootsPath: caPath) + + case .pemBytes: + return .pemBytes(trustRoots: Array(try chain.ca.serializeAsPEM().pemString.utf8)) + + case .derFile: + let (_, caPath, _) = try chain.writeToDisk(encoding: .der) + return .derFile(trustRootPath: caPath) + + case .derBytes: + return .derBytes(trustRoot: try chain.ca.serializeAsPEM().derBytes) + + case .customCallback: + return .customCertificateVerificationCallback { _ in .certificateVerified(.init(nil)) } + } + } +} diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift index d91ef34..42953a9 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerEndToEndTests.swift @@ -62,7 +62,7 @@ struct NIOHTTPServerEndToEndTests { transportSecurity: .tls( credentials: .x509(.certificates(chain: serverChain.chain, privateKey: serverChain.privateKey)) ), - supportedHTTPVersions: [.http1_1, .http2(config: .defaults)], + supportedHTTPVersions: [.http1_1, .http2], handler: HTTPServerClosureRequestHandler { request, reqContext, reqReader, resSender in var buffer = UniqueArray(copying: [1, 2]) try await resSender.sendAndFinish(.init(status: .ok), buffer: &buffer, trailer: [.serverTiming: "test"]) diff --git a/Tests/NIOHTTPServerTests/NIOHTTPServerSwiftConfigurationTests.swift b/Tests/NIOHTTPServerTests/NIOHTTPServerSwiftConfigurationTests.swift index 515c448..5bf4cfb 100644 --- a/Tests/NIOHTTPServerTests/NIOHTTPServerSwiftConfigurationTests.swift +++ b/Tests/NIOHTTPServerTests/NIOHTTPServerSwiftConfigurationTests.swift @@ -556,6 +556,7 @@ struct NIOHTTPServerSwiftConfigurationTests { @Test("End-to-end HTTP/3 configuration over TLS") @available(anyAppleOS 26.0, *) func testEndToEnd() throws { + let (leafPath, _, keyPath) = try TestCA.makeSelfSignedChain().writeToDisk() let provider = InMemoryProvider(values: [ "bindTarget.host": "127.0.0.1", "bindTarget.port": 8000, @@ -566,8 +567,8 @@ struct NIOHTTPServerSwiftConfigurationTests { "http.http3.quicConfiguration.sendRetry": true, "transportSecurity.mode": "tls", "transportSecurity.credentialSource": "file", - "transportSecurity.certificateChainPEMPath": .init(.string("cert.pem"), isSecret: false), - "transportSecurity.privateKeyPEMPath": .init(.string("key.pem"), isSecret: true), + "transportSecurity.certificateChainPEMPath": .init(.string(leafPath), isSecret: false), + "transportSecurity.privateKeyPEMPath": .init(.string(keyPath), isSecret: true), ]) let config = ConfigReader(provider: provider) diff --git a/Tests/NIOHTTPServerTests/ServerChannelTests.swift b/Tests/NIOHTTPServerTests/ServerChannelTests.swift index 130bffb..2eb6f37 100644 --- a/Tests/NIOHTTPServerTests/ServerChannelTests.swift +++ b/Tests/NIOHTTPServerTests/ServerChannelTests.swift @@ -42,38 +42,24 @@ struct ServerChannelTests { } @available(anyAppleOS 26.0, *) - @Test("transport: TLS, versions: {HTTP/1.1} -> secure upgrade channel") - func tlsHTTP1_1() async throws { - let chain = try TestCA.makeSelfSignedChain() - - let server = NIOHTTPServer( - logger: self.logger, - configuration: try .init( - bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), - supportedHTTPVersions: [.http1_1], - transportSecurity: .tls( - credentials: .x509(.certificates(chain: chain.chain, privateKey: chain.privateKey)) - ) - ) - ) - - let channels = try await server.makeServerChannels() - defer { server.close(serverChannels: channels) } - - #expect(channels.count == 1) - #expect(channels[0].isSecureUpgrade) - } - - @available(anyAppleOS 26.0, *) - @Test("transport: TLS, versions: {HTTP/1.1, HTTP/2} -> secure upgrade channel") - func tlsHTTP1_1AndHTTP2() async throws { + @Test( + "transport: TLS, versions: {HTTP/1.1 and/or HTTP/2} -> secure upgrade channel", + arguments: [ + [NIOHTTPServerConfiguration.HTTPVersion.http1_1], + [.http2], + [.http1_1, .http2], + ] + ) + func tlsHTTP1_1AndOrHTTP2( + supportedHTTPVersions: Set + ) async throws { let chain = try TestCA.makeSelfSignedChain() let server = NIOHTTPServer( logger: self.logger, configuration: try .init( bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), - supportedHTTPVersions: [.http1_1, .http2(config: .defaults)], + supportedHTTPVersions: supportedHTTPVersions, transportSecurity: .tls( credentials: .x509(.certificates(chain: chain.chain, privateKey: chain.privateKey)) ) @@ -98,7 +84,7 @@ struct ServerChannelTests { logger: self.logger, configuration: try .init( bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), - supportedHTTPVersions: [.http3(config: .defaults)], + supportedHTTPVersions: [.http3], transportSecurity: .tls( credentials: .x509(.pemFile(certificateChainPath: leafPath, privateKeyPath: keyPath)) ) @@ -116,9 +102,9 @@ struct ServerChannelTests { @Test( "transport: TLS, versions: {HTTP/1.1 and/or HTTP/2} + {HTTP/3} -> HTTP/3 and secure upgrade channels", arguments: [ - [Self.http1_1, Self.http3], - [Self.http2, Self.http3], - [Self.http1_1, Self.http2, Self.http3], + [NIOHTTPServerConfiguration.HTTPVersion.http1_1, .http3], + [.http2, .http3], + [.http1_1, .http2, .http3], ] ) func tlsCombinationOfSecureUpgradeAndHTTP3( @@ -150,43 +136,9 @@ struct ServerChannelTests { let secureUpgradeAddress = try #require(channels[1].localAddress) #expect(http3Address.port == secureUpgradeAddress.port) } - - @available(anyAppleOS 26.0, *) - @Test( - "transport: plaintext, versions: HTTP/2 and/or HTTP/3 -> rejected", - arguments: [ - [Self.http2], - [Self.http3], - [Self.http2, Self.http3], - // Even when HTTP/1.1 is specified, the presence of HTTP/2 and/or HTTP/3 should make the config invalid - [Self.http1_1, Self.http2], - [Self.http1_1, Self.http3], - [Self.http1_1, Self.http2, Self.http3], - ] - ) - func plaintextNotSupportedForHTTP2OrHTTP3(supportedHTTPVersions: Set) { - #expect(throws: NIOHTTPServerConfigurationError.incompatibleTransportSecurity) { - try NIOHTTPServerConfiguration( - bindTarget: .hostAndPort(host: "127.0.0.1", port: 0), - supportedHTTPVersions: supportedHTTPVersions, - transportSecurity: .plaintext - ) - } - } #endif // HTTP3 } -@available(anyAppleOS 26.0, *) -extension ServerChannelTests { - private static let http1_1 = NIOHTTPServerConfiguration.HTTPVersion.http1_1 - - private static let http2 = NIOHTTPServerConfiguration.HTTPVersion.http2(config: .defaults) - - #if HTTP3 - private static let http3 = NIOHTTPServerConfiguration.HTTPVersion.http3(config: .defaults) - #endif -} - @available(anyAppleOS 26.0, *) extension NIOHTTPServer.ServerChannel { var isPlaintextHTTP1_1: Bool { diff --git a/Tests/NIOHTTPServerTests/Utilities/Certificates.swift b/Tests/NIOHTTPServerTests/Utilities/Certificates.swift index 42562d9..7146e57 100644 --- a/Tests/NIOHTTPServerTests/Utilities/Certificates.swift +++ b/Tests/NIOHTTPServerTests/Utilities/Certificates.swift @@ -17,6 +17,9 @@ import Foundation import SwiftASN1 import X509 +@testable import NIOHTTPServer + +@available(anyAppleOS 26.0, *) struct ChainPrivateKeyPair { let leaf: Certificate let ca: Certificate @@ -33,20 +36,31 @@ struct ChainPrivateKeyPair { } } - func writeToDisk() throws -> (leafPath: String, caPath: String, keyPath: String) { + func writeToDisk( + encoding: NIOHTTPServerConfiguration.TransportSecurity.Encoding = .pem + ) throws -> (leafPath: String, caPath: String, keyPath: String) { let uuid = UUID().uuidString - let leafPath = FileManager.default.temporaryDirectory.appendingPathComponent("leaf-\(uuid).pem") - let caPath = FileManager.default.temporaryDirectory.appendingPathComponent("ca-\(uuid).pem") - let keyPath = FileManager.default.temporaryDirectory.appendingPathComponent("key-\(uuid).pem") - - try self.leaf.serializeAsPEM().pemString.data(using: .utf8)!.write(to: leafPath) - try self.ca.serializeAsPEM().pemString.data(using: .utf8)!.write(to: caPath) - try self.privateKey.serializeAsPEM().pemString.data(using: .utf8)!.write(to: keyPath) + let leafPath = FileManager.default.temporaryDirectory.appendingPathComponent("leaf-\(uuid)") + let caPath = FileManager.default.temporaryDirectory.appendingPathComponent("ca-\(uuid)") + let keyPath = FileManager.default.temporaryDirectory.appendingPathComponent("key-\(uuid)") + + switch encoding { + case .pem: + try Data(self.leaf.serializeAsPEM().pemString.utf8).write(to: leafPath) + try Data(self.ca.serializeAsPEM().pemString.utf8).write(to: caPath) + try Data(self.privateKey.serializeAsPEM().pemString.utf8).write(to: keyPath) + + case .der: + try Data(self.leaf.serializeAsPEM().derBytes).write(to: leafPath) + try Data(self.ca.serializeAsPEM().derBytes).write(to: caPath) + try Data(self.privateKey.serializeAsPEM().derBytes).write(to: keyPath) + } return (leafPath.path, caPath.path, keyPath.path) } } +@available(anyAppleOS 26.0, *) struct TestCA { static func makeSelfSignedChain(leafExtensions: Certificate.Extensions = .init()) throws -> ChainPrivateKeyPair { let caKey = P384.Signing.PrivateKey() diff --git a/Tests/NIOHTTPServerTests/Utilities/HTTPVersion.swift b/Tests/NIOHTTPServerTests/Utilities/HTTPVersion.swift index 0a94da4..116f709 100644 --- a/Tests/NIOHTTPServerTests/Utilities/HTTPVersion.swift +++ b/Tests/NIOHTTPServerTests/Utilities/HTTPVersion.swift @@ -46,11 +46,11 @@ extension NIOHTTPServerConfiguration.HTTPVersion { self = .http1_1 case .http2: - self = .http2(config: .defaults) + self = .http2 #if HTTP3 case .http3: - self = .http3(config: .defaults) + self = .http3 #endif } } diff --git a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift index 4ddab65..77eddab 100644 --- a/Tests/NIOHTTPServerTests/Utilities/Helpers.swift +++ b/Tests/NIOHTTPServerTests/Utilities/Helpers.swift @@ -101,6 +101,7 @@ extension TLSConfiguration { } /// Like ``makeTestClientConfiguration``, but with mTLS. + @available(anyAppleOS 26.0, *) static func makeTestClientMTLSConfiguration( testTrustRoots: NIOSSLTrustRoots, clientChain: ChainPrivateKeyPair, @@ -366,10 +367,7 @@ struct TestHelpers { @available(anyAppleOS 26.0, *) extension TestHelpers { static func makeSecureUpgradeServerConfiguration( - supportedHTTPVersions: Set = [ - .http1_1, - .http2(config: .defaults), - ], + supportedHTTPVersions: Set = [.http1_1, .http2], concurrentListeners: Int = 1 ) throws -> (NIOHTTPServerConfiguration, String) { let (leafPath, caPath, privateKeyPath) = try TestCA.makeSelfSignedChainWithSAN().writeToDisk() diff --git a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift index 846cbb9..be659d2 100644 --- a/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift +++ b/Tests/NIOHTTPServerTests/Utilities/TestingChannelClientServer/TestingChannelServer+SecureUpgrade.swift @@ -79,16 +79,15 @@ struct TestingChannelSecureUpgradeServer { // Create a connection channel: we will write this to the server channel to simulate an incoming connection. let serverTestConnectionChannel = try await NIOAsyncTestingChannel.createActiveChannel() - let sslContext = try NIOSSLContext.makeServerContext( - transportSecurity: self.server.configuration.transportSecurity, - alpnIdentifiers: self.server.configuration.supportedHTTPVersions.alpnIdentifiers - ) + guard let sslContext = self.server.configuration.sslContext else { + throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity + } // Set up the required channel handlers on `serverTestConnectionChannel` let negotiatedServerConnectionFuture = try await serverTestConnectionChannel.eventLoop.flatSubmit { self.server.setupSecureUpgradeConnectionChildChannel( channel: serverTestConnectionChannel, - supportedHTTPVersions: self.server.configuration.supportedHTTPVersions, + http2Configuration: self.server.configuration.supportedHTTPVersions.http2ConfigIfSupported, sslContext: sslContext ) }.get()