Skip to content

Commit

Permalink
Split HTTP2 Channel out from upgrade channel (#612)
Browse files Browse the repository at this point in the history
* Split HTTP2 Channel out from upgrade channel

* Make HTTP2Channel internal

* Move HTTP2ChannelConfiguration out of HTTP2Channel
  • Loading branch information
adam-fowler authored Nov 20, 2024
1 parent 70b0714 commit c0754a8
Show file tree
Hide file tree
Showing 2 changed files with 240 additions and 179 deletions.
236 changes: 57 additions & 179 deletions Sources/HummingbirdHTTP2/HTTP2Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,102 +16,60 @@ 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>
public struct Value: ServerChildChannelValue {
let negotiatedHTTPVersion: EventLoopFuture<NIONegotiatedHTTPVersion<HTTP1ConnectionOutput, HTTP2ConnectionOutput>>
public let channel: Channel
}
/// HTTP2 configuration
public struct HTTP2ChannelConfiguration: Sendable {
/// Idle timeout, how long connection is kept idle before closing
public var idleTimeout: Duration?
/// Maximum amount of time to wait for client response before all streams are closed after second GOAWAY has been sent
public var gracefulCloseTimeout: Duration?
/// Maximum amount of time a connection can be open
public var maxAgeTimeout: Duration?
/// Configuration applied to HTTP2 stream channels
public var streamConfiguration: HTTP1Channel.Configuration

/// HTTP2 Upgrade configuration
public struct Configuration: Sendable {
/// Idle timeout, how long connection is kept idle before closing
public var idleTimeout: Duration?
/// Maximum amount of time to wait for client response before all streams are closed after second GOAWAY has been sent
public var gracefulCloseTimeout: Duration?
/// Maximum amount of time a connection can be open
public var maxAgeTimeout: Duration?
/// Configuration applied to HTTP2 stream channels
public var streamConfiguration: HTTP1Channel.Configuration
/// Initialize HTTP2UpgradeChannel.Configuration
/// - Parameters:
/// - idleTimeout: How long connection is kept idle before closing. A connection is considered idle when it has no open streams
/// - maxGraceCloseTimeout: Maximum amount of time to wait for client response before all streams are closed after second GOAWAY
/// - maxAgeTimeout: Maximum amount of time for a connection to be open.
/// - streamConfiguration: Configuration applieds to HTTP2 stream channels
public init(
idleTimeout: Duration? = nil,
gracefulCloseTimeout: Duration? = nil,
maxAgeTimeout: Duration? = nil,
streamConfiguration: HTTP1Channel.Configuration = .init()
) {
self.idleTimeout = idleTimeout
self.gracefulCloseTimeout = gracefulCloseTimeout
self.streamConfiguration = streamConfiguration
}
}

/// Initialize HTTP2UpgradeChannel.Configuration
/// - Parameters:
/// - idleTimeout: How long connection is kept idle before closing. A connection is considered idle when it has no open streams
/// - maxGraceCloseTimeout: Maximum amount of time to wait for client response before all streams are closed after second GOAWAY
/// - maxAgeTimeout: Maximum amount of time for a connection to be open.
/// - streamConfiguration: Configuration applieds to HTTP2 stream channels
public init(
idleTimeout: Duration? = nil,
gracefulCloseTimeout: Duration? = nil,
maxAgeTimeout: Duration? = nil,
streamConfiguration: HTTP1Channel.Configuration = .init()
) {
self.idleTimeout = idleTimeout
self.gracefulCloseTimeout = gracefulCloseTimeout
self.streamConfiguration = streamConfiguration
}
/// Child channel for processing HTTP2
internal struct HTTP2Channel: ServerChildChannel {
public typealias Configuration = HTTP2ChannelConfiguration
typealias HTTP2Connection = NIOHTTP2Handler.AsyncStreamMultiplexer<HTTP2StreamChannel.Value>
public struct Value: ServerChildChannelValue {
let http2Connection: HTTP2Connection
public let channel: Channel
}

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 +79,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 +104,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

0 comments on commit c0754a8

Please sign in to comment.