Skip to content

Introduce a ConnectionTarget enum #501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 30, 2021
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
31 changes: 15 additions & 16 deletions Sources/AsyncHTTPClient/ConnectionPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@
enum ConnectionPool {
/// Used by the `ConnectionPool` to index its `HTTP1ConnectionProvider`s
///
/// A key is initialized from a `URL`, it uses the components to derive a hashed value
/// A key is initialized from a `Request`, it uses the components to derive a hashed value
/// used by the `providers` dictionary to allow retrieving and creating
/// connection providers associated to a certain request in constant time.
struct Key: Hashable, CustomStringConvertible {
var scheme: Scheme
var connectionTarget: ConnectionTarget
private var tlsConfiguration: BestEffortHashableTLSConfiguration?

init(_ request: HTTPClient.Request) {
self.connectionTarget = request.connectionTarget
switch request.scheme {
case "http":
self.scheme = .http
Expand All @@ -34,20 +39,11 @@ enum ConnectionPool {
default:
fatalError("HTTPClient.Request scheme should already be a valid one")
}
self.port = request.port
self.host = request.host
self.unixPath = request.socketPath
if let tls = request.tlsConfiguration {
self.tlsConfiguration = BestEffortHashableTLSConfiguration(wrapping: tls)
}
}

var scheme: Scheme
var host: String
var port: Int
var unixPath: String
private var tlsConfiguration: BestEffortHashableTLSConfiguration?

enum Scheme: Hashable {
case http
case https
Expand Down Expand Up @@ -78,13 +74,16 @@ enum ConnectionPool {
var hasher = Hasher()
self.tlsConfiguration?.hash(into: &hasher)
let hash = hasher.finalize()
var path = ""
if self.unixPath != "" {
path = self.unixPath
} else {
path = "\(self.host):\(self.port)"
var hostDescription = ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Swift should see that we set hostDescription exactly once on every code path. If we remove the default value "" we should therefore be able to make this a let.

switch self.connectionTarget {
case .ipAddress(let serialization, let addr):
hostDescription = "\(serialization):\(addr.port!)"
case .domain(let domain, port: let port):
hostDescription = "\(domain):\(port)"
case .unixSocket(let socketPath):
hostDescription = socketPath
}
return "\(self.scheme)://\(path) TLS-hash: \(hash)"
return "\(self.scheme)://\(hostDescription) TLS-hash: \(hash)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,30 @@ final class HTTP1ProxyConnectHandler: ChannelDuplexHandler, RemovableChannelHand
return self.proxyEstablishedPromise?.futureResult
}

convenience
init(target: ConnectionTarget,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: I'd like to put convenience init on the same line.

proxyAuthorization: HTTPClient.Authorization?,
deadline: NIODeadline) {
let targetHost: String
let targetPort: Int
switch target {
case .ipAddress(serialization: let serialization, address: let address):
targetHost = serialization
targetPort = address.port!
case .domain(name: let domain, port: let port):
targetHost = domain
targetPort = port
case .unixSocket:
fatalError("Unix Domain Sockets do not support proxies")
}
self.init(
targetHost: targetHost,
targetPort: targetPort,
proxyAuthorization: proxyAuthorization,
deadline: deadline
)
}

init(targetHost: String,
targetPort: Int,
proxyAuthorization: HTTPClient.Authorization?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,16 +197,8 @@ extension HTTPConnectionPool.ConnectionFactory {
}

private func makePlainChannel(deadline: NIODeadline, eventLoop: EventLoop) -> EventLoopFuture<Channel> {
let bootstrap = self.makePlainBootstrap(deadline: deadline, eventLoop: eventLoop)

switch self.key.scheme {
case .http:
return bootstrap.connect(host: self.key.host, port: self.key.port)
case .http_unix, .unix:
return bootstrap.connect(unixDomainSocketPath: self.key.unixPath)
case .https, .https_unix:
preconditionFailure("Unexpected scheme")
}
precondition(!self.key.scheme.requiresTLS, "Unexpected scheme")
return self.makePlainBootstrap(deadline: deadline, eventLoop: eventLoop).connect(target: self.key.connectionTarget)
}

private func makeHTTPProxyChannel(
Expand All @@ -224,8 +216,7 @@ extension HTTPConnectionPool.ConnectionFactory {
let encoder = HTTPRequestEncoder()
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes))
let proxyHandler = HTTP1ProxyConnectHandler(
targetHost: self.key.host,
targetPort: self.key.port,
target: self.key.connectionTarget,
proxyAuthorization: proxy.authorization,
deadline: deadline
)
Expand Down Expand Up @@ -264,7 +255,7 @@ extension HTTPConnectionPool.ConnectionFactory {
// upgraded to TLS before we send our first request.
let bootstrap = self.makePlainBootstrap(deadline: deadline, eventLoop: eventLoop)
return bootstrap.connect(host: proxy.host, port: proxy.port).flatMap { channel in
let socksConnectHandler = SOCKSClientHandler(targetAddress: .domain(self.key.host, port: self.key.port))
let socksConnectHandler = SOCKSClientHandler(targetAddress: SOCKSAddress(self.key.connectionTarget))
let socksEventHandler = SOCKSEventsHandler(deadline: deadline)

do {
Expand Down Expand Up @@ -310,6 +301,7 @@ extension HTTPConnectionPool.ConnectionFactory {
}
let tlsEventHandler = TLSEventsHandler(deadline: deadline)

let sslServerHostname = self.key.connectionTarget.sslServerHostname
let sslContextFuture = self.sslContextCache.sslContext(
tlsConfiguration: tlsConfig,
eventLoop: channel.eventLoop,
Expand All @@ -320,7 +312,7 @@ extension HTTPConnectionPool.ConnectionFactory {
do {
let sslHandler = try NIOSSLClientHandler(
context: sslContext,
serverHostname: self.key.host
serverHostname: sslServerHostname
)
try channel.pipeline.syncOperations.addHandler(sslHandler)
try channel.pipeline.syncOperations.addHandler(tlsEventHandler)
Expand Down Expand Up @@ -364,21 +356,15 @@ extension HTTPConnectionPool.ConnectionFactory {
}

private func makeTLSChannel(deadline: NIODeadline, eventLoop: EventLoop, logger: Logger) -> EventLoopFuture<(Channel, String?)> {
precondition(self.key.scheme.requiresTLS, "Unexpected scheme")
let bootstrapFuture = self.makeTLSBootstrap(
deadline: deadline,
eventLoop: eventLoop,
logger: logger
)

var channelFuture = bootstrapFuture.flatMap { bootstrap -> EventLoopFuture<Channel> in
switch self.key.scheme {
case .https:
return bootstrap.connect(host: self.key.host, port: self.key.port)
case .https_unix:
return bootstrap.connect(unixDomainSocketPath: self.key.unixPath)
case .http, .http_unix, .unix:
preconditionFailure("Unexpected scheme")
}
return bootstrap.connect(target: self.key.connectionTarget)
}.flatMap { channel -> EventLoopFuture<(Channel, String?)> in
// It is save to use `try!` here, since we are sure, that a `TLSEventsHandler` exists
// within the pipeline. It is added in `makeTLSBootstrap`.
Expand Down Expand Up @@ -441,9 +427,7 @@ extension HTTPConnectionPool.ConnectionFactory {
}
#endif

let host = self.key.host
let hostname = (host.isIPAddress || host.isEmpty) ? nil : host

let sslServerHostname = self.key.connectionTarget.sslServerHostname
let sslContextFuture = sslContextCache.sslContext(
tlsConfiguration: tlsConfig,
eventLoop: eventLoop,
Expand All @@ -458,7 +442,7 @@ extension HTTPConnectionPool.ConnectionFactory {
let sync = channel.pipeline.syncOperations
let sslHandler = try NIOSSLClientHandler(
context: sslContext,
serverHostname: hostname
serverHostname: sslServerHostname
)
let tlsEventHandler = TLSEventsHandler(deadline: deadline)

Expand Down Expand Up @@ -497,14 +481,34 @@ extension ConnectionPool.Key.Scheme {
}
}

extension String {
fileprivate var isIPAddress: Bool {
var ipv4Addr = in_addr()
var ipv6Addr = in6_addr()
extension ConnectionTarget {
fileprivate var sslServerHostname: String? {
switch self {
case .domain(let domain, _): return domain
case .ipAddress, .unixSocket: return nil
}
}
}

extension SOCKSAddress {
fileprivate init(_ host: ConnectionTarget) {
switch host {
case .ipAddress(_, let address): self = .address(address)
case .domain(let domain, let port): self = .domain(domain, port: port)
case .unixSocket: fatalError("Unix Domain Sockets are not supported by SOCKSAddress")
}
}
}

return self.withCString { ptr in
inet_pton(AF_INET, ptr, &ipv4Addr) == 1 ||
inet_pton(AF_INET6, ptr, &ipv6Addr) == 1
extension NIOClientTCPBootstrapProtocol {
func connect(target: ConnectionTarget) -> EventLoopFuture<Channel> {
switch target {
case .ipAddress(_, let socketAddress):
return self.connect(to: socketAddress)
case .domain(let domain, let port):
return self.connect(host: domain, port: port)
case .unixSocket(let path):
return self.connect(unixDomainSocketPath: path)
}
}
}
42 changes: 42 additions & 0 deletions Sources/AsyncHTTPClient/ConnectionTarget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2019-2021 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import enum NIOCore.SocketAddress

enum ConnectionTarget: Equatable, Hashable {
// We keep the IP address serialization precisely as it is in the URL.
// Some platforms have quirks in their implementations of 'ntop', for example
// writing IPv6 addresses as having embedded IPv4 sections (e.g. [::192.168.0.1] vs [::c0a8:1]).
// This serialization includes square brackets, so it is safe to write next to a port number.
// Note: `address` must have an explicit port.
case ipAddress(serialization: String, address: SocketAddress)
case domain(name: String, port: Int)
case unixSocket(path: String)

init(remoteHost: String, port: Int) {
if let addr = try? SocketAddress(ipAddress: remoteHost, port: port) {
switch addr {
case .v6:
self = .ipAddress(serialization: "[\(remoteHost)]", address: addr)
case .v4:
self = .ipAddress(serialization: remoteHost, address: addr)
case .unixDomainSocket:
fatalError("Expected a remote host")
}
} else {
precondition(!remoteHost.isEmpty, "HTTPClient.Request should already reject empty remote hostnames")
self = .domain(name: remoteHost, port: port)
}
}
}
43 changes: 29 additions & 14 deletions Sources/AsyncHTTPClient/HTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,7 @@ extension HTTPClient {

static func deconstructURL(
_ url: URL
) throws -> (
kind: Kind, scheme: String, hostname: String, port: Int, socketPath: String, uri: String
) {
) throws -> (kind: Kind, scheme: String, connectionTarget: ConnectionTarget, uri: String) {
guard let scheme = url.scheme?.lowercased() else {
throw HTTPClientError.emptyScheme
}
Expand All @@ -138,20 +136,23 @@ extension HTTPClient {
throw HTTPClientError.emptyHost
}
let defaultPort = self.useTLS(scheme) ? 443 : 80
return (.host, scheme, host, url.port ?? defaultPort, "", url.uri)
let hostTarget = ConnectionTarget(remoteHost: host, port: url.port ?? defaultPort)
return (.host, scheme, hostTarget, url.uri)
case "http+unix", "https+unix":
guard let socketPath = url.host, !socketPath.isEmpty else {
throw HTTPClientError.missingSocketPath
}
let (kind, defaultPort) = self.useTLS(scheme) ? (Kind.UnixScheme.https_unix, 443) : (.http_unix, 80)
return (.unixSocket(kind), scheme, "", url.port ?? defaultPort, socketPath, url.uri)
let socketTarget = ConnectionTarget.unixSocket(path: socketPath)
let kind = self.useTLS(scheme) ? Kind.UnixScheme.https_unix : .http_unix
return (.unixSocket(kind), scheme, socketTarget, url.uri)
case "unix":
let socketPath = url.baseURL?.path ?? url.path
let uri = url.baseURL != nil ? url.uri : "/"
guard !socketPath.isEmpty else {
throw HTTPClientError.missingSocketPath
}
return (.unixSocket(.baseURL), scheme, "", url.port ?? 80, socketPath, uri)
let socketTarget = ConnectionTarget.unixSocket(path: socketPath)
return (.unixSocket(.baseURL), scheme, socketTarget, uri)
default:
throw HTTPClientError.unsupportedScheme(url.scheme!)
}
Expand All @@ -163,12 +164,8 @@ extension HTTPClient {
public let url: URL
/// Remote HTTP scheme, resolved from `URL`.
public let scheme: String
/// Remote host, resolved from `URL`.
public let host: String
/// Resolved port.
public let port: Int
/// Socket path, resolved from `URL`.
let socketPath: String
/// The connection target, resolved from `URL`.
let connectionTarget: ConnectionTarget
/// URI composed of the path and query, resolved from `URL`.
let uri: String
/// Request custom HTTP Headers, defaults to no headers.
Expand Down Expand Up @@ -255,7 +252,7 @@ extension HTTPClient {
/// - `emptyHost` if URL does not contains a host.
/// - `missingSocketPath` if URL does not contains a socketPath as an encoded host.
public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws {
(self.kind, self.scheme, self.host, self.port, self.socketPath, self.uri) = try Request.deconstructURL(url)
(self.kind, self.scheme, self.connectionTarget, self.uri) = try Request.deconstructURL(url)
self.redirectState = nil
self.url = url
self.method = method
Expand All @@ -269,6 +266,24 @@ extension HTTPClient {
return Request.useTLS(self.scheme)
}

/// Remote host, resolved from `URL`.
public var host: String {
switch self.connectionTarget {
case .ipAddress(let serialization, _): return serialization
case .domain(let name, _): return name
case .unixSocket: return ""
}
}

/// Resolved port.
public var port: Int {
switch self.connectionTarget {
case .ipAddress(_, let address): return address.port!
case .domain(_, let port): return port
case .unixSocket: return Request.useTLS(self.scheme) ? 443 : 80
}
}

func createRequestHead() throws -> (HTTPRequestHead, RequestFramingMetadata) {
var head = HTTPRequestHead(
version: .http1_1,
Expand Down
Loading