Skip to content
Draft
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
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ let package = Package(
.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.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"),
// TODO: Update `branch` once NIOAsyncTestingChannel patch (https://github.com/apple/swift-nio/pull/3464) is released.
.package(url: "https://github.com/apple/swift-nio.git", branch: "main"),
.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
109 changes: 109 additions & 0 deletions Sources/HTTPServer/NIOHTTPServer+HTTP1_1.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP Server open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIOCore
import NIOEmbedded
import NIOHTTP1
import NIOHTTPTypes
import NIOHTTPTypesHTTP1
import NIOPosix

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension NIOHTTPServer {
func serveInsecureHTTP1_1(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
) async throws {
let serverChannel = try await self.setupHTTP1_1ServerChannel(
bindTarget: bindTarget,
asyncChannelConfiguration: asyncChannelConfiguration
)

try await _serveInsecureHTTP1_1(serverChannel: serverChannel, handler: handler)
}

private func setupHTTP1_1ServerChannel(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
) async throws -> NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never> {
switch bindTarget.backing {
case .hostAndPort(let host, let port):
let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
.bind(host: host, port: port) { channel in
self.setupHTTP1_1ConnectionChildChannel(
channel: channel,
asyncChannelConfiguration: asyncChannelConfiguration
)
}

try self.addressBound(serverChannel.channel.localAddress)

return serverChannel
}
}

func setupHTTP1_1ConnectionChildChannel(
channel: any Channel,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
) -> EventLoopFuture<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>> {
channel.pipeline.configureHTTPServerPipeline().flatMapThrowing {
try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: false))

return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
wrappingChannelSynchronously: channel,
configuration: asyncChannelConfiguration
)
}
}

private func _serveInsecureHTTP1_1(
serverChannel: NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never>,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>
) async throws {
try await withThrowingDiscardingTaskGroup { group in
try await serverChannel.executeThenClose { inbound in
for try await http1Channel in inbound {
group.addTask {
try await self.handleRequestChannel(
channel: http1Channel,
handler: handler
)
}
}
}
}
}

func serveInsecureHTTP1_1WithTestChannel(
testChannel: NIOAsyncTestingChannel,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>
) async throws {
// The server requires a NIOAsyncChannel, so we create one from the test channel
let serverTestAsyncChannel = try await testChannel.eventLoop.submit {
return try NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never>(
wrappingChannelSynchronously: testChannel,
configuration: .init()
)
}.get()

// Trick the server into thinking it's been bound to an address so that we don't leak the listening address
// promise. In reality, the server hasn't been bound to any address: we will manually feed in requests and
// observe responses.
try self.addressBound(.init(ipAddress: "127.0.0.1", port: 8000))
_ = try await self.listeningAddress

try await _serveInsecureHTTP1_1(serverChannel: serverTestAsyncChannel, handler: handler)
}
}
229 changes: 229 additions & 0 deletions Sources/HTTPServer/NIOHTTPServer+SecureUpgrade.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP Server open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Logging
import NIOCore
import NIOEmbedded
import NIOHTTP2
import NIOHTTPTypes
import NIOHTTPTypesHTTP1
import NIOHTTPTypesHTTP2
import NIOPosix
import NIOSSL
import X509

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension NIOHTTPServer {
func serveSecureUpgrade(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
tlsConfiguration: TLSConfiguration,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.Configuration,
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? = nil
) async throws {
let serverChannel = try await self.setupSecureUpgradeServerChannel(
bindTarget: bindTarget,
tlsConfiguration: tlsConfiguration,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Configuration,
verificationCallback: verificationCallback
)

try await self._serveSecureUpgrade(serverChannel: serverChannel, handler: handler)
}

typealias NegotiatedChannel = NIONegotiatedHTTPVersion<
NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>,
(any Channel, NIOHTTP2Handler.AsyncStreamMultiplexer<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>>)
>

private func setupSecureUpgradeServerChannel(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
tlsConfiguration: TLSConfiguration,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.Configuration,
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
) async throws -> NIOAsyncChannel<EventLoopFuture<NegotiatedChannel>, Never> {
switch bindTarget.backing {
case .hostAndPort(let host, let port):
let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
.bind(host: host, port: port) { channel in
self.setupSecureUpgradeConnectionChildChannel(
channel: channel,
tlsConfiguration: tlsConfiguration,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Configuration,
verificationCallback: verificationCallback
)
}

try self.addressBound(serverChannel.channel.localAddress)

return serverChannel
}
}

func setupSecureUpgradeConnectionChildChannel(
channel: any Channel,
tlsConfiguration: TLSConfiguration,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.Configuration,
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
) -> EventLoopFuture<EventLoopFuture<NegotiatedChannel>> {
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.makeSSLServerHandler(tlsConfiguration, verificationCallback)
)
}.flatMap {
channel.configureAsyncHTTPServerPipeline(
http2Configuration: http2Configuration,
http1ConnectionInitializer: { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: true))

return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
wrappingChannelSynchronously: channel,
configuration: asyncChannelConfiguration
)
}
},
http2ConnectionInitializer: { channel in channel.eventLoop.makeCompletedFuture(.success(channel)) },
http2StreamInitializer: { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations
.addHandler(
HTTP2FramePayloadToHTTPServerCodec()
)

return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
wrappingChannelSynchronously: channel,
configuration: asyncChannelConfiguration
)
}
}
)
}
}

private func _serveSecureUpgrade(
serverChannel: NIOAsyncChannel<EventLoopFuture<NegotiatedChannel>, Never>,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>
) async throws {
try await withThrowingDiscardingTaskGroup { group in
try await serverChannel.executeThenClose { inbound in
for try await upgradeResult in inbound {
group.addTask {
do {
try await withThrowingDiscardingTaskGroup { connectionGroup in
switch try await upgradeResult.get() {
case .http1_1(let http1Channel):
let chainFuture = http1Channel.channel.nioSSL_peerValidatedCertificateChain()
Self.$connectionContext.withValue(ConnectionContext(chainFuture)) {
connectionGroup.addTask {
try await self.handleRequestChannel(
channel: http1Channel,
handler: handler
)
}
}
case .http2((let http2Connection, let http2Multiplexer)):
do {
let chainFuture = http2Connection.nioSSL_peerValidatedCertificateChain()
try await Self.$connectionContext.withValue(ConnectionContext(chainFuture)) {
for try await http2StreamChannel in http2Multiplexer.inbound {
connectionGroup.addTask {
try await self.handleRequestChannel(
channel: http2StreamChannel,
handler: handler
)
}
}
}
} catch {
self.logger.debug("HTTP2 connection closed: \(error)")
}
}
}
} catch {
self.logger.debug("Negotiating ALPN failed: \(error)")
}
}
}
}
}
}

func serveSecureUpgradeWithTestChannel(
testChannel: NIOAsyncTestingChannel,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>
) async throws {
// The server requires a NIOAsyncChannel, so we create one from the test channel
let testAsyncChannel = try await testChannel.eventLoop.submit {
return try NIOAsyncChannel<EventLoopFuture<NIOHTTPServer.NegotiatedChannel>, Never>(
wrappingChannelSynchronously: testChannel,
configuration: .init()
)
}.get()

// Trick the server into thinking it's been bound to an address so that we don't leak the listening address
// promise. In reality, the server hasn't been bound to any address: we will manually feed in requests and
// observe responses.
try self.addressBound(.init(ipAddress: "127.0.0.1", port: 8000))
_ = try await self.listeningAddress

try await self._serveSecureUpgrade(serverChannel: testAsyncChannel, handler: handler)
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension NIOHTTPServer {
func makeSSLServerHandler(
_ tlsConfiguration: TLSConfiguration,
_ customVerificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
) 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", metadata: [
"failure-reason": .string(error.reason)
])
return .failed
}
}
}
)
} else {
return try NIOSSLServerHandler(context: .init(configuration: tlsConfiguration))
}
}
}
Loading
Loading