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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ extension NIOHTTPServerConfiguration {
/// ``NIOHTTPServerConfiguration`` is comprised of four types. Provide configuration for each type under the
/// specified key:
///
/// - **`"bindTarget"`**: The address and port to bind to (see ``BindTarget/init(config:)``).
/// - **`"bindTarget"`**: A single address and port to bind to (see ``BindTarget/init(config:)``). Use this when
/// binding to exactly one address.
///
/// - **`"bindTargets"`**: Multiple addresses to bind to, provided as parallel string and int arrays under
/// `bindTargets.hosts` and `bindTargets.ports`. Exactly one of `"bindTarget"` or `"bindTargets"` must be
/// provided.
///
/// - **`"http"`**: Supported HTTP versions and protocol settings. Supported keys are `"versions"`
/// (a string array of `"http1_1"` and/or `"http2"`) and, when HTTP/2 is enabled, `"http2"` (see
Expand All @@ -50,6 +55,10 @@ extension NIOHTTPServerConfiguration {
/// - Throws `NIOHTTPServerSwiftConfigurationError/trustRootsSourceAndVerificationCallbackMismatch` if there
/// is a mismatch between `transportSecurity.trustRootsSource` and whether a custom certificate verification
/// callback is provided.
/// - Throws `NIOHTTPServerSwiftConfigurationError/singularAndPluralBindTargetsProvided` if both
/// `"bindTarget"` and `"bindTargets"` are provided.
/// - Throws `NIOHTTPServerSwiftConfigurationError/bindTargetsHostsAndPortsLengthMismatch` if
/// `bindTargets.hosts` and `bindTargets.ports` have different lengths.
public init(
config: ConfigReader,
customCertificateVerificationCallback: (
Expand All @@ -59,7 +68,7 @@ extension NIOHTTPServerConfiguration {
let snapshot = config.snapshot()

try self.init(
bindTarget: try .init(config: snapshot.scoped(to: "bindTarget")),
bindTargets: try Self.readBindTargets(from: snapshot),
supportedHTTPVersions: try .init(config: snapshot.scoped(to: "http")),
transportSecurity: try .init(
config: snapshot.scoped(to: "transportSecurity"),
Expand All @@ -68,6 +77,37 @@ extension NIOHTTPServerConfiguration {
backpressureStrategy: .init(config: snapshot.scoped(to: "backpressureStrategy"))
)
}

/// Reads bind targets from either the singular `bindTarget` scope or the plural `bindTargets` scope.
/// Exactly one of the two must be provided.
private static func readBindTargets(
from snapshot: ConfigSnapshotReader
) throws -> [BindTarget] {
let bindTargetsScope = snapshot.scoped(to: "bindTargets")
let hosts = bindTargetsScope.stringArray(forKey: "hosts")
let ports = bindTargetsScope.intArray(forKey: "ports")
let hasPlural = hosts != nil || ports != nil

let bindTargetScope = snapshot.scoped(to: "bindTarget")
let singularHost = bindTargetScope.string(forKey: "host")
let singularPort = bindTargetScope.int(forKey: "port")
let hasSingular = singularHost != nil || singularPort != nil

if hasSingular && hasPlural {
throw NIOHTTPServerSwiftConfigurationError.singularAndPluralBindTargetsProvided
}

if hasPlural {
let hosts = hosts ?? []
let ports = ports ?? []
guard hosts.count == ports.count else {
throw NIOHTTPServerSwiftConfigurationError.bindTargetsHostsAndPortsLengthMismatch
}
return zip(hosts, ports).map { .hostAndPort(host: $0, port: $1) }
}

return [try BindTarget(config: bindTargetScope)]
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ public struct NIOHTTPServerConfiguration: Sendable {
}
}

/// Network binding configuration
public var bindTarget: BindTarget
/// Network binding configuration specifying all addresses where the server should listen.
public var bindTargets: [BindTarget]

/// TLS configuration for the server.
public var transportSecurity: TransportSecurity
Expand All @@ -242,19 +242,23 @@ public struct NIOHTTPServerConfiguration: Sendable {
/// Backpressure strategy to use in the server.
public var backpressureStrategy: BackPressureStrategy

/// Create a new configuration.
/// Create a new configuration with multiple bind targets.
Comment thread
aryan-25 marked this conversation as resolved.
/// - Parameters:
/// - bindTarget: A ``BindTarget``.
/// - bindTargets: An array of ``BindTarget`` values specifying where the server should listen.
/// - 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.
public init(
bindTarget: BindTarget,
bindTargets: [BindTarget],
supportedHTTPVersions: Set<HTTPVersion>,
transportSecurity: TransportSecurity,
backpressureStrategy: BackPressureStrategy = .defaults
) throws {
if bindTargets.isEmpty {
throw NIOHTTPServerConfigurationError.noBindTargetsSpecified
}

// 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 {
Expand All @@ -267,11 +271,32 @@ public struct NIOHTTPServerConfiguration: Sendable {
throw NIOHTTPServerConfigurationError.noSupportedHTTPVersionsSpecified
}

self.bindTarget = bindTarget
self.bindTargets = bindTargets
self.supportedHTTPVersions = supportedHTTPVersions
self.transportSecurity = transportSecurity
self.backpressureStrategy = backpressureStrategy
}

/// Create a new configuration with a single bind target.
/// - 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.
public init(
bindTarget: BindTarget,
supportedHTTPVersions: Set<HTTPVersion>,
transportSecurity: TransportSecurity,
backpressureStrategy: BackPressureStrategy = .defaults
) throws {
try self.init(
bindTargets: [bindTarget],
supportedHTTPVersions: supportedHTTPVersions,
transportSecurity: transportSecurity,
backpressureStrategy: backpressureStrategy
)
}
}

/// Represents the outcome of certificate verification.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible {
case noSupportedHTTPVersionsSpecified
case incompatibleTransportSecurity
case noBindTargetsSpecified

var description: String {
switch self {
Expand All @@ -24,6 +25,9 @@ enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible {

case .incompatibleTransportSecurity:
"Invalid configuration: only HTTP/1.1 can be served over plaintext. `transportSecurity` must be set to (m)TLS for serving HTTP/2."

case .noBindTargetsSpecified:
"Invalid configuration: at least one bind target must be specified."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ enum NIOHTTPServerSwiftConfigurationError: Error, CustomStringConvertible {
case customVerificationCallbackAndTrustRootsProvided
case customVerificationCallbackProvidedWhenNotUsingMTLS
case trustRootsSourceAndVerificationCallbackMismatch
case singularAndPluralBindTargetsProvided
case bindTargetsHostsAndPortsLengthMismatch

var description: String {
switch self {
Expand All @@ -30,6 +32,12 @@ enum NIOHTTPServerSwiftConfigurationError: Error, CustomStringConvertible {

case .trustRootsSourceAndVerificationCallbackMismatch:
"Invalid configuration: there is a mismatch between the trustRootsSource key and the provided customCertificateVerificationCallback."

case .singularAndPluralBindTargetsProvided:
"Invalid configuration: both the singular 'bindTarget' scope and the plural 'bindTargets' scope were provided. Use only one."

case .bindTargetsHostsAndPortsLengthMismatch:
"Invalid configuration: 'bindTargets.hosts' and 'bindTargets.ports' must have the same number of elements."
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,18 @@ let serverConfiguration = try NIOHTTPServerConfiguration(config: config)
``NIOHTTPServerConfiguration`` is comprised of four components. Provide the configuration for each component under its
respective key prefix.

> Important: Exactly one of `bindTarget` (singular, for a single address) or `bindTargets` (plural, for multiple
> addresses) must be provided. Providing both results in an error.

> Important: HTTP/2 cannot be served over plaintext. If `"http2"` is included in `http.versions`, the transport
> security must be set to `"tls"` or `"mTLS"`.

| Prefix | Configuration Key | Type | Required/Optional | Default |
|-------------------------------|-----------------------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------|---------|
| `bindTarget` | `host` | `string` | Required | - |
| | `port` | `int` | Required | - |
| `bindTarget` | `host` | `string` | Required when binding to a single address (mutually exclusive with `bindTargets`) | - |
| | `port` | `int` | Required when binding to a single address (mutually exclusive with `bindTargets`) | - |
| `bindTargets` | `hosts` | `string array` | Required when binding to multiple addresses (mutually exclusive with `bindTarget`); must match length of `ports` | - |
| | `ports` | `int array` | Required when binding to multiple addresses (mutually exclusive with `bindTarget`); must match length of `hosts` | - |
| `http` | `versions` | `string array` | Required (permitted values: `"http1_1"`, `"http2"`) | - |
| `http.http2` | `maxFrameSize` | `int` | Optional | 2^14 |
| | `targetWindowSize` | `int` | Optional | 2^16-1 |
Expand Down Expand Up @@ -112,6 +117,19 @@ key were omitted.
}
```

To bind to multiple addresses, replace `bindTarget` with `bindTargets`, providing parallel `hosts` and `ports` arrays
of the same length:

```json
{
"bindTargets": {
"hosts": ["0.0.0.0", "::"],
"ports": [443, 443]
},
// ...rest of the configuration
}
```

### Custom certificate verification

When using mTLS, you can provide a custom certificate verification callback instead of relying on trust roots. To do
Expand Down
64 changes: 40 additions & 24 deletions Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,34 +62,50 @@ extension NIOHTTPServer {
}
}

func setupHTTP1_1ServerChannel(
bindTarget: NIOHTTPServerConfiguration.BindTarget
) 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)
.serverChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.serverQuiescingHelper.makeServerChannelHandler(channel: channel)
)
}
}
.bind(host: host, port: port) { channel in
self.setupHTTP1_1ConnectionChildChannel(
channel: channel,
asyncChannelConfiguration: .init(
backPressureStrategy: .init(self.configuration.backpressureStrategy),
isOutboundHalfClosureEnabled: true
)
func setupHTTP1_1ServerChannels(
bindTargets: [NIOHTTPServerConfiguration.BindTarget]
) async throws -> [NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never>] {
let bootstrap = ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
.serverChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.serverQuiescingHelper.makeServerChannelHandler(channel: channel)
)
}
}

try self.addressBound(serverChannel.channel.localAddress)

return serverChannel
var serverChannels = [NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never>]()
do {
for bindTarget in bindTargets {
switch bindTarget.backing {
case .hostAndPort(let host, let port):
let serverChannel =
try await bootstrap.bind(host: host, port: port) { channel in
self.setupHTTP1_1ConnectionChildChannel(
channel: channel,
asyncChannelConfiguration: .init(
backPressureStrategy: .init(self.configuration.backpressureStrategy),
isOutboundHalfClosureEnabled: true
)
)
}
serverChannels.append(serverChannel)
}
}
} catch {
// A later bind failed: close any channels we already bound to avoid leaking sockets.
// We await the closes so the sockets are fully released by the time we throw, giving the
// caller deterministic semantics: when `serve` throws, all cleanup is done.
for serverChannel in serverChannels {
try? await serverChannel.channel.close()
}
throw error
}

try self.addressesBound(serverChannels.map { $0.channel.localAddress })

return serverChannels
}

func setupHTTP1_1ConnectionChildChannel(
Expand Down
Loading
Loading