Skip to content

Commit 0812429

Browse files
committed
Move Host out as a top-level ConnectionTarget type, and use it in Request.
1 parent 7e0f136 commit 0812429

File tree

6 files changed

+140
-67
lines changed

6 files changed

+140
-67
lines changed

Sources/AsyncHTTPClient/ConnectionPool.swift

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,63 +12,30 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import enum NIOCore.SocketAddress
16-
1715
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-
4516
/// Used by the `ConnectionPool` to index its `HTTP1ConnectionProvider`s
4617
///
47-
/// 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
4819
/// used by the `providers` dictionary to allow retrieving and creating
4920
/// connection providers associated to a certain request in constant time.
5021
struct Key: Hashable, CustomStringConvertible {
5122
var scheme: Scheme
52-
var host: Host
23+
var connectionTarget: ConnectionTarget
5324
private var tlsConfiguration: BestEffortHashableTLSConfiguration?
5425

5526
init(_ request: HTTPClient.Request) {
27+
self.connectionTarget = request.connectionTarget
5628
switch request.scheme {
5729
case "http":
5830
self.scheme = .http
59-
self.host = Host(remoteHost: request.host, port: request.port)
6031
case "https":
6132
self.scheme = .https
62-
self.host = Host(remoteHost: request.host, port: request.port)
6333
case "unix":
6434
self.scheme = .unix
65-
self.host = .unixSocket(path: request.socketPath)
6635
case "http+unix":
6736
self.scheme = .http_unix
68-
self.host = .unixSocket(path: request.socketPath)
6937
case "https+unix":
7038
self.scheme = .https_unix
71-
self.host = .unixSocket(path: request.socketPath)
7239
default:
7340
fatalError("HTTPClient.Request scheme should already be a valid one")
7441
}
@@ -108,7 +75,7 @@ enum ConnectionPool {
10875
self.tlsConfiguration?.hash(into: &hasher)
10976
let hash = hasher.finalize()
11077
var hostDescription = ""
111-
switch host {
78+
switch self.connectionTarget {
11279
case .ipAddress(let serialization, let addr):
11380
hostDescription = "\(serialization):\(addr.port!)"
11481
case .domain(let domain, port: let port):

Sources/AsyncHTTPClient/ConnectionPool/ChannelHandler/HTTP1ProxyConnectHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ final class HTTP1ProxyConnectHandler: ChannelDuplexHandler, RemovableChannelHand
4646
}
4747

4848
convenience
49-
init(target: ConnectionPool.Host,
49+
init(target: ConnectionTarget,
5050
proxyAuthorization: HTTPClient.Authorization?,
5151
deadline: NIODeadline) {
5252
let targetHost: String

Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ extension HTTPConnectionPool.ConnectionFactory {
198198

199199
private func makePlainChannel(deadline: NIODeadline, eventLoop: EventLoop) -> EventLoopFuture<Channel> {
200200
precondition(!self.key.scheme.requiresTLS, "Unexpected scheme")
201-
return self.makePlainBootstrap(deadline: deadline, eventLoop: eventLoop).connect(host: self.key.host)
201+
return self.makePlainBootstrap(deadline: deadline, eventLoop: eventLoop).connect(target: self.key.connectionTarget)
202202
}
203203

204204
private func makeHTTPProxyChannel(
@@ -216,7 +216,7 @@ extension HTTPConnectionPool.ConnectionFactory {
216216
let encoder = HTTPRequestEncoder()
217217
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes))
218218
let proxyHandler = HTTP1ProxyConnectHandler(
219-
target: self.key.host,
219+
target: self.key.connectionTarget,
220220
proxyAuthorization: proxy.authorization,
221221
deadline: deadline
222222
)
@@ -255,7 +255,7 @@ extension HTTPConnectionPool.ConnectionFactory {
255255
// upgraded to TLS before we send our first request.
256256
let bootstrap = self.makePlainBootstrap(deadline: deadline, eventLoop: eventLoop)
257257
return bootstrap.connect(host: proxy.host, port: proxy.port).flatMap { channel in
258-
let socksConnectHandler = SOCKSClientHandler(targetAddress: SOCKSAddress(self.key.host))
258+
let socksConnectHandler = SOCKSClientHandler(targetAddress: SOCKSAddress(self.key.connectionTarget))
259259
let socksEventHandler = SOCKSEventsHandler(deadline: deadline)
260260

261261
do {
@@ -301,7 +301,7 @@ extension HTTPConnectionPool.ConnectionFactory {
301301
}
302302
let tlsEventHandler = TLSEventsHandler(deadline: deadline)
303303

304-
let sslServerHostname = self.key.host.sslServerHostname
304+
let sslServerHostname = self.key.connectionTarget.sslServerHostname
305305
let sslContextFuture = self.sslContextCache.sslContext(
306306
tlsConfiguration: tlsConfig,
307307
eventLoop: channel.eventLoop,
@@ -364,7 +364,7 @@ extension HTTPConnectionPool.ConnectionFactory {
364364
)
365365

366366
var channelFuture = bootstrapFuture.flatMap { bootstrap -> EventLoopFuture<Channel> in
367-
return bootstrap.connect(host: self.key.host)
367+
return bootstrap.connect(target: self.key.connectionTarget)
368368
}.flatMap { channel -> EventLoopFuture<(Channel, String?)> in
369369
// It is save to use `try!` here, since we are sure, that a `TLSEventsHandler` exists
370370
// within the pipeline. It is added in `makeTLSBootstrap`.
@@ -427,7 +427,7 @@ extension HTTPConnectionPool.ConnectionFactory {
427427
}
428428
#endif
429429

430-
let sslServerHostname = self.key.host.sslServerHostname
430+
let sslServerHostname = self.key.connectionTarget.sslServerHostname
431431
let sslContextFuture = sslContextCache.sslContext(
432432
tlsConfiguration: tlsConfig,
433433
eventLoop: eventLoop,
@@ -481,7 +481,7 @@ extension ConnectionPool.Key.Scheme {
481481
}
482482
}
483483

484-
extension ConnectionPool.Host {
484+
extension ConnectionTarget {
485485
fileprivate var sslServerHostname: String? {
486486
switch self {
487487
case .domain(let domain, _): return domain
@@ -491,7 +491,7 @@ extension ConnectionPool.Host {
491491
}
492492

493493
extension SOCKSAddress {
494-
fileprivate init(_ host: ConnectionPool.Host) {
494+
fileprivate init(_ host: ConnectionTarget) {
495495
switch host {
496496
case .ipAddress(_, let address): self = .address(address)
497497
case .domain(let domain, let port): self = .domain(domain, port: port)
@@ -501,8 +501,8 @@ extension SOCKSAddress {
501501
}
502502

503503
extension NIOClientTCPBootstrapProtocol {
504-
func connect(host: ConnectionPool.Host) -> EventLoopFuture<Channel> {
505-
switch host {
504+
func connect(target: ConnectionTarget) -> EventLoopFuture<Channel> {
505+
switch target {
506506
case .ipAddress(_, let socketAddress):
507507
return self.connect(to: socketAddress)
508508
case .domain(let domain, let port):
Lines changed: 42 additions & 0 deletions
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

Lines changed: 29 additions & 14 deletions
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,

Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,27 +447,76 @@ class HTTPClientInternalTests: XCTestCase {
447447
func testInternalRequestURI() throws {
448448
let request1 = try Request(url: "https://someserver.com:8888/some/path?foo=bar")
449449
XCTAssertEqual(request1.kind, .host)
450-
XCTAssertEqual(request1.socketPath, "")
450+
XCTAssertEqual(request1.connectionTarget, .domain(name: "someserver.com", port: 8888))
451451
XCTAssertEqual(request1.uri, "/some/path?foo=bar")
452452

453453
let request2 = try Request(url: "https://someserver.com")
454454
XCTAssertEqual(request2.kind, .host)
455-
XCTAssertEqual(request2.socketPath, "")
455+
XCTAssertEqual(request2.connectionTarget, .domain(name: "someserver.com", port: 443))
456456
XCTAssertEqual(request2.uri, "/")
457457

458458
let request3 = try Request(url: "unix:///tmp/file")
459459
XCTAssertEqual(request3.kind, .unixSocket(.baseURL))
460-
XCTAssertEqual(request3.socketPath, "/tmp/file")
460+
XCTAssertEqual(request3.connectionTarget, .unixSocket(path: "/tmp/file"))
461461
XCTAssertEqual(request3.uri, "/")
462462

463463
let request4 = try Request(url: "http+unix://%2Ftmp%2Ffile/file/path")
464464
XCTAssertEqual(request4.kind, .unixSocket(.http_unix))
465-
XCTAssertEqual(request4.socketPath, "/tmp/file")
465+
XCTAssertEqual(request4.connectionTarget, .unixSocket(path: "/tmp/file"))
466466
XCTAssertEqual(request4.uri, "/file/path")
467467

468468
let request5 = try Request(url: "https+unix://%2Ftmp%2Ffile/file/path")
469469
XCTAssertEqual(request5.kind, .unixSocket(.https_unix))
470-
XCTAssertEqual(request5.socketPath, "/tmp/file")
470+
XCTAssertEqual(request5.connectionTarget, .unixSocket(path: "/tmp/file"))
471471
XCTAssertEqual(request5.uri, "/file/path")
472+
473+
let request6 = try Request(url: "https://127.0.0.1")
474+
XCTAssertEqual(request6.kind, .host)
475+
XCTAssertEqual(request6.connectionTarget, .ipAddress(
476+
serialization: "127.0.0.1",
477+
address: try! SocketAddress(ipAddress: "127.0.0.1", port: 443)
478+
))
479+
XCTAssertEqual(request6.uri, "/")
480+
481+
let request7 = try Request(url: "https://0x7F.1:9999")
482+
XCTAssertEqual(request7.kind, .host)
483+
XCTAssertEqual(request7.connectionTarget, .domain(name: "0x7F.1", port: 9999))
484+
XCTAssertEqual(request7.uri, "/")
485+
486+
let request8 = try Request(url: "http://[::1]")
487+
XCTAssertEqual(request8.kind, .host)
488+
XCTAssertEqual(request8.connectionTarget, .ipAddress(
489+
serialization: "[::1]",
490+
address: try! SocketAddress(ipAddress: "::1", port: 80)
491+
))
492+
XCTAssertEqual(request8.uri, "/")
493+
494+
let request9 = try Request(url: "http://[763e:61d9::6ACA:3100:6274]:4242/foo/bar?baz")
495+
XCTAssertEqual(request9.kind, .host)
496+
XCTAssertEqual(request9.connectionTarget, .ipAddress(
497+
serialization: "[763e:61d9::6ACA:3100:6274]",
498+
address: try! SocketAddress(ipAddress: "763e:61d9::6aca:3100:6274", port: 4242)
499+
))
500+
XCTAssertEqual(request9.uri, "/foo/bar?baz")
501+
502+
// Some systems have quirks in their implementations of 'ntop' which cause them to write
503+
// certain IPv6 addresses with embedded IPv4 parts (e.g. "::192.168.0.1" vs "::c0a8:1").
504+
// We want to make sure that our request formatting doesn't depend on the platform's quirks,
505+
// so the serialization must be kept verbatim as it was given in the request.
506+
let request10 = try Request(url: "http://[::c0a8:1]:4242/foo/bar?baz")
507+
XCTAssertEqual(request10.kind, .host)
508+
XCTAssertEqual(request10.connectionTarget, .ipAddress(
509+
serialization: "[::c0a8:1]",
510+
address: try! SocketAddress(ipAddress: "::c0a8:1", port: 4242)
511+
))
512+
XCTAssertEqual(request9.uri, "/foo/bar?baz")
513+
514+
let request11 = try Request(url: "http://[::192.168.0.1]:4242/foo/bar?baz")
515+
XCTAssertEqual(request11.kind, .host)
516+
XCTAssertEqual(request11.connectionTarget, .ipAddress(
517+
serialization: "[::192.168.0.1]",
518+
address: try! SocketAddress(ipAddress: "::192.168.0.1", port: 4242)
519+
))
520+
XCTAssertEqual(request11.uri, "/foo/bar?baz")
472521
}
473522
}

0 commit comments

Comments
 (0)