Skip to content

Commit a5ff1eb

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

File tree

3 files changed

+112
-48
lines changed

3 files changed

+112
-48
lines changed

Sources/AsyncHTTPClient/ConnectionPool.swift

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,72 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import enum NIOCore.SocketAddress
16+
1517
enum ConnectionPool {
18+
19+
enum Host: Hashable {
20+
// We keep the IP address serialization precisely as it is in the URL.
21+
// Some platforms have quirks in their implementations of 'ntop', for example
22+
// writing IPv6 addresses as having embedded IPv4 sections (e.g. [::192.168.0.1] vs [::c0a8:1]).
23+
// This serialization includes square brackets, so it is safe to write next to a port number.
24+
// Note: `address` must always have a non-nil port.
25+
case ipAddress(serialization: String, address: SocketAddress)
26+
case domain(name: String, port: Int)
27+
case unixSocket(path: String)
28+
29+
init(remoteHost: String, port: Int) {
30+
if let addr = try? SocketAddress(ipAddress: remoteHost, port: port) {
31+
switch addr {
32+
case .v6:
33+
self = .ipAddress(serialization: "[\(remoteHost)]", address: addr)
34+
case .v4:
35+
self = .ipAddress(serialization: remoteHost, address: addr)
36+
case .unixDomainSocket:
37+
fatalError("Expected a remote host")
38+
}
39+
} else {
40+
precondition(!remoteHost.isEmpty, "HTTPClient.Request should already reject empty remote hostnames")
41+
self = .domain(name: remoteHost, port: port)
42+
}
43+
}
44+
}
45+
1646
/// Used by the `ConnectionPool` to index its `HTTP1ConnectionProvider`s
1747
///
1848
/// A key is initialized from a `URL`, it uses the components to derive a hashed value
1949
/// used by the `providers` dictionary to allow retrieving and creating
2050
/// connection providers associated to a certain request in constant time.
2151
struct Key: Hashable, CustomStringConvertible {
52+
var scheme: Scheme
53+
var host: Host
54+
private var tlsConfiguration: BestEffortHashableTLSConfiguration?
55+
2256
init(_ request: HTTPClient.Request) {
2357
switch request.scheme {
2458
case "http":
2559
self.scheme = .http
60+
self.host = Host(remoteHost: request.host, port: request.port)
2661
case "https":
2762
self.scheme = .https
63+
self.host = Host(remoteHost: request.host, port: request.port)
2864
case "unix":
2965
self.scheme = .unix
66+
self.host = .unixSocket(path: request.socketPath)
3067
case "http+unix":
3168
self.scheme = .http_unix
69+
self.host = .unixSocket(path: request.socketPath)
3270
case "https+unix":
3371
self.scheme = .https_unix
72+
self.host = .unixSocket(path: request.socketPath)
3473
default:
3574
fatalError("HTTPClient.Request scheme should already be a valid one")
3675
}
37-
self.port = request.port
38-
self.host = request.host
39-
self.unixPath = request.socketPath
4076
if let tls = request.tlsConfiguration {
4177
self.tlsConfiguration = BestEffortHashableTLSConfiguration(wrapping: tls)
4278
}
4379
}
4480

45-
var scheme: Scheme
46-
var host: String
47-
var port: Int
48-
var unixPath: String
49-
private var tlsConfiguration: BestEffortHashableTLSConfiguration?
50-
5181
enum Scheme: Hashable {
5282
case http
5383
case https
@@ -78,13 +108,16 @@ enum ConnectionPool {
78108
var hasher = Hasher()
79109
self.tlsConfiguration?.hash(into: &hasher)
80110
let hash = hasher.finalize()
81-
var path = ""
82-
if self.unixPath != "" {
83-
path = self.unixPath
84-
} else {
85-
path = "\(self.host):\(self.port)"
111+
var hostDescription = ""
112+
switch host {
113+
case .ipAddress(let serialization, let addr):
114+
hostDescription = "\(serialization):\(addr.port!)"
115+
case .domain(let domain, port: let port):
116+
hostDescription = "\(domain):\(port)"
117+
case .unixSocket(let socketPath):
118+
hostDescription = socketPath
86119
}
87-
return "\(self.scheme)://\(path) TLS-hash: \(hash)"
120+
return "\(self.scheme)://\(hostDescription) TLS-hash: \(hash)"
88121
}
89122
}
90123
}

Sources/AsyncHTTPClient/ConnectionPool/ChannelHandler/HTTP1ProxyConnectHandler.swift

Lines changed: 24 additions & 0 deletions
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

Lines changed: 40 additions & 33 deletions
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,37 @@ 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+
486+
fileprivate var sslServerHostname: String? {
487+
switch self {
488+
case .domain(let domain, _): return domain
489+
case .ipAddress, .unixSocket: return nil
490+
}
491+
}
492+
}
493+
494+
extension SOCKSAddress {
495+
496+
fileprivate init(_ host: ConnectionPool.Host) {
497+
switch host {
498+
case .ipAddress(_, let address): self = .address(address)
499+
case .domain(let domain, let port): self = .domain(domain, port: port)
500+
case .unixSocket: fatalError("Unix Domain Sockets are not supported by SOCKSAddress")
501+
}
502+
}
503+
}
504+
505+
extension NIOClientTCPBootstrapProtocol {
504506

505-
return self.withCString { ptr in
506-
inet_pton(AF_INET, ptr, &ipv4Addr) == 1 ||
507-
inet_pton(AF_INET6, ptr, &ipv6Addr) == 1
507+
func connect(host: ConnectionPool.Host) -> EventLoopFuture<Channel> {
508+
switch host {
509+
case .ipAddress(_, let socketAddress):
510+
return self.connect(to: socketAddress)
511+
case .domain(let domain, let port):
512+
return self.connect(host: domain, port: port)
513+
case .unixSocket(let path):
514+
return self.connect(unixDomainSocketPath: path)
508515
}
509516
}
510517
}

0 commit comments

Comments
 (0)