Skip to content

Commit 7e0f136

Browse files
committed
Add a ConnectionPool.Host enum
1 parent 83c2625 commit 7e0f136

File tree

3 files changed

+108
-48
lines changed

3 files changed

+108
-48
lines changed

Sources/AsyncHTTPClient/ConnectionPool.swift

+47-15
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,71 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import enum NIOCore.SocketAddress
16+
1517
enum ConnectionPool {
18+
enum Host: Hashable {
19+
// We keep the IP address serialization precisely as it is in the URL.
20+
// Some platforms have quirks in their implementations of 'ntop', for example
21+
// writing IPv6 addresses as having embedded IPv4 sections (e.g. [::192.168.0.1] vs [::c0a8:1]).
22+
// This serialization includes square brackets, so it is safe to write next to a port number.
23+
// Note: `address` must always have a non-nil port.
24+
case ipAddress(serialization: String, address: SocketAddress)
25+
case domain(name: String, port: Int)
26+
case unixSocket(path: String)
27+
28+
init(remoteHost: String, port: Int) {
29+
if let addr = try? SocketAddress(ipAddress: remoteHost, port: port) {
30+
switch addr {
31+
case .v6:
32+
self = .ipAddress(serialization: "[\(remoteHost)]", address: addr)
33+
case .v4:
34+
self = .ipAddress(serialization: remoteHost, address: addr)
35+
case .unixDomainSocket:
36+
fatalError("Expected a remote host")
37+
}
38+
} else {
39+
precondition(!remoteHost.isEmpty, "HTTPClient.Request should already reject empty remote hostnames")
40+
self = .domain(name: remoteHost, port: port)
41+
}
42+
}
43+
}
44+
1645
/// Used by the `ConnectionPool` to index its `HTTP1ConnectionProvider`s
1746
///
1847
/// A key is initialized from a `URL`, it uses the components to derive a hashed value
1948
/// used by the `providers` dictionary to allow retrieving and creating
2049
/// connection providers associated to a certain request in constant time.
2150
struct Key: Hashable, CustomStringConvertible {
51+
var scheme: Scheme
52+
var host: Host
53+
private var tlsConfiguration: BestEffortHashableTLSConfiguration?
54+
2255
init(_ request: HTTPClient.Request) {
2356
switch request.scheme {
2457
case "http":
2558
self.scheme = .http
59+
self.host = Host(remoteHost: request.host, port: request.port)
2660
case "https":
2761
self.scheme = .https
62+
self.host = Host(remoteHost: request.host, port: request.port)
2863
case "unix":
2964
self.scheme = .unix
65+
self.host = .unixSocket(path: request.socketPath)
3066
case "http+unix":
3167
self.scheme = .http_unix
68+
self.host = .unixSocket(path: request.socketPath)
3269
case "https+unix":
3370
self.scheme = .https_unix
71+
self.host = .unixSocket(path: request.socketPath)
3472
default:
3573
fatalError("HTTPClient.Request scheme should already be a valid one")
3674
}
37-
self.port = request.port
38-
self.host = request.host
39-
self.unixPath = request.socketPath
4075
if let tls = request.tlsConfiguration {
4176
self.tlsConfiguration = BestEffortHashableTLSConfiguration(wrapping: tls)
4277
}
4378
}
4479

45-
var scheme: Scheme
46-
var host: String
47-
var port: Int
48-
var unixPath: String
49-
private var tlsConfiguration: BestEffortHashableTLSConfiguration?
50-
5180
enum Scheme: Hashable {
5281
case http
5382
case https
@@ -78,13 +107,16 @@ enum ConnectionPool {
78107
var hasher = Hasher()
79108
self.tlsConfiguration?.hash(into: &hasher)
80109
let hash = hasher.finalize()
81-
var path = ""
82-
if self.unixPath != "" {
83-
path = self.unixPath
84-
} else {
85-
path = "\(self.host):\(self.port)"
110+
var hostDescription = ""
111+
switch host {
112+
case .ipAddress(let serialization, let addr):
113+
hostDescription = "\(serialization):\(addr.port!)"
114+
case .domain(let domain, port: let port):
115+
hostDescription = "\(domain):\(port)"
116+
case .unixSocket(let socketPath):
117+
hostDescription = socketPath
86118
}
87-
return "\(self.scheme)://\(path) TLS-hash: \(hash)"
119+
return "\(self.scheme)://\(hostDescription) TLS-hash: \(hash)"
88120
}
89121
}
90122
}

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: ConnectionPool.Host,
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(host: self.key.host)
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.host,
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.host))
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.host.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(host: self.key.host)
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.host.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 ConnectionPool.Host {
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: ConnectionPool.Host) {
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(host: ConnectionPool.Host) -> EventLoopFuture<Channel> {
505+
switch host {
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
}

0 commit comments

Comments
 (0)