Skip to content

Commit 5298f20

Browse files
adam-fowlerYasumotoweissi
authored
Support NIO Transport Services - part 2 (#184)
This is the continuation of the good work of @Yasumoto and @weissi in #135 The following code adds support for NIO Transport services. When the ConnectionPool asks for a connection bootstrap it is returned a NIOClientTCPBootstrap which wraps either a NIOTSConnectionBootstrap or a ClientBootstrap depending on whether the EventLoop we are running on is NIOTSEventLoop. If you initialize an HTTPClient with eventLoopGroupProvider set to .createNew then if you are running on iOS, macOS 10.14 or later it will provide a NIOTSEventLoopGroup instead of a EventLoopGroup. Currently a number of tests are failing. 4 of these are related to the NIOSSLUncleanShutdown error the others all seem related to various race conditions which are being dealt with on other PRs. I have tested this code with aws-sdk-swift and it is working on both macOS and iOS. Things look into: The aws-sdk-swift NIOTS HTTP client had issues with on Mojave. We should check if this is the case for async-http-client as well. Co-authored-by: Joe Smith <[email protected]> Co-authored-by: Johannes Weiss <[email protected]>
1 parent be517e3 commit 5298f20

12 files changed

+698
-151
lines changed

Diff for: Package.swift

+4-3
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ let package = Package(
2121
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]),
2222
],
2323
dependencies: [
24-
.package(url: "https://github.com/apple/swift-nio.git", from: "2.13.1"),
25-
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.4.1"),
24+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.16.0"),
25+
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.7.0"),
2626
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.3.0"),
27+
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.5.1"),
2728
],
2829
targets: [
2930
.target(
3031
name: "AsyncHTTPClient",
31-
dependencies: ["NIO", "NIOHTTP1", "NIOSSL", "NIOConcurrencyHelpers", "NIOHTTPCompression", "NIOFoundationCompat"]
32+
dependencies: ["NIO", "NIOHTTP1", "NIOSSL", "NIOConcurrencyHelpers", "NIOHTTPCompression", "NIOFoundationCompat", "NIOTransportServices"]
3233
),
3334
.testTarget(
3435
name: "AsyncHTTPClientTests",

Diff for: Sources/AsyncHTTPClient/ConnectionPool.swift

+24-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import NIO
1717
import NIOConcurrencyHelpers
1818
import NIOHTTP1
1919
import NIOTLS
20+
import NIOTransportServices
2021

2122
/// A connection pool that manages and creates new connections to hosts respecting the specified preferences
2223
///
@@ -373,9 +374,15 @@ final class ConnectionPool {
373374

374375
private func makeConnection(on eventLoop: EventLoop) -> EventLoopFuture<Connection> {
375376
self.activityPrecondition(expected: [.opened])
376-
let handshakePromise = eventLoop.makePromise(of: Void.self)
377-
let bootstrap = ClientBootstrap.makeHTTPClientBootstrapBase(group: eventLoop, host: self.key.host, port: self.key.port, configuration: self.configuration)
378377
let address = HTTPClient.resolveAddress(host: self.key.host, port: self.key.port, proxy: self.configuration.proxy)
378+
let requiresTLS = self.key.scheme == .https
379+
let bootstrap: NIOClientTCPBootstrap
380+
do {
381+
bootstrap = try NIOClientTCPBootstrap.makeHTTPClientBootstrapBase(on: eventLoop, host: self.key.host, port: self.key.port, requiresTLS: requiresTLS, configuration: self.configuration)
382+
} catch {
383+
return eventLoop.makeFailedFuture(error)
384+
}
385+
let handshakePromise = eventLoop.makePromise(of: Void.self)
379386

380387
let channel: EventLoopFuture<Channel>
381388
switch self.key.scheme {
@@ -386,9 +393,17 @@ final class ConnectionPool {
386393
}
387394

388395
return channel.flatMap { channel -> EventLoopFuture<ConnectionPool.Connection> in
389-
channel.pipeline.addSSLHandlerIfNeeded(for: self.key, tlsConfiguration: self.configuration.tlsConfiguration, handshakePromise: handshakePromise)
396+
let requiresSSLHandler = self.configuration.proxy != nil && self.key.scheme == .https
397+
channel.pipeline.addSSLHandlerIfNeeded(for: self.key, tlsConfiguration: self.configuration.tlsConfiguration, addSSLClient: requiresSSLHandler, handshakePromise: handshakePromise)
390398
return handshakePromise.futureResult.flatMap {
391399
channel.pipeline.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
400+
}.flatMap {
401+
#if canImport(Network)
402+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
403+
return channel.pipeline.addHandler(HTTPClient.NWErrorHandler(), position: .first)
404+
}
405+
#endif
406+
return eventLoop.makeSucceededFuture(())
392407
}.map {
393408
let connection = Connection(key: self.key, channel: channel, parentPool: self.parentPool)
394409
connection.isLeased = true
@@ -398,6 +413,12 @@ final class ConnectionPool {
398413
self.configureCloseCallback(of: connection)
399414
return connection
400415
}.flatMapError { error in
416+
var error = error
417+
#if canImport(Network)
418+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
419+
error = HTTPClient.NWErrorHandler.translateError(error)
420+
}
421+
#endif
401422
// This promise may not have been completed if we reach this
402423
// so we fail it to avoid any leak
403424
handshakePromise.fail(error)

Diff for: Sources/AsyncHTTPClient/HTTPClient.swift

+22-8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import NIOHTTP1
1919
import NIOHTTPCompression
2020
import NIOSSL
2121
import NIOTLS
22+
import NIOTransportServices
2223

2324
/// HTTPClient class provides API for request execution.
2425
///
@@ -65,7 +66,15 @@ public class HTTPClient {
6566
case .shared(let group):
6667
self.eventLoopGroup = group
6768
case .createNew:
68-
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
69+
#if canImport(Network)
70+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
71+
self.eventLoopGroup = NIOTSEventLoopGroup()
72+
} else {
73+
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
74+
}
75+
#else
76+
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
77+
#endif
6978
}
7079
self.configuration = configuration
7180
self.pool = ConnectionPool(configuration: configuration)
@@ -672,19 +681,24 @@ extension ChannelPipeline {
672681
return addHandlers([encoder, decoder, handler])
673682
}
674683

675-
func addSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, handshakePromise: EventLoopPromise<Void>) {
684+
func addSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, addSSLClient: Bool, handshakePromise: EventLoopPromise<Void>) {
676685
guard key.scheme == .https else {
677686
handshakePromise.succeed(())
678687
return
679688
}
680689

681690
do {
682-
let tlsConfiguration = tlsConfiguration ?? TLSConfiguration.forClient()
683-
let context = try NIOSSLContext(configuration: tlsConfiguration)
684-
let handlers: [ChannelHandler] = [
685-
try NIOSSLClientHandler(context: context, serverHostname: key.host.isIPAddress ? nil : key.host),
686-
TLSEventsHandler(completionPromise: handshakePromise),
687-
]
691+
let handlers: [ChannelHandler]
692+
if addSSLClient {
693+
let tlsConfiguration = tlsConfiguration ?? TLSConfiguration.forClient()
694+
let context = try NIOSSLContext(configuration: tlsConfiguration)
695+
handlers = [
696+
try NIOSSLClientHandler(context: context, serverHostname: key.host.isIPAddress ? nil : key.host),
697+
TLSEventsHandler(completionPromise: handshakePromise),
698+
]
699+
} else {
700+
handlers = [TLSEventsHandler(completionPromise: handshakePromise)]
701+
}
688702
self.addHandlers(handlers).cascadeFailure(to: handshakePromise)
689703
} catch {
690704
handshakePromise.fail(error)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2020 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+
#if canImport(Network)
16+
17+
import Network
18+
import NIO
19+
import NIOHTTP1
20+
import NIOTransportServices
21+
22+
extension HTTPClient {
23+
public struct NWPOSIXError: Error, CustomStringConvertible {
24+
/// POSIX error code (enum)
25+
public let errorCode: POSIXErrorCode
26+
27+
/// actual reason, in human readable form
28+
private let reason: String
29+
30+
/// Initialise a NWPOSIXError
31+
/// - Parameters:
32+
/// - errorType: posix error type
33+
/// - reason: String describing reason for error
34+
public init(_ errorCode: POSIXErrorCode, reason: String) {
35+
self.errorCode = errorCode
36+
self.reason = reason
37+
}
38+
39+
public var description: String { return self.reason }
40+
}
41+
42+
public struct NWTLSError: Error, CustomStringConvertible {
43+
/// TLS error status. List of TLS errors can be found in <Security/SecureTransport.h>
44+
public let status: OSStatus
45+
46+
/// actual reason, in human readable form
47+
private let reason: String
48+
49+
/// initialise a NWTLSError
50+
/// - Parameters:
51+
/// - status: TLS status
52+
/// - reason: String describing reason for error
53+
public init(_ status: OSStatus, reason: String) {
54+
self.status = status
55+
self.reason = reason
56+
}
57+
58+
public var description: String { return self.reason }
59+
}
60+
61+
@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
62+
class NWErrorHandler: ChannelInboundHandler {
63+
typealias InboundIn = HTTPClientResponsePart
64+
65+
func errorCaught(context: ChannelHandlerContext, error: Error) {
66+
context.fireErrorCaught(NWErrorHandler.translateError(error))
67+
}
68+
69+
static func translateError(_ error: Error) -> Error {
70+
if let error = error as? NWError {
71+
switch error {
72+
case .tls(let status):
73+
return NWTLSError(status, reason: error.localizedDescription)
74+
case .posix(let errorCode):
75+
return NWPOSIXError(errorCode, reason: error.localizedDescription)
76+
default:
77+
return error
78+
}
79+
}
80+
return error
81+
}
82+
}
83+
}
84+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2020 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+
#if canImport(Network)
16+
17+
import Foundation
18+
import Network
19+
import NIOSSL
20+
import NIOTransportServices
21+
22+
extension TLSVersion {
23+
/// return Network framework TLS protocol version
24+
var nwTLSProtocolVersion: tls_protocol_version_t {
25+
switch self {
26+
case .tlsv1:
27+
return .TLSv10
28+
case .tlsv11:
29+
return .TLSv11
30+
case .tlsv12:
31+
return .TLSv12
32+
case .tlsv13:
33+
return .TLSv13
34+
}
35+
}
36+
}
37+
38+
extension TLSVersion {
39+
/// return as SSL protocol
40+
var sslProtocol: SSLProtocol {
41+
switch self {
42+
case .tlsv1:
43+
return .tlsProtocol1
44+
case .tlsv11:
45+
return .tlsProtocol11
46+
case .tlsv12:
47+
return .tlsProtocol12
48+
case .tlsv13:
49+
return .tlsProtocol13
50+
}
51+
}
52+
}
53+
54+
@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
55+
extension TLSConfiguration {
56+
/// Dispatch queue used by Network framework TLS to control certificate verification
57+
static var tlsDispatchQueue = DispatchQueue(label: "TLSDispatch")
58+
59+
/// create NWProtocolTLS.Options for use with NIOTransportServices from the NIOSSL TLSConfiguration
60+
///
61+
/// - Parameter queue: Dispatch queue to run `sec_protocol_options_set_verify_block` on.
62+
/// - Returns: Equivalent NWProtocolTLS Options
63+
func getNWProtocolTLSOptions() -> NWProtocolTLS.Options {
64+
let options = NWProtocolTLS.Options()
65+
66+
// minimum TLS protocol
67+
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
68+
sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, self.minimumTLSVersion.nwTLSProtocolVersion)
69+
} else {
70+
sec_protocol_options_set_tls_min_version(options.securityProtocolOptions, self.minimumTLSVersion.sslProtocol)
71+
}
72+
73+
// maximum TLS protocol
74+
if let maximumTLSVersion = self.maximumTLSVersion {
75+
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
76+
sec_protocol_options_set_max_tls_protocol_version(options.securityProtocolOptions, maximumTLSVersion.nwTLSProtocolVersion)
77+
} else {
78+
sec_protocol_options_set_tls_max_version(options.securityProtocolOptions, maximumTLSVersion.sslProtocol)
79+
}
80+
}
81+
82+
// application protocols
83+
for applicationProtocol in self.applicationProtocols {
84+
applicationProtocol.withCString { buffer in
85+
sec_protocol_options_add_tls_application_protocol(options.securityProtocolOptions, buffer)
86+
}
87+
}
88+
89+
// the certificate chain
90+
if self.certificateChain.count > 0 {
91+
preconditionFailure("TLSConfiguration.certificateChain is not supported")
92+
}
93+
94+
// cipher suites
95+
if self.cipherSuites.count > 0 {
96+
// TODO: Requires NIOSSL to provide list of cipher values before we can continue
97+
// https://github.com/apple/swift-nio-ssl/issues/207
98+
}
99+
100+
// key log callback
101+
if self.keyLogCallback != nil {
102+
preconditionFailure("TLSConfiguration.keyLogCallback is not supported")
103+
}
104+
105+
// private key
106+
if self.privateKey != nil {
107+
preconditionFailure("TLSConfiguration.privateKey is not supported")
108+
}
109+
110+
// renegotiation support key is unsupported
111+
112+
// trust roots
113+
if let trustRoots = self.trustRoots {
114+
guard case .default = trustRoots else {
115+
preconditionFailure("TLSConfiguration.trustRoots != .default is not supported")
116+
}
117+
}
118+
119+
switch self.certificateVerification {
120+
case .none:
121+
// add verify block to control certificate verification
122+
sec_protocol_options_set_verify_block(
123+
options.securityProtocolOptions,
124+
{ _, _, sec_protocol_verify_complete in
125+
sec_protocol_verify_complete(true)
126+
}, TLSConfiguration.tlsDispatchQueue
127+
)
128+
129+
case .noHostnameVerification:
130+
precondition(self.certificateVerification != .noHostnameVerification, "TLSConfiguration.certificateVerification = .noHostnameVerification is not supported")
131+
132+
case .fullVerification:
133+
break
134+
}
135+
136+
return options
137+
}
138+
}
139+
140+
#endif

0 commit comments

Comments
 (0)