Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b97d82c

Browse files
committedJun 16, 2021
Channel creation refactored
1 parent 210b54f commit b97d82c

16 files changed

+992
-496
lines changed
 

‎Sources/AsyncHTTPClient/ConnectionPool.swift

+32-6
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ final class ConnectionPool {
8686
let provider = HTTP1ConnectionProvider(key: key,
8787
eventLoop: taskEventLoop,
8888
configuration: key.config(overriding: self.configuration),
89+
tlsConfiguration: request.tlsConfiguration,
8990
pool: self,
91+
sslContextCache: self.sslContextCache,
9092
backgroundActivityLogger: self.backgroundActivityLogger)
9193
let enqueued = provider.enqueue()
9294
assert(enqueued)
@@ -213,6 +215,8 @@ class HTTP1ConnectionProvider {
213215

214216
private let backgroundActivityLogger: Logger
215217

218+
private let factory: HTTPConnectionPool.ConnectionFactory
219+
216220
/// Creates a new `HTTP1ConnectionProvider`
217221
///
218222
/// - parameters:
@@ -225,7 +229,9 @@ class HTTP1ConnectionProvider {
225229
init(key: ConnectionPool.Key,
226230
eventLoop: EventLoop,
227231
configuration: HTTPClient.Configuration,
232+
tlsConfiguration: TLSConfiguration?,
228233
pool: ConnectionPool,
234+
sslContextCache: SSLContextCache,
229235
backgroundActivityLogger: Logger) {
230236
self.eventLoop = eventLoop
231237
self.configuration = configuration
@@ -234,6 +240,13 @@ class HTTP1ConnectionProvider {
234240
self.closePromise = eventLoop.makePromise()
235241
self.state = .init(eventLoop: eventLoop)
236242
self.backgroundActivityLogger = backgroundActivityLogger
243+
244+
self.factory = HTTPConnectionPool.ConnectionFactory(
245+
key: self.key,
246+
tlsConfiguration: tlsConfiguration ?? configuration.tlsConfiguration ?? .forClient(),
247+
clientConfiguration: self.configuration,
248+
sslContextCache: sslContextCache
249+
)
237250
}
238251

239252
deinit {
@@ -440,12 +453,25 @@ class HTTP1ConnectionProvider {
440453

441454
private func makeChannel(preference: HTTPClient.EventLoopPreference,
442455
logger: Logger) -> EventLoopFuture<Channel> {
443-
return NIOClientTCPBootstrap.makeHTTP1Channel(destination: self.key,
444-
eventLoop: self.eventLoop,
445-
configuration: self.configuration,
446-
sslContextCache: self.pool.sslContextCache,
447-
preference: preference,
448-
logger: logger)
456+
let connectionID = HTTPConnectionPool.Connection.ID.globalGenerator.next()
457+
let eventLoop = preference.bestEventLoop ?? self.eventLoop
458+
return self.factory.makeBestChannel(connectionID: connectionID, eventLoop: eventLoop, logger: logger).flatMapThrowing {
459+
(channel, _) -> Channel in
460+
461+
// add the http1.1 channel handlers
462+
let syncOperations = channel.pipeline.syncOperations
463+
try syncOperations.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
464+
465+
switch self.configuration.decompression {
466+
case .disabled:
467+
()
468+
case .enabled(let limit):
469+
let decompressHandler = NIOHTTPResponseDecompressor(limit: limit)
470+
try syncOperations.addHandler(decompressHandler)
471+
}
472+
473+
return channel
474+
}
449475
}
450476

451477
/// A `Waiter` represents a request that waits for a connection when none is
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 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 NIO
16+
import NIOHTTP1
17+
18+
final class HTTP1ProxyConnectHandler: ChannelDuplexHandler, RemovableChannelHandler {
19+
typealias OutboundIn = Never
20+
typealias OutboundOut = HTTPClientRequestPart
21+
typealias InboundIn = HTTPClientResponsePart
22+
23+
enum State {
24+
case initialized(EventLoopPromise<Void>)
25+
case connectSent(EventLoopPromise<Void>)
26+
case headReceived(EventLoopPromise<Void>)
27+
case failed(Error)
28+
case completed
29+
}
30+
31+
private var state: State
32+
33+
let targetHost: String
34+
let targetPort: Int
35+
let proxyAuthorization: HTTPClient.Authorization?
36+
37+
init(targetHost: String,
38+
targetPort: Int,
39+
proxyAuthorization: HTTPClient.Authorization?,
40+
connectPromise: EventLoopPromise<Void>) {
41+
self.targetHost = targetHost
42+
self.targetPort = targetPort
43+
self.proxyAuthorization = proxyAuthorization
44+
45+
self.state = .initialized(connectPromise)
46+
}
47+
48+
func handlerAdded(context: ChannelHandlerContext) {
49+
precondition(context.channel.isActive, "Expected to be added to an active channel")
50+
51+
self.sendConnect(context: context)
52+
}
53+
54+
func handlerRemoved(context: ChannelHandlerContext) {
55+
switch self.state {
56+
case .failed, .completed:
57+
break
58+
case .initialized, .connectSent, .headReceived:
59+
preconditionFailure("Removing the handler, while connecting seems wrong")
60+
}
61+
}
62+
63+
func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
64+
preconditionFailure("We don't support outgoing traffic during HTTP Proxy update.")
65+
}
66+
67+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
68+
switch self.unwrapInboundIn(data) {
69+
case .head(let head):
70+
guard case .connectSent(let promise) = self.state else {
71+
preconditionFailure("HTTPDecoder should throw an error, if we have not send a request")
72+
}
73+
74+
switch head.status.code {
75+
case 200..<300:
76+
// Any 2xx (Successful) response indicates that the sender (and all
77+
// inbound proxies) will switch to tunnel mode immediately after the
78+
// blank line that concludes the successful response's header section
79+
self.state = .headReceived(promise)
80+
case 407:
81+
let error = HTTPClientError.proxyAuthenticationRequired
82+
self.state = .failed(error)
83+
context.close(promise: nil)
84+
promise.fail(error)
85+
default:
86+
// Any response other than a successful response
87+
// indicates that the tunnel has not yet been formed and that the
88+
// connection remains governed by HTTP.
89+
let error = HTTPClientError.invalidProxyResponse
90+
self.state = .failed(error)
91+
context.close(promise: nil)
92+
promise.fail(error)
93+
}
94+
case .body:
95+
switch self.state {
96+
case .headReceived(let promise):
97+
// we don't expect a body
98+
let error = HTTPClientError.invalidProxyResponse
99+
self.state = .failed(error)
100+
context.close(promise: nil)
101+
promise.fail(error)
102+
case .failed:
103+
// ran into an error before... ignore this one
104+
break
105+
case .completed, .connectSent, .initialized:
106+
preconditionFailure("Invalid state")
107+
}
108+
109+
case .end:
110+
switch self.state {
111+
case .headReceived(let promise):
112+
self.state = .completed
113+
promise.succeed(())
114+
case .failed:
115+
// ran into an error before... ignore this one
116+
break
117+
case .initialized, .connectSent, .completed:
118+
preconditionFailure("Invalid state")
119+
}
120+
}
121+
}
122+
123+
func sendConnect(context: ChannelHandlerContext) {
124+
guard case .initialized(let promise) = self.state else {
125+
preconditionFailure("Invalid state")
126+
}
127+
128+
self.state = .connectSent(promise)
129+
130+
var head = HTTPRequestHead(
131+
version: .init(major: 1, minor: 1),
132+
method: .CONNECT,
133+
uri: "\(self.targetHost):\(self.targetPort)"
134+
)
135+
head.headers.add(name: "proxy-connection", value: "keep-alive")
136+
if let authorization = self.proxyAuthorization {
137+
head.headers.add(name: "proxy-authorization", value: authorization.headerValue)
138+
}
139+
context.write(self.wrapOutboundOut(.head(head)), promise: nil)
140+
context.write(self.wrapOutboundOut(.end(nil)), promise: nil)
141+
context.flush()
142+
}
143+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 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 Logging
16+
import NIO
17+
import NIOHTTP1
18+
import NIOSSL
19+
import NIOTLS
20+
#if canImport(Network)
21+
import NIOTransportServices
22+
#endif
23+
24+
extension HTTPConnectionPool {
25+
enum NegotiatedProtocol {
26+
case http1_1(Channel)
27+
case http2_0(Channel)
28+
}
29+
30+
final class ConnectionFactory {
31+
let key: ConnectionPool.Key
32+
let clientConfiguration: HTTPClient.Configuration
33+
let tlsConfiguration: TLSConfiguration
34+
let sslContextCache: SSLContextCache
35+
36+
init(key: ConnectionPool.Key,
37+
tlsConfiguration: TLSConfiguration?,
38+
clientConfiguration: HTTPClient.Configuration,
39+
sslContextCache: SSLContextCache) {
40+
self.key = key
41+
self.clientConfiguration = clientConfiguration
42+
self.sslContextCache = sslContextCache
43+
self.tlsConfiguration = tlsConfiguration ?? clientConfiguration.tlsConfiguration ?? .forClient()
44+
}
45+
}
46+
}
47+
48+
extension HTTPConnectionPool.ConnectionFactory {
49+
func makeBestChannel(connectionID: HTTPConnectionPool.Connection.ID, eventLoop: EventLoop, logger: Logger) -> EventLoopFuture<(Channel, HTTPVersion)> {
50+
if self.key.scheme.isProxyable, let proxy = self.clientConfiguration.proxy {
51+
return self.makeHTTPProxyChannel(proxy, connectionID: connectionID, eventLoop: eventLoop, logger: logger)
52+
} else {
53+
return self.makeChannel(eventLoop: eventLoop, logger: logger)
54+
}
55+
}
56+
57+
private func makeChannel(eventLoop: EventLoop, logger: Logger) -> EventLoopFuture<(Channel, HTTPVersion)> {
58+
switch self.key.scheme {
59+
case .http, .http_unix, .unix:
60+
return self.makePlainChannel(eventLoop: eventLoop).map { ($0, .http1_1) }
61+
case .https, .https_unix:
62+
return self.makeTLSChannel(eventLoop: eventLoop, logger: logger).map {
63+
(channel, negotiated) -> (Channel, HTTPVersion) in
64+
let version = negotiated == "h2" ? HTTPVersion.http2 : HTTPVersion.http1_1
65+
return (channel, version)
66+
}
67+
}
68+
}
69+
70+
private func makePlainChannel(eventLoop: EventLoop) -> EventLoopFuture<Channel> {
71+
let bootstrap = self.makePlainBootstrap(eventLoop: eventLoop)
72+
73+
switch self.key.scheme {
74+
case .http:
75+
return bootstrap.connect(host: self.key.host, port: self.key.port)
76+
case .http_unix, .unix:
77+
return bootstrap.connect(unixDomainSocketPath: self.key.unixPath)
78+
case .https, .https_unix:
79+
preconditionFailure("Unexpected schema")
80+
}
81+
}
82+
83+
private func makeHTTPProxyChannel(
84+
_ proxy: HTTPClient.Configuration.Proxy,
85+
connectionID: HTTPConnectionPool.Connection.ID,
86+
eventLoop: EventLoop,
87+
logger: Logger
88+
) -> EventLoopFuture<(Channel, HTTPVersion)> {
89+
// A proxy connection starts with a plain text connection to the proxy server. After
90+
// the connection has been established with the proxy server, the connection might be
91+
// upgraded to TLS before we send our first request.
92+
let bootstrap = self.makePlainBootstrap(eventLoop: eventLoop)
93+
return bootstrap.connect(host: proxy.host, port: proxy.port).flatMap { channel in
94+
let connectPromise = channel.eventLoop.makePromise(of: Void.self)
95+
96+
let encoder = HTTPRequestEncoder()
97+
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes))
98+
let proxyHandler = HTTP1ProxyConnectHandler(
99+
targetHost: self.key.host,
100+
targetPort: self.key.port,
101+
proxyAuthorization: proxy.authorization,
102+
connectPromise: connectPromise
103+
)
104+
105+
do {
106+
try channel.pipeline.syncOperations.addHandler(encoder)
107+
try channel.pipeline.syncOperations.addHandler(decoder)
108+
try channel.pipeline.syncOperations.addHandler(proxyHandler)
109+
} catch {
110+
return channel.eventLoop.makeFailedFuture(error)
111+
}
112+
113+
return connectPromise.futureResult.flatMap {
114+
channel.pipeline.removeHandler(proxyHandler).flatMap {
115+
channel.pipeline.removeHandler(decoder).flatMap {
116+
channel.pipeline.removeHandler(encoder)
117+
}
118+
}
119+
}.flatMap { () -> EventLoopFuture<(Channel, HTTPVersion)> in
120+
switch self.key.scheme {
121+
case .unix, .http_unix, .https_unix:
122+
preconditionFailure("Unexpected scheme. Not supported for proxy!")
123+
case .http:
124+
return channel.eventLoop.makeSucceededFuture((channel, .http1_1))
125+
case .https:
126+
var tlsConfig = self.tlsConfiguration
127+
// since we can support h2, we need to advertise this in alpn
128+
tlsConfig.applicationProtocols = ["http/1.1" /* , "h2" */ ]
129+
let tlsEventHandler = TLSEventsHandler()
130+
131+
let sslContextFuture = self.sslContextCache.sslContext(
132+
tlsConfiguration: tlsConfig,
133+
eventLoop: channel.eventLoop,
134+
logger: logger
135+
)
136+
137+
return sslContextFuture.flatMap { sslContext -> EventLoopFuture<String?> in
138+
do {
139+
let sslHandler = try NIOSSLClientHandler(
140+
context: sslContext,
141+
serverHostname: self.key.host
142+
)
143+
try channel.pipeline.syncOperations.addHandler(sslHandler)
144+
try channel.pipeline.syncOperations.addHandler(tlsEventHandler)
145+
return tlsEventHandler.tlsEstablishedFuture
146+
} catch {
147+
return channel.eventLoop.makeFailedFuture(error)
148+
}
149+
}.flatMap { negotiated -> EventLoopFuture<(Channel, HTTPVersion)> in
150+
channel.pipeline.removeHandler(tlsEventHandler).map {
151+
switch negotiated {
152+
case "h2":
153+
return (channel, .http2)
154+
default:
155+
return (channel, .http1_1)
156+
}
157+
}
158+
}
159+
}
160+
}
161+
}
162+
}
163+
164+
private func makePlainBootstrap(eventLoop: EventLoop) -> NIOClientTCPBootstrapProtocol {
165+
#if canImport(Network)
166+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) {
167+
return tsBootstrap
168+
.addTimeoutIfNeeded(self.clientConfiguration.timeout)
169+
.channelInitializer { channel in
170+
do {
171+
try channel.pipeline.syncOperations.addHandler(HTTPClient.NWErrorHandler())
172+
return channel.eventLoop.makeSucceededFuture(())
173+
} catch {
174+
return channel.eventLoop.makeFailedFuture(error)
175+
}
176+
}
177+
}
178+
#endif
179+
180+
if let nioBootstrap = ClientBootstrap(validatingGroup: eventLoop) {
181+
return nioBootstrap
182+
.addTimeoutIfNeeded(self.clientConfiguration.timeout)
183+
}
184+
185+
preconditionFailure("No matching bootstrap found")
186+
}
187+
188+
private func makeTLSChannel(eventLoop: EventLoop, logger: Logger) -> EventLoopFuture<(Channel, String?)> {
189+
let bootstrapFuture = self.makeTLSBootstrap(
190+
eventLoop: eventLoop,
191+
logger: logger
192+
)
193+
194+
var channelFuture = bootstrapFuture.flatMap { bootstrap -> EventLoopFuture<Channel> in
195+
switch self.key.scheme {
196+
case .https:
197+
return bootstrap.connect(host: self.key.host, port: self.key.port)
198+
case .https_unix:
199+
return bootstrap.connect(unixDomainSocketPath: self.key.unixPath)
200+
case .http, .http_unix, .unix:
201+
preconditionFailure("Unexpected schema")
202+
}
203+
}.flatMap { channel -> EventLoopFuture<(Channel, String?)> in
204+
let tlsEventHandler = try! channel.pipeline.syncOperations.handler(type: TLSEventsHandler.self)
205+
return tlsEventHandler.tlsEstablishedFuture.flatMap { negotiated in
206+
channel.pipeline.removeHandler(tlsEventHandler).map { (channel, negotiated) }
207+
}
208+
}
209+
210+
#if canImport(Network)
211+
// If NIOTransportSecurity is used, we want to map NWErrors into NWPOsixErrors or NWTLSError.
212+
channelFuture = channelFuture.flatMapErrorThrowing { error in
213+
throw HTTPClient.NWErrorHandler.translateError(error)
214+
}
215+
#endif
216+
217+
return channelFuture
218+
}
219+
220+
private func makeTLSBootstrap(eventLoop: EventLoop, logger: Logger)
221+
-> EventLoopFuture<NIOClientTCPBootstrapProtocol> {
222+
// since we can support h2, we need to advertise this in alpn
223+
var tlsConfig = self.tlsConfiguration
224+
tlsConfig.applicationProtocols = ["http/1.1" /* , "h2" */ ]
225+
226+
#if canImport(Network)
227+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) {
228+
// create NIOClientTCPBootstrap with NIOTS TLS provider
229+
let bootstrapFuture = tlsConfig.getNWProtocolTLSOptions(on: eventLoop).map {
230+
options -> NIOClientTCPBootstrapProtocol in
231+
232+
tsBootstrap
233+
.addTimeoutIfNeeded(self.clientConfiguration.timeout)
234+
.tlsOptions(options)
235+
.channelInitializer { channel in
236+
do {
237+
try channel.pipeline.syncOperations.addHandler(HTTPClient.NWErrorHandler())
238+
try channel.pipeline.syncOperations.addHandler(TLSEventsHandler())
239+
return channel.eventLoop.makeSucceededFuture(())
240+
} catch {
241+
return channel.eventLoop.makeFailedFuture(error)
242+
}
243+
} as NIOClientTCPBootstrapProtocol
244+
}
245+
return bootstrapFuture
246+
}
247+
#endif
248+
249+
let host = self.key.host
250+
let hostname = (host.isIPAddress || host.isEmpty) ? nil : host
251+
252+
let sslContextFuture = sslContextCache.sslContext(
253+
tlsConfiguration: tlsConfig,
254+
eventLoop: eventLoop,
255+
logger: logger
256+
)
257+
258+
let bootstrap = ClientBootstrap(group: eventLoop)
259+
.addTimeoutIfNeeded(self.clientConfiguration.timeout)
260+
.channelInitializer { channel in
261+
sslContextFuture.flatMap { (sslContext) -> EventLoopFuture<Void> in
262+
let sync = channel.pipeline.syncOperations
263+
264+
do {
265+
let sslHandler = try NIOSSLClientHandler(
266+
context: sslContext,
267+
serverHostname: hostname
268+
)
269+
let tlsEventHandler = TLSEventsHandler()
270+
271+
try sync.addHandler(sslHandler)
272+
try sync.addHandler(tlsEventHandler)
273+
return channel.eventLoop.makeSucceededFuture(())
274+
} catch {
275+
return channel.eventLoop.makeFailedFuture(error)
276+
}
277+
}
278+
}
279+
280+
return eventLoop.makeSucceededFuture(bootstrap)
281+
}
282+
}
283+
284+
extension ConnectionPool.Key.Scheme {
285+
var isProxyable: Bool {
286+
switch self {
287+
case .http, .https:
288+
return true
289+
case .unix, .http_unix, .https_unix:
290+
return false
291+
}
292+
}
293+
}
294+
295+
extension NIOClientTCPBootstrapProtocol {
296+
func addTimeoutIfNeeded(_ config: HTTPClient.Configuration.Timeout?) -> Self {
297+
guard let connectTimeamount = config?.connect else {
298+
return self
299+
}
300+
return self.connectTimeout(connectTimeamount)
301+
}
302+
}
303+
304+
private extension String {
305+
var isIPAddress: Bool {
306+
var ipv4Addr = in_addr()
307+
var ipv6Addr = in6_addr()
308+
309+
return self.withCString { ptr in
310+
inet_pton(AF_INET, ptr, &ipv4Addr) == 1 ||
311+
inet_pton(AF_INET6, ptr, &ipv6Addr) == 1
312+
}
313+
}
314+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 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 NIOConcurrencyHelpers
16+
17+
extension HTTPConnectionPool.Connection.ID {
18+
static var globalGenerator = Generator()
19+
20+
struct Generator {
21+
private let atomic: NIOAtomic<Int>
22+
23+
init() {
24+
self.atomic = .makeAtomic(value: 0)
25+
}
26+
27+
func next() -> Int {
28+
return self.atomic.add(1)
29+
}
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 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+
enum HTTPConnectionPool {
16+
struct Connection {
17+
typealias ID = Int
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 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 NIO
16+
import NIOTLS
17+
18+
final class TLSEventsHandler: ChannelInboundHandler, RemovableChannelHandler {
19+
typealias InboundIn = NIOAny
20+
21+
private var tlsEstablishedPromise: EventLoopPromise<String?>?
22+
var tlsEstablishedFuture: EventLoopFuture<String?>! {
23+
return self.tlsEstablishedPromise?.futureResult
24+
}
25+
26+
init() {}
27+
28+
func handlerAdded(context: ChannelHandlerContext) {
29+
self.tlsEstablishedPromise = context.eventLoop.makePromise(of: String?.self)
30+
}
31+
32+
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
33+
if let tlsEvent = event as? TLSUserEvent {
34+
switch tlsEvent {
35+
case .handshakeCompleted(negotiatedProtocol: let negotiated):
36+
self.tlsEstablishedPromise!.succeed(negotiated)
37+
case .shutdownCompleted:
38+
break
39+
}
40+
}
41+
context.fireUserInboundEventTriggered(event)
42+
}
43+
44+
func errorCaught(context: ChannelHandlerContext, error: Error) {
45+
self.tlsEstablishedPromise!.fail(error)
46+
context.fireErrorCaught(error)
47+
}
48+
49+
func handlerRemoved(context: ChannelHandlerContext) {
50+
struct NoResult: Error {}
51+
self.tlsEstablishedPromise!.fail(NoResult())
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2018-2019 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 NIO
16+
import NIOHTTP1
17+
18+
public extension HTTPClient.Configuration {
19+
/// Proxy server configuration
20+
/// Specifies the remote address of an HTTP proxy.
21+
///
22+
/// Adding an `Proxy` to your client's `HTTPClient.Configuration`
23+
/// will cause requests to be passed through the specified proxy using the
24+
/// HTTP `CONNECT` method.
25+
///
26+
/// If a `TLSConfiguration` is used in conjunction with `HTTPClient.Configuration.Proxy`,
27+
/// TLS will be established _after_ successful proxy, between your client
28+
/// and the destination server.
29+
struct Proxy {
30+
/// Specifies Proxy server host.
31+
public var host: String
32+
/// Specifies Proxy server port.
33+
public var port: Int
34+
/// Specifies Proxy server authorization.
35+
public var authorization: HTTPClient.Authorization?
36+
37+
/// Create proxy.
38+
///
39+
/// - parameters:
40+
/// - host: proxy server host.
41+
/// - port: proxy server port.
42+
public static func server(host: String, port: Int) -> Proxy {
43+
return .init(host: host, port: port, authorization: nil)
44+
}
45+
46+
/// Create proxy.
47+
///
48+
/// - parameters:
49+
/// - host: proxy server host.
50+
/// - port: proxy server port.
51+
/// - authorization: proxy server authorization.
52+
public static func server(host: String, port: Int, authorization: HTTPClient.Authorization? = nil) -> Proxy {
53+
return .init(host: host, port: port, authorization: authorization)
54+
}
55+
}
56+
}

‎Sources/AsyncHTTPClient/HTTPClient.swift

-78
Original file line numberDiff line numberDiff line change
@@ -882,84 +882,6 @@ extension HTTPClient.Configuration {
882882
}
883883
}
884884

885-
extension ChannelPipeline {
886-
func syncAddProxyHandler(host: String, port: Int, authorization: HTTPClient.Authorization?) throws {
887-
let encoder = HTTPRequestEncoder()
888-
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .forwardBytes))
889-
let handler = HTTPClientProxyHandler(host: host, port: port, authorization: authorization) { channel in
890-
let encoderRemovePromise = self.eventLoop.next().makePromise(of: Void.self)
891-
channel.pipeline.removeHandler(encoder, promise: encoderRemovePromise)
892-
return encoderRemovePromise.futureResult.flatMap {
893-
channel.pipeline.removeHandler(decoder)
894-
}
895-
}
896-
897-
let sync = self.syncOperations
898-
try sync.addHandler(encoder)
899-
try sync.addHandler(decoder)
900-
try sync.addHandler(handler)
901-
}
902-
903-
func syncAddLateSSLHandlerIfNeeded(for key: ConnectionPool.Key,
904-
sslContext: NIOSSLContext,
905-
handshakePromise: EventLoopPromise<Void>) {
906-
precondition(key.scheme.requiresTLS)
907-
908-
do {
909-
let synchronousPipelineView = self.syncOperations
910-
911-
// We add the TLSEventsHandler first so that it's always in the pipeline before any other TLS handler we add.
912-
// If we're here, we must not have one in the channel already.
913-
assert((try? synchronousPipelineView.context(name: TLSEventsHandler.handlerName)) == nil)
914-
let eventsHandler = TLSEventsHandler(completionPromise: handshakePromise)
915-
try synchronousPipelineView.addHandler(eventsHandler, name: TLSEventsHandler.handlerName)
916-
917-
// Then we add the SSL handler.
918-
try synchronousPipelineView.addHandler(
919-
try NIOSSLClientHandler(context: sslContext,
920-
serverHostname: (key.host.isIPAddress || key.host.isEmpty) ? nil : key.host),
921-
position: .before(eventsHandler)
922-
)
923-
} catch {
924-
handshakePromise.fail(error)
925-
}
926-
}
927-
}
928-
929-
class TLSEventsHandler: ChannelInboundHandler, RemovableChannelHandler {
930-
typealias InboundIn = NIOAny
931-
932-
static let handlerName: String = "AsyncHTTPClient.HTTPClient.TLSEventsHandler"
933-
934-
var completionPromise: EventLoopPromise<Void>
935-
936-
init(completionPromise: EventLoopPromise<Void>) {
937-
self.completionPromise = completionPromise
938-
}
939-
940-
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
941-
if let tlsEvent = event as? TLSUserEvent {
942-
switch tlsEvent {
943-
case .handshakeCompleted:
944-
self.completionPromise.succeed(())
945-
case .shutdownCompleted:
946-
break
947-
}
948-
}
949-
context.fireUserInboundEventTriggered(event)
950-
}
951-
952-
func errorCaught(context: ChannelHandlerContext, error: Error) {
953-
self.completionPromise.fail(error)
954-
context.fireErrorCaught(error)
955-
}
956-
957-
func handlerRemoved(context: ChannelHandlerContext) {
958-
struct NoResult: Error {}
959-
self.completionPromise.fail(NoResult())
960-
}
961-
}
962-
963885
/// Possible client errors.
964886
public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
965887
private enum Code: Equatable {

‎Sources/AsyncHTTPClient/HTTPClientProxyHandler.swift

-180
This file was deleted.

‎Sources/AsyncHTTPClient/Utils.swift

-232
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,6 @@ import NIOHTTPCompression
2323
import NIOSSL
2424
import NIOTransportServices
2525

26-
internal extension String {
27-
var isIPAddress: Bool {
28-
var ipv4Addr = in_addr()
29-
var ipv6Addr = in6_addr()
30-
31-
return self.withCString { ptr in
32-
inet_pton(AF_INET, ptr, &ipv4Addr) == 1 ||
33-
inet_pton(AF_INET6, ptr, &ipv6Addr) == 1
34-
}
35-
}
36-
}
37-
3826
public final class HTTPClientCopyingDelegate: HTTPClientResponseDelegate {
3927
public typealias Response = Void
4028

@@ -53,230 +41,10 @@ public final class HTTPClientCopyingDelegate: HTTPClientResponseDelegate {
5341
}
5442
}
5543

56-
extension NIOClientTCPBootstrap {
57-
static func makeHTTP1Channel(destination: ConnectionPool.Key,
58-
eventLoop: EventLoop,
59-
configuration: HTTPClient.Configuration,
60-
sslContextCache: SSLContextCache,
61-
preference: HTTPClient.EventLoopPreference,
62-
logger: Logger) -> EventLoopFuture<Channel> {
63-
let channelEventLoop = preference.bestEventLoop ?? eventLoop
64-
65-
let key = destination
66-
let requiresTLS = key.scheme.requiresTLS
67-
let sslContext: EventLoopFuture<NIOSSLContext?>
68-
if key.scheme.requiresTLS, configuration.proxy != nil {
69-
// If we use a proxy & also require TLS, then we always use NIOSSL (and not Network.framework TLS because
70-
// it can't be added later) and therefore require a `NIOSSLContext`.
71-
// In this case, `makeAndConfigureBootstrap` will not create another `NIOSSLContext`.
72-
//
73-
// Note that TLS proxies are not supported at the moment. This means that we will always speak
74-
// plaintext to the proxy but we do support sending HTTPS traffic through the proxy.
75-
sslContext = sslContextCache.sslContext(tlsConfiguration: configuration.tlsConfiguration ?? .forClient(),
76-
eventLoop: eventLoop,
77-
logger: logger).map { $0 }
78-
} else {
79-
sslContext = eventLoop.makeSucceededFuture(nil)
80-
}
81-
82-
let bootstrap = NIOClientTCPBootstrap.makeAndConfigureBootstrap(on: channelEventLoop,
83-
host: key.host,
84-
port: key.port,
85-
requiresTLS: requiresTLS,
86-
configuration: configuration,
87-
sslContextCache: sslContextCache,
88-
logger: logger)
89-
return bootstrap.flatMap { bootstrap -> EventLoopFuture<Channel> in
90-
let channel: EventLoopFuture<Channel>
91-
switch key.scheme {
92-
case .http, .https:
93-
let address = HTTPClient.resolveAddress(host: key.host, port: key.port, proxy: configuration.proxy)
94-
channel = bootstrap.connect(host: address.host, port: address.port)
95-
case .unix, .http_unix, .https_unix:
96-
channel = bootstrap.connect(unixDomainSocketPath: key.unixPath)
97-
}
98-
99-
return channel.flatMap { channel -> EventLoopFuture<(Channel, NIOSSLContext?)> in
100-
sslContext.map { sslContext -> (Channel, NIOSSLContext?) in
101-
(channel, sslContext)
102-
}
103-
}.flatMap { channel, sslContext in
104-
configureChannelPipeline(channel,
105-
isNIOTS: bootstrap.isNIOTS,
106-
sslContext: sslContext,
107-
configuration: configuration,
108-
key: key)
109-
}.flatMapErrorThrowing { error in
110-
if bootstrap.isNIOTS {
111-
throw HTTPClient.NWErrorHandler.translateError(error)
112-
} else {
113-
throw error
114-
}
115-
}
116-
}
117-
}
118-
119-
/// Creates and configures a bootstrap given the `eventLoop`, if TLS/a proxy is being used.
120-
private static func makeAndConfigureBootstrap(
121-
on eventLoop: EventLoop,
122-
host: String,
123-
port: Int,
124-
requiresTLS: Bool,
125-
configuration: HTTPClient.Configuration,
126-
sslContextCache: SSLContextCache,
127-
logger: Logger
128-
) -> EventLoopFuture<NIOClientTCPBootstrap> {
129-
return self.makeBestBootstrap(host: host,
130-
eventLoop: eventLoop,
131-
requiresTLS: requiresTLS,
132-
sslContextCache: sslContextCache,
133-
tlsConfiguration: configuration.tlsConfiguration ?? .forClient(),
134-
useProxy: configuration.proxy != nil,
135-
logger: logger)
136-
.map { bootstrap -> NIOClientTCPBootstrap in
137-
var bootstrap = bootstrap
138-
139-
if let timeout = configuration.timeout.connect {
140-
bootstrap = bootstrap.connectTimeout(timeout)
141-
}
142-
143-
// Don't enable TLS if we have a proxy, this will be enabled later on (outside of this method).
144-
if requiresTLS, configuration.proxy == nil {
145-
bootstrap = bootstrap.enableTLS()
146-
}
147-
148-
return bootstrap.channelInitializer { channel in
149-
do {
150-
if let proxy = configuration.proxy {
151-
try channel.pipeline.syncAddProxyHandler(host: host,
152-
port: port,
153-
authorization: proxy.authorization)
154-
} else if requiresTLS {
155-
// We only add the handshake verifier if we need TLS and we're not going through a proxy.
156-
// If we're going through a proxy we add it later (outside of this method).
157-
let completionPromise = channel.eventLoop.makePromise(of: Void.self)
158-
try channel.pipeline.syncOperations.addHandler(TLSEventsHandler(completionPromise: completionPromise),
159-
name: TLSEventsHandler.handlerName)
160-
}
161-
return channel.eventLoop.makeSucceededVoidFuture()
162-
} catch {
163-
return channel.eventLoop.makeFailedFuture(error)
164-
}
165-
}
166-
}
167-
}
168-
169-
/// Creates the best-suited bootstrap given an `EventLoop` and pairs it with an appropriate TLS provider.
170-
private static func makeBestBootstrap(
171-
host: String,
172-
eventLoop: EventLoop,
173-
requiresTLS: Bool,
174-
sslContextCache: SSLContextCache,
175-
tlsConfiguration: TLSConfiguration,
176-
useProxy: Bool,
177-
logger: Logger
178-
) -> EventLoopFuture<NIOClientTCPBootstrap> {
179-
#if canImport(Network)
180-
// if eventLoop is compatible with NIOTransportServices create a NIOTSConnectionBootstrap
181-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) {
182-
// create NIOClientTCPBootstrap with NIOTS TLS provider
183-
return tlsConfiguration.getNWProtocolTLSOptions(on: eventLoop)
184-
.map { parameters in
185-
let tlsProvider = NIOTSClientTLSProvider(tlsOptions: parameters)
186-
return NIOClientTCPBootstrap(tsBootstrap, tls: tlsProvider)
187-
}
188-
}
189-
#endif
190-
191-
if let clientBootstrap = ClientBootstrap(validatingGroup: eventLoop) {
192-
// If there is a proxy don't create TLS provider as it will be added at a later point.
193-
if !requiresTLS || useProxy {
194-
return eventLoop.makeSucceededFuture(NIOClientTCPBootstrap(clientBootstrap,
195-
tls: NIOInsecureNoTLS()))
196-
} else {
197-
return sslContextCache.sslContext(tlsConfiguration: tlsConfiguration,
198-
eventLoop: eventLoop,
199-
logger: logger)
200-
.flatMapThrowing { sslContext in
201-
let hostname = (host.isIPAddress || host.isEmpty) ? nil : host
202-
let tlsProvider = try NIOSSLClientTLSProvider<ClientBootstrap>(context: sslContext, serverHostname: hostname)
203-
return NIOClientTCPBootstrap(clientBootstrap, tls: tlsProvider)
204-
}
205-
}
206-
}
207-
208-
preconditionFailure("Cannot create bootstrap for event loop \(eventLoop)")
209-
}
210-
}
211-
212-
private func configureChannelPipeline(_ channel: Channel,
213-
isNIOTS: Bool,
214-
sslContext: NIOSSLContext?,
215-
configuration: HTTPClient.Configuration,
216-
key: ConnectionPool.Key) -> EventLoopFuture<Channel> {
217-
let requiresTLS = key.scheme.requiresTLS
218-
let handshakeFuture: EventLoopFuture<Void>
219-
220-
if requiresTLS, configuration.proxy != nil {
221-
let handshakePromise = channel.eventLoop.makePromise(of: Void.self)
222-
channel.pipeline.syncAddLateSSLHandlerIfNeeded(for: key,
223-
sslContext: sslContext!,
224-
handshakePromise: handshakePromise)
225-
handshakeFuture = handshakePromise.futureResult
226-
} else if requiresTLS {
227-
do {
228-
handshakeFuture = try channel.pipeline.syncOperations.handler(type: TLSEventsHandler.self).completionPromise.futureResult
229-
} catch {
230-
return channel.eventLoop.makeFailedFuture(error)
231-
}
232-
} else {
233-
handshakeFuture = channel.eventLoop.makeSucceededVoidFuture()
234-
}
235-
236-
return handshakeFuture.flatMapThrowing {
237-
let syncOperations = channel.pipeline.syncOperations
238-
239-
// If we got here and we had a TLSEventsHandler in the pipeline, we can remove it ow.
240-
if requiresTLS {
241-
channel.pipeline.removeHandler(name: TLSEventsHandler.handlerName, promise: nil)
242-
}
243-
244-
try syncOperations.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
245-
246-
if isNIOTS {
247-
try syncOperations.addHandler(HTTPClient.NWErrorHandler(), position: .first)
248-
}
249-
250-
switch configuration.decompression {
251-
case .disabled:
252-
()
253-
case .enabled(let limit):
254-
let decompressHandler = NIOHTTPResponseDecompressor(limit: limit)
255-
try syncOperations.addHandler(decompressHandler)
256-
}
257-
258-
return channel
259-
}
260-
}
261-
26244
extension Connection {
26345
func removeHandler<Handler: RemovableChannelHandler>(_ type: Handler.Type) -> EventLoopFuture<Void> {
26446
return self.channel.pipeline.handler(type: type).flatMap { handler in
26547
self.channel.pipeline.removeHandler(handler)
26648
}.recover { _ in }
26749
}
26850
}
269-
270-
extension NIOClientTCPBootstrap {
271-
var isNIOTS: Bool {
272-
#if canImport(Network)
273-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
274-
return self.underlyingBootstrap is NIOTSConnectionBootstrap
275-
} else {
276-
return false
277-
}
278-
#else
279-
return false
280-
#endif
281-
}
282-
}

‎Tests/AsyncHTTPClientTests/ConnectionTests.swift

+2
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ class ConnectionTests: XCTestCase {
142142
try HTTP1ConnectionProvider(key: .init(.init(url: "http://some.test")),
143143
eventLoop: self.eventLoop,
144144
configuration: .init(),
145+
tlsConfiguration: nil,
145146
pool: self.pool,
147+
sslContextCache: .init(),
146148
backgroundActivityLogger: HTTPClient.loggingDisabled))
147149
}
148150

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2018-2019 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+
// HTTP1ProxyConnectHandlerTests+XCTest.swift
16+
//
17+
import XCTest
18+
19+
///
20+
/// NOTE: This file was generated by generate_linux_tests.rb
21+
///
22+
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
23+
///
24+
25+
extension HTTP1ProxyConnectHandlerTests {
26+
static var allTests: [(String, (HTTP1ProxyConnectHandlerTests) -> () throws -> Void)] {
27+
return [
28+
("testProxyConnectWithoutAuthorizationSuccess", testProxyConnectWithoutAuthorizationSuccess),
29+
("testProxyConnectWithAuthorization", testProxyConnectWithAuthorization),
30+
("testProxyConnectWithoutAuthorizationFailure500", testProxyConnectWithoutAuthorizationFailure500),
31+
("testProxyConnectWithoutAuthorizationButAuthorizationNeeded", testProxyConnectWithoutAuthorizationButAuthorizationNeeded),
32+
("testProxyConnectReceivesBody", testProxyConnectReceivesBody),
33+
]
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 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+
@testable import AsyncHTTPClient
16+
import NIO
17+
import NIOHTTP1
18+
import XCTest
19+
20+
class HTTP1ProxyConnectHandlerTests: XCTestCase {
21+
func testProxyConnectWithoutAuthorizationSuccess() {
22+
let embedded = EmbeddedChannel()
23+
defer { XCTAssertNoThrow(try embedded.finish(acceptAlreadyClosed: false)) }
24+
25+
let socketAddress = try! SocketAddress(ipAddress: "127.0.0.1", port: 3000)
26+
XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait())
27+
28+
let connectPromise = embedded.eventLoop.makePromise(of: Void.self)
29+
let proxyConnectHandler = HTTP1ProxyConnectHandler(
30+
targetHost: "swift.org",
31+
targetPort: 443,
32+
proxyAuthorization: .none,
33+
connectPromise: connectPromise
34+
)
35+
36+
XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler))
37+
38+
var maybeHead: HTTPClientRequestPart?
39+
XCTAssertNoThrow(maybeHead = try embedded.readOutbound(as: HTTPClientRequestPart.self))
40+
guard case .head(let head) = maybeHead else {
41+
return XCTFail("Expected the proxy connect handler to first send a http head part")
42+
}
43+
44+
XCTAssertEqual(head.method, .CONNECT)
45+
XCTAssertEqual(head.uri, "swift.org:443")
46+
XCTAssertEqual(head.headers["proxy-connection"].first, "keep-alive")
47+
XCTAssertNil(head.headers["proxy-authorization"].first)
48+
XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil))
49+
50+
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
51+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead)))
52+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil)))
53+
54+
XCTAssertNoThrow(try connectPromise.futureResult.wait())
55+
}
56+
57+
func testProxyConnectWithAuthorization() {
58+
let embedded = EmbeddedChannel()
59+
60+
let socketAddress = try! SocketAddress(ipAddress: "127.0.0.1", port: 3000)
61+
XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait())
62+
63+
let connectPromise = embedded.eventLoop.makePromise(of: Void.self)
64+
let proxyConnectHandler = HTTP1ProxyConnectHandler(
65+
targetHost: "swift.org",
66+
targetPort: 443,
67+
proxyAuthorization: .basic(credentials: "abc123"),
68+
connectPromise: connectPromise
69+
)
70+
71+
XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler))
72+
73+
var maybeHead: HTTPClientRequestPart?
74+
XCTAssertNoThrow(maybeHead = try embedded.readOutbound(as: HTTPClientRequestPart.self))
75+
guard case .head(let head) = maybeHead else {
76+
return XCTFail("Expected the proxy connect handler to first send a http head part")
77+
}
78+
79+
XCTAssertEqual(head.method, .CONNECT)
80+
XCTAssertEqual(head.uri, "swift.org:443")
81+
XCTAssertEqual(head.headers["proxy-connection"].first, "keep-alive")
82+
XCTAssertEqual(head.headers["proxy-authorization"].first, "Basic abc123")
83+
XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil))
84+
85+
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
86+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead)))
87+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil)))
88+
89+
connectPromise.succeed(())
90+
}
91+
92+
func testProxyConnectWithoutAuthorizationFailure500() {
93+
let embedded = EmbeddedChannel()
94+
95+
let socketAddress = try! SocketAddress(ipAddress: "127.0.0.1", port: 3000)
96+
XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait())
97+
98+
let connectPromise = embedded.eventLoop.makePromise(of: Void.self)
99+
let proxyConnectHandler = HTTP1ProxyConnectHandler(
100+
targetHost: "swift.org",
101+
targetPort: 443,
102+
proxyAuthorization: .none,
103+
connectPromise: connectPromise
104+
)
105+
106+
XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler))
107+
108+
var maybeHead: HTTPClientRequestPart?
109+
XCTAssertNoThrow(maybeHead = try embedded.readOutbound(as: HTTPClientRequestPart.self))
110+
guard case .head(let head) = maybeHead else {
111+
return XCTFail("Expected the proxy connect handler to first send a http head part")
112+
}
113+
114+
XCTAssertEqual(head.method, .CONNECT)
115+
XCTAssertEqual(head.uri, "swift.org:443")
116+
XCTAssertEqual(head.headers["proxy-connection"].first, "keep-alive")
117+
XCTAssertNil(head.headers["proxy-authorization"].first)
118+
XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil))
119+
120+
let responseHead = HTTPResponseHead(version: .http1_1, status: .internalServerError)
121+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead)))
122+
XCTAssertEqual(embedded.isActive, false)
123+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil)))
124+
125+
XCTAssertThrowsError(try connectPromise.futureResult.wait()) { error in
126+
XCTAssertEqual(error as? HTTPClientError, .invalidProxyResponse)
127+
}
128+
}
129+
130+
func testProxyConnectWithoutAuthorizationButAuthorizationNeeded() {
131+
let embedded = EmbeddedChannel()
132+
133+
let socketAddress = try! SocketAddress(ipAddress: "127.0.0.1", port: 3000)
134+
XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait())
135+
136+
let connectPromise = embedded.eventLoop.makePromise(of: Void.self)
137+
let proxyConnectHandler = HTTP1ProxyConnectHandler(
138+
targetHost: "swift.org",
139+
targetPort: 443,
140+
proxyAuthorization: .none,
141+
connectPromise: connectPromise
142+
)
143+
144+
XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler))
145+
146+
var maybeHead: HTTPClientRequestPart?
147+
XCTAssertNoThrow(maybeHead = try embedded.readOutbound(as: HTTPClientRequestPart.self))
148+
guard case .head(let head) = maybeHead else {
149+
return XCTFail("Expected the proxy connect handler to first send a http head part")
150+
}
151+
152+
XCTAssertEqual(head.method, .CONNECT)
153+
XCTAssertEqual(head.uri, "swift.org:443")
154+
XCTAssertEqual(head.headers["proxy-connection"].first, "keep-alive")
155+
XCTAssertNil(head.headers["proxy-authorization"].first)
156+
XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil))
157+
158+
let responseHead = HTTPResponseHead(version: .http1_1, status: .proxyAuthenticationRequired)
159+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead)))
160+
XCTAssertEqual(embedded.isActive, false)
161+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil)))
162+
163+
XCTAssertThrowsError(try connectPromise.futureResult.wait()) { error in
164+
XCTAssertEqual(error as? HTTPClientError, .proxyAuthenticationRequired)
165+
}
166+
}
167+
168+
func testProxyConnectReceivesBody() {
169+
let embedded = EmbeddedChannel()
170+
171+
let socketAddress = try! SocketAddress(ipAddress: "127.0.0.1", port: 3000)
172+
XCTAssertNoThrow(try embedded.connect(to: socketAddress).wait())
173+
174+
let connectPromise = embedded.eventLoop.makePromise(of: Void.self)
175+
let proxyConnectHandler = HTTP1ProxyConnectHandler(
176+
targetHost: "swift.org",
177+
targetPort: 443,
178+
proxyAuthorization: .none,
179+
connectPromise: connectPromise
180+
)
181+
182+
XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(proxyConnectHandler))
183+
184+
var maybeHead: HTTPClientRequestPart?
185+
XCTAssertNoThrow(maybeHead = try embedded.readOutbound(as: HTTPClientRequestPart.self))
186+
guard case .head(let head) = maybeHead else {
187+
return XCTFail("Expected the proxy connect handler to first send a http head part")
188+
}
189+
190+
XCTAssertEqual(head.method, .CONNECT)
191+
XCTAssertEqual(head.uri, "swift.org:443")
192+
XCTAssertEqual(head.headers["proxy-connection"].first, "keep-alive")
193+
XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil))
194+
195+
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
196+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead)))
197+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.body(ByteBuffer(bytes: [0, 1, 2, 3]))))
198+
XCTAssertEqual(embedded.isActive, false)
199+
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.end(nil)))
200+
201+
XCTAssertThrowsError(try connectPromise.futureResult.wait()) { error in
202+
XCTAssertEqual(error as? HTTPClientError, .invalidProxyResponse)
203+
}
204+
}
205+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2018-2019 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+
// TLSEventsHandlerTests+XCTest.swift
16+
//
17+
import XCTest
18+
19+
///
20+
/// NOTE: This file was generated by generate_linux_tests.rb
21+
///
22+
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
23+
///
24+
25+
extension TLSEventsHandlerTests {
26+
static var allTests: [(String, (TLSEventsHandlerTests) -> () throws -> Void)] {
27+
return [
28+
("testHandlerHappyPath", testHandlerHappyPath),
29+
("testHandlerFailsFutureWhenRemovedWithoutEvent", testHandlerFailsFutureWhenRemovedWithoutEvent),
30+
("testHandlerFailsFutureWhenHandshakeFails", testHandlerFailsFutureWhenHandshakeFails),
31+
("testHandlerIgnoresShutdownCompletedEvent", testHandlerIgnoresShutdownCompletedEvent),
32+
]
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 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+
@testable import AsyncHTTPClient
16+
import NIO
17+
import NIOSSL
18+
import NIOTLS
19+
import XCTest
20+
21+
class TLSEventsHandlerTests: XCTestCase {
22+
func testHandlerHappyPath() {
23+
let tlsEventsHandler = TLSEventsHandler()
24+
XCTAssertNil(tlsEventsHandler.tlsEstablishedFuture)
25+
let embedded = EmbeddedChannel(handlers: [tlsEventsHandler])
26+
XCTAssertNotNil(tlsEventsHandler.tlsEstablishedFuture)
27+
28+
embedded.pipeline.fireUserInboundEventTriggered(TLSUserEvent.handshakeCompleted(negotiatedProtocol: "abcd1234"))
29+
XCTAssertEqual(try tlsEventsHandler.tlsEstablishedFuture.wait(), "abcd1234")
30+
}
31+
32+
func testHandlerFailsFutureWhenRemovedWithoutEvent() {
33+
let tlsEventsHandler = TLSEventsHandler()
34+
XCTAssertNil(tlsEventsHandler.tlsEstablishedFuture)
35+
let embedded = EmbeddedChannel(handlers: [tlsEventsHandler])
36+
XCTAssertNotNil(tlsEventsHandler.tlsEstablishedFuture)
37+
38+
XCTAssertNoThrow(try embedded.pipeline.removeHandler(tlsEventsHandler).wait())
39+
XCTAssertThrowsError(try tlsEventsHandler.tlsEstablishedFuture.wait())
40+
}
41+
42+
func testHandlerFailsFutureWhenHandshakeFails() {
43+
let tlsEventsHandler = TLSEventsHandler()
44+
XCTAssertNil(tlsEventsHandler.tlsEstablishedFuture)
45+
let embedded = EmbeddedChannel(handlers: [tlsEventsHandler])
46+
XCTAssertNotNil(tlsEventsHandler.tlsEstablishedFuture)
47+
48+
embedded.pipeline.fireErrorCaught(NIOSSLError.handshakeFailed(BoringSSLError.wantConnect))
49+
XCTAssertThrowsError(try tlsEventsHandler.tlsEstablishedFuture.wait()) {
50+
XCTAssertEqual($0 as? NIOSSLError, .handshakeFailed(BoringSSLError.wantConnect))
51+
}
52+
}
53+
54+
func testHandlerIgnoresShutdownCompletedEvent() {
55+
let tlsEventsHandler = TLSEventsHandler()
56+
XCTAssertNil(tlsEventsHandler.tlsEstablishedFuture)
57+
let embedded = EmbeddedChannel(handlers: [tlsEventsHandler])
58+
XCTAssertNotNil(tlsEventsHandler.tlsEstablishedFuture)
59+
60+
// ignore event
61+
embedded.pipeline.fireUserInboundEventTriggered(TLSUserEvent.shutdownCompleted)
62+
63+
embedded.pipeline.fireUserInboundEventTriggered(TLSUserEvent.handshakeCompleted(negotiatedProtocol: "alpn"))
64+
XCTAssertEqual(try tlsEventsHandler.tlsEstablishedFuture.wait(), "alpn")
65+
}
66+
}

‎Tests/LinuxMain.swift

+2
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ import XCTest
2828
XCTMain([
2929
testCase(ConnectionPoolTests.allTests),
3030
testCase(ConnectionTests.allTests),
31+
testCase(HTTP1ProxyConnectHandlerTests.allTests),
3132
testCase(HTTPClientCookieTests.allTests),
3233
testCase(HTTPClientInternalTests.allTests),
3334
testCase(HTTPClientNIOTSTests.allTests),
3435
testCase(HTTPClientTests.allTests),
3536
testCase(LRUCacheTests.allTests),
3637
testCase(RequestValidationTests.allTests),
3738
testCase(SSLContextCacheTests.allTests),
39+
testCase(TLSEventsHandlerTests.allTests),
3840
])
3941
#endif

0 commit comments

Comments
 (0)
Please sign in to comment.