-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathNIOHTTPServerConfiguration.swift
More file actions
513 lines (439 loc) · 20.3 KB
/
NIOHTTPServerConfiguration.swift
File metadata and controls
513 lines (439 loc) · 20.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
//===----------------------------------------------------------------------===//
//
// 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
/// Configuration settings for ``NIOHTTPServer``.
///
/// This structure contains all the necessary configuration options for setting up
/// and running ``NIOHTTPServer``, including network binding and TLS settings.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct NIOHTTPServerConfiguration: Sendable {
/// Specifies where the server should bind and listen for incoming connections.
///
/// Currently supports binding to a specific host and port combination.
/// Additional binding targets may be added in the future.
public struct BindTarget: Sendable {
enum Backing {
case hostAndPort(host: String, port: Int)
}
let backing: Backing
/// Creates a bind target for a specific host and port.
///
/// - Parameters:
/// - host: The hostname or IP address to bind to (e.g., "localhost", "0.0.0.0")
/// - port: The port number to listen on (e.g., 8080, 443)
/// - Returns: A configured `BindTarget` instance
///
/// ## Example
/// ```swift
/// let target = BindTarget.hostAndPort(host: "localhost", port: 8080)
/// ```
public static func hostAndPort(host: String, port: Int) -> Self {
Self(backing: .hostAndPort(host: host, port: port))
}
}
/// Configuration for transport security settings.
///
/// Provides options for running the server with or without TLS encryption.
/// When using TLS, you must either provide a certificate chain and private key, or a `CertificateReloader`.
public struct TransportSecurity: Sendable {
enum Backing {
case plaintext
case tls(credentials: TLSCredentials)
case mTLS(
credentials: TLSCredentials,
trustConfiguration: MTLSTrustConfiguration
)
}
let backing: Backing
/// Configures the server for plaintext HTTP without TLS encryption.
public static let plaintext: Self = Self(backing: .plaintext)
/// Configures the server for TLS with the provided credentials.
///
/// - Parameter credentials: The TLS credentials containing the certificate chain and private key
/// to present during the TLS handshake.
public static func tls(credentials: TLSCredentials) -> Self {
Self(backing: .tls(credentials: credentials))
}
/// Configures the server for mTLS with the provided credentials and trust configuration.
///
/// - Parameters:
/// - credentials: The TLS credentials containing the certificate chain and private key
/// to present during the TLS handshake.
/// - trustConfiguration: The trust roots and certificate verification mode to use when
/// validating client certificates.
public static func mTLS(
credentials: TLSCredentials,
trustConfiguration: MTLSTrustConfiguration
) -> Self {
Self(
backing: .mTLS(
credentials: credentials,
trustConfiguration: trustConfiguration
)
)
}
/// The custom mTLS certificate verification callback, if one was configured.
///
/// Returns the callback when the transport security is configured for mTLS with a
/// ``MTLSTrustConfiguration/customCertificateVerificationCallback(_:certificateVerification:)``,
/// or `nil` otherwise.
var customVerificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
{
switch self.backing {
case .tls, .plaintext:
// A custom certificate verification callback is an mTLS concept (the callback verifies the certificates
// presented by the client); it doesn't apply for plaintext and TLS.
return nil
case .mTLS(_, let trustRoots):
switch trustRoots.backing {
case .customCertificateVerificationCallback(let callback):
return callback
case .systemDefaults, .inMemory, .pemFile:
return nil
}
}
}
}
/// HTTP/2 specific configuration.
public struct HTTP2: Sendable, Hashable {
/// The maximum frame size to be used in an HTTP/2 connection.
public var maxFrameSize: Int
/// The target window size for this connection.
///
/// - Note: This will also be set as the initial window size for the connection.
public var targetWindowSize: Int
/// The number of concurrent streams on the HTTP/2 connection.
public var maxConcurrentStreams: Int?
/// The graceful shutdown configuration.
public var gracefulShutdown: GracefulShutdownConfiguration
/// Configuration options for HTTP/2 graceful shutdown behavior.
public struct GracefulShutdownConfiguration: Sendable, Hashable {
/// The maximum amount of time that the connection has to close gracefully.
/// If set to `nil`, no time limit is enforced on the graceful shutdown process.
public var maximumGracefulShutdownDuration: Duration?
/// Creates a graceful shutdown configuration with the specified timeout value.
///
/// - Parameters:
/// - maximumGracefulShutdownDuration: The maximum amount of time that the connection has to close
/// gracefully. When `nil`, no time limit is enforced for active streams to finish during graceful
/// shutdown.
public init(maximumGracefulShutdownDuration: Duration? = nil) {
self.maximumGracefulShutdownDuration = maximumGracefulShutdownDuration
}
}
/// - Parameters:
/// - maxFrameSize: The maximum frame size to be used in connections.
/// - targetWindowSize: The target window size for connections. This will also be set as the initial window
/// size.
/// - maxConcurrentStreams: The maximum number of concurrent streams permitted on connections.
/// - gracefulShutdown: The graceful shutdown configuration.
public init(
maxFrameSize: Int = Self.defaultMaxFrameSize,
targetWindowSize: Int = Self.defaultTargetWindowSize,
maxConcurrentStreams: Int? = Self.defaultMaxConcurrentStreams,
gracefulShutdown: GracefulShutdownConfiguration = .init()
) {
self.maxFrameSize = maxFrameSize
self.targetWindowSize = targetWindowSize
self.maxConcurrentStreams = maxConcurrentStreams
self.gracefulShutdown = gracefulShutdown
}
@inlinable
static var defaultMaxFrameSize: Int { 1 << 14 }
@inlinable
static var defaultTargetWindowSize: Int { (1 << 16) - 1 }
@inlinable
static var defaultMaxConcurrentStreams: Int? { nil }
/// Default values. The max frame size defaults to 2^14, the target window size defaults to 2^16-1, and
/// the max concurrent streams default to infinite.
public static var defaults: Self {
Self(
maxFrameSize: Self.defaultMaxFrameSize,
targetWindowSize: Self.defaultTargetWindowSize,
maxConcurrentStreams: Self.defaultMaxConcurrentStreams,
gracefulShutdown: GracefulShutdownConfiguration()
)
}
}
/// Configuration for the backpressure strategy to use when reading requests and writing back responses.
public struct BackPressureStrategy: Sendable {
enum Backing {
case watermark(low: Int, high: Int)
}
internal let backing: Backing
init(backing: Backing) {
self.backing = backing
}
/// A low/high watermark will be applied when reading requests and writing responses.
/// - Parameters:
/// - low: The threshold below which the consumer will ask the producer to produce more elements.
/// - high: The threshold above which the producer will stop producing elements.
/// - Returns: A low/high watermark strategy with the configured thresholds.
public static func watermark(low: Int, high: Int) -> Self {
.init(backing: .watermark(low: low, high: high))
}
@inlinable
static var defaultWatermarkLow: Int { 2 }
@inlinable
static var defaultWatermarkHigh: Int { 10 }
/// Default values. The watermark low value defaults to 2, and the watermark high value default to 10.
public static var defaults: Self {
Self.init(
backing: .watermark(
low: Self.defaultWatermarkLow,
high: Self.defaultWatermarkHigh
)
)
}
}
/// Configuration for connection timeouts.
///
/// Timeouts are enabled by default with reasonable values to protect against
/// slow or idle connections. Individual timeouts can be disabled by setting
/// them to `nil`.
public struct ConnectionTimeouts: Sendable {
/// Maximum time a connection can remain idle (no data read or written)
/// before being closed. `nil` means no idle timeout.
public var idle: Duration?
/// Maximum time allowed to receive the complete request headers
/// after a connection is established. `nil` means no timeout.
public var readHeader: Duration?
/// Maximum time allowed to receive the complete request body
/// after headers have been received. `nil` means no timeout.
public var readBody: Duration?
/// - Parameters:
/// - idle: Maximum idle time before the connection is closed.
/// - readHeader: Maximum time to receive request headers.
/// - readBody: Maximum time to receive the request body.
public init(
idle: Duration? = Self.defaultIdle,
readHeader: Duration? = Self.defaultReadHeader,
readBody: Duration? = Self.defaultReadBody
) {
self.idle = idle
self.readHeader = readHeader
self.readBody = readBody
}
@inlinable
static var defaultIdle: Duration? { .seconds(60) }
@inlinable
static var defaultReadHeader: Duration? { .seconds(30) }
@inlinable
static var defaultReadBody: Duration? { .seconds(60) }
/// Default timeout values: 60s idle, 30s read header, 60s read body.
public static var defaults: Self { .init() }
}
/// Network binding configuration
public var bindTarget: BindTarget
/// TLS configuration for the server.
public var transportSecurity: TransportSecurity
/// The HTTP protocol versions the server advertises and accepts connections for.
public var supportedHTTPVersions: Set<HTTPVersion>
/// Backpressure strategy to use in the server.
public var backpressureStrategy: BackPressureStrategy
/// The maximum number of concurrent connections the server will accept.
///
/// When this limit is reached, the server stops accepting new connections
/// until existing ones close. `nil` means unlimited (the default).
public var maxConnections: Int?
/// Configuration for connection timeouts.
public var connectionTimeouts: ConnectionTimeouts
/// Create a new configuration.
/// - Parameters:
/// - bindTarget: A ``BindTarget``.
/// - supportedHTTPVersions: The HTTP protocol versions the server should support.
/// - transportSecurity: The transport security mode (plaintext, TLS, or mTLS).
/// - backpressureStrategy: A ``BackPressureStrategy``.
/// Defaults to ``BackPressureStrategy/watermark(low:high:)`` with a low watermark of 2 and a high of 10.
/// - maxConnections: The maximum number of concurrent connections. `nil` means unlimited.
/// - connectionTimeouts: The connection timeout configuration.
public init(
bindTarget: BindTarget,
supportedHTTPVersions: Set<HTTPVersion>,
transportSecurity: TransportSecurity,
backpressureStrategy: BackPressureStrategy = .defaults,
maxConnections: Int? = nil,
connectionTimeouts: ConnectionTimeouts = .defaults
) throws {
// If `transportSecurity`` is set to `.plaintext`, the server can only support HTTP/1.1.
// To support HTTP/2, `transportSecurity` must be set to `.tls` or `.mTLS`.
if case .plaintext = transportSecurity.backing {
guard supportedHTTPVersions == [.http1_1] else {
throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity
}
}
if supportedHTTPVersions.isEmpty {
throw NIOHTTPServerConfigurationError.noSupportedHTTPVersionsSpecified
}
if let maxConnections, maxConnections <= 0 {
throw NIOHTTPServerConfigurationError.invalidMaxConnections
}
self.bindTarget = bindTarget
self.supportedHTTPVersions = supportedHTTPVersions
self.transportSecurity = transportSecurity
self.backpressureStrategy = backpressureStrategy
self.maxConnections = maxConnections
self.connectionTimeouts = connectionTimeouts
}
}
/// Represents the outcome of certificate verification.
///
/// Indicates whether certificate verification succeeded or failed, and provides associated metadata when verification
/// is successful.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public enum CertificateVerificationResult: Sendable, Hashable {
/// Metadata resulting from successful certificate verification.
public struct VerificationMetadata: Sendable, Hashable {
/// A container for the validated certificate chain: an array of certificates forming a verified and ordered
/// chain of trust, starting from the peer's leaf certificate to a trusted root certificate.
public var validatedCertificateChain: X509.ValidatedCertificateChain?
/// Creates an instance with the peer's *validated* certificate chain.
///
/// - Parameter validatedCertificateChain: An optional *validated* certificate chain. If provided, it must
/// **only** contain the **validated** chain of trust that was built and verified from the certificates
/// presented by the peer.
public init(_ validatedCertificateChain: X509.ValidatedCertificateChain?) {
self.validatedCertificateChain = validatedCertificateChain
}
}
/// An error representing certificate verification failure.
public struct VerificationError: Swift.Error, Hashable {
public let reason: String
/// Creates a verification error with the reason why verification failed.
/// - Parameter reason: The reason of why certificate verification failed.
public init(reason: String) {
self.reason = reason
}
}
/// Certificate verification succeeded.
///
/// The associated metadata contains information captured during verification.
case certificateVerified(VerificationMetadata)
/// Certificate verification failed.
case failed(VerificationError)
}
/// Represents the certificate verification behavior.
public struct CertificateVerificationMode: Sendable {
enum VerificationMode {
case optionalVerification
case noHostnameVerification
}
let mode: VerificationMode
/// Allows peers to connect without presenting any certificates. However, if the peer *does* present
/// certificates, they are validated like normal (exactly like with ``noHostnameVerification``), and the TLS
/// handshake will fail if verification fails.
///
/// - Warning: With this mode, a peer can successfully connect even without presenting any certificates. As such,
/// this mode must be used with great caution.
public static var optionalVerification: Self {
Self(mode: .optionalVerification)
}
/// Validates the certificates presented by the peer but skips hostname verification as it cannot succeed in
/// a server context.
public static var noHostnameVerification: Self {
Self(mode: .noHostnameVerification)
}
}
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOSSL.CertificateVerification {
/// Maps ``CertificateVerificationMode`` to the NIOSSL representation.
init(_ verificationMode: CertificateVerificationMode) {
switch verificationMode.mode {
case .noHostnameVerification:
self = .noHostnameVerification
case .optionalVerification:
self = .optionalVerification
}
}
}
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServerConfiguration {
/// Represents an HTTP version.
public struct HTTPVersion: Sendable, Hashable {
enum Version {
case http1_1
case http2(config: HTTP2)
/// The HTTP/2 configuration if this version is HTTP/2, or `nil` if it is HTTP/1.1.
var http2Config: HTTP2? {
switch self {
case .http1_1:
return nil
case .http2(let config):
return config
}
}
}
let version: Version
/// The HTTP/1.1 protocol version.
public static var http1_1: Self {
Self(version: .http1_1)
}
/// The HTTP/2 protocol version.
///
/// - Parameter config: The configuration to use for HTTP/2.
public static func http2(config: HTTP2) -> Self {
Self(version: .http2(config: config))
}
/// Two values are equal if they represent the same protocol version, regardless of any differences in HTTP/2
/// configuration.
public static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs.version, rhs.version) {
case (.http1_1, .http1_1), (.http2, .http2):
return true
default:
return false
}
}
/// Hashes by protocol version only. Consistent with the `Equatable` conformance.
public func hash(into hasher: inout Hasher) {
switch self.version {
case .http1_1:
hasher.combine(1)
case .http2:
hasher.combine(2)
}
}
}
}
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark {
init(_ backpressureStrategy: NIOHTTPServerConfiguration.BackPressureStrategy) {
switch backpressureStrategy.backing {
case .watermark(let low, let high):
self.init(lowWatermark: low, highWatermark: high)
}
}
}
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension Set where Element == NIOHTTPServerConfiguration.HTTPVersion {
/// The ALPN protocol identifiers to advertise during the TLS handshake, derived from the supported HTTP versions.
///
/// Returns `"h2"` if HTTP/2 is supported, and `"http/1.1"` if HTTP/1.1 is supported, in that order of preference.
var alpnIdentifiers: [String] {
var identifiers = [String]()
if self.http2ConfigIfSupported != nil {
identifiers.append("h2")
}
if self.contains(.http1_1) {
identifiers.append("http/1.1")
}
return identifiers
}
/// The HTTP/2 configuration if HTTP/2 is among the supported versions, or `nil` if only HTTP/1.1 is supported.
var http2ConfigIfSupported: NIOHTTPServerConfiguration.HTTP2? {
self.compactMap({ $0.version.http2Config }).first
}
}