Skip to content

Commit b075d19

Browse files
authored
Unconditionally insert TLSEventsHandler (#349)
Motivation: AsyncHTTPClient attempts to avoid the problem of Happy Eyeballs making it hard to know which Channel will be returned by only inserting the TLSEventsHandler upon completion of the connect promise. Unfortunately, as this may involve event loop hops, there are some awkward timing windows in play where the connect may complete before this handler gets added. We should remove that timing window by ensuring that all channels always have this handler in place, and instead of trying to wait until we know which Channel will win, we can find the TLSEventsHandler that belongs to the winning channel after the fact. Modifications: - TLSEventsHandler no longer removes itself from the pipeline or throws away its promise. - makeHTTP1Channel now searches for the TLSEventsHandler from the pipeline that was created and is also responsible for removing it. - Better sanity checking that the proxy TLS case does not overlap with the connection-level TLS case. Results: Further shrinking windows for pipeline management issues.
1 parent ae5f185 commit b075d19

File tree

4 files changed

+71
-28
lines changed

4 files changed

+71
-28
lines changed

Sources/AsyncHTTPClient/HTTPClient.swift

+19-23
Original file line numberDiff line numberDiff line change
@@ -900,27 +900,25 @@ extension ChannelPipeline {
900900
try sync.addHandler(handler)
901901
}
902902

903-
func syncAddSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, addSSLClient: Bool, handshakePromise: EventLoopPromise<Void>) {
904-
guard key.scheme.requiresTLS else {
905-
handshakePromise.succeed(())
906-
return
907-
}
903+
func syncAddLateSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, handshakePromise: EventLoopPromise<Void>) {
904+
precondition(key.scheme.requiresTLS)
908905

909906
do {
910907
let synchronousPipelineView = self.syncOperations
911908

912909
// We add the TLSEventsHandler first so that it's always in the pipeline before any other TLS handler we add.
910+
// If we're here, we must not have one in the channel already.
911+
assert((try? synchronousPipelineView.context(name: TLSEventsHandler.handlerName)) == nil)
913912
let eventsHandler = TLSEventsHandler(completionPromise: handshakePromise)
914-
try synchronousPipelineView.addHandler(eventsHandler)
915-
916-
if addSSLClient {
917-
let tlsConfiguration = tlsConfiguration ?? TLSConfiguration.forClient()
918-
let context = try NIOSSLContext(configuration: tlsConfiguration)
919-
try synchronousPipelineView.addHandler(
920-
try NIOSSLClientHandler(context: context, serverHostname: (key.host.isIPAddress || key.host.isEmpty) ? nil : key.host),
921-
position: .before(eventsHandler)
922-
)
923-
}
913+
try synchronousPipelineView.addHandler(eventsHandler, name: TLSEventsHandler.handlerName)
914+
915+
// Then we add the SSL handler.
916+
let tlsConfiguration = tlsConfiguration ?? TLSConfiguration.forClient()
917+
let context = try NIOSSLContext(configuration: tlsConfiguration)
918+
try synchronousPipelineView.addHandler(
919+
try NIOSSLClientHandler(context: context, serverHostname: (key.host.isIPAddress || key.host.isEmpty) ? nil : key.host),
920+
position: .before(eventsHandler)
921+
)
924922
} catch {
925923
handshakePromise.fail(error)
926924
}
@@ -930,7 +928,9 @@ extension ChannelPipeline {
930928
class TLSEventsHandler: ChannelInboundHandler, RemovableChannelHandler {
931929
typealias InboundIn = NIOAny
932930

933-
var completionPromise: EventLoopPromise<Void>?
931+
static let handlerName: String = "AsyncHTTPClient.HTTPClient.TLSEventsHandler"
932+
933+
var completionPromise: EventLoopPromise<Void>
934934

935935
init(completionPromise: EventLoopPromise<Void>) {
936936
self.completionPromise = completionPromise
@@ -940,9 +940,7 @@ class TLSEventsHandler: ChannelInboundHandler, RemovableChannelHandler {
940940
if let tlsEvent = event as? TLSUserEvent {
941941
switch tlsEvent {
942942
case .handshakeCompleted:
943-
self.completionPromise?.succeed(())
944-
self.completionPromise = nil
945-
context.pipeline.removeHandler(self, promise: nil)
943+
self.completionPromise.succeed(())
946944
case .shutdownCompleted:
947945
break
948946
}
@@ -951,15 +949,13 @@ class TLSEventsHandler: ChannelInboundHandler, RemovableChannelHandler {
951949
}
952950

953951
func errorCaught(context: ChannelHandlerContext, error: Error) {
954-
self.completionPromise?.fail(error)
955-
self.completionPromise = nil
956-
context.pipeline.removeHandler(self, promise: nil)
952+
self.completionPromise.fail(error)
957953
context.fireErrorCaught(error)
958954
}
959955

960956
func handlerRemoved(context: ChannelHandlerContext) {
961957
struct NoResult: Error {}
962-
self.completionPromise?.fail(NoResult())
958+
self.completionPromise.fail(NoResult())
963959
}
964960
}
965961

Sources/AsyncHTTPClient/Utils.swift

+28-5
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ extension NIOClientTCPBootstrap {
131131
do {
132132
if let proxy = configuration.proxy {
133133
try channel.pipeline.syncAddProxyHandler(host: host, port: port, authorization: proxy.authorization)
134+
} else if requiresTLS {
135+
// We only add the handshake verifier if we need TLS and we're not going through a proxy. If we're going
136+
// through a proxy we add it later.
137+
let completionPromise = channel.eventLoop.makePromise(of: Void.self)
138+
try channel.pipeline.syncOperations.addHandler(TLSEventsHandler(completionPromise: completionPromise), name: TLSEventsHandler.handlerName)
134139
}
135140
return channel.eventLoop.makeSucceededVoidFuture()
136141
} catch {
@@ -162,14 +167,32 @@ extension NIOClientTCPBootstrap {
162167
}
163168

164169
return channel.flatMap { channel in
165-
let requiresSSLHandler = configuration.proxy != nil && key.scheme.requiresTLS
166-
let handshakePromise = channel.eventLoop.makePromise(of: Void.self)
167-
168-
channel.pipeline.syncAddSSLHandlerIfNeeded(for: key, tlsConfiguration: configuration.tlsConfiguration, addSSLClient: requiresSSLHandler, handshakePromise: handshakePromise)
170+
let requiresTLS = key.scheme.requiresTLS
171+
let requiresLateSSLHandler = configuration.proxy != nil && requiresTLS
172+
let handshakeFuture: EventLoopFuture<Void>
173+
174+
if requiresLateSSLHandler {
175+
let handshakePromise = channel.eventLoop.makePromise(of: Void.self)
176+
channel.pipeline.syncAddLateSSLHandlerIfNeeded(for: key, tlsConfiguration: configuration.tlsConfiguration, handshakePromise: handshakePromise)
177+
handshakeFuture = handshakePromise.futureResult
178+
} else if requiresTLS {
179+
do {
180+
handshakeFuture = try channel.pipeline.syncOperations.handler(type: TLSEventsHandler.self).completionPromise.futureResult
181+
} catch {
182+
return channel.eventLoop.makeFailedFuture(error)
183+
}
184+
} else {
185+
handshakeFuture = channel.eventLoop.makeSucceededVoidFuture()
186+
}
169187

170-
return handshakePromise.futureResult.flatMapThrowing {
188+
return handshakeFuture.flatMapThrowing {
171189
let syncOperations = channel.pipeline.syncOperations
172190

191+
// If we got here and we had a TLSEventsHandler in the pipeline, we can remove it ow.
192+
if requiresTLS {
193+
channel.pipeline.removeHandler(name: TLSEventsHandler.handlerName, promise: nil)
194+
}
195+
173196
try syncOperations.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
174197

175198
#if canImport(Network)

Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ extension HTTPClientTests {
129129
("testSSLHandshakeErrorPropagationDelayedClose", testSSLHandshakeErrorPropagationDelayedClose),
130130
("testWeCloseConnectionsWhenConnectionCloseSetByServer", testWeCloseConnectionsWhenConnectionCloseSetByServer),
131131
("testBiDirectionalStreaming", testBiDirectionalStreaming),
132+
("testSynchronousHandshakeErrorReporting", testSynchronousHandshakeErrorReporting),
132133
]
133134
}
134135
}

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

+23
Original file line numberDiff line numberDiff line change
@@ -2821,4 +2821,27 @@ class HTTPClientTests: XCTestCase {
28212821

28222822
XCTAssertNoThrow(try future.wait())
28232823
}
2824+
2825+
func testSynchronousHandshakeErrorReporting() throws {
2826+
// This only affects cases where we use NIOSSL.
2827+
guard !isTestingNIOTS() else { return }
2828+
2829+
// We use a specially crafted client that has no cipher suites to offer. To do this we ask
2830+
// only for cipher suites incompatible with our TLS version.
2831+
let tlsConfig = TLSConfiguration.forClient(minimumTLSVersion: .tlsv13, maximumTLSVersion: .tlsv12, certificateVerification: .none)
2832+
let localHTTPBin = HTTPBin(ssl: true)
2833+
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
2834+
configuration: HTTPClient.Configuration(tlsConfiguration: tlsConfig))
2835+
defer {
2836+
XCTAssertNoThrow(try localClient.syncShutdown())
2837+
XCTAssertNoThrow(try localHTTPBin.shutdown())
2838+
}
2839+
2840+
XCTAssertThrowsError(try localClient.get(url: "https://localhost:\(localHTTPBin.port)/").wait()) { error in
2841+
guard let clientError = error as? NIOSSLError, case NIOSSLError.handshakeFailed = clientError else {
2842+
XCTFail("Unexpected error: \(error)")
2843+
return
2844+
}
2845+
}
2846+
}
28242847
}

0 commit comments

Comments
 (0)