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

Add custom verification handler to NIOSSLServerHandler #673

Merged
merged 8 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
34 changes: 29 additions & 5 deletions Sources/HummingbirdHTTP2/HTTP2UpgradeChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
public let channel: Channel
}

private let sslContext: NIOSSLContext
private let tlsChannelConfiguration: TLSChannelInternalConfiguration
private let http1: HTTP1Channel
private let http2: HTTP2Channel
public let configuration: Configuration
Expand All @@ -54,7 +54,7 @@
) throws {
var tlsConfiguration = tlsConfiguration
tlsConfiguration.applicationProtocols = NIOHTTP2SupportedALPNProtocols
self.sslContext = try NIOSSLContext(configuration: tlsConfiguration)
self.tlsChannelConfiguration = try .init(configuration: .init(tlsConfiguration: tlsConfiguration))

Check warning on line 57 in Sources/HummingbirdHTTP2/HTTP2UpgradeChannel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/HummingbirdHTTP2/HTTP2UpgradeChannel.swift#L57

Added line #L57 was not covered by tests
self.configuration = .init()
self.http1 = HTTP1Channel(
responder: responder,
Expand All @@ -78,7 +78,25 @@
) throws {
var tlsConfiguration = tlsConfiguration
tlsConfiguration.applicationProtocols = NIOHTTP2SupportedALPNProtocols
self.sslContext = try NIOSSLContext(configuration: tlsConfiguration)
self.tlsChannelConfiguration = try .init(configuration: .init(tlsConfiguration: tlsConfiguration))
self.configuration = configuration
self.http1 = HTTP1Channel(responder: responder, configuration: configuration.streamConfiguration)
self.http2 = HTTP2Channel(responder: responder, configuration: configuration)
}

/// Initialize HTTP2UpgradeChannel
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - configuration: HTTP2 channel configuration
/// - responder: Function returning a HTTP response for a HTTP request
public init(
tlsChannelConfiguration: TLSChannelConfiguration,
configuration: Configuration = .init(),
responder: @escaping HTTPChannelHandler.Responder
) throws {
var tlsChannelConfiguration = tlsChannelConfiguration
tlsChannelConfiguration.tlsConfiguration.applicationProtocols = NIOHTTP2SupportedALPNProtocols
self.tlsChannelConfiguration = try .init(configuration: tlsChannelConfiguration)
self.configuration = configuration
self.http1 = HTTP1Channel(responder: responder, configuration: configuration.streamConfiguration)
self.http2 = HTTP2Channel(responder: responder, configuration: configuration)
Expand All @@ -91,7 +109,13 @@
/// - 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))
try channel.pipeline.syncOperations.addHandler(
NIOSSLServerHandler(
context: self.tlsChannelConfiguration.sslContext,
customVerificationCallback: self.tlsChannelConfiguration.customVerificationCallback,
configuration: .init()
)
)
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
Expand Down Expand Up @@ -120,7 +144,7 @@
await self.http2.handle(value: http2, logger: logger)
}
} catch {
logger.error("Error getting HTTP2 upgrade negotiated value: \(error)")
logger.debug("Error getting HTTP2 upgrade negotiated value: \(error)")
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions Sources/HummingbirdHTTP2/HTTPServerBuilder+http2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,30 @@ extension HTTPServerBuilder {
)
}
}

/// Build HTTP channel with HTTP2 upgrade
///
/// Use in ``Hummingbird/Application`` initialization.
/// ```
/// let app = Application(
/// router: router,
/// server: .http2Upgrade(configuration: .init(tlsConfiguration: tlsConfiguration))
/// )
/// ```
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - configuration: HTTP2 Upgrade channel configuration
/// - Returns: HTTPChannelHandler builder
public static func http2Upgrade(
tlsChannelConfiguration: TLSChannelConfiguration,
configuration: HTTP2UpgradeChannel.Configuration = .init()
) throws -> HTTPServerBuilder {
.init { responder in
try HTTP2UpgradeChannel(
tlsChannelConfiguration: tlsChannelConfiguration,
configuration: configuration,
responder: responder
)
}
}
}
78 changes: 78 additions & 0 deletions Sources/HummingbirdHTTP2/TLSChannelConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2025 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 NIOCore
import NIOSSL

