diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 70b331a..b8c06f0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,14 +19,16 @@ jobs: linux_6_1_enabled: false linux_6_2_enabled: false linux_6_3_enabled: false - # linux_6_3_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 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" + linux_env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' static-sdk: name: Static SDK uses: apple/swift-nio/.github/workflows/static_sdk.yml@main + with: + env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' release-builds: name: Release builds @@ -38,3 +40,4 @@ jobs: linux_6_2_enabled: false linux_6_3_enabled: false linux_nightly_next_enabled: true + linux_env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 88fcd38..6f62569 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,11 +10,12 @@ on: jobs: soundness: name: Soundness - uses: swiftlang/github-workflows/.github/workflows/soundness.yml@0.0.11 + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@497d9ab8410e69981b3b2f28da24cf7bf4b8733d with: api_breakage_check_container_image: "swiftlang/swift:nightly-6.4.x-noble" format_check_container_image: "swiftlang/swift:nightly-6.4.x-noble" license_header_check_project_name: "Swift HTTP Server" + linux_pre_build_command: "export SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA=1" unit-tests: name: Unit tests @@ -29,6 +30,7 @@ jobs: linux_nightly_next_enabled: true linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" + linux_env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' cxx-interop: name: Cxx interop @@ -40,10 +42,13 @@ jobs: linux_6_2_enabled: false linux_6_3_enabled: false linux_nightly_next_enabled: true + linux_env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' static-sdk: name: Static SDK uses: apple/swift-nio/.github/workflows/static_sdk.yml@main + with: + env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' release-builds: name: Release builds @@ -55,3 +60,4 @@ jobs: linux_6_2_enabled: false linux_6_3_enabled: false linux_nightly_next_enabled: false # should be disabled until next is 6.4 + linux_env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' diff --git a/Package.swift b/Package.swift index 5cbec88..5cf7be8 100644 --- a/Package.swift +++ b/Package.swift @@ -53,9 +53,12 @@ let package = Package( .upToNextMinor(from: "0.2.0") ), .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.4.1"), - .package(url: "https://github.com/apple/swift-certificates.git", from: "1.19.1"), + .package(url: "https://github.com/apple/swift-certificates.git", from: "1.19.3"), .package(url: "https://github.com/apple/swift-log.git", from: "1.14.0"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.101.3"), + .package(url: "https://github.com/apple/swift-nio-quic.git", .upToNextMinor(from: "0.1.0")), + .package(url: "https://github.com/apple/swift-nio-quic-helpers.git", .upToNextMinor(from: "0.1.0")), + .package(url: "https://github.com/apple/swift-nio-http3.git", .upToNextMinor(from: "0.1.0")), .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"), @@ -110,6 +113,9 @@ let package = Package( condition: .when(traits: ["Configuration"]) ), .product(name: "NIOExtras", package: "swift-nio-extras"), + .product(name: "NIOQUIC", package: "swift-nio-quic"), + .product(name: "NIOQUICHelpers", package: "swift-nio-quic-helpers"), + .product(name: "NIOHTTP3", package: "swift-nio-http3"), .product(name: "HTTPAPIs", package: "swift-http-api-proposal"), ], swiftSettings: extraSettings diff --git a/README.md b/README.md index c77a295..5b82c0a 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,23 @@ This package offers additional integrations you can enable using Available traits: - **`Configuration`** (default): Enables initializing `NIOHTTPServerConfiguration` from a `swift-configuration` `ConfigProvider`. + +## HTTP/3 support + +Packages in the dependency tree depend on a beta release of swift-crypto. +Set the environment variable +`SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA` to allow swift-certificates +(in the dependency tree) to adopt swift-crypto beta releases as well. + +``` +SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA=1 swift build +``` + +To run all unit tests, run + +``` +SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA=1 swift test +``` + +Use `SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA=1 xed Package.swift` to open +the project in Xcode with the environment variable set. diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift new file mode 100644 index 0000000..6949816 --- /dev/null +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// 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 HTTP3 + +@available(anyAppleOS 26.0, *) +extension NIOHTTPServerConfiguration.HTTP3 { + /// HTTP/3 connection settings sent to the peer during connection establishment. + public struct ConnectionSettings: Sendable, Hashable { + /// The maximum capacity of the QPACK dynamic table. + /// + /// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9204.html#section-5-2.2.1. Corresponds to + /// `SETTINGS_QPACK_MAX_TABLE_CAPACITY`. + public var qpackMaximumTableCapacity: UInt64 + + /// The maximum number of streams which may be blocked on QPACK at any one time. + /// + /// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9204.html#section-5-2.4.1. Corresponds to + /// `SETTINGS_QPACK_BLOCKED_STREAMS`. + public var qpackBlockedStreams: UInt64 + + /// The maximum size of a field section. + /// + /// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-2.2.1. Corresponds to + /// `SETTINGS_MAX_FIELD_SECTION_SIZE`. + public var maximumFieldSectionSize: UInt64? + + /// The default HTTP/3 connection settings configuration. + /// + /// Uses the following default values: + /// - `qpackMaximumTableCapacity`: 0. + /// - `qpackBlockedStreams`: 0. + /// - `maximumFieldSectionSize`: `nil` (no field section size limit). + public static var defaults: Self { + Self( + qpackMaximumTableCapacity: 0, + qpackBlockedStreams: 0, + maximumFieldSectionSize: nil + ) + } + } +} + +@available(anyAppleOS 26.0, *) +extension HTTP3.HTTP3Settings { + init(_ configuration: NIOHTTPServerConfiguration.HTTP3.ConnectionSettings) { + self.init( + qpackMaximumTableCapacity: configuration.qpackMaximumTableCapacity, + qpackBlockedStreams: configuration.qpackBlockedStreams, + maximumFieldSectionSize: configuration.maximumFieldSectionSize + ) + } +} diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift new file mode 100644 index 0000000..2a44a8f --- /dev/null +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift @@ -0,0 +1,280 @@ +//===----------------------------------------------------------------------===// +// +// 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 NIOQUIC + +@available(anyAppleOS 26.0, *) +extension NIOHTTPServerConfiguration.HTTP3 { + /// QUIC transport configuration for an HTTP/3 server. + public struct QUICConfiguration: Sendable, Hashable { + /// Configuration for writing qlog files, which capture QUIC and HTTP/3 events for debugging and analysis. + /// + /// - SeeAlso: https://www.ietf.org/archive/id/draft-ietf-quic-qlog-main-schema-13.html + public struct QLogConfiguration: Sendable, Hashable { + /// The directory to where the qlog files are written to. + public var path: String + + /// The title to use when logging. + public var topic: String + + /// The description to use when logging. + public var description: String + + /// Creates a qlog configuration with the given directory, topic, and description. + /// + /// - Parameters: + /// - path: The directory to write qlog files to. + /// - topic: The title to use when logging. + /// - description: The description to use when logging. + public init(path: String, topic: String, description: String) { + self.path = path + self.topic = topic + self.description = description + } + } + + /// The TLS 1.3 key exchange named group. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.7 and + /// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8 + public struct KeyExchangeGroup: Sendable, Hashable { + enum Backing: UInt16 { + case secp256 = 0x0017 + case secp384 = 0x0018 + case x25519 = 0x001D + case x25519MLKEM768 = 0x11EC + } + + let backing: Backing + + /// The NIST P-256 elliptic curve. + public static var secp256: Self { + .init(backing: .secp256) + } + + /// The NIST P-384 elliptic curve. + public static var secp384: Self { + .init(backing: .secp384) + } + + /// The X25519 elliptic curve (Curve25519). + public static var x25519: Self { + .init(backing: .x25519) + } + + /// A post-quantum hybrid group that combines X25519 with the ML-KEM-768 key encapsulation mechanism. + public static var x25519MLKEM768: Self { + .init(backing: .x25519MLKEM768) + } + } + + /// The server's hostname for the TLS handshake. + /// + /// - Important: SwiftTLS currently just ignores the server name sent in the ClientHello. See + /// https://github.com/apple/swift-nio-quic/issues/4. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc6066#section-3 + public var serverName: String + + /// The named group to use for the TLS 1.3 key exchange. + public var keyExchangeGroup: KeyExchangeGroup + + /// The idle timeout advertised to the client. A connection may time out sooner than this value if the client + /// advertises a shorter idle timeout. + /// + /// - Important: The effective idle timeout enforced on a connection is the minimum of both endpoints' + /// advertised values. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.4.1 and + /// https://datatracker.ietf.org/doc/html/rfc9000#name-idle-timeout + public var maxIdleTimeout: Duration + + /// The initial value for the maximum amount of data (in bytes) that can be sent on the connection. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.14.1 + public var initialMaxData: Int + + /// The initial flow control limit for locally initiated bidirectional streams. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.16.1 + public var initialMaxStreamDataBidirectionalLocal: Int + + /// The initial flow control limit for client-initiated bidirectional streams. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.18.1 + public var initialMaxStreamDataBidirectionalRemote: Int + + /// The initial flow control limit for unidirectional streams. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.20.1 + public var initialMaxStreamDataUnidirectional: Int + + /// The initial maximum number of bidirectional streams the server is permitted to initiate. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.22.1 + public var initialMaxStreamsBidirectional: Int + + /// The initial maximum number of unidirectional streams the server is permitted to initiate. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.24.1 + public var initialMaxStreamsUnidirectional: Int + + /// The interval at which the server sends keep-alive PING frames. + /// + /// Each PING restarts both endpoints' idle timers. The server's idle timer is restarted when the PING is sent, + /// and the peer's idle timer is restarted when the PING is received. This prevents the connection from being + /// closed by ``maxIdleTimeout``. + /// + /// - Important: For keep-alive pings to be effective, the interval must be shorter than the negotiated idle + /// timeout. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-10.1.2 + public var keepAliveInterval: Duration? + + /// Whether the server sends a Retry packet before accepting a new connection. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-8.1.2 + public var sendRetry: Bool + + /// The path to a file where TLS session keys are logged in NSS Key Log format. + /// + /// When set, tools such as Wireshark can use this file to decrypt captured QUIC traffic. + public var keyLogPath: String? + + /// Optional qlog configuration. + /// + /// When set, QUIC and HTTP/3 events are written to qlog files in the specified directory, which is useful for + /// debugging and analysis. + public var qLogConfiguration: QLogConfiguration? + + /// The default QUIC transport configuration. + /// + /// Uses the following default values: + /// - `keyExchangeGroup`: ``KeyExchangeGroup/x25519``. + /// - `maxIdleTimeout`: 30 seconds. + /// - `initialMaxData`: 1 MiB. + /// - `initialMaxStreamDataBidirectionalLocal`: 1 MiB. + /// - `initialMaxStreamDataBidirectionalRemote`: 1 MiB. + /// - `initialMaxStreamDataUnidirectional`: 1 MiB. + /// - `initialMaxStreamsBidirectional`: 100 streams. + /// - `initialMaxStreamsUnidirectional`: 100 streams. + /// - `keepAliveInterval`: `nil` (no keep-alive PINGs are sent). + /// - `sendRetry`: `false`. + /// - `keyLogPath`: `nil` (TLS session keys are not logged). + /// - `qLogConfiguration`: `nil` (qlog is not enabled). + public static var defaults: Self { + Self( + // SwiftTLS currently just ignores the `serverName` sent in the ClientHello. This default configuration + // just sets `serverName` to an empty string. See https://github.com/apple/swift-nio-quic/issues/4. + serverName: "", + keyExchangeGroup: .x25519, + maxIdleTimeout: .seconds(30), + initialMaxData: 1024 * 1024, + initialMaxStreamDataBidirectionalLocal: 1024 * 1024, + initialMaxStreamDataBidirectionalRemote: 1024 * 1024, + initialMaxStreamDataUnidirectional: 1024 * 1024, + initialMaxStreamsBidirectional: 100, + initialMaxStreamsUnidirectional: 100, + keepAliveInterval: nil, + sendRetry: false, + keyLogPath: nil, + qLogConfiguration: nil + ) + } + } +} + +@available(anyAppleOS 26.0, *) +extension NIOQUIC.QUICConfiguration.QLogConfiguration { + fileprivate init(_ configuration: NIOHTTPServerConfiguration.HTTP3.QUICConfiguration.QLogConfiguration) { + self.init( + path: configuration.path, + topic: configuration.topic, + description: configuration.description + ) + } +} + +@available(anyAppleOS 26.0, *) +extension NIOQUIC.KeyExchangeGroup { + fileprivate init(_ configuration: NIOHTTPServerConfiguration.HTTP3.QUICConfiguration.KeyExchangeGroup) { + switch configuration.backing { + case .secp256: + self = .secp256 + + case .secp384: + self = .secp384 + + case .x25519: + self = .x25519 + + case .x25519MLKEM768: + self = .x25519MLKEM768 + } + } +} + +@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 credentials): + switch credentials.backing { + case .inMemory, .reloading: + throw NIOHTTPServerConfigurationError.onlyPEMFileCredentialsCurrentlySupportedOverHTTP3 + + case .pemFile(let certificateChainPath, let privateKeyPath): + self = .x509Certificates( + certificateChainFilePath: certificateChainPath, + privateKeyFilePath: privateKeyPath + ) + } + } + } +} + +@available(anyAppleOS 26.0, *) +extension NIOQUIC.QUICConfiguration { + /// Creates a `NIOQUIC.QUICConfiguration` from a `NIOHTTPServerConfiguration.HTTP3.QUICConfiguration` instance. + init( + _ config: NIOHTTPServerConfiguration.HTTP3.QUICConfiguration, + authenticationConfiguration: NIOQUIC.AuthenticationConfiguration + ) { + self = .server( + // SwiftTLS currently just ignores the `serverName` sent in the ClientHello. See + // https://github.com/apple/swift-nio-quic/issues/4. + serverName: config.serverName, + authenticationConfiguration: authenticationConfiguration, + keyExchangeGroup: .init(config.keyExchangeGroup), + applicationProtocols: ["h3"], + maxIdleTimeout: config.maxIdleTimeout, + initialMaxData: config.initialMaxData, + initialMaxStreamDataBidiLocal: config.initialMaxStreamDataBidirectionalLocal, + initialMaxStreamDataBidiRemote: config.initialMaxStreamDataBidirectionalRemote, + initialMaxStreamDataUni: config.initialMaxStreamDataUnidirectional, + initialMaxStreamsBidi: config.initialMaxStreamsBidirectional, + initialMaxStreamsUni: config.initialMaxStreamsUnidirectional, + keepAliveInterval: config.keepAliveInterval, + sendRetry: config.sendRetry, + keyLogPath: config.keyLogPath, + qLogConfiguration: config.qLogConfiguration.map { .init($0) } + ) + } +} diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift new file mode 100644 index 0000000..1069a9d --- /dev/null +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// 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 NIOCore +import NIOHTTP3 + +@available(anyAppleOS 26.0, *) +extension NIOHTTPServerConfiguration { + /// Configuration for HTTP/3. + public struct HTTP3: Sendable, Hashable { + /// If true, Huffman encoding will be used where applicable, e.g. for header field sections. + /// + /// - Note: Huffman encoding will not be used if it would result in a larger payload than not using it, even if + /// this property is true. + public var preferHuffmanEncoding = true + + /// QUIC transport configuration. + public var quicConfiguration: QUICConfiguration = .defaults + + /// HTTP/3 connection settings exchanged with the client during connection establishment. + public var connectionSettings: ConnectionSettings = .defaults + + /// Creates an HTTP/3 configuration. + /// + /// - Parameters: + /// - preferHuffmanEncoding: Whether Huffman encoding is used where applicable. + /// - quicConfiguration: QUIC transport parameters. + /// - connectionSettings: HTTP/3 connection-level settings exchanged with the client. + public init( + preferHuffmanEncoding: Bool, + quicConfiguration: QUICConfiguration, + connectionSettings: ConnectionSettings, + ) { + self.preferHuffmanEncoding = preferHuffmanEncoding + self.quicConfiguration = quicConfiguration + self.connectionSettings = connectionSettings + } + + /// The default HTTP/3 configuration. + /// + /// Uses the default configurations of the sub-components: + /// - `preferHuffmanEncoding`: `true`. + /// - `quicConfiguration`: ``QUICConfiguration/defaults``. + /// - `connectionSettings`: ``ConnectionSettings/defaults``. + public static var defaults: Self { + Self( + preferHuffmanEncoding: true, + quicConfiguration: .defaults, + connectionSettings: .defaults, + ) + } + + // The fallback connection RTT to use if there is an error obtaining the RTT estimate channel option. + static var fallbackConnectionRTT: TimeAmount { + .milliseconds(100) + } + } +} + +@available(anyAppleOS 26.0, *) +extension NIOHTTP3.HTTP3ServerConfiguration { + init(_ configuration: NIOHTTPServerConfiguration.HTTP3) { + self = .defaults + self.preferHuffmanEncoding = configuration.preferHuffmanEncoding + } +} diff --git a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift index 2f462e3..4a5ce10 100644 --- a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift +++ b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift @@ -463,16 +463,29 @@ extension NIOHTTPServerConfiguration { enum Version { case http1_1 case http2(config: HTTP2) + case http3(config: HTTP3) - /// The HTTP/2 configuration if this version is HTTP/2, or `nil` if it is HTTP/1.1. + /// The HTTP/2 configuration if this version is HTTP/2, or `nil` if it is HTTP/1.1 or HTTP/3. var http2Config: HTTP2? { switch self { - case .http1_1: + case .http1_1, .http3: return nil + case .http2(let config): return config } } + + /// The HTTP/3 configuration if this version is HTTP/3, or `nil` if it is HTTP/1.1 or HTTP/2. + var http3Config: HTTP3? { + switch self { + case .http1_1, .http2: + return nil + + case .http3(let config): + return config + } + } } let version: Version @@ -489,11 +502,18 @@ extension NIOHTTPServerConfiguration { Self(version: .http2(config: config)) } + /// The HTTP/3 protocol version. + /// + /// - Parameter config: The configuration to use for HTTP/3. + public static func http3(config: HTTP3) -> Self { + Self(version: .http3(config: config)) + } + /// Two values are equal if they represent the same protocol version, regardless of any differences in HTTP/2 /// configuration. public static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs.version, rhs.version) { - case (.http1_1, .http1_1), (.http2, .http2): + case (.http1_1, .http1_1), (.http2, .http2), (.http3, .http3): return true default: @@ -509,6 +529,9 @@ extension NIOHTTPServerConfiguration { case .http2: hasher.combine(2) + + case .http3: + hasher.combine(3) } } } @@ -543,8 +566,15 @@ extension Set where Element == NIOHTTPServerConfiguration.HTTPVersion { return identifiers } - /// The HTTP/2 configuration if HTTP/2 is among the supported versions, or `nil` if only HTTP/1.1 is supported. + /// The HTTP/2 configuration if HTTP/2 is among the supported versions, or `nil` if only HTTP/1.1 and/or HTTP/3 is + /// supported. var http2ConfigIfSupported: NIOHTTPServerConfiguration.HTTP2? { self.compactMap({ $0.version.http2Config }).first } + + /// The HTTP/3 configuration if HTTP/3 is among the supported versions, or `nil` if only HTTP/1.1 and/or HTTP/2 is + /// supported. + var http3ConfigIfSupported: NIOHTTPServerConfiguration.HTTP3? { + self.compactMap({ $0.version.http3Config }).first + } } diff --git a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift index 7f56f9c..19ba873 100644 --- a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift +++ b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift @@ -17,6 +17,9 @@ enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible { case noSupportedHTTPVersionsSpecified case incompatibleTransportSecurity case noBindTargetsSpecified + case onlyPEMFileCredentialsCurrentlySupportedOverHTTP3 + // swift-nio-quic doesn't currently support mTLS. See https://github.com/apple/swift-nio-quic/issues/5. + case mTLSNotCurrentlySupportedOverHTTP3 var description: String { switch self { @@ -24,10 +27,16 @@ enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible { "Invalid configuration: at least one supported HTTP version must be specified." case .incompatibleTransportSecurity: - "Invalid configuration: only HTTP/1.1 can be served over plaintext. `transportSecurity` must be set to (m)TLS for serving HTTP/2." + "Invalid configuration: only HTTP/1.1 can be served over plaintext. `transportSecurity` must be set to (m)TLS for serving HTTP/2 or HTTP/3." case .noBindTargetsSpecified: "Invalid configuration: at least one bind target must be specified." + + case .onlyPEMFileCredentialsCurrentlySupportedOverHTTP3: + "Invalid configuration: only PEM-file X.509 credentials are supported over HTTP/3. In-memory or reloading credential sources are not currently supported." + + case .mTLSNotCurrentlySupportedOverHTTP3: + "Invalid configuration: mTLS is not currently supported over HTTP/3." } } } diff --git a/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift b/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift new file mode 100644 index 0000000..a42a063 --- /dev/null +++ b/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// 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 NIOQUIC +import Testing + +@testable import NIOHTTPServer + +@Suite +struct HTTP3ConfigurationTests { + @Test("HTTP/3 default configuration uses the defaults of the sub-components") + @available(anyAppleOS 26.0, *) + func http3DefaultConfiguration() { + let config = NIOHTTPServerConfiguration.HTTP3.defaults + #expect(config.quicConfiguration == .defaults) + #expect(config.connectionSettings == .defaults) + } + + @Test("HTTP/3 configuration with custom values") + @available(anyAppleOS 26.0, *) + func http3ConfigurationCustomValues() { + var connectionSettings = NIOHTTPServerConfiguration.HTTP3.ConnectionSettings.defaults + connectionSettings.qpackMaximumTableCapacity = 4096 + connectionSettings.qpackBlockedStreams = 16 + connectionSettings.maximumFieldSectionSize = 8192 + + var quic = NIOHTTPServerConfiguration.HTTP3.QUICConfiguration.defaults + quic.keepAliveInterval = .seconds(10) + quic.sendRetry = true + + let config = NIOHTTPServerConfiguration.HTTP3( + preferHuffmanEncoding: false, + quicConfiguration: quic, + connectionSettings: connectionSettings + ) + + #expect(config.preferHuffmanEncoding == false) + #expect(config.connectionSettings.qpackMaximumTableCapacity == 4096) + #expect(config.connectionSettings.qpackBlockedStreams == 16) + #expect(config.connectionSettings.maximumFieldSectionSize == 8192) + #expect(config.quicConfiguration.keepAliveInterval == .seconds(10)) + #expect(config.quicConfiguration.sendRetry == true) + } + + @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: .pemFile(certificateChainPath: "/cert.pem", privateKeyPath: "/key.pem"), + trustConfiguration: .pemFile(path: "/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) { + _ = try NIOQUIC.AuthenticationConfiguration( + .tls(credentials: .inMemory(certificateChain: chain.chain, privateKey: chain.privateKey)) + ) + } + } + + @Test("PEM-file TLS credentials are accepted") + @available(anyAppleOS 26.0, *) + func pemFileCredentialsAccepted() { + #expect(throws: Never.self) { + _ = try NIOQUIC.AuthenticationConfiguration( + .tls(credentials: .pemFile(certificateChainPath: "/cert.pem", privateKeyPath: "/key.pem")) + ) + } + } + } +}