Skip to content

Commit

Permalink
Add custom verification handler to NIOSSLServerHandler (#673)
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler authored Feb 12, 2025
1 parent fa528b6 commit 567c1fd
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 8 deletions.
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 struct HTTP2UpgradeChannel: HTTPChannelHandler {
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 @@ public struct HTTP2UpgradeChannel: HTTPChannelHandler {
) throws {
var tlsConfiguration = tlsConfiguration
tlsConfiguration.applicationProtocols = NIOHTTP2SupportedALPNProtocols
self.sslContext = try NIOSSLContext(configuration: tlsConfiguration)
self.tlsChannelConfiguration = try .init(configuration: .init(tlsConfiguration: tlsConfiguration))
self.configuration = .init()
self.http1 = HTTP1Channel(
responder: responder,
Expand All @@ -78,7 +78,25 @@ public struct HTTP2UpgradeChannel: HTTPChannelHandler {
) 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 @@ public struct HTTP2UpgradeChannel: HTTPChannelHandler {
/// - 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 @@ public struct HTTP2UpgradeChannel: HTTPChannelHandler {
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

// 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)
}
}
}
}

/// 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

0 comments on commit 567c1fd

Please sign in to comment.