Skip to content

Commit f1a9187

Browse files
authored
Introduce a ConnectionTarget enum (#501)
* Add a ConnectionPool.Host enum * Move Host out as a top-level ConnectionTarget type, and use it in Request.
1 parent 83c2625 commit f1a9187

File tree

6 files changed

+201
-68
lines changed

6 files changed

+201
-68
lines changed

Sources/AsyncHTTPClient/ConnectionPool.swift

+15-16
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@
1515
enum ConnectionPool {
1616
/// Used by the `ConnectionPool` to index its `HTTP1ConnectionProvider`s
1717
///
18-
/// A key is initialized from a `URL`, it uses the components to derive a hashed value
18+
/// A key is initialized from a `Request`, it uses the components to derive a hashed value
1919
/// used by the `providers` dictionary to allow retrieving and creating
2020
/// connection providers associated to a certain request in constant time.
2121
struct Key: Hashable, CustomStringConvertible {
22+
var scheme: Scheme
23+
var connectionTarget: ConnectionTarget
24+
private var tlsConfiguration: BestEffortHashableTLSConfiguration?
25+
2226
init(_ request: HTTPClient.Request) {
27+
self.connectionTarget = request.connectionTarget
2328
switch request.scheme {
2429
case "http":
2530
self.scheme = .http
@@ -34,20 +39,11 @@ enum ConnectionPool {
3439
default:
3540
fatalError("HTTPClient.Request scheme should already be a valid one")
3641
}
37-
self.port = request.port
38-
self.host = request.host
39-
self.unixPath = request.socketPath
4042
if let tls = request.tlsConfiguration {
4143
self.tlsConfiguration = BestEffortHashableTLSConfiguration(wrapping: tls)
4244
}
4345
}
4446

45-
var scheme: Scheme
46-
var host: String
47-
var port: Int
48-
var unixPath: String
49-
private var tlsConfiguration: BestEffortHashableTLSConfiguration?
50-
5147
enum Scheme: Hashable {
5248
case http
5349
case https
@@ -78,13 +74,16 @@ enum ConnectionPool {
7874
var hasher = Hasher()
7975
self.tlsConfiguration?.hash(into: &hasher)
8076
let hash = hasher.finalize()
81-
var path = ""
82-
if self.unixPath != "" {
83-
path = self.unixPath
84-
} else {
85-
path = "\(self.host):\(self.port)"
77+
var hostDescription = ""
78+
switch self.connectionTarget {
79+
case .ipAddress(let serialization, let addr):
80+
hostDescription = "\(serialization):\(addr.port!)"
81+
case .domain(let domain, port: let port):
82+
hostDescription = "\(domain):\(port)"
83+
case .unixSocket(let socketPath):
84+
hostDescription = socketPath
8685
}
87-
return "\(self.scheme)://\(path) TLS-hash: \(hash)"
86+
return "\(self.scheme)://\(hostDescription) TLS-hash: \(hash)"
8887
}
8988
}
9089
}

Sources/AsyncHTTPClient/ConnectionPool/ChannelHandler/HTTP1ProxyConnectHandler.swift

+24
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,30 @@ final class HTTP1ProxyConnectHandler: ChannelDuplexHandler, RemovableChannelHand
4545
return self.proxyEstablishedPromise?.futureResult
4646
}
4747

48+
convenience
49+
init(target: ConnectionTarget,
50+
proxyAuthorization: HTTPClient.Authorization?,
51+
deadline: NIODeadline) {
52+
let targetHost: String
53+
let targetPort: Int
54+
switch target {
55+
case .ipAddress(serialization: let serialization, address: let address):
56+
targetHost = serialization
57+
targetPort = address.port!
58+
case .domain(name: let domain, port: let port):
59+
targetHost = domain
60+
targetPort = port
61+
case .unixSocket:
62+
fatalError("Unix Domain Sockets do not support proxies")
63+
}
64+
self.init(
65+
targetHost: targetHost,
66+
targetPort: targetPort,
67+
proxyAuthorization: proxyAuthorization,
68+
deadline: deadline
69+
)
70+
}
71+
4872
init(targetHost: String,
4973
targetPort: Int,
5074
proxyAuthorization: HTTPClient.Authorization?,

Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift

+37-33
Original file line numberDiff line numberDiff line change
@@ -197,16 +197,8 @@ extension HTTPConnectionPool.ConnectionFactory {
197197
}
198198

199199
private func makePlainChannel(deadline: NIODeadline, eventLoop: EventLoop) -> EventLoopFuture<Channel> {
200-
let bootstrap = self.makePlainBootstrap(deadline: deadline, eventLoop: eventLoop)
201-
202-
switch self.key.scheme {
203-
case .http:
204-
return bootstrap.connect(host: self.key.host, port: self.key.port)
205-
case .http_unix, .unix:
206-
return bootstrap.connect(unixDomainSocketPath: self.key.unixPath)
207-
case .https, .https_unix:
208-
preconditionFailure("Unexpected scheme")
209-
}
200+
precondition(!self.key.scheme.requiresTLS, "Unexpected scheme")
201+
return self.makePlainBootstrap(deadline: deadline, eventLoop: eventLoop).connect(target: self.key.connectionTarget)
210202
}
211203

212204
private func makeHTTPProxyChannel(
@@ -224,8 +216,7 @@ extension HTTPConnectionPool.ConnectionFactory {
224216
let encoder = HTTPRequestEncoder()
225217
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes))
226218
let proxyHandler = HTTP1ProxyConnectHandler(
227-
targetHost: self.key.host,
228-
targetPort: self.key.port,
219+
target: self.key.connectionTarget,
229220
proxyAuthorization: proxy.authorization,
230221
deadline: deadline
231222
)
@@ -264,7 +255,7 @@ extension HTTPConnectionPool.ConnectionFactory {
264255
// upgraded to TLS before we send our first request.
265256
let bootstrap = self.makePlainBootstrap(deadline: deadline, eventLoop: eventLoop)
266257
return bootstrap.connect(host: proxy.host, port: proxy.port).flatMap { channel in
267-
let socksConnectHandler = SOCKSClientHandler(targetAddress: .domain(self.key.host, port: self.key.port))
258+
let socksConnectHandler = SOCKSClientHandler(targetAddress: SOCKSAddress(self.key.connectionTarget))
268259
let socksEventHandler = SOCKSEventsHandler(deadline: deadline)
269260

270261
do {
@@ -310,6 +301,7 @@ extension HTTPConnectionPool.ConnectionFactory {
310301
}
311302
let tlsEventHandler = TLSEventsHandler(deadline: deadline)
312303

304+
let sslServerHostname = self.key.connectionTarget.sslServerHostname
313305
let sslContextFuture = self.sslContextCache.sslContext(
314306
tlsConfiguration: tlsConfig,
315307
eventLoop: channel.eventLoop,
@@ -320,7 +312,7 @@ extension HTTPConnectionPool.ConnectionFactory {
320312
do {
321313
let sslHandler = try NIOSSLClientHandler(
322314
context: sslContext,
323-
serverHostname: self.key.host
315+
serverHostname: sslServerHostname
324316
)
325317
try channel.pipeline.syncOperations.addHandler(sslHandler)
326318
try channel.pipeline.syncOperations.addHandler(tlsEventHandler)
@@ -364,21 +356,15 @@ extension HTTPConnectionPool.ConnectionFactory {
364356
}
365357

366358
private func makeTLSChannel(deadline: NIODeadline, eventLoop: EventLoop, logger: Logger) -> EventLoopFuture<(Channel, String?)> {
359+
precondition(self.key.scheme.requiresTLS, "Unexpected scheme")
367360
let bootstrapFuture = self.makeTLSBootstrap(
368361
deadline: deadline,
369362
eventLoop: eventLoop,
370363
logger: logger
371364
)
372365

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

444-
let host = self.key.host
445-
let hostname = (host.isIPAddress || host.isEmpty) ? nil : host
446-
430+
let sslServerHostname = self.key.connectionTarget.sslServerHostname
447431
let sslContextFuture = sslContextCache.sslContext(
448432
tlsConfiguration: tlsConfig,
449433
eventLoop: eventLoop,
@@ -458,7 +442,7 @@ extension HTTPConnectionPool.ConnectionFactory {
458442
let sync = channel.pipeline.syncOperations
459443
let sslHandler = try NIOSSLClientHandler(
460444
context: sslContext,
461-
serverHostname: hostname
445+
serverHostname: sslServerHostname
462446
)
463447
let tlsEventHandler = TLSEventsHandler(deadline: deadline)
464448

@@ -497,14 +481,34 @@ extension ConnectionPool.Key.Scheme {
497481
}
498482
}
499483

500-
extension String {
501-
fileprivate var isIPAddress: Bool {
502-
var ipv4Addr = in_addr()
503-
var ipv6Addr = in6_addr()
484+
extension ConnectionTarget {
485+
fileprivate var sslServerHostname: String? {
486+
switch self {
487+
case .domain(let domain, _): return domain
488+
case .ipAddress, .unixSocket: return nil
489+
}
490+
}
491+
}
492+
493+
extension SOCKSAddress {
494+
fileprivate init(_ host: ConnectionTarget) {
495+
switch host {
496+
case .ipAddress(_, let address): self = .address(address)
497+
case .domain(let domain, let port): self = .domain(domain, port: port)
498+
case .unixSocket: fatalError("Unix Domain Sockets are not supported by SOCKSAddress")
499+
}
500+
}
501+
}
504502

505-
return self.withCString { ptr in
506-
inet_pton(AF_INET, ptr, &ipv4Addr) == 1 ||
507-
inet_pton(AF_INET6, ptr, &ipv6Addr) == 1
503+
extension NIOClientTCPBootstrapProtocol {
504+
func connect(target: ConnectionTarget) -> EventLoopFuture<Channel> {
505+
switch target {
506+
case .ipAddress(_, let socketAddress):
507+
return self.connect(to: socketAddress)
508+
case .domain(let domain, let port):
509+
return self.connect(host: domain, port: port)
510+
case .unixSocket(let path):
511+
return self.connect(unixDomainSocketPath: path)
508512
}
509513
}
510514
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2019-2021 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import enum NIOCore.SocketAddress
16+
17+
enum ConnectionTarget: Equatable, Hashable {
18+
// We keep the IP address serialization precisely as it is in the URL.
19+
// Some platforms have quirks in their implementations of 'ntop', for example
20+
// writing IPv6 addresses as having embedded IPv4 sections (e.g. [::192.168.0.1] vs [::c0a8:1]).
21+
// This serialization includes square brackets, so it is safe to write next to a port number.
22+
// Note: `address` must have an explicit port.
23+
case ipAddress(serialization: String, address: SocketAddress)
24+
case domain(name: String, port: Int)
25+
case unixSocket(path: String)
26+
27+
init(remoteHost: String, port: Int) {
28+
if let addr = try? SocketAddress(ipAddress: remoteHost, port: port) {
29+
switch addr {
30+
case .v6:
31+
self = .ipAddress(serialization: "[\(remoteHost)]", address: addr)
32+
case .v4:
33+
self = .ipAddress(serialization: remoteHost, address: addr)
34+
case .unixDomainSocket:
35+
fatalError("Expected a remote host")
36+
}
37+
} else {
38+
precondition(!remoteHost.isEmpty, "HTTPClient.Request should already reject empty remote hostnames")
39+
self = .domain(name: remoteHost, port: port)
40+
}
41+
}
42+
}

Sources/AsyncHTTPClient/HTTPHandler.swift

+29-14
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,7 @@ extension HTTPClient {
126126

127127
static func deconstructURL(
128128
_ url: URL
129-
) throws -> (
130-
kind: Kind, scheme: String, hostname: String, port: Int, socketPath: String, uri: String
131-
) {
129+
) throws -> (kind: Kind, scheme: String, connectionTarget: ConnectionTarget, uri: String) {
132130
guard let scheme = url.scheme?.lowercased() else {
133131
throw HTTPClientError.emptyScheme
134132
}
@@ -138,20 +136,23 @@ extension HTTPClient {
138136
throw HTTPClientError.emptyHost
139137
}
140138
let defaultPort = self.useTLS(scheme) ? 443 : 80
141-
return (.host, scheme, host, url.port ?? defaultPort, "", url.uri)
139+
let hostTarget = ConnectionTarget(remoteHost: host, port: url.port ?? defaultPort)
140+
return (.host, scheme, hostTarget, url.uri)
142141
case "http+unix", "https+unix":
143142
guard let socketPath = url.host, !socketPath.isEmpty else {
144143
throw HTTPClientError.missingSocketPath
145144
}
146-
let (kind, defaultPort) = self.useTLS(scheme) ? (Kind.UnixScheme.https_unix, 443) : (.http_unix, 80)
147-
return (.unixSocket(kind), scheme, "", url.port ?? defaultPort, socketPath, url.uri)
145+
let socketTarget = ConnectionTarget.unixSocket(path: socketPath)
146+
let kind = self.useTLS(scheme) ? Kind.UnixScheme.https_unix : .http_unix
147+
return (.unixSocket(kind), scheme, socketTarget, url.uri)
148148
case "unix":
149149
let socketPath = url.baseURL?.path ?? url.path
150150
let uri = url.baseURL != nil ? url.uri : "/"
151151
guard !socketPath.isEmpty else {
152152
throw HTTPClientError.missingSocketPath
153153
}
154-
return (.unixSocket(.baseURL), scheme, "", url.port ?? 80, socketPath, uri)
154+
let socketTarget = ConnectionTarget.unixSocket(path: socketPath)
155+
return (.unixSocket(.baseURL), scheme, socketTarget, uri)
155156
default:
156157
throw HTTPClientError.unsupportedScheme(url.scheme!)
157158
}
@@ -163,12 +164,8 @@ extension HTTPClient {
163164
public let url: URL
164165
/// Remote HTTP scheme, resolved from `URL`.
165166
public let scheme: String
166-
/// Remote host, resolved from `URL`.
167-
public let host: String
168-
/// Resolved port.
169-
public let port: Int
170-
/// Socket path, resolved from `URL`.
171-
let socketPath: String
167+
/// The connection target, resolved from `URL`.
168+
let connectionTarget: ConnectionTarget
172169
/// URI composed of the path and query, resolved from `URL`.
173170
let uri: String
174171
/// Request custom HTTP Headers, defaults to no headers.
@@ -255,7 +252,7 @@ extension HTTPClient {
255252
/// - `emptyHost` if URL does not contains a host.
256253
/// - `missingSocketPath` if URL does not contains a socketPath as an encoded host.
257254
public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws {
258-
(self.kind, self.scheme, self.host, self.port, self.socketPath, self.uri) = try Request.deconstructURL(url)
255+
(self.kind, self.scheme, self.connectionTarget, self.uri) = try Request.deconstructURL(url)
259256
self.redirectState = nil
260257
self.url = url
261258
self.method = method
@@ -269,6 +266,24 @@ extension HTTPClient {
269266
return Request.useTLS(self.scheme)
270267
}
271268

269+
/// Remote host, resolved from `URL`.
270+
public var host: String {
271+
switch self.connectionTarget {
272+
case .ipAddress(let serialization, _): return serialization
273+
case .domain(let name, _): return name
274+
case .unixSocket: return ""
275+
}
276+
}
277+
278+
/// Resolved port.
279+
public var port: Int {
280+
switch self.connectionTarget {
281+
case .ipAddress(_, let address): return address.port!
282+
case .domain(_, let port): return port
283+
case .unixSocket: return Request.useTLS(self.scheme) ? 443 : 80
284+
}
285+
}
286+
272287
func createRequestHead() throws -> (HTTPRequestHead, RequestFramingMetadata) {
273288
var head = HTTPRequestHead(
274289
version: .http1_1,

0 commit comments

Comments
 (0)