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
14 changes: 14 additions & 0 deletions Sources/AsyncHTTPClient/ConnectionPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ 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.
///
/// Only `nil` for unix sockets.
var originHost: String? {
self.serverNameIndicatorOverride ?? self.connectionTarget.host
}

var originPort: Int? {
self.connectionTarget.port
}
}

extension ConnectionPool.Key {
init(
url: DeconstructedURL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,10 @@ extension HTTPConnectionPool.ConnectionFactory {
on: eventLoop,
serverNameIndicatorOverride: key.serverNameIndicatorOverride,
customVerification: self.clientConfiguration.tlsCustomVerificationNetworkFramework,
localIdentity: self.clientConfiguration.tlsLocalIdentityNetworkFramework
localIdentity: self.clientConfiguration.localIdentityNetworkFramework(
forHost: self.key.originHost,
port: self.key.originPort
)
).map {
options -> NIOClientTCPBootstrapProtocol in

Expand Down
23 changes: 23 additions & 0 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,29 @@ public final class HTTPClient: Sendable {
/// then look them back up as a paired `kSecClassIdentity` item) produces one. AsyncHTTPClient
/// does not perform that round-trip itself; a caller who already has a Keychain-backed identity
/// (or has already done that round-trip) hands it over directly here.
///
/// - Warning: This identity is not scoped to an origin. It is offered to **every** server a
/// connection is opened to, including the targets of redirects. Prefer
/// ``tlsLocalIdentityProviderNetworkFramework``, which is only given the identity's own
/// origin. Ignored when ``tlsLocalIdentityProviderNetworkFramework`` is set.
public var tlsLocalIdentityNetworkFramework: SecIdentity?

/// Chooses the client identity (certificate + private key) to present for mTLS, per origin, on
/// direct (non-proxied) connections that use Network.framework instead of 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.
///
/// 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.
///
/// See ``tlsLocalIdentityNetworkFramework`` for how to obtain a `SecIdentity`. Takes precedence
/// over it when both are set.
public var tlsLocalIdentityProviderNetworkFramework: (@Sendable (_ host: String, _ port: Int) -> SecIdentity?)?
#endif

public init(
Expand All @@ -1054,6 +1076,7 @@ public final class HTTPClient: Sendable {
#if canImport(Network)
self.tlsCustomVerificationNetworkFramework = nil
self.tlsLocalIdentityNetworkFramework = nil
self.tlsLocalIdentityProviderNetworkFramework = nil
#endif
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,27 @@ extension TLSConfiguration {
}
}

extension HTTPClient.Configuration {
/// The client identity to present on a connection opened to `host`:`port`, 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? {
if let provider = self.tlsLocalIdentityProviderNetworkFramework {
guard var host, let port else {
return nil
}
if host.hasPrefix("["), host.hasSuffix("]") {
host = String(host.dropFirst().dropLast())
}
return provider(host, port)
}
return self.tlsLocalIdentityNetworkFramework
}
}

enum NWLocalIdentityError: Error, CustomStringConvertible {
case identityCreationFailed

Expand Down
16 changes: 0 additions & 16 deletions Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,22 +362,6 @@ enum TestTLS {
certificateChain: [.certificate(TestTLS.certificate)],
privateKey: .privateKey(TestTLS.privateKey)
)

/// DER-encoded form of `certificate`, for APIs (like `SecCertificateCreateWithData`) that need
/// raw bytes rather than a parsed `NIOSSLCertificate`.
static let certificateDER: [UInt8] = try! certificate.toDERBytes()

/// `key` (a PKCS#8-wrapped RSA private key, "BEGIN PRIVATE KEY") with its PEM armor stripped
/// down to the raw DER payload — the PKCS#8 envelope itself, not yet unwrapped to bare PKCS#1.
static let privateKeyPKCS8DER: [UInt8] = {
let base64 =
key
.split(separator: "\n")
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.hasPrefix("-----") }
.joined()
return Array(Data(base64Encoded: base64)!)
}()
}

#if compiler(>=6.2)
Expand Down
Loading
Loading