Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ let package = Package(
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.0.4"),
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.16.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.36.0"),
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.30.0"),
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.0.0"),
],
Expand Down
192 changes: 108 additions & 84 deletions Sources/HTTPServer/NIOHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import NIOPosix
import NIOSSL
import SwiftASN1
import Synchronization
import X509
public import X509

/// A generic HTTP server that can handle incoming HTTP requests.
///
Expand All @@ -40,7 +40,7 @@ import X509
/// ## Usage
///
/// ```swift
/// let configuration = HTTPServerConfiguration(
/// let configuration = NIOHTTPServerConfiguration(
/// bindTarget: .hostAndPort(host: "localhost", port: 8080),
/// tlsConfiguration: .insecure()
/// )
Expand Down Expand Up @@ -81,17 +81,46 @@ public struct NIOHTTPServer: HTTPServerProtocol {
public typealias ResponseWriter = HTTPResponseConcludingAsyncWriter

private let logger: Logger
private let configuration: HTTPServerConfiguration
private let configuration: NIOHTTPServerConfiguration

var listeningAddressState: NIOLockedValueBox<State>

/// Task-local storage for connection-specific information accessible from request handlers.
///
/// Use this to access data such as the peer's validated certificate chain.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this line and just link to the docs for ConnectionContext instead (you can use - SeeAlso:

@TaskLocal public static var connectionContext = ConnectionContext()

/// Connection-specific information available during request handling.
///
/// Provides access to data such as the peer's validated certificate chain.
public struct ConnectionContext: Sendable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this type to an extension on its own file?

var peerCertificateChainFuture: EventLoopFuture<NIOSSL.ValidatedCertificateChain?>?

init(_ peerCertificateChainFuture: EventLoopFuture<NIOSSL.ValidatedCertificateChain?>? = nil) {
self.peerCertificateChainFuture = peerCertificateChainFuture
}

/// The peer's validated certificate chain. This returns `nil` if a
/// ``NIOHTTPServerConfiguration/TransportSecurity/CustomCertificateVerificationCallback`` was not set in the
/// ``NIOHTTPServerConfiguration/TransportSecurity`` property of the server configuration, or if the peer did
/// not authenticate with certificates.
public var peerCertificateChain: X509.ValidatedCertificateChain? {
get async throws {
if let certs = try await self.peerCertificateChainFuture?.get() {
return .init(uncheckedCertificateChain: try certs.map { try Certificate($0) })
}
return nil
}
}
}

