Skip to content

Commit c2e1f5a

Browse files
authored
Add support for serving HTTP/3 (#101)
### Motivation Following from #98, this PR introduces support for serving HTTP/3. ### Modifications - Added `NIOHTTPServer+HTTP3.swift` which contains helper methods for setting up a QUIC channel, adding HTTP/3 handlers, and serving HTTP/3 connections. - Wired HTTP/3 into `makeServerChannels()`. - When HTTP/3 is the only version specified in `supportedHTTPVersions`, only a QUIC channel is bound to the specified port. When `supportedHTTPVersions` includes HTTP/1.1 and/or HTTP/2 alongside HTTP/3, the QUIC UDP channel is bound on the same port as the TCP channel serving HTTP/1.1 and/or HTTP/2. - Note: I will introduce Alt-Svc advertising in a subsequent PR. ### Result `NIOHTTPServer` can now serve HTTP/3.
1 parent c794698 commit c2e1f5a

11 files changed

Lines changed: 516 additions & 107 deletions

Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,40 @@ extension NIOQUIC.QUICConfiguration {
279279
)
280280
}
281281
}
282+
283+
@available(anyAppleOS 26.0, *)
284+
extension NIOQUIC.Authenticator {
285+
/// Creates an `Authenticator` instance from X.509 TLS credentials.
286+
///
287+
/// Returns `nil` for raw public key credentials, because NIOQUIC reads the public/private key paths directly from
288+
/// `QUICConfiguration.authenticationConfiguration` (no `Authenticator` instance is required in that case).
289+
///
290+
/// - Parameter transportSecurity: The server's transport security configuration.
291+
///
292+
/// - Throws:
293+
/// - ``NIOHTTPServerConfigurationError/incompatibleTransportSecurity`` if `transportSecurity` is `.plaintext`.
294+
/// - ``NIOHTTPServerConfigurationError/inMemoryOrReloadingTLSCredentialsNotSupportedOverHTTP3`` if the X.509
295+
/// credentials are provided as in-memory `X509.Certificate`/`X509.Certificate.PrivateKey` objects or as a
296+
/// `CertificateReloader` instance.
297+
/// - An underlying error from `Authenticator`'s initializer if the certificate chain or private key cannot be
298+
/// loaded.
299+
convenience init(_ transportSecurity: NIOHTTPServerConfiguration.TransportSecurity) throws {
300+
switch transportSecurity.backing {
301+
case .plaintext:
302+
throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity
303+
304+
case .tls(let tlsCredentials), .mTLS(let tlsCredentials, _):
305+
switch tlsCredentials.backing {
306+
case .reloading:
307+
throw NIOHTTPServerConfigurationError.onlyPEMFileCredentialsCurrentlySupportedOverHTTP3
308+
309+
case .pemFile(let certificateChainPath, let privateKeyPath):
310+
try self.init(certificateFilePath: certificateChainPath, privateKeyFilePath: privateKeyPath)
311+
312+
case .inMemory(let certificateChain, let privateKey):
313+
try self.init(certificates: certificateChain, privateKey: privateKey)
314+
}
315+
}
316+
}
317+
}
282318
#endif // HTTP3

Sources/NIOHTTPServer/NIOHTTPServer+Connection.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import NIOCore
1616
import NIOHTTP2
1717
import NIOHTTPTypes
1818

19+
#if HTTP3
20+
@_spi(HTTP3AsyncInterface) import NIOHTTP3
21+
import NIOQUIC
22+
#endif
23+
1924
@available(anyAppleOS 26.0, *)
2025
extension NIOHTTPServer {
2126
/// An active HTTP server connection.
@@ -32,15 +37,26 @@ extension NIOHTTPServer {
3237
/// owns the channel and drives `executeThenClose`, so the writer is finished cleanly even if the connection
3338
/// handler returns without calling ``handleRequests(handler:)``).
3439
/// - `http2` carries the connection channel and stream multiplexer.
40+
/// - `http3` carries an ``HTTP3ServerConnection``.
3541
enum HTTPProtocol: Sendable {
3642
case http1_1(
3743
inbound: NIOAsyncChannelInboundStream<HTTPRequestPart>,
3844
outbound: NIOAsyncChannelOutboundWriter<HTTPResponsePart>
3945
)
46+
4047
case http2(
4148
connectionChannel: any Channel,
4249
multiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>>
4350
)
51+
52+
#if HTTP3
53+
case http3(
54+
connection: HTTP3ServerConnection<
55+
NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>,
56+
NIOQUIC.QUICStreamCreator
57+
>
58+
)
59+
#endif
4460
}
4561

4662
let server: NIOHTTPServer
@@ -76,13 +92,19 @@ extension NIOHTTPServer {
7692
handler: handler,
7793
context: context
7894
)
95+
7996
case .http2(let connectionChannel, let multiplexer):
8097
await server.handleHTTP2Connection(
8198
connectionChannel: connectionChannel,
8299
multiplexer: multiplexer,
83100
handler: handler,
84101
context: context
85102
)
103+
104+
#if HTTP3
105+
case .http3(let connection):
106+
await server.handleHTTP3Connection(connection: connection, handler: handler, context: context)
107+
#endif
86108
}
87109
}
88110

Sources/NIOHTTPServer/NIOHTTPServer+ConnectionContext.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ public import X509
2020
extension NIOHTTPServer {
2121
/// The application-level HTTP version negotiated for a connection.
2222
public enum HTTPVersion: String, Sendable, Hashable {
23-
case http1_1 = "http/1.1"
24-
case http2 = "http/2"
23+
case plaintextHTTP1_1 = "Plaintext HTTP/1.1"
24+
case http1_1 = "HTTP/1.1"
25+
case http2 = "HTTP/2"
26+
#if HTTP3
27+
case http3 = "HTTP/3"
28+
#endif
2529
}
2630
}
2731

Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,10 @@ extension NIOHTTPServer {
7979
) async {
8080
do {
8181
try await requestChannel.executeThenClose { inbound, outbound in
82-
let context = NIOHTTPServer.makeHTTP1ConnectionContext(
83-
requestChannel: requestChannel,
82+
let context = ConnectionContext(
83+
httpVersion: .plaintextHTTP1_1,
84+
remoteAddress: try? NIOHTTPServer.SocketAddress(requestChannel.channel.remoteAddress),
85+
localAddress: try? NIOHTTPServer.SocketAddress(requestChannel.channel.localAddress),
8486
peerCertificateChainFuture: nil
8587
)
8688
let connection = Connection(
@@ -158,8 +160,6 @@ extension NIOHTTPServer {
158160
throw error
159161
}
160162

161-
try self.addressesBound(serverChannels.map { (serverChannel, _) in serverChannel.channel.localAddress })
162-
163163
return serverChannels
164164
}
165165

@@ -172,10 +172,7 @@ extension NIOHTTPServer {
172172
channel.pipeline.configureHTTPServerPipeline().flatMapThrowing {
173173
try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: isSecure))
174174
try channel.pipeline.syncOperations.addHandler(HTTPKeepAliveHandler())
175-
try channel
176-
.pipeline
177-
.syncOperations
178-
.addTimeoutHandlers(self.configuration.connectionTimeouts)
175+
try channel.pipeline.syncOperations.addTimeoutHandlers(self.configuration.connectionTimeouts)
179176

180177
return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
181178
wrappingChannelSynchronously: channel,
@@ -184,19 +181,6 @@ extension NIOHTTPServer {
184181
}
185182
}
186183

187-
/// Builds a ``ConnectionContext`` for an HTTP/1.1 request channel.
188-
static func makeHTTP1ConnectionContext(
189-
requestChannel: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>,
190-
peerCertificateChainFuture: EventLoopFuture<NIOSSL.ValidatedCertificateChain?>?
191-
) -> ConnectionContext {
192-
ConnectionContext(
193-
httpVersion: .http1_1,
194-
remoteAddress: try? NIOHTTPServer.SocketAddress(requestChannel.channel.remoteAddress),
195-
localAddress: try? NIOHTTPServer.SocketAddress(requestChannel.channel.localAddress),
196-
peerCertificateChainFuture: peerCertificateChainFuture
197-
)
198-
}
199-
200184
/// Drives the request loop on an HTTP/1.1 connection that may carry
201185
/// multiple serial requests (keep-alive). Invoked from
202186
/// ``NIOHTTPServer/Connection/handleRequests(handler:)`` for the

0 commit comments

Comments
 (0)