/// TLSChannel configuration
public struct TLSChannelConfiguration: Sendable {
public typealias CustomVerificationCallback = @Sendable ([NIOSSLCertificate], EventLoopPromise<NIOSSLVerificationResult>) -> Void
adam-fowler marked this conversation as resolved.
Show resolved Hide resolved

// Manages configuration of TLS
public var tlsConfiguration: TLSConfiguration
/// A custom verification callback that allows completely overriding the certificate verification logic of BoringSSL.
public var customVerificationCallback: CustomVerificationCallback?

/// Initialize TLSChannel.Configuration
///
/// For details on custom callback see swift-nio-ssl documentation
/// https://swiftpackageindex.com/apple/swift-nio-ssl/main/documentation/niossl/niosslcustomverificationcallback
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - customVerificationCallback: A custom verification callback that allows completely overriding the
/// certificate verification logic of BoringSSL.
public init(
tlsConfiguration: TLSConfiguration,
customVerificationCallback: CustomVerificationCallback? = nil
) {
self.tlsConfiguration = tlsConfiguration
self.customVerificationCallback = customVerificationCallback
}

/// Initialize TLSChannel.Configuration
///
/// For details on custom callback see swift-nio-ssl documentation
/// https://swiftpackageindex.com/apple/swift-nio-ssl/main/documentation/niossl/niosslcustomverificationcallback
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - customAsyncVerificationCallback: A custom verification callback that allows completely overriding the
/// certificate verification logic of BoringSSL.
public init(
tlsConfiguration: TLSConfiguration,
customAsyncVerificationCallback: @escaping @Sendable ([NIOSSLCertificate]) async throws -> NIOSSLVerificationResult
) {
self.tlsConfiguration = tlsConfiguration
self.customVerificationCallback = { certificates, promise in
promise.completeWithTask {
try await customAsyncVerificationCallback(certificates)
}
}
}

Check warning on line 61 in Sources/HummingbirdHTTP2/TLSChannelConfiguration.swift

View check run for this annotation

Codecov / codecov/patch

Sources/HummingbirdHTTP2/TLSChannelConfiguration.swift#L54-L61

Added lines #L54 - L61 were not covered by tests
}

/// TLSChannel configuration
@usableFromInline
package struct TLSChannelInternalConfiguration: Sendable {
// Manages configuration of TLS
@usableFromInline
let sslContext: NIOSSLContext
/// A custom verification callback that allows completely overriding the certificate verification logic of BoringSSL.
@usableFromInline
let customVerificationCallback: TLSChannelConfiguration.CustomVerificationCallback?

init(configuration: TLSChannelConfiguration) throws {
self.sslContext = try NIOSSLContext(configuration: configuration.tlsConfiguration)
self.customVerificationCallback = configuration.customVerificationCallback
}
}
22 changes: 22 additions & 0 deletions Sources/HummingbirdTLS/HTTPServerBuilder+tls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,26 @@ extension HTTPServerBuilder {
try base.buildChildChannel(responder).withTLS(tlsConfiguration: tlsConfiguration)
}
}

/// Build server supporting HTTP with TLS
///
/// Use in ``Hummingbird/Application`` initialization.
/// ```
/// let app = Application(
/// router: router,
/// server: .tls(.http1(), tlsConfiguration: tlsConfiguration)
/// )
/// ```
/// - Parameters:
/// - base: Base child channel to wrap with TLS
/// - configuration: TLS channel configuration
/// - Returns: HTTPChannelHandler builder
public static func tls(
_ base: HTTPServerBuilder = .http1(),
configuration: TLSChannelConfiguration
) throws -> HTTPServerBuilder {
.init { responder in
try base.buildChildChannel(responder).withTLS(configuration: configuration)
}
}
}
88 changes: 85 additions & 3 deletions Sources/HummingbirdTLS/TLSChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@ public struct TLSChannel<BaseChannel: ServerChildChannel>: ServerChildChannel {
/// - baseChannel: Base child channel wrap
/// - tlsConfiguration: TLS configuration
public init(_ baseChannel: BaseChannel, tlsConfiguration: TLSConfiguration) throws {
self.sslContext = try NIOSSLContext(configuration: tlsConfiguration)
self.configuration = try .init(configuration: .init(tlsConfiguration: tlsConfiguration))
self.baseChannel = baseChannel
}

/// Initialize TLSChannel
/// - Parameters:
/// - baseChannel: Base child channel wrap
/// - tlsConfiguration: TLS configuration
public init(_ baseChannel: BaseChannel, configuration: TLSChannelConfiguration) throws {
self.configuration = try .init(configuration: configuration)
self.baseChannel = baseChannel
}

Expand All @@ -38,7 +47,13 @@ public struct TLSChannel<BaseChannel: ServerChildChannel>: ServerChildChannel {
@inlinable
public func setup(channel: Channel, logger: Logger) -> EventLoopFuture<Value> {
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(NIOSSLServerHandler(context: self.sslContext))
try channel.pipeline.syncOperations.addHandler(
NIOSSLServerHandler(
context: self.configuration.sslContext,
customVerificationCallback: self.configuration.customVerificationCallback,
configuration: .init()
)
)
}.flatMap {
self.baseChannel.setup(channel: channel, logger: logger)
}
Expand All @@ -54,7 +69,7 @@ public struct TLSChannel<BaseChannel: ServerChildChannel>: ServerChildChannel {
}

@usableFromInline
let sslContext: NIOSSLContext
let configuration: TLSChannelInternalConfiguration
@usableFromInline
var baseChannel: BaseChannel
}
Expand All @@ -70,4 +85,71 @@ extension ServerChildChannel {
func withTLS(tlsConfiguration: TLSConfiguration) throws -> any ServerChildChannel {
try TLSChannel(self, tlsConfiguration: tlsConfiguration)
}

/// Construct existential ``TLSChannel`` from existential `ServerChildChannel`
func withTLS(configuration: TLSChannelConfiguration) throws -> any ServerChildChannel {
try TLSChannel(self, configuration: configuration)
}
}

/// TLSChannel configuration
public struct TLSChannelConfiguration: Sendable {
public typealias CustomVerificationCallback = @Sendable ([NIOSSLCertificate], EventLoopPromise<NIOSSLVerificationResult>) -> Void

// Manages configuration of TLS
public let tlsConfiguration: TLSConfiguration
/// A custom verification callback that allows completely overriding the certificate verification logic of BoringSSL.
public let customVerificationCallback: CustomVerificationCallback?

/// Initialize TLSChannel.Configuration
///
/// For details on custom callback see swift-nio-ssl documentation
/// https://swiftpackageindex.com/apple/swift-nio-ssl/main/documentation/niossl/niosslcustomverificationcallback
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - customVerificationCallback: A custom verification callback that allows completely overriding the
/// certificate verification logic of BoringSSL.
public init(
tlsConfiguration: TLSConfiguration,
customVerificationCallback: CustomVerificationCallback? = nil
) {
self.tlsConfiguration = tlsConfiguration
self.customVerificationCallback = customVerificationCallback
}

/// Initialize TLSChannel.Configuration
///
/// For details on custom callback see swift-nio-ssl documentation
/// https://swiftpackageindex.com/apple/swift-nio-ssl/main/documentation/niossl/niosslcustomverificationcallback
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - customAsyncVerificationCallback: A custom verification callback that allows completely overriding the
/// certificate verification logic of BoringSSL.
public init(
tlsConfiguration: TLSConfiguration,
customAsyncVerificationCallback: @escaping @Sendable ([NIOSSLCertificate]) async throws -> NIOSSLVerificationResult
) {
self.tlsConfiguration = tlsConfiguration
self.customVerificationCallback = { certificates, promise in
promise.completeWithTask {
try await customAsyncVerificationCallback(certificates)
}
}
}
}

/// TLSChannel configuration
@usableFromInline
package struct TLSChannelInternalConfiguration: Sendable {
// Manages configuration of TLS
@usableFromInline
let sslContext: NIOSSLContext
/// A custom verification callback that allows completely overriding the certificate verification logic of BoringSSL.
@usableFromInline
let customVerificationCallback: TLSChannelConfiguration.CustomVerificationCallback?

init(configuration: TLSChannelConfiguration) throws {
self.sslContext = try NIOSSLContext(configuration: configuration.tlsConfiguration)
self.customVerificationCallback = configuration.customVerificationCallback
}
}
27 changes: 27 additions & 0 deletions Sources/HummingbirdTesting/TestClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,33 @@ public struct TestClient: Sendable {
self.configuration = configuration
}

/// Run closure with temporary test client
/// - Parameters:
/// - host: host to connect
/// - port: port to connect to
/// - configuration: Client configuration
/// - eventLoopGroupProvider: EventLoopGroup to use
/// - operation: Closure to run
public static func withClient<Value>(
host: String,
port: Int,
configuration: Configuration = .init(),
eventLoopGroupProvider: NIOEventLoopGroupProvider = .shared(MultiThreadedEventLoopGroup.singleton),
operation: @escaping @Sendable (Self) async throws -> Value
) async throws -> Value {
let client = Self(host: host, port: port, configuration: configuration, eventLoopGroupProvider: eventLoopGroupProvider)
client.connect()
let value: Value
do {
value = try await operation(client)
} catch {
try? await client.shutdown()
throw error
}
try await client.shutdown()
return value
}

/// connect to HTTP server
public func connect() {
do {
Expand Down
Loading