/// Create a new ``HTTPServer`` implemented over `SwiftNIO`.
/// - Parameters:
/// - logger: A logger instance for recording server events and debugging information.
/// - configuration: The server configuration including bind target and TLS settings.
public init(
logger: Logger,
configuration: HTTPServerConfiguration,
configuration: NIOHTTPServerConfiguration,
) {
self.logger = logger
self.configuration = configuration
Expand Down Expand Up @@ -171,20 +200,8 @@ public struct NIOHTTPServer: HTTPServerProtocol {
httpServerHTTP2Configuration: self.configuration.http2
)

let certificateChain = try certificateChain
.map {
try NIOSSLCertificate(
bytes: $0.serializeAsPEM().derBytes,
format: .der
)
}
.map { NIOSSLCertificateSource.certificate($0) }
let privateKey = NIOSSLPrivateKeySource.privateKey(
try NIOSSLPrivateKey(
bytes: privateKey.serializeAsPEM().derBytes,
format: .der
)
)
let certificateChain = try certificateChain.map { try NIOSSLCertificateSource($0) }
let privateKey = try NIOSSLPrivateKeySource(privateKey)

var tlsConfiguration: TLSConfiguration = .makeServerConfiguration(
certificateChain: certificateChain,
Expand Down Expand Up @@ -218,39 +235,14 @@ public struct NIOHTTPServer: HTTPServerProtocol {
http2Configuration: http2Config
)

case .mTLS(let certificateChain, let privateKey, let trustRoots):
case .mTLS(let certificateChain, let privateKey, let trustRoots, let verificationCallback):
let http2Config = NIOHTTP2Handler.Configuration(
httpServerHTTP2Configuration: configuration.http2
)

let certificateChain = try certificateChain
.map {
try NIOSSLCertificate(
bytes: $0.serializeAsPEM().derBytes,
format: .der
)
}
.map { NIOSSLCertificateSource.certificate($0) }
let privateKey = NIOSSLPrivateKeySource.privateKey(
try NIOSSLPrivateKey(
bytes: privateKey.serializeAsPEM().derBytes,
format: .der
)
)

let nioTrustRoots: NIOSSLTrustRoots
if let trustRoots {
nioTrustRoots = .certificates(
try trustRoots.map {
try NIOSSLCertificate(
bytes: $0.serializeAsPEM().derBytes,
format: .der
)
}
)
} else {
nioTrustRoots = .default
}
let certificateChain = try certificateChain.map { try NIOSSLCertificateSource($0) }
let privateKey = try NIOSSLPrivateKeySource(privateKey)
let nioTrustRoots = try NIOSSLTrustRoots(treatingNilAsSystemTrustRoots: trustRoots)

var tlsConfiguration: TLSConfiguration = .makeServerConfigurationWithMTLS(
certificateChain: certificateChain,
Expand All @@ -264,27 +256,16 @@ public struct NIOHTTPServer: HTTPServerProtocol {
tlsConfiguration: tlsConfiguration,
handler: handler,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Config
http2Configuration: http2Config,
verificationCallback: verificationCallback
)

case .reloadingMTLS(let certificateReloader, let trustRoots):
case .reloadingMTLS(let certificateReloader, let trustRoots, let verificationCallback):
let http2Config = NIOHTTP2Handler.Configuration(
httpServerHTTP2Configuration: configuration.http2
)

let nioTrustRoots: NIOSSLTrustRoots
if let trustRoots {
nioTrustRoots = .certificates(
try trustRoots.map {
try NIOSSLCertificate(
bytes: $0.serializeAsPEM().derBytes,
format: .der
)
}
)
} else {
nioTrustRoots = .default
}
let nioTrustRoots = try NIOSSLTrustRoots(treatingNilAsSystemTrustRoots: trustRoots)

var tlsConfiguration: TLSConfiguration = try .makeServerConfigurationWithMTLS(
certificateReloader: certificateReloader,
Expand All @@ -297,13 +278,14 @@ public struct NIOHTTPServer: HTTPServerProtocol {
tlsConfiguration: tlsConfiguration,
handler: handler,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Config
http2Configuration: http2Config,
verificationCallback: verificationCallback
)
}
}

private func serveInsecureHTTP1_1(
bindTarget: HTTPServerConfiguration.BindTarget,
bindTarget: NIOHTTPServerConfiguration.BindTarget,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
) async throws {
Expand Down Expand Up @@ -339,11 +321,12 @@ public struct NIOHTTPServer: HTTPServerProtocol {
}

private func serveSecureUpgrade(
bindTarget: HTTPServerConfiguration.BindTarget,
bindTarget: NIOHTTPServerConfiguration.BindTarget,
tlsConfiguration: TLSConfiguration,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.Configuration
http2Configuration: NIOHTTP2Handler.Configuration,
verificationCallback: NIOHTTPServerConfiguration.TransportSecurity.CustomCertificateVerificationCallback? = nil
) async throws {
switch bindTarget.backing {
case .hostAndPort(let host, let port):
Expand All @@ -352,11 +335,7 @@ public struct NIOHTTPServer: HTTPServerProtocol {
.bind(host: host, port: port) { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations
.addHandler(
NIOSSLServerHandler(
context: .init(configuration: tlsConfiguration)
)
)
.addHandler(self.makeSSLServerHandler(tlsConfiguration, verificationCallback))
}.flatMap {
channel.configureAsyncHTTPServerPipeline(http2Configuration: http2Configuration) { channel in
channel.eventLoop.makeCompletedFuture {
Expand All @@ -368,7 +347,7 @@ public struct NIOHTTPServer: HTTPServerProtocol {
)
}
} http2ConnectionInitializer: { channel in
channel.eventLoop.makeSucceededVoidFuture()
channel.eventLoop.makeCompletedFuture(.success(channel))
} http2StreamInitializer: { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations
Expand All @@ -395,20 +374,26 @@ public struct NIOHTTPServer: HTTPServerProtocol {
try await withThrowingDiscardingTaskGroup { connectionGroup in
switch try await upgradeResult.get() {
case .http1_1(let http1Channel):
connectionGroup.addTask {
try await self.handleRequestChannel(
channel: http1Channel,
handler: handler
)
let chainFuture = http1Channel.channel.nioSSL_peerValidatedCertificateChain()
Self.$connectionContext.withValue(ConnectionContext(chainFuture)) {
connectionGroup.addTask {
try await self.handleRequestChannel(
channel: http1Channel,
handler: handler
)
}
}
case .http2((_, let http2Multiplexer)):
case .http2((let http2Connection, let http2Multiplexer)):
do {
let chainFuture = http2Connection.nioSSL_peerValidatedCertificateChain()
for try await http2StreamChannel in http2Multiplexer.inbound {
connectionGroup.addTask {
try await self.handleRequestChannel(
channel: http2StreamChannel,
handler: handler
)
Self.$connectionContext.withValue(ConnectionContext(chainFuture)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move this outside of the for...await loop - I don't think new streams would trigger the chain to change, right?

connectionGroup.addTask {
try await self.handleRequestChannel(
channel: http2StreamChannel,
handler: handler
)
}
}
}
} catch {
Expand Down Expand Up @@ -507,9 +492,48 @@ public struct NIOHTTPServer: HTTPServerProtocol {
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension NIOHTTPServer {
fileprivate func makeSSLServerHandler(
_ tlsConfiguration: TLSConfiguration,
_ customVerificationCallback: NIOHTTPServerConfiguration.TransportSecurity.CustomCertificateVerificationCallback?
) throws -> NIOSSLServerHandler {
if let customVerificationCallback {
return try NIOSSLServerHandler(
context: .init(configuration: tlsConfiguration),
customVerificationCallbackWithMetadata: { certificates, promise in
promise.completeWithTask {
// Convert input [NIOSSLCertificate] to [X509.Certificate]
let x509Certs = try certificates.map { try Certificate($0) }

let callbackResult = try await customVerificationCallback(x509Certs)

switch callbackResult {
case .certificateVerified(let verificationMetadata):
guard let peerChain = verificationMetadata.validatedCertificateChain else {
return .certificateVerified(.init(nil))
}

// Convert the result into [NIOSSLCertificate]
let nioSSLCerts = try peerChain.map { try NIOSSLCertificate($0) }
return .certificateVerified(.init(.init(nioSSLCerts)))

case .failed(let error):
self.logger.error("Custom certificate verification failed: \(error)")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add this error as metadata instead of interpolating it?

return .failed
}
}
}
)
} else {
return try NIOSSLServerHandler(context: .init(configuration: tlsConfiguration))
}
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension NIOHTTP2Handler.Configuration {
init(httpServerHTTP2Configuration http2Config: HTTPServerConfiguration.HTTP2) {
init(httpServerHTTP2Configuration http2Config: NIOHTTPServerConfiguration.HTTP2) {
let clampedTargetWindowSize = Self.clampTargetWindowSize(http2Config.targetWindowSize)
let clampedMaxFrameSize = Self.clampMaxFrameSize(http2Config.maxFrameSize)

Expand Down
Loading
Loading