Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
105 changes: 90 additions & 15 deletions Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand 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<HTTPVersion>
///
/// - 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<HTTPVersion> {
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? {
Expand All @@ -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``,
Expand All @@ -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
}
Expand All @@ -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.
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, but I think additionally having a static var for h2 and h3 that defaults the config to .defaults may make the API slightly nicer since we can avoid the empty brackets (.http2 instead of .http2())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit b32144d.

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading