Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split HTTP2 Channel out from upgrade channel #612

Merged
merged 6 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
179 changes: 28 additions & 151 deletions Sources/HummingbirdHTTP2/HTTP2Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,19 @@ import HTTPTypes
import HummingbirdCore
import Logging
import NIOCore
import NIOHTTP1
import NIOHTTP2
import NIOHTTPTypes
import NIOHTTPTypesHTTP1
import NIOHTTPTypesHTTP2
import NIOPosix
import NIOSSL
import NIOTLS

/// Child channel for processing HTTP1 with the option of upgrading to HTTP2
public struct HTTP2UpgradeChannel: HTTPChannelHandler {
typealias HTTP1ConnectionOutput = HTTP1Channel.Value
typealias HTTP2ConnectionOutput = NIOHTTP2Handler.AsyncStreamMultiplexer<HTTP2StreamChannel.Value>
/// Child channel for processing HTTP2
public struct HTTP2Channel: ServerChildChannel {
typealias HTTP2Connection = NIOHTTP2Handler.AsyncStreamMultiplexer<HTTP2StreamChannel.Value>
public struct Value: ServerChildChannelValue {
let negotiatedHTTPVersion: EventLoopFuture<NIONegotiatedHTTPVersion<HTTP1ConnectionOutput, HTTP2ConnectionOutput>>
let http2Connection: HTTP2Connection
public let channel: Channel
}

/// HTTP2 Upgrade configuration
/// HTTP2 configuration
public struct Configuration: Sendable {
/// Idle timeout, how long connection is kept idle before closing
public var idleTimeout: Duration?
Expand Down Expand Up @@ -63,55 +57,18 @@ public struct HTTP2UpgradeChannel: HTTPChannelHandler {
}
}

private let sslContext: NIOSSLContext
private let http1: HTTP1Channel
private let http2Stream: HTTP2StreamChannel
public let configuration: Configuration
public var responder: Responder {
self.http2Stream.responder
}

/// Initialize HTTP2Channel
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - additionalChannelHandlers: Additional channel handlers to add to stream channel pipeline after HTTP part decoding and
/// before HTTP request handling
/// - responder: Function returning a HTTP response for a HTTP request
@available(*, deprecated, renamed: "HTTP1Channel(tlsConfiguration:configuration:responder:)")
public init(
tlsConfiguration: TLSConfiguration,
additionalChannelHandlers: @escaping @Sendable () -> [any RemovableChannelHandler],
responder: @escaping HTTPChannelHandler.Responder
) throws {
var tlsConfiguration = tlsConfiguration
tlsConfiguration.applicationProtocols = NIOHTTP2SupportedALPNProtocols
self.sslContext = try NIOSSLContext(configuration: tlsConfiguration)
self.configuration = .init()
self.http1 = HTTP1Channel(
responder: responder,
configuration: .init(additionalChannelHandlers: additionalChannelHandlers())
)
self.http2Stream = HTTP2StreamChannel(
responder: responder,
configuration: .init(additionalChannelHandlers: additionalChannelHandlers())
)
}

/// Initialize HTTP2Channel
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - configuration: HTTP2 channel configuration
/// - responder: Function returning a HTTP response for a HTTP request
public init(
tlsConfiguration: TLSConfiguration,
configuration: Configuration = .init(),
responder: @escaping HTTPChannelHandler.Responder
) throws {
var tlsConfiguration = tlsConfiguration
tlsConfiguration.applicationProtocols = NIOHTTP2SupportedALPNProtocols
self.sslContext = try NIOSSLContext(configuration: tlsConfiguration)
responder: @escaping HTTPChannelHandler.Responder,
configuration: Configuration = .init()
) {
self.configuration = configuration
self.http1 = HTTP1Channel(responder: responder, configuration: configuration.streamConfiguration)
self.http2Stream = HTTP2StreamChannel(responder: responder, configuration: configuration.streamConfiguration)
}

Expand All @@ -121,35 +78,22 @@ public struct HTTP2UpgradeChannel: HTTPChannelHandler {
/// - logger: Logger used during setup
/// - Returns: Object to process input/output on child channel
public func setup(channel: Channel, logger: Logger) -> EventLoopFuture<Value> {
do {
try channel.pipeline.syncOperations.addHandler(NIOSSLServerHandler(context: self.sslContext))
} catch {
return channel.eventLoop.makeFailedFuture(error)
}

return channel.configureHTTP2AsyncSecureUpgrade { channel in
self.http1.setup(channel: channel, logger: logger)
} http2ConnectionInitializer: { channel in
channel.eventLoop.makeCompletedFuture {
let connectionManager = HTTP2ServerConnectionManager(
eventLoop: channel.eventLoop,
idleTimeout: self.configuration.idleTimeout,
maxAgeTimeout: self.configuration.maxAgeTimeout,
gracefulCloseTimeout: self.configuration.gracefulCloseTimeout
)
let handler: HTTP2ConnectionOutput = try channel.pipeline.syncOperations.configureAsyncHTTP2Pipeline(
mode: .server,
streamDelegate: connectionManager.streamDelegate,
configuration: .init()
) { http2ChildChannel in
self.http2Stream.setup(channel: http2ChildChannel, logger: logger)
}
try channel.pipeline.syncOperations.addHandler(connectionManager)
return handler
channel.eventLoop.makeCompletedFuture {
let connectionManager = HTTP2ServerConnectionManager(
eventLoop: channel.eventLoop,
idleTimeout: self.configuration.idleTimeout,
maxAgeTimeout: self.configuration.maxAgeTimeout,
gracefulCloseTimeout: self.configuration.gracefulCloseTimeout
)
let handler: HTTP2Connection = try channel.pipeline.syncOperations.configureAsyncHTTP2Pipeline(
mode: .server,
streamDelegate: connectionManager.streamDelegate,
configuration: .init()
) { http2ChildChannel in
self.http2Stream.setup(channel: http2ChildChannel, logger: logger)
}
}
.map {
.init(negotiatedHTTPVersion: $0, channel: channel)
try channel.pipeline.syncOperations.addHandler(connectionManager)
return .init(http2Connection: handler, channel: channel)
}
}

Expand All @@ -159,82 +103,15 @@ public struct HTTP2UpgradeChannel: HTTPChannelHandler {
/// - logger: Logger to use while processing messages
public func handle(value: Value, logger: Logger) async {
do {
let channel = try await value.negotiatedHTTPVersion.get()
switch channel {
case .http1_1(let http1):
await self.http1.handle(value: http1, logger: logger)
case .http2(let multiplexer):
do {
try await withThrowingDiscardingTaskGroup { group in
for try await client in multiplexer.inbound {
group.addTask {
await self.http2Stream.handle(value: client, logger: logger)
}
}
try await withThrowingDiscardingTaskGroup { group in
for try await client in value.http2Connection.inbound {
group.addTask {
await self.http2Stream.handle(value: client, logger: logger)
}
} catch {
logger.error("Error handling inbound connection for HTTP2 handler: \(error)")
}
}
} catch {
logger.error("Error getting HTTP2 upgrade negotiated value: \(error)")
}
}
}

// Code taken from NIOHTTP2
extension Channel {
/// Configures a channel to perform an HTTP/2 secure upgrade with typed negotiation results.
///
/// HTTP/2 secure upgrade uses the Application Layer Protocol Negotiation TLS extension to
/// negotiate the inner protocol as part of the TLS handshake. For this reason, until the TLS
/// handshake is complete, the ultimate configuration of the channel pipeline cannot be known.
///
/// This function configures the channel with a pair of callbacks that will handle the result
/// of the negotiation. It explicitly **does not** configure a TLS handler to actually attempt
/// to negotiate ALPN. The supported ALPN protocols are provided in
/// `NIOHTTP2SupportedALPNProtocols`: please ensure that the TLS handler you are using for your
/// pipeline is appropriately configured to perform this protocol negotiation.
///
/// If negotiation results in an unexpected protocol, the pipeline will close the connection
/// and no callback will fire.
///
/// This configuration is acceptable for use on both client and server channel pipelines.
///
/// - Parameters:
/// - http1ConnectionInitializer: A callback that will be invoked if HTTP/1.1 has been explicitly
/// negotiated, or if no protocol was negotiated. Must return a future that completes when the
/// channel has been fully mutated.
/// - http2ConnectionInitializer: A callback that will be invoked if HTTP/2 has been negotiated, and that
/// should configure the channel for HTTP/2 use. Must return a future that completes when the
/// channel has been fully mutated.
/// - Returns: An `EventLoopFuture` of an `EventLoopFuture` containing the `NIOProtocolNegotiationResult` that completes when the channel
/// is ready to negotiate.
@inlinable
internal func configureHTTP2AsyncSecureUpgrade<HTTP1Output: Sendable, HTTP2Output: Sendable>(
http1ConnectionInitializer: @escaping NIOChannelInitializerWithOutput<HTTP1Output>,
http2ConnectionInitializer: @escaping NIOChannelInitializerWithOutput<HTTP2Output>
) -> EventLoopFuture<EventLoopFuture<NIONegotiatedHTTPVersion<HTTP1Output, HTTP2Output>>> {
return self.eventLoop.makeCompletedFuture {
let alpnHandler = NIOTypedApplicationProtocolNegotiationHandler<NIONegotiatedHTTPVersion<HTTP1Output, HTTP2Output>>() { result in
switch result {
case .negotiated("h2"):
// Successful upgrade to HTTP/2. Let the user configure the pipeline.
return http2ConnectionInitializer(self).map { http2Output in .http2(http2Output) }
case .negotiated("http/1.1"), .fallback:
// Explicit or implicit HTTP/1.1 choice.
return http1ConnectionInitializer(self).map { http1Output in .http1_1(http1Output) }
case .negotiated:
// We negotiated something that isn't HTTP/1.1. This is a bad scene, and is a good indication
// of a user configuration error. We're going to close the connection directly.
return self.close().flatMap { self.eventLoop.makeFailedFuture(NIOHTTP2Errors.invalidALPNToken()) }
}
}
try self.pipeline.syncOperations.addHandler(alpnHandler)
}.flatMap { _ in
self.pipeline.handler(type: NIOTypedApplicationProtocolNegotiationHandler<NIONegotiatedHTTPVersion<HTTP1Output, HTTP2Output>>.self).map { alpnHandler in
alpnHandler.protocolNegotiationResult
}
logger.error("Error handling inbound connection for HTTP2 handler: \(error)")
}
}
}
Loading