Skip to content

Commit 2832c6b

Browse files
authored
Add swift-configuration support (#50)
Motivation: Users should be able to initialize `NIOHTTPServerConfiguration` via [`swift-configuration`](https://github.com/apple/swift-configuration). Modifications: Added a new initializer on `NIOHTTPServerConfiguration`, which has a `Configuration.ConfigReader` argument. This initializer populates the four child types (`BindTarget`, `TransportSecurity`, `BackpressureStrategy`, and `HTTP2`) from the reader. Result: Support for `swift-configuration` added.
1 parent 02fa5ec commit 2832c6b

File tree

5 files changed

+883
-5
lines changed

5 files changed

+883
-5
lines changed

Package.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ let package = Package(
3535
targets: ["HTTPServer"]
3636
)
3737
],
38+
traits: [
39+
.trait(name: "SwiftConfiguration"),
40+
.default(enabledTraits: ["SwiftConfiguration"]),
41+
],
3842
dependencies: [
3943
.package(
4044
url: "https://github.com/FranzBusch/swift-collections.git",
@@ -48,6 +52,7 @@ let package = Package(
4852
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.36.0"),
4953
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.30.0"),
5054
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.0.0"),
55+
.package(url: "https://github.com/apple/swift-configuration", from: "1.0.0"),
5156
],
5257
targets: [
5358
.executableTarget(
@@ -80,6 +85,11 @@ let package = Package(
8085
.product(name: "NIOHTTPTypesHTTP1", package: "swift-nio-extras"),
8186
.product(name: "NIOHTTPTypesHTTP2", package: "swift-nio-extras"),
8287
.product(name: "NIOCertificateReloading", package: "swift-nio-extras"),
88+
.product(
89+
name: "Configuration",
90+
package: "swift-configuration",
91+
condition: .when(traits: ["SwiftConfiguration"])
92+
),
8393
],
8494
swiftSettings: extraSettings
8595
),
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift HTTP Server open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#if SwiftConfiguration
16+
public import Configuration
17+
import NIOCertificateReloading
18+
import SwiftASN1
19+
public import X509
20+
21+
enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible {
22+
case customVerificationCallbackProvidedWhenNotUsingMTLS
23+
24+
var description: String {
25+
switch self {
26+
case .customVerificationCallbackProvidedWhenNotUsingMTLS:
27+
"Invalid configuration. A custom certificate verification callback was provided despite the server not being configured for mTLS."
28+
}
29+
}
30+
}
31+
32+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
33+
extension NIOHTTPServerConfiguration {
34+
/// Initialize the server configuration from a config reader.
35+
///
36+
/// ## Configuration keys:
37+
///
38+
/// ``NIOHTTPServerConfiguration`` is comprised of four types. Provide configuration for each type under the
39+
/// specified key:
40+
/// - ``BindTarget`` - Provide under key `"bindTarget"` (keys listed in ``BindTarget/init(config:)``).
41+
/// - ``TransportSecurity`` - Provide under key `"transportSecurity"` (keys listed in
42+
/// ``TransportSecurity/init(config:customCertificateVerificationCallback:)``).
43+
/// - ``BackPressureStrategy`` - Provide under key `"backpressureStrategy"` (keys listed in
44+
/// ``BackPressureStrategy/init(config:)``).
45+
/// - ``HTTP2`` - Provide under key `"http2"` (keys listed in ``HTTP2/init(config:)``).
46+
///
47+
/// - Parameters:
48+
/// - config: The configuration reader to read configuration values from.
49+
/// - customCertificateVerificationCallback: An optional client certificate verification callback to use when
50+
/// mTLS is configured (i.e., when `"transportSecurity.security"` is `"mTLS"` or `"reloadingMTLS"`). If provided
51+
/// when mTLS is *not* configured, this initializer throws
52+
/// ``NIOHTTPServerConfigurationError/customVerificationCallbackProvidedWhenNotUsingMTLS``. If set to `nil` when
53+
/// mTLS *is* configured, the default client certificate verification logic of the underlying SSL implementation
54+
/// is used.
55+
public init(
56+
config: ConfigReader,
57+
customCertificateVerificationCallback: (
58+
@Sendable ([Certificate]) async throws -> CertificateVerificationResult
59+
)? = nil
60+
) throws {
61+
let snapshot = config.snapshot()
62+
63+
self.init(
64+
bindTarget: try .init(config: snapshot.scoped(to: "bindTarget")),
65+
transportSecurity: try .init(
66+
config: snapshot.scoped(to: "transportSecurity"),
67+
customCertificateVerificationCallback: customCertificateVerificationCallback
68+
),
69+
backpressureStrategy: .init(config: snapshot.scoped(to: "backpressureStrategy")),
70+
http2: .init(config: snapshot.scoped(to: "http2"))
71+
)
72+
}
73+
}
74+
75+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
76+
extension NIOHTTPServerConfiguration.BindTarget {
77+
/// Initialize a bind target configuration from a config reader.
78+
///
79+
/// ## Configuration keys:
80+
/// - `host` (string, required): The hostname or IP address the server will bind to (e.g., "localhost", "0.0.0.0").
81+
/// - `port` (int, required): The port number the server will listen on (e.g., 8080, 443).
82+
///
83+
/// - Parameter config: The configuration reader.
84+
public init(config: ConfigSnapshotReader) throws {
85+
self.init(
86+
backing: .hostAndPort(
87+
host: try config.requiredString(forKey: "host"),
88+
port: try config.requiredInt(forKey: "port")
89+
)
90+
)
91+
}
92+
}
93+
94+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
95+
extension NIOHTTPServerConfiguration.TransportSecurity {
96+
/// Initialize a transport security configuration from a config reader.
97+
///
98+
/// ## Configuration keys:
99+
/// - `security` (string, required): The transport security for the server (permitted values: `"plaintext"`,
100+
/// `"tls"`, `"reloadingTLS"`, `"mTLS"`, `"reloadingMTLS"`).
101+
///
102+
/// ### Configuration keys for `"tls"`:
103+
/// - `certificateChainPEMString` (string, required): PEM-formatted certificate chain content.
104+
/// - `privateKeyPEMString` (string, required, secret): PEM-formatted private key content.
105+
///
106+
/// ### Configuration keys for `"reloadingTLS"`:
107+
/// - `refreshInterval` (int, optional, default: 30): The interval (in seconds) at which the certificate chain and
108+
/// private key will be reloaded.
109+
/// - `certificateChainPEMPath` (string, required): Path to the certificate chain PEM file.
110+
/// - `privateKeyPEMPath` (string, required): Path to the private key PEM file.
111+
///
112+
/// ### Configuration keys for `"mTLS"`:
113+
/// - `certificateChainPEMString` (string, required): PEM-formatted certificate chain content.
114+
/// - `privateKeyPEMString` (string, required, secret): PEM-formatted private key content.
115+
/// - `trustRoots` (string array, optional, default: system trust roots): The root certificates to trust when
116+
/// verifying client certificates.
117+
/// - `certificateVerificationMode` (string, required): The client certificate validation behavior (permitted
118+
/// values: "optionalVerification" or "noHostnameVerification").
119+
///
120+
/// ### Configuration keys for `"reloadingMTLS"`:
121+
/// - `refreshInterval` (int, optional, default: 30): The interval (in seconds) at which the certificate chain and
122+
/// private key will be reloaded.
123+
/// - `certificateChainPEMPath` (string, required): Path to the certificate chain PEM file.
124+
/// - `privateKeyPEMPath` (string, required): Path to the private key PEM file.
125+
/// - `trustRoots` (string array, optional, default: system trust roots): The root certificates to trust when
126+
/// verifying client certificates.
127+
/// - `certificateVerificationMode` (string, required): The client certificate validation behavior (permitted
128+
/// values: "optionalVerification" or "noHostnameVerification").
129+
///
130+
/// - Parameters:
131+
/// - config: The configuration reader.
132+
/// - customCertificateVerificationCallback: An optional client certificate verification callback to use when
133+
/// mTLS is configured (i.e., when `"transportSecurity.security"` is `"mTLS"` or `"reloadingMTLS"`). If provided
134+
/// when mTLS is *not* configured, this initializer throws
135+
/// ``NIOHTTPServerConfigurationError/customVerificationCallbackProvidedWhenNotUsingMTLS``. If set to `nil` when
136+
/// mTLS *is* configured, the default client certificate verification logic of the underlying SSL implementation
137+
/// is used.
138+
public init(
139+
config: ConfigSnapshotReader,
140+
customCertificateVerificationCallback: (
141+
@Sendable ([Certificate]) async throws -> CertificateVerificationResult
142+
)? = nil
143+
) throws {
144+
let security = try config.requiredString(forKey: "security", as: TransportSecurityKind.self)
145+
146+
// A custom verification callback can only be used when the server is configured for mTLS.
147+
if let _ = customCertificateVerificationCallback, !security.isMTLS() {
148+
throw NIOHTTPServerConfigurationError.customVerificationCallbackProvidedWhenNotUsingMTLS
149+
}
150+
151+
switch security {
152+
case .plaintext:
153+
self = .plaintext
154+
155+
case .tls:
156+
self = try .tls(config: config)
157+
158+
case .reloadingTLS:
159+
self = try .reloadingTLS(config: config)
160+
161+
case .mTLS:
162+
self = try .mTLS(
163+
config: config,
164+
customCertificateVerificationCallback: customCertificateVerificationCallback
165+
)
166+
167+
case .reloadingMTLS:
168+
self = try .reloadingMTLS(
169+
config: config,
170+
customCertificateVerificationCallback: customCertificateVerificationCallback
171+
)
172+
}
173+
}
174+
175+
private static func tls(config: ConfigSnapshotReader) throws -> Self {
176+
let certificateChainPEMString = try config.requiredString(forKey: "certificateChainPEMString")
177+
let privateKeyPEMString = try config.requiredString(forKey: "privateKeyPEMString", isSecret: true)
178+
179+
return Self.tls(
180+
certificateChain: try PEMDocument.parseMultiple(pemString: certificateChainPEMString)
181+
.map { try Certificate(pemEncoded: $0.pemString) },
182+
privateKey: try .init(pemEncoded: privateKeyPEMString)
183+
)
184+
}
185+
186+
private static func reloadingTLS(config: ConfigSnapshotReader) throws -> Self {
187+
let refreshInterval = config.int(forKey: "refreshInterval", default: 30)
188+
let certificateChainPEMPath = try config.requiredString(forKey: "certificateChainPEMPath")
189+
let privateKeyPEMPath = try config.requiredString(forKey: "privateKeyPEMPath")
190+
191+
return try Self.tls(
192+
certificateReloader: TimedCertificateReloader(
193+
refreshInterval: .seconds(refreshInterval),
194+
certificateSource: .init(location: .file(path: certificateChainPEMPath), format: .pem),
195+
privateKeySource: .init(location: .file(path: privateKeyPEMPath), format: .pem)
196+
)
197+
)
198+
}
199+
200+
private static func mTLS(
201+
config: ConfigSnapshotReader,
202+
customCertificateVerificationCallback: (
203+
@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult
204+
)? = nil
205+
) throws -> Self {
206+
let certificateChainPEMString = try config.requiredString(forKey: "certificateChainPEMString")
207+
let privateKeyPEMString = try config.requiredString(forKey: "privateKeyPEMString", isSecret: true)
208+
let trustRoots = config.stringArray(forKey: "trustRoots")
209+
let verificationMode = try config.requiredString(
210+
forKey: "certificateVerificationMode",
211+
as: VerificationMode.self
212+
)
213+
214+
return Self.mTLS(
215+
certificateChain: try PEMDocument.parseMultiple(pemString: certificateChainPEMString)
216+
.map { try Certificate(pemEncoded: $0.pemString) },
217+
privateKey: try .init(pemEncoded: privateKeyPEMString),
218+
trustRoots: try trustRoots?.map { try Certificate(pemEncoded: $0) },
219+
certificateVerification: .init(verificationMode),
220+
customCertificateVerificationCallback: customCertificateVerificationCallback
221+
)
222+
}
223+
224+
private static func reloadingMTLS(
225+
config: ConfigSnapshotReader,
226+
customCertificateVerificationCallback: (
227+
@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult
228+
)? = nil
229+
) throws -> Self {
230+
let refreshInterval = config.int(forKey: "refreshInterval", default: 30)
231+
let certificateChainPEMPath = try config.requiredString(forKey: "certificateChainPEMPath")
232+
let privateKeyPEMPath = try config.requiredString(forKey: "privateKeyPEMPath")
233+
let trustRoots = config.stringArray(forKey: "trustRoots")
234+
let verificationMode = try config.requiredString(
235+
forKey: "certificateVerificationMode",
236+
as: VerificationMode.self
237+
)
238+
239+
return try Self.mTLS(
240+
certificateReloader: TimedCertificateReloader(
241+
refreshInterval: .seconds(refreshInterval),
242+
certificateSource: .init(location: .file(path: certificateChainPEMPath), format: .pem),
243+
privateKeySource: .init(location: .file(path: privateKeyPEMPath), format: .pem)
244+
),
245+
trustRoots: try trustRoots?.map { try Certificate(pemEncoded: $0) },
246+
certificateVerification: .init(verificationMode),
247+
customCertificateVerificationCallback: customCertificateVerificationCallback
248+
)
249+
}
250+
}
251+
252+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
253+
extension NIOHTTPServerConfiguration.BackPressureStrategy {
254+
/// Initialize the backpressure strategy configuration from a config reader.
255+
///
256+
/// ## Configuration keys:
257+
/// - `low` (int, optional, default: 2): The threshold below which the consumer will ask the producer to produce
258+
/// more elements.
259+
/// - `high` (int, optional, default: 10): The threshold above which the producer will stop producing elements.
260+
///
261+
/// - Parameter config: The configuration reader.
262+
public init(config: ConfigSnapshotReader) {
263+
self.init(
264+
backing: .watermark(
265+
low: config.int(
266+
forKey: "low",
267+
default: NIOHTTPServerConfiguration.BackPressureStrategy.defaultWatermarkLow
268+
),
269+
high: config.int(
270+
forKey: "high",
271+
default: NIOHTTPServerConfiguration.BackPressureStrategy.defaultWatermarkHigh
272+
)
273+
)
274+
)
275+
}
276+
}
277+
278+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
279+
extension NIOHTTPServerConfiguration.HTTP2 {
280+
/// Initialize a HTTP/2 configuration from a config reader.
281+
///
282+
/// ## Configuration keys:
283+
/// - `maxFrameSize` (int, optional, default: 2^14): The maximum frame size to be used in an HTTP/2 connection.
284+
/// - `targetWindowSize` (int, optional, default: 2^16 - 1): The target window size to be used in an HTTP/2
285+
/// connection.
286+
/// - `maxConcurrentStreams` (int, optional, default: 100): The maximum number of concurrent streams in an HTTP/2
287+
/// connection.
288+
///
289+
/// - Parameter config: The configuration reader.
290+
public init(config: ConfigSnapshotReader) {
291+
self.init(
292+
maxFrameSize: config.int(
293+
forKey: "maxFrameSize",
294+
default: NIOHTTPServerConfiguration.HTTP2.defaultMaxFrameSize
295+
),
296+
targetWindowSize: config.int(
297+
forKey: "targetWindowSize",
298+
default: NIOHTTPServerConfiguration.HTTP2.defaultTargetWindowSize
299+
),
300+
/// The default value, ``NIOHTTPServerConfiguration.HTTP2.DEFAULT_TARGET_WINDOW_SIZE``, is `nil`. However,
301+
/// we can only specify a non-nil `default` argument to `config.int(...)`. But `config.int(...)` already
302+
/// defaults to `nil` if it can't find the `"maxConcurrentStreams"` key, so that works for us.
303+
maxConcurrentStreams: config.int(forKey: "maxConcurrentStreams")
304+
)
305+
}
306+
}
307+
308+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
309+
extension NIOHTTPServerConfiguration.TransportSecurity {
310+
fileprivate enum TransportSecurityKind: String {
311+
case plaintext
312+
case tls
313+
case reloadingTLS
314+
case mTLS
315+
case reloadingMTLS
316+
317+
func isMTLS() -> Bool {
318+
switch self {
319+
case .mTLS, .reloadingMTLS:
320+
return true
321+
322+
default:
323+
return false
324+
}
325+
}
326+
}
327+
328+
/// A wrapper over ``CertificateVerificationMode``.
329+
fileprivate enum VerificationMode: String {
330+
case optionalVerification
331+
case noHostnameVerification
332+
}
333+
}
334+
335+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
336+
extension CertificateVerificationMode {
337+
fileprivate init(_ mode: NIOHTTPServerConfiguration.TransportSecurity.VerificationMode) {
338+
switch mode {
339+
case .optionalVerification:
340+
self.init(mode: .optionalVerification)
341+
case .noHostnameVerification:
342+
self.init(mode: .noHostnameVerification)
343+
}
344+
}
345+
}
346+
#endif // SwiftConfiguration

0 commit comments

Comments
 (0)