Skip to content
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
35 changes: 19 additions & 16 deletions Sources/Example/Example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,27 @@ struct Example {
let privateKey = P256.Signing.PrivateKey()
let server = NIOHTTPServer(
logger: logger,
configuration: .init(
configuration: try .init(
bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345),
supportedHTTPVersions: [.http1_1, .http2(config: .init())],
transportSecurity: .tls(
certificateChain: [
try Certificate(
version: .v3,
serialNumber: .init(bytes: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
publicKey: .init(privateKey.publicKey),
notValidBefore: Date.now.addingTimeInterval(-60),
notValidAfter: Date.now.addingTimeInterval(60 * 60),
issuer: DistinguishedName(),
subject: DistinguishedName(),
signatureAlgorithm: .ecdsaWithSHA256,
extensions: .init(),
issuerPrivateKey: Certificate.PrivateKey(privateKey)
)
],
privateKey: Certificate.PrivateKey(privateKey)
credentials: .inMemory(
certificateChain: [
try Certificate(
version: .v3,
serialNumber: .init(bytes: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
publicKey: .init(privateKey.publicKey),
notValidBefore: Date.now.addingTimeInterval(-60),
notValidAfter: Date.now.addingTimeInterval(60 * 60),
issuer: DistinguishedName(),
subject: DistinguishedName(),
signatureAlgorithm: .ecdsaWithSHA256,
extensions: .init(),
issuerPrivateKey: Certificate.PrivateKey(privateKey)
)
],
privateKey: Certificate.PrivateKey(privateKey)
)
)
)
)
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//===----------------------------------------------------------------------===//
//
// 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
// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// A configuration error arising from an invalid ``NIOHTTPServerConfiguration``.
enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible {
case noSupportedHTTPVersionsSpecified
case incompatibleTransportSecurity

var description: String {
switch self {
case .noSupportedHTTPVersionsSpecified:
"Invalid configuration: at least one supported HTTP version must be specified."

case .incompatibleTransportSecurity:
"Invalid configuration: only HTTP/1.1 can be served over plaintext. `transportSecurity` must be set to (m)TLS for serving HTTP/2."
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//===----------------------------------------------------------------------===//
//
// 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
// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if Configuration

/// A configuration error arising from an invalid ``NIOHTTPServerConfiguration`` specified via `swift-configuration`.
enum NIOHTTPServerSwiftConfigurationError: Error, CustomStringConvertible {
case customVerificationCallbackAndTrustRootsProvided
case customVerificationCallbackProvidedWhenNotUsingMTLS

var description: String {
switch self {
case .customVerificationCallbackAndTrustRootsProvided:
"Invalid configuration: both a custom certificate verification callback and a set of trust roots were provided. When a custom verification callback is provided, trust must be established directly within the callback."

case .customVerificationCallbackProvidedWhenNotUsingMTLS:
"Invalid configuration: a custom certificate verification callback was provided despite the server not being configured for mTLS."
}
}
}

#endif // Configuration
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//===----------------------------------------------------------------------===//
//
// 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
// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIOCore
import NIOSSL
public import X509

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServerConfiguration.TransportSecurity {
/// Configures how the server verifies client certificates during mTLS.
public struct MTLSTrustConfiguration: Sendable {
enum Backing {
case systemDefaults
case inMemory(trustRoots: [Certificate])
case pemFile(path: String)
case customCertificateVerificationCallback(
@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult
)
}

let backing: Backing
let certificateVerification: CertificateVerificationMode

/// Verifies client certificates against the operating system's default trust store.
///
/// - Parameter certificateVerification: The client certificate verification behavior. Defaults to
/// ``CertificateVerificationMode/noHostnameVerification``.
public static func systemDefaults(
certificateVerification: CertificateVerificationMode = .noHostnameVerification
) -> Self {
Self(backing: .systemDefaults, certificateVerification: certificateVerification)
}

/// Verifies client certificates against the provided in-memory trust roots.
///
/// - Parameters:
/// - trustRoots: The root certificates to trust when verifying client certificates.
/// - certificateVerification: The client certificate verification behavior. Defaults to
/// ``CertificateVerificationMode/noHostnameVerification``.
public static func inMemory(
trustRoots: [Certificate],
certificateVerification: CertificateVerificationMode = .noHostnameVerification
) -> Self {
Self(
backing: .inMemory(trustRoots: trustRoots),
certificateVerification: certificateVerification
)
}

/// Verifies client certificates against trust roots loaded from a PEM-encoded file.
///
/// - Parameters:
/// - path: The file path to the PEM-encoded trust root certificates.
/// - certificateVerification: The client certificate verification behavior. Defaults to
/// ``CertificateVerificationMode/noHostnameVerification``.
public static func pemFile(
path: String,
certificateVerification: CertificateVerificationMode = .noHostnameVerification
) -> Self {
Self(
backing: .pemFile(path: path),
certificateVerification: certificateVerification
)
}

/// Uses a custom callback to verify client certificates, overriding the default NIOSSL verification logic.
///
/// - Parameters:
/// - callback: This callback *overrides* the default NIOSSL client certificate verification logic. The
/// callback receives the certificates presented by the peer. Within the callback, you must validate these
/// certificates against your trust roots and derive a validated chain of trust per
/// [RFC 4158](https://datatracker.ietf.org/doc/html/rfc4158). Return
/// ``CertificateVerificationResult/certificateVerified(_:)`` from the callback if verification succeeds,
/// optionally including the validated certificate chain you derived. Returning the validated certificate
/// chain allows ``NIOHTTPServer`` to provide access to it in the request handler through
/// ``NIOHTTPServer/ConnectionContext/peerCertificateChain``, accessed via the task-local
/// ``NIOHTTPServer/connectionContext`` property. Otherwise, return
/// ``CertificateVerificationResult/failed(_:)`` if verification fails.
/// - certificateVerification: The client certificate verification behavior. Defaults to
/// ``CertificateVerificationMode/noHostnameVerification``.
///
/// - Warning: The provided `callback` will override NIOSSL's default certificate verification logic.
public static func customCertificateVerificationCallback(
_ callback: @escaping @Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult,
certificateVerification: CertificateVerificationMode = .noHostnameVerification
) -> Self {
Self(
backing: .customCertificateVerificationCallback(callback),
certificateVerification: certificateVerification
)
}
}
}
67 changes: 67 additions & 0 deletions Sources/NIOHTTPServer/Configuration/TransportSecurity+NIOSSL.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//===----------------------------------------------------------------------===//
//
// 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
// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIOCertificateReloading
import NIOSSL
import X509

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOSSL.TLSConfiguration {
/// Creates a `NIOSSL.TLSConfiguration` from the server's TLS credentials and mTLS trust configuration.
static func makeServerConfiguration(
tlsCredentials: NIOHTTPServerConfiguration.TransportSecurity.TLSCredentials,
mTLSConfiguration: NIOHTTPServerConfiguration.TransportSecurity.MTLSTrustConfiguration?
) throws -> Self {
var config: Self

switch tlsCredentials.backing {
case .inMemory(let certificateChain, let privateKey):
config = .makeServerConfiguration(
certificateChain: try certificateChain.map { try NIOSSLCertificateSource($0) },
privateKey: try NIOSSLPrivateKeySource(privateKey)
)

case .reloading(let certificateReloader):
config = try .makeServerConfiguration(certificateReloader: certificateReloader)

case .pemFile(let certificateChainPath, let privateKeyPath):
config = try .makeServerConfiguration(
certificateChain: NIOSSLCertificate.fromPEMFile(certificateChainPath).map { .certificate($0) },
privateKey: .privateKey(.init(file: privateKeyPath, format: .pem))
)
}

if let mTLSConfiguration {
switch mTLSConfiguration.backing {
case .systemDefaults:
config.trustRoots = .default

case .inMemory(let trustRoots):
config.trustRoots = .certificates(try trustRoots.map { try NIOSSLCertificate($0) })

case .pemFile(let path):
config.trustRoots = .file(path)

case .customCertificateVerificationCallback:
// There are no trust roots when a custom certificate verification callback is specified: the callback
// itself is responsible for establishing trust.
()
}

config.certificateVerification = .init(mTLSConfiguration.certificateVerification)
}

return config
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//===----------------------------------------------------------------------===//
//
// 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
// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

public import NIOCertificateReloading
public import X509

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServerConfiguration.TransportSecurity {
/// Represents the server's TLS credentials: a certificate chain and its corresponding private key.
///
/// Credentials can be provided as in-memory objects, loaded from PEM files on disk, or automatically reloaded at
/// runtime using a `CertificateReloader`.
public struct TLSCredentials: Sendable {
enum Backing {
case inMemory(certificateChain: [Certificate], privateKey: Certificate.PrivateKey)
case reloading(certificateReloader: any CertificateReloader)
case pemFile(certificateChainPath: String, privateKeyPath: String)
}

let backing: Backing

/// Credentials from in-memory certificate objects.
///
/// - Parameters:
/// - certificateChain: The certificate chain to present during the TLS handshake.
/// - privateKey: The private key corresponding to the leaf certificate in `certificateChain`.
public static func inMemory(certificateChain: [Certificate], privateKey: Certificate.PrivateKey) -> Self {
Self(backing: .inMemory(certificateChain: certificateChain, privateKey: privateKey))
}

/// Credentials backed by a `CertificateReloader` that periodically refreshes the certificate chain and
/// private key.
///
/// - Parameter certificateReloader: The reloader responsible for refreshing the credentials.
public static func reloading(certificateReloader: any CertificateReloader) -> Self {
Self(backing: .reloading(certificateReloader: certificateReloader))
}

/// Credentials loaded from PEM-encoded files on disk.
///
/// - Parameters:
/// - certificateChainPath: The file path to the PEM-encoded certificate chain.
/// - privateKeyPath: The file path to the PEM-encoded private key, corresponding to the leaf certificate in
/// `certificateChainPath`.
public static func pemFile(certificateChainPath: String, privateKeyPath: String) -> Self {
Self(backing: .pemFile(certificateChainPath: certificateChainPath, privateKeyPath: privateKeyPath))
}
}
}
8 changes: 5 additions & 3 deletions Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ extension NIOHTTPServer {
}

func setupHTTP1_1ServerChannel(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
bindTarget: NIOHTTPServerConfiguration.BindTarget
) async throws -> NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never> {
switch bindTarget.backing {
case .hostAndPort(let host, let port):
Expand All @@ -58,7 +57,10 @@ extension NIOHTTPServer {
.bind(host: host, port: port) { channel in
self.setupHTTP1_1ConnectionChildChannel(
channel: channel,
asyncChannelConfiguration: asyncChannelConfiguration
asyncChannelConfiguration: .init(
backPressureStrategy: .init(self.configuration.backpressureStrategy),
isOutboundHalfClosureEnabled: true
)
)
}

Expand Down
Loading
Loading