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
22 changes: 14 additions & 8 deletions Sources/AsyncHTTPClient/ConnectionPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,22 @@ extension DeconstructedURL {
}

extension ConnectionPool.Key {
/// The host the request named, i.e. what a user (or a certificate) knows the server as. That is
/// not the connection target's host when a DNS override is in effect.
/// The origin the request named, as handed to the per-origin TLS identity providers
/// (`tlsLocalIdentityProvider…`): what a user (or a certificate) knows the server as. The host is
/// the URL's, which is not the connection target's host when a DNS override is in effect, and an
/// IPv6 literal comes without its square brackets.
///
/// Only `nil` for unix sockets.
var originHost: String? {
self.serverNameIndicatorOverride ?? self.connectionTarget.host
}

var originPort: Int? {
self.connectionTarget.port
var origin: (host: String, port: Int)? {
guard var host = self.serverNameIndicatorOverride ?? self.connectionTarget.host,
let port = self.connectionTarget.port
else {
return nil
}
if host.hasPrefix("["), host.hasSuffix("]") {
host = String(host.dropFirst().dropLast())
}
return (host, port)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ extension HTTPConnectionPool.ConnectionFactory {
case .http1Only:
tlsConfig.applicationProtocols = ["http/1.1"]
}
self.clientConfiguration.applyLocalIdentityNIOSSL(to: &tlsConfig, for: self.key.origin)

let sslServerHostname = self.key.serverNameIndicator
let sslContextFuture = self.sslContextCache.sslContext(
Expand Down Expand Up @@ -664,10 +665,7 @@ extension HTTPConnectionPool.ConnectionFactory {
on: eventLoop,
serverNameIndicatorOverride: key.serverNameIndicatorOverride,
customVerification: self.clientConfiguration.tlsCustomVerificationNetworkFramework,
localIdentity: self.clientConfiguration.localIdentityNetworkFramework(
forHost: self.key.originHost,
port: self.key.originPort
)
localIdentity: self.clientConfiguration.localIdentityNetworkFramework(for: self.key.origin)
).map {
options -> NIOClientTCPBootstrapProtocol in

Expand Down Expand Up @@ -711,6 +709,7 @@ extension HTTPConnectionPool.ConnectionFactory {
}
#endif

self.clientConfiguration.applyLocalIdentityNIOSSL(to: &tlsConfig, for: self.key.origin)
let sslContextFuture = sslContextCache.sslContext(
tlsConfiguration: tlsConfig,
eventLoop: eventLoop,
Expand Down
56 changes: 56 additions & 0 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,43 @@ public final class HTTPClient: Sendable {
/// Defaults to `nil` (OS default interface selection).
public var localAddress: String?

/// A client identity (certificate chain + private key) to present for mTLS on connections that
/// use NIOSSL: every connection on platforms without Network.framework, connections on
/// Apple platforms whose event loop is not a Network.framework one, and proxied connections
/// everywhere.
public struct NIOSSLClientIdentity: Sendable {
/// The certificate chain, leaf first.
public var certificateChain: [NIOSSLCertificateSource]

/// The private key matching the leaf certificate.
public var privateKey: NIOSSLPrivateKeySource

public init(certificateChain: [NIOSSLCertificateSource], privateKey: NIOSSLPrivateKeySource) {
self.certificateChain = certificateChain
self.privateKey = privateKey
}
}

/// Chooses the client identity to present for mTLS, per origin, on connections that use NIOSSL.
///
/// This follows the model of `URLSession`'s authentication challenge: the identity is selected
/// for the origin that is actually being connected to, and returning `nil` presents none. A
/// connection is opened per origin, so a redirect to a different host asks the provider again
/// with that host, and an identity meant for the original host is never sent to it.
///
/// When set, the provider is the only source of the client identity: any
/// `TLSConfiguration.certificateChain` or `TLSConfiguration.privateKey` in
/// ``tlsConfiguration`` or in a request's own TLS configuration is replaced by its answer (and
/// cleared when it returns `nil`). Setting those directly, without a provider, presents the
/// identity to every origin the client connects to, including redirect targets.
///
/// The closure receives the host and port of the origin the request targets (an IPv6 literal is
/// passed without its square brackets, and the host is the one named in the URL even when a
/// DNS override is configured). It is called on the connection's event loop each time a
/// connection is opened, so it must be cheap and must not block. Connections are pooled per
/// origin, so a changed answer only applies to connections opened after the change.
public var tlsLocalIdentityProviderNIOSSL: (@Sendable (_ host: String, _ port: Int) -> NIOSSLClientIdentity?)?

/// A method with access to the HTTP/1 connection channel that is called when creating the connection.
public var http1_1ConnectionDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)?

Expand Down Expand Up @@ -1900,3 +1937,22 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
)
public static let httpEndReceivedAfterHeadWith1xx = HTTPClientError(code: .httpEndReceivedAfterHeadWith1xx)
}

extension HTTPClient.Configuration {
/// Replaces the client identity in `tlsConfiguration` with the one the NIOSSL provider chooses for
/// `origin`, if a provider is configured; otherwise leaves it untouched.
///
/// A connection is bound to a single origin, and redirects to another origin open a new connection
/// to it, so deciding here — rather than once for the whole client — is what keeps an identity from
/// following a redirect to a host it was not meant for. It also overrides an identity carried in a
/// request's own TLS configuration, which redirects preserve. A `nil` origin (unix sockets) is
/// treated as an origin the provider has no identity for.
func applyLocalIdentityNIOSSL(to tlsConfiguration: inout TLSConfiguration, for origin: (host: String, port: Int)?) {
guard let provider = self.tlsLocalIdentityProviderNIOSSL else {
return
}
let identity = origin.flatMap { provider($0.host, $0.port) }
tlsConfiguration.certificateChain = identity?.certificateChain ?? []
tlsConfiguration.privateKey = identity?.privateKey
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -281,21 +281,18 @@ extension TLSConfiguration {
}

extension HTTPClient.Configuration {
/// The client identity to present on a connection opened to `host`:`port`, if any.
/// The client identity to present on a connection opened to `origin`, if any.
///
/// A connection is bound to a single origin, and redirects to another origin open a new connection
/// to it, so deciding here — rather than once for the whole client — is what keeps an identity from
/// following a redirect to a host it was not meant for. `nil` host/port (unix sockets) never
/// consult the provider.
func localIdentityNetworkFramework(forHost host: String?, port: Int?) -> SecIdentity? {
/// following a redirect to a host it was not meant for. A `nil` origin (unix sockets) never
/// consults the provider.
func localIdentityNetworkFramework(for origin: (host: String, port: Int)?) -> SecIdentity? {
if let provider = self.tlsLocalIdentityProviderNetworkFramework {
guard var host, let port else {
guard let origin else {
return nil
}
if host.hasPrefix("["), host.hasSuffix("]") {
host = String(host.dropFirst().dropLast())
}
return provider(host, port)
return provider(origin.host, origin.port)
}
return self.tlsLocalIdentityNetworkFramework
}
Expand Down
Loading
Loading