From 08a6e607e89dd595761f838b913067cf091bfb8b Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Thu, 9 Nov 2023 14:44:21 +0100 Subject: [PATCH] Implement an HTTP2 channel setup # Conflicts: # Sources/HummingbirdCore/Server/HTTP/HTTP1Channel.swift # Sources/HummingbirdCore/Server/HTTP/HTTPChannelHandler.swift # Sources/HummingbirdCore/Server/Server.swift # Sources/HummingbirdTLS/TLSChannelSetup.swift # Sources/PerformanceTest/SimpleHTTP1Channel.swift --- Package.swift | 14 +-- .../HummingbirdHTTP2/ChannelInitializer.swift | 87 ----------------- Sources/HummingbirdHTTP2/HTTP2Channel.swift | 97 +++++++++++++++++++ Sources/PerformanceTest/main.swift | 10 ++ 4 files changed, 115 insertions(+), 93 deletions(-) delete mode 100644 Sources/HummingbirdHTTP2/ChannelInitializer.swift create mode 100644 Sources/HummingbirdHTTP2/HTTP2Channel.swift diff --git a/Package.swift b/Package.swift index de181d075..483666242 100644 --- a/Package.swift +++ b/Package.swift @@ -8,6 +8,7 @@ let package = Package( platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17)], products: [ .library(name: "Hummingbird", targets: ["Hummingbird"]), + .library(name: "HummingbirdHTTP2", targets: ["HummingbirdHTTP2"]), .library(name: "HummingbirdCore", targets: ["HummingbirdCore"]), .library(name: "HummingbirdFoundation", targets: ["HummingbirdFoundation"]), .library(name: "HummingbirdJobs", targets: ["HummingbirdJobs"]), @@ -74,12 +75,12 @@ let package = Package( .product(name: "NIOPosix", package: "swift-nio"), .product(name: "NIOSSL", package: "swift-nio-ssl"), ]), - /* .target(name: "HummingbirdHTTP2", dependencies: [ - .byName(name: "HummingbirdCore"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOHTTP2", package: "swift-nio-http2"), - .product(name: "NIOSSL", package: "swift-nio-ssl"), - ]),*/ + .target(name: "HummingbirdHTTP2", dependencies: [ + .byName(name: "HummingbirdCore"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOHTTP2", package: "swift-nio-http2"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), + ]), .target(name: "HummingbirdTLS", dependencies: [ .byName(name: "HummingbirdCore"), .product(name: "NIOCore", package: "swift-nio"), @@ -87,6 +88,7 @@ let package = Package( ]), .executableTarget(name: "PerformanceTest", dependencies: [ .byName(name: "Hummingbird"), + .byName(name: "HummingbirdHTTP2"), .byName(name: "HummingbirdFoundation"), .product(name: "NIOPosix", package: "swift-nio"), ]), diff --git a/Sources/HummingbirdHTTP2/ChannelInitializer.swift b/Sources/HummingbirdHTTP2/ChannelInitializer.swift deleted file mode 100644 index 032d2ef02..000000000 --- a/Sources/HummingbirdHTTP2/ChannelInitializer.swift +++ /dev/null @@ -1,87 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Hummingbird server framework project -// -// Copyright (c) 2021-2021 the Hummingbird authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import HummingbirdCore -import NIOCore -import NIOHTTP1 -import NIOHTTP2 -import NIOSSL - -/// Setup child channel for HTTP2 -public struct HTTP2Channel: HBChannelInitializer { - public init(tlsConfiguration: TLSConfiguration?) throws { - if var tlsConfiguration = tlsConfiguration { - tlsConfiguration.applicationProtocols.append("h2") - tlsConfiguration.applicationProtocols.append("http/1.1") - self.sslContext = try NIOSSLContext(configuration: tlsConfiguration) - } else { - self.sslContext = nil - } - } - - public func initialize(channel: Channel, childHandlers: [RemovableChannelHandler], configuration: HBHTTPServer.Configuration) -> EventLoopFuture { - if let sslContext = self.sslContext { - do { - try channel.pipeline.syncOperations.addHandler(NIOSSLServerHandler(context: sslContext)) - } catch { - return channel.eventLoop.makeFailedFuture(error) - } - } - let loopBoundHandlers = NIOLoopBound(childHandlers, eventLoop: channel.eventLoop) - return channel.configureHTTP2Pipeline(mode: .server) { streamChannel -> EventLoopFuture in - return streamChannel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).flatMap { _ in - streamChannel.pipeline.addHandlers(loopBoundHandlers.value) - } - .map { _ in } - } - .map { _ in } - } - - let sslContext: NIOSSLContext? -} - -/// Setup child channel for HTTP2 upgrade -struct HTTP2UpgradeChannel: HBChannelInitializer { - var http1: HTTP1Channel - let http2: HTTP2Channel - let sslContext: NIOSSLContext - - public init(tlsConfiguration: TLSConfiguration, upgraders: [HTTPServerProtocolUpgrader] = []) throws { - self.sslContext = try NIOSSLContext(configuration: tlsConfiguration) - self.http1 = .init(upgraders: upgraders) - self.http2 = try .init(tlsConfiguration: nil) - } - - func initialize(channel: Channel, childHandlers: [RemovableChannelHandler], configuration: HBHTTPServer.Configuration) -> EventLoopFuture { - do { - try channel.pipeline.syncOperations.addHandler(NIOSSLServerHandler(context: self.sslContext)) - } catch { - return channel.eventLoop.makeFailedFuture(error) - } - return channel.configureHTTP2SecureUpgrade( - h2ChannelConfigurator: { channel in - self.http2.initialize(channel: channel, childHandlers: childHandlers, configuration: configuration) - }, - http1ChannelConfigurator: { channel in - self.http1.initialize(channel: channel, childHandlers: childHandlers, configuration: configuration) - } - ) - } - - /// Add protocol upgrader to channel initializer - /// - Parameter upgrader: HTTP server protocol upgrader to add - public mutating func addProtocolUpgrader(_ upgrader: HTTPServerProtocolUpgrader) { - self.http1.addProtocolUpgrader(upgrader) - } -} diff --git a/Sources/HummingbirdHTTP2/HTTP2Channel.swift b/Sources/HummingbirdHTTP2/HTTP2Channel.swift new file mode 100644 index 000000000..666363ed6 --- /dev/null +++ b/Sources/HummingbirdHTTP2/HTTP2Channel.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2023 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOCore +import NIOPosix +import NIOHTTP1 +import NIOHTTP2 +import Hummingbird +import HummingbirdCore +import NIOSSL + +public struct HTTP2Channel: HTTPChannelHandler { + public typealias Value = EventLoopFuture, NIOHTTP2Handler.AsyncStreamMultiplexer)>> + + private var tlsConfiguration: TLSConfiguration + private var http1: HTTP1Channel + public var responder: @Sendable (HBHTTPRequest, Channel) async throws -> HBHTTPResponse { + get { http1.responder } + set { http1.responder = newValue } + } + + public init( + tlsConfiguration: TLSConfiguration, + http1: HTTP1Channel = HTTP1Channel(), + responder: @escaping @Sendable (HBHTTPRequest, Channel) async throws -> HBHTTPResponse = { _, _ in throw HBHTTPError(.notImplemented) } + ) { + self.tlsConfiguration = tlsConfiguration + self.http1 = http1 + // self.additionalChannelHandlers = additionalChannelHandlers + self.responder = responder + } + + public func initialize(channel: Channel, configuration: HBServerConfiguration, logger: Logger) -> EventLoopFuture { + channel.eventLoop.flatSubmit { + do { + let sslContext = try NIOSSLContext(configuration: tlsConfiguration) + try channel.pipeline.syncOperations.addHandler(NIOSSLServerHandler(context: sslContext)) + } catch { + return channel.eventLoop.makeFailedFuture(error) + } + + return channel.configureAsyncHTTPServerPipeline { http1Channel -> EventLoopFuture in + http1Channel + .pipeline + .addHandler(HBHTTPSendableResponseChannelHandler()) + .flatMapThrowing { + try HTTP1Channel.Value(synchronouslyWrapping: http1Channel) + } + } http2ConnectionInitializer: { http2Channel -> EventLoopFuture> in + http2Channel.eventLoop.makeCompletedFuture { + try NIOAsyncChannel(synchronouslyWrapping: http2Channel) + } + } http2StreamInitializer: { http2ChildChannel -> EventLoopFuture in + http2ChildChannel + .pipeline + .addHandlers(HTTP2FramePayloadToHTTP1ServerCodec(), HBHTTPSendableResponseChannelHandler()) + .flatMapThrowing { + try HTTP1Channel.Value(synchronouslyWrapping: http2ChildChannel) + } + } + } + } + + public func handle(value: Value, logger: Logger) async { + do { + let channel = try await value.get() + switch channel { + case .http1_1(let http1): + await handleHTTP(asyncChannel: http1, logger: logger) + case .http2((let http2, let multiplexer)): + try await withThrowingDiscardingTaskGroup { group in + for try await client in multiplexer.inbound { + group.addTask { + await handleHTTP(asyncChannel: client, logger: logger) + } + } + } + + try await http2.channel.close() + } + } catch { + logger.error("Error handling inbound connection for HTTP2 handler: \(error)") + } + } +} diff --git a/Sources/PerformanceTest/main.swift b/Sources/PerformanceTest/main.swift index a6a22b371..f09059594 100644 --- a/Sources/PerformanceTest/main.swift +++ b/Sources/PerformanceTest/main.swift @@ -14,6 +14,8 @@ import Hummingbird import HummingbirdFoundation +import HummingbirdHTTP2 +import NIOTLS import NIOPosix // get environment @@ -44,6 +46,14 @@ router.get("json") { _, _ in var app = HBApplication( responder: router.buildResponder(), + channelSetup: try HTTP2Channel( + tlsConfiguration: .makeServerConfiguration( + certificateChain: [ + .certificate(.init(file: "/Users/joannisorlandos/git/hummingbird/hummingbird/cert.pem", format: .pem)) + ], + privateKey: .file("/Users/joannisorlandos/git/hummingbird/hummingbird/key.pem") + ) + ), configuration: .init( address: .hostname(hostname, port: port), serverName: "Hummingbird"