Skip to content
Open
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 @@ -19,6 +19,7 @@ import NIOCertificateReloading
import NIOHTTP2
import SwiftASN1
public import X509
import System

@available(anyAppleOS 26.0, *)
extension NIOHTTPServerConfiguration {
Expand Down Expand Up @@ -95,7 +96,8 @@ extension NIOHTTPServerConfiguration {
let bindTargetScope = snapshot.scoped(to: "bindTarget")
let singularHost = bindTargetScope.string(forKey: "host")
let singularPort = bindTargetScope.int(forKey: "port")
let hasSingular = singularHost != nil || singularPort != nil
let singularSocketPath = bindTargetScope.string(forKey: "socketPath")
let hasSingular = singularHost != nil || singularPort != nil || singularSocketPath != nil

if hasSingular && hasPlural {
throw NIOHTTPServerSwiftConfigurationError.singularAndPluralBindTargetsProvided
Expand All @@ -119,17 +121,31 @@ extension NIOHTTPServerConfiguration.BindTarget {
/// Initialize a bind target configuration from a config reader.
///
/// ## Configuration keys:
/// - `host` (string, required): The hostname or IP address the server will bind to (e.g., "localhost", "0.0.0.0").
/// - `port` (int, required): The port number the server will listen on (e.g., 8080, 443).
/// - `host` (string): The hostname or IP address to bind to. Required unless `socketPath` is given.
/// - `port` (int): The port to listen on. Required unless `socketPath` is given.
/// - `socketPath` (string): A unix domain socket path to bind to. Mutually exclusive with `host`/`port`.
///
/// - Parameter config: The configuration reader.
public init(config: ConfigSnapshotReader) throws {
self.init(
backing: .hostAndPort(
let host = config.string(forKey: "host")
let port = config.int(forKey: "port")
let socketPath = config.string(forKey: "socketPath")

let backing: Backing
if let socketPath {
guard host == nil, port == nil else {
throw NIOHTTPServerSwiftConfigurationError.hostPortAndSocketPathProvided
}
let filePath = FilePath(socketPath)
backing = .unixDomainSocket(path: filePath)
} else {
backing = .hostAndPort(
host: try config.requiredString(forKey: "host"),
port: try config.requiredInt(forKey: "port")
)
)
}

self.init(backing: backing)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import NIOCore
import NIOSSL
public import X509
public import System

/// Configuration settings for ``NIOHTTPServer``.
///
Expand All @@ -29,6 +30,7 @@ public struct NIOHTTPServerConfiguration: Sendable {
public struct BindTarget: Sendable {
enum Backing {
case hostAndPort(host: String, port: Int)
case unixDomainSocket(path: FilePath)
}

let backing: Backing
Expand All @@ -47,8 +49,22 @@ public struct NIOHTTPServerConfiguration: Sendable {
public static func hostAndPort(host: String, port: Int) -> Self {
Self(backing: .hostAndPort(host: host, port: port))
}

/// Creates a bind target for a unix domain socket.
///
/// - Parameter path: The file system path to bind the unix domain socket to (e.g., "/tmp/server.sock")
/// - Returns: A configured `BindTarget` instance
///
/// ## Example
/// ```swift
/// let target = BindTarget.unixDomainSocket(path: "/tmp/server.sock")
/// ```
public static func unixDomainSocket(path: FilePath) -> Self {
Self(backing: .unixDomainSocket(path: path))
}
}


/// Configuration for transport security settings.
///
/// Provides options for running the server with or without TLS encryption.
Expand Down Expand Up @@ -344,6 +360,17 @@ public struct NIOHTTPServerConfiguration: Sendable {
throw NIOHTTPServerConfigurationError.noSupportedHTTPVersionsSpecified
}

#if HTTP3
// HTTP/3 runs over QUIC/UDP and cannot be served over a unix domain socket.
if supportedHTTPVersions.http3ConfigIfSupported != nil {
for bindTarget in bindTargets {
if case .unixDomainSocket = bindTarget.backing {
throw NIOHTTPServerConfigurationError.unixDomainSocketNotSupportedOverHTTP3
}
}
}
#endif

Comment thread
0xTim marked this conversation as resolved.
self.bindTargets = bindTargets
self.supportedHTTPVersions = supportedHTTPVersions
self.transportSecurity = transportSecurity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible {
case onlyPEMFileCredentialsCurrentlySupportedOverHTTP3
// swift-nio-quic doesn't currently support mTLS. See https://github.com/apple/swift-nio-quic/issues/5.
case mTLSNotCurrentlySupportedOverHTTP3
case unixDomainSocketNotSupportedOverHTTP3

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

case .mTLSNotCurrentlySupportedOverHTTP3:
"Invalid configuration: mTLS is not currently supported over HTTP/3."

case .unixDomainSocketNotSupportedOverHTTP3:
"Invalid configuration: unix domain socket bind targets are not supported over HTTP/3, which runs over QUIC/UDP."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ enum NIOHTTPServerSwiftConfigurationError: Error, CustomStringConvertible {
case trustRootsSourceAndVerificationCallbackMismatch
case singularAndPluralBindTargetsProvided
case bindTargetsHostsAndPortsLengthMismatch
case hostPortAndSocketPathProvided

var description: String {
switch self {
Expand All @@ -38,6 +39,9 @@ enum NIOHTTPServerSwiftConfigurationError: Error, CustomStringConvertible {

case .bindTargetsHostsAndPortsLengthMismatch:
"Invalid configuration: 'bindTargets.hosts' and 'bindTargets.ports' must have the same number of elements."

case .hostPortAndSocketPathProvided:
"Invalid configuration: a bind target has both 'host'/'port' and 'socketPath' set. Use either a host and port, or a unix domain socket path, not both."
}
}
}
Expand Down
28 changes: 26 additions & 2 deletions Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import NIOHTTPTypes
import NIOHTTPTypesHTTP1
import NIOPosix
import NIOSSL
import System

@available(anyAppleOS 26.0, *)
extension NIOHTTPServer {
Expand Down Expand Up @@ -121,10 +122,9 @@ extension NIOHTTPServer {

do {
for bindTarget in bindTargets {
let serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup)
switch bindTarget.backing {
case .hostAndPort(let host, let port):
let serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup)

let serverChannel = try await bootstrap.serverChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
Expand All @@ -148,6 +148,30 @@ extension NIOHTTPServer {
)
}
serverChannels.append((serverChannel, serverQuiescingHelper))
case .unixDomainSocket(let path):
let serverChannel = try await bootstrap.serverChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
serverQuiescingHelper.makeServerChannelHandler(channel: channel)
)

if let maxConnections = self.configuration.maxConnections {
try channel.pipeline.syncOperations.addHandler(
ConnectionLimitHandler(maxConnections: maxConnections)
)
}
}
}.bind(unixDomainSocketPath: path.string) { channel in
self.setupHTTP1_1Connection(
channel: channel,
asyncChannelConfiguration: .init(
backPressureStrategy: .init(self.configuration.backpressureStrategy),
isOutboundHalfClosureEnabled: true
),
isSecure: false
)
}
serverChannels.append((serverChannel, serverQuiescingHelper))
}
}
} catch {
Expand Down
4 changes: 4 additions & 0 deletions Sources/NIOHTTPServer/NIOHTTPServer+HTTP3.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ extension NIOHTTPServer {
}

serverChannels.append((quicChannel, multiplexer))

case .unixDomainSocket(_):
// Rejected during configuration validation; see NIOHTTPServerConfiguration.init.
preconditionFailure("Unix domain sockets are not supported for HTTP/3")
}
}
} catch {
Expand Down
77 changes: 59 additions & 18 deletions Sources/NIOHTTPServer/NIOHTTPServer+ListeningAddress.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,26 @@
import NIOConcurrencyHelpers
import NIOCore
import NIOPosix
import System

enum ListeningAddressError: CustomStringConvertible, Error {
case addressOrPortNotAvailable
case unsupportedAddressType
case addressNotAvailable
case portNotAvailable
case serverClosed
case pathnameNotAvailable

var description: String {
switch self {
case .addressOrPortNotAvailable:
return "Unable to retrieve the bound address or port from the underlying socket"
case .unsupportedAddressType:
return "Unsupported address type: only IPv4 and IPv6 are supported"
case .addressNotAvailable:
return "Unable to retrieve the bound address from the underlying socket"
case .portNotAvailable:
return "Unable to retrieve the bound port from the underlying socket"
case .serverClosed:
return """
There is no listening address bound for this server: there may have been an error which caused the server to close, or it may have shut down.
"""
case .pathnameNotAvailable:
return "Unable to retrieve the unix domain socket path from the underlying socket"
}
}
}
Expand Down Expand Up @@ -134,39 +138,76 @@ extension NIOHTTPServer {
@available(anyAppleOS 26.0, *)
extension NIOHTTPServer.SocketAddress {
init(_ address: NIOCore.SocketAddress?) throws(ListeningAddressError) {
guard let address, let port = address.port else {
throw ListeningAddressError.addressOrPortNotAvailable
guard let address else {
throw .addressNotAvailable
}

var port: Int {
get throws(ListeningAddressError) {
guard let port = address.port else {
throw .portNotAvailable
}
return port
}
}

var pathname: String {
get throws(ListeningAddressError) {
guard let pathname = address.pathname else {
throw .pathnameNotAvailable
}
return pathname
}
}

switch address {
case .v4(let ipv4Address):
self.init(base: .ipv4(.init(host: ipv4Address.host, port: port)))
try self.init(base: .ipv4(.init(host: ipv4Address.host, port: port)))

case .v6(let ipv6Address):
self.init(base: .ipv6(.init(host: ipv6Address.host, port: port)))
try self.init(base: .ipv6(.init(host: ipv6Address.host, port: port)))

case .unixDomainSocket:
throw ListeningAddressError.unsupportedAddressType
case .unixDomainSocket(_):
try self.init(base: .unixDomainSocket(path: pathname))
}
}
}

@available(anyAppleOS 26.0, *)
extension NIOHTTPServerConfiguration.BindTarget {
init(_ address: NIOCore.SocketAddress?) throws(ListeningAddressError) {
guard let address, let port = address.port else {
throw ListeningAddressError.addressOrPortNotAvailable
guard let address else {
throw .addressNotAvailable
}

var port: Int {
get throws(ListeningAddressError) {
guard let port = address.port else {
throw .portNotAvailable
}
return port
}
}

var filePath: FilePath {
get throws(ListeningAddressError) {
guard let pathname = address.pathname else {
throw .pathnameNotAvailable
}
let filePath = FilePath(pathname)
return filePath
}
}

switch address {
case .v4(let ipv4Address):
self.init(backing: .hostAndPort(host: ipv4Address.host, port: port))
try self.init(backing: .hostAndPort(host: ipv4Address.host, port: port))

case .v6(let ipv6Address):
self.init(backing: .hostAndPort(host: ipv6Address.host, port: port))
try self.init(backing: .hostAndPort(host: ipv6Address.host, port: port))

case .unixDomainSocket:
throw ListeningAddressError.unsupportedAddressType
case .unixDomainSocket(_):
try self.init(backing: .unixDomainSocket(path: filePath))
}
}
}
25 changes: 23 additions & 2 deletions Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import NIOPosix
import NIOSSL
import NIOTLS
import X509
import System

@available(anyAppleOS 26.0, *)
extension NIOHTTPServer {
Expand Down Expand Up @@ -223,10 +224,9 @@ extension NIOHTTPServer {
var serverChannels = [(NIOAsyncChannel<EventLoopFuture<NegotiatedChannel>, Never>, ServerQuiescingHelper)]()
do {
for bindTarget in bindTargets {
let serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup)
switch bindTarget.backing {
case .hostAndPort(let host, let port):
let serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup)

let serverChannel = try await bootstrap.serverChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
Expand All @@ -247,6 +247,27 @@ extension NIOHTTPServer {
)
}
serverChannels.append((serverChannel, serverQuiescingHelper))
case .unixDomainSocket(let path):
let serverChannel = try await bootstrap.serverChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
serverQuiescingHelper.makeServerChannelHandler(channel: channel)
)

if let maxConnections = self.configuration.maxConnections {
try channel.pipeline.syncOperations.addHandler(
ConnectionLimitHandler(maxConnections: maxConnections)
)
}
}
}.bind(unixDomainSocketPath: path.string) { channel in
self.setupSecureUpgradeConnectionChildChannel(
channel: channel,
supportedHTTPVersions: supportedHTTPVersions,
sslContext: sslContext
)
}
serverChannels.append((serverChannel, serverQuiescingHelper))
}
}
} catch {
Expand Down
Loading