Skip to content

Support NIO Transport Services - part 2 #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Apr 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a138c04
NIO Transport Services
adam-fowler Mar 28, 2020
33b6de0
Tidying up tests
adam-fowler Mar 28, 2020
eb81523
Make sure ENABLE_TS_TESTS toggles TS on and off for all tests
adam-fowler Mar 28, 2020
0b468f2
Don't set TLS hostname for unix connections
adam-fowler Mar 28, 2020
b06210f
Removed Handshake promise as it is not used
adam-fowler Mar 29, 2020
f40a084
Removed iOS platform requirements, swift-nio-ssl from:2.7.0
adam-fowler Mar 30, 2020
6451389
Moved makeHTTPClientBootstrapBase back to Utils.swift
adam-fowler Mar 30, 2020
a7815d1
Updated tests
adam-fowler Mar 30, 2020
8e18b0b
Fixed testProxyTLS()
adam-fowler Mar 30, 2020
61c735e
Got HTTPS tests working, fixed testStressGetHttpsSSLError for TS.
adam-fowler Mar 30, 2020
eeea672
TLSEventsHandler wasn't getting added
adam-fowler Mar 30, 2020
4a1ff5d
Renabled ssl in redirect tests
adam-fowler Mar 31, 2020
014cef3
NIOTS set waitForActivity to false
adam-fowler Mar 31, 2020
d500a0c
Fixed testAvoidLeakingTLSHandshakeCompletionPromise for TS
adam-fowler Mar 31, 2020
76b3618
Moved my public NIOTSEventLoop to another branch
adam-fowler Mar 31, 2020
8574edd
Simplified requiresSSLHandler check
adam-fowler Mar 31, 2020
9a41fc3
Added TLSConfiguration.getNWProtocolTLSOptions()
adam-fowler Apr 2, 2020
639d0d5
Translate NWError into easier to use structs
adam-fowler Apr 2, 2020
8d43633
Use NIOTSConnectionBootstrap(validatingGroup:)
adam-fowler Apr 4, 2020
299e14c
Re-enabled testStressGetClose()
adam-fowler Apr 4, 2020
69b0e38
Changed NWDNSError into an enum
adam-fowler Apr 4, 2020
0eef5f0
TLSConfiguration convert to NWProtocolTLS.Options changes
adam-fowler Apr 6, 2020
f7a3a67
NIOClientTCPBootstrap.makeBootstrap() changes
adam-fowler Apr 6, 2020
131f96a
Clean up suggestions
adam-fowler Apr 7, 2020
72c384b
Run stubs of NIOTS tests on Linux
adam-fowler Apr 14, 2020
9d4e9ef
Formatting changes
adam-fowler Apr 17, 2020
05c0fe1
Re-formatted to make easier to read
adam-fowler Apr 17, 2020
262a6fe
Moved NWTLSError, NWPosixError inside HTTPClient
adam-fowler Apr 17, 2020
038ef0b
Flipped Testing NIOTS switch to be DISABLE_TS_TESTS
adam-fowler Apr 17, 2020
8d92958
swift format fix up
adam-fowler Apr 17, 2020
30d943c
isTestingNIOTS() should return false if you cannot import Network
adam-fowler Apr 18, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ let package = Package(
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.13.1"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.4.1"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.16.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.7.0"),
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.5.1"),
],
targets: [
.target(
name: "AsyncHTTPClient",
dependencies: ["NIO", "NIOHTTP1", "NIOSSL", "NIOConcurrencyHelpers", "NIOHTTPCompression", "NIOFoundationCompat"]
dependencies: ["NIO", "NIOHTTP1", "NIOSSL", "NIOConcurrencyHelpers", "NIOHTTPCompression", "NIOFoundationCompat", "NIOTransportServices"]
),
.testTarget(
name: "AsyncHTTPClientTests",
Expand Down
27 changes: 24 additions & 3 deletions Sources/AsyncHTTPClient/ConnectionPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import NIO
import NIOConcurrencyHelpers
import NIOHTTP1
import NIOTLS
import NIOTransportServices

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

private func makeConnection(on eventLoop: EventLoop) -> EventLoopFuture<Connection> {
self.activityPrecondition(expected: [.opened])
let handshakePromise = eventLoop.makePromise(of: Void.self)
let bootstrap = ClientBootstrap.makeHTTPClientBootstrapBase(group: eventLoop, host: self.key.host, port: self.key.port, configuration: self.configuration)
let address = HTTPClient.resolveAddress(host: self.key.host, port: self.key.port, proxy: self.configuration.proxy)
let requiresTLS = self.key.scheme == .https
let bootstrap: NIOClientTCPBootstrap
do {
bootstrap = try NIOClientTCPBootstrap.makeHTTPClientBootstrapBase(on: eventLoop, host: self.key.host, port: self.key.port, requiresTLS: requiresTLS, configuration: self.configuration)
} catch {
return eventLoop.makeFailedFuture(error)
}
let handshakePromise = eventLoop.makePromise(of: Void.self)

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

return channel.flatMap { channel -> EventLoopFuture<ConnectionPool.Connection> in
channel.pipeline.addSSLHandlerIfNeeded(for: self.key, tlsConfiguration: self.configuration.tlsConfiguration, handshakePromise: handshakePromise)
let requiresSSLHandler = self.configuration.proxy != nil && self.key.scheme == .https
channel.pipeline.addSSLHandlerIfNeeded(for: self.key, tlsConfiguration: self.configuration.tlsConfiguration, addSSLClient: requiresSSLHandler, handshakePromise: handshakePromise)
return handshakePromise.futureResult.flatMap {
channel.pipeline.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
}.flatMap {
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
return channel.pipeline.addHandler(HTTPClient.NWErrorHandler(), position: .first)
}
#endif
return eventLoop.makeSucceededFuture(())
}.map {
let connection = Connection(key: self.key, channel: channel, parentPool: self.parentPool)
connection.isLeased = true
Expand All @@ -398,6 +413,12 @@ final class ConnectionPool {
self.configureCloseCallback(of: connection)
return connection
}.flatMapError { error in
var error = error
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
error = HTTPClient.NWErrorHandler.translateError(error)
}
#endif
// This promise may not have been completed if we reach this
// so we fail it to avoid any leak
handshakePromise.fail(error)
Expand Down
30 changes: 22 additions & 8 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import NIOHTTP1
import NIOHTTPCompression
import NIOSSL
import NIOTLS
import NIOTransportServices

/// HTTPClient class provides API for request execution.
///
Expand Down Expand Up @@ -65,7 +66,15 @@ public class HTTPClient {
case .shared(let group):
self.eventLoopGroup = group
case .createNew:
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
self.eventLoopGroup = NIOTSEventLoopGroup()
} else {
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
}
#else
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
#endif
}
self.configuration = configuration
self.pool = ConnectionPool(configuration: configuration)
Expand Down Expand Up @@ -672,19 +681,24 @@ extension ChannelPipeline {
return addHandlers([encoder, decoder, handler])
}

func addSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, handshakePromise: EventLoopPromise<Void>) {
func addSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, addSSLClient: Bool, handshakePromise: EventLoopPromise<Void>) {
guard key.scheme == .https else {
handshakePromise.succeed(())
return
}

do {
let tlsConfiguration = tlsConfiguration ?? TLSConfiguration.forClient()
let context = try NIOSSLContext(configuration: tlsConfiguration)
let handlers: [ChannelHandler] = [
try NIOSSLClientHandler(context: context, serverHostname: key.host.isIPAddress ? nil : key.host),
TLSEventsHandler(completionPromise: handshakePromise),
]
let handlers: [ChannelHandler]
if addSSLClient {
let tlsConfiguration = tlsConfiguration ?? TLSConfiguration.forClient()
let context = try NIOSSLContext(configuration: tlsConfiguration)
handlers = [
try NIOSSLClientHandler(context: context, serverHostname: key.host.isIPAddress ? nil : key.host),
TLSEventsHandler(completionPromise: handshakePromise),
]
} else {
handlers = [TLSEventsHandler(completionPromise: handshakePromise)]
}
self.addHandlers(handlers).cascadeFailure(to: handshakePromise)
} catch {
handshakePromise.fail(error)
Expand Down
84 changes: 84 additions & 0 deletions Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2020 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if canImport(Network)

import Network
import NIO
import NIOHTTP1
import NIOTransportServices

extension HTTPClient {
public struct NWPOSIXError: Error, CustomStringConvertible {
/// POSIX error code (enum)
public let errorCode: POSIXErrorCode

/// actual reason, in human readable form
private let reason: String

/// Initialise a NWPOSIXError
/// - Parameters:
/// - errorType: posix error type
/// - reason: String describing reason for error
public init(_ errorCode: POSIXErrorCode, reason: String) {
self.errorCode = errorCode
self.reason = reason
}

public var description: String { return self.reason }
}

public struct NWTLSError: Error, CustomStringConvertible {
/// TLS error status. List of TLS errors can be found in <Security/SecureTransport.h>
public let status: OSStatus

/// actual reason, in human readable form
private let reason: String

/// initialise a NWTLSError
/// - Parameters:
/// - status: TLS status
/// - reason: String describing reason for error
public init(_ status: OSStatus, reason: String) {
self.status = status
self.reason = reason
}

public var description: String { return self.reason }
}

@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
class NWErrorHandler: ChannelInboundHandler {
typealias InboundIn = HTTPClientResponsePart

func errorCaught(context: ChannelHandlerContext, error: Error) {
context.fireErrorCaught(NWErrorHandler.translateError(error))
}

static func translateError(_ error: Error) -> Error {
if let error = error as? NWError {
switch error {
case .tls(let status):
return NWTLSError(status, reason: error.localizedDescription)
case .posix(let errorCode):
return NWPOSIXError(errorCode, reason: error.localizedDescription)
default:
return error
}
}
return error
}
}
}
#endif
140 changes: 140 additions & 0 deletions Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2020 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if canImport(Network)

import Foundation
import Network
import NIOSSL
import NIOTransportServices

extension TLSVersion {
/// return Network framework TLS protocol version
var nwTLSProtocolVersion: tls_protocol_version_t {
switch self {
case .tlsv1:
return .TLSv10
case .tlsv11:
return .TLSv11
case .tlsv12:
return .TLSv12
case .tlsv13:
return .TLSv13
}
}
}

extension TLSVersion {
/// return as SSL protocol
var sslProtocol: SSLProtocol {
switch self {
case .tlsv1:
return .tlsProtocol1
case .tlsv11:
return .tlsProtocol11
case .tlsv12:
return .tlsProtocol12
case .tlsv13:
return .tlsProtocol13
}
}
}

@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
extension TLSConfiguration {
/// Dispatch queue used by Network framework TLS to control certificate verification
static var tlsDispatchQueue = DispatchQueue(label: "TLSDispatch")

/// create NWProtocolTLS.Options for use with NIOTransportServices from the NIOSSL TLSConfiguration
///
/// - Parameter queue: Dispatch queue to run `sec_protocol_options_set_verify_block` on.
/// - Returns: Equivalent NWProtocolTLS Options
func getNWProtocolTLSOptions() -> NWProtocolTLS.Options {
let options = NWProtocolTLS.Options()

// minimum TLS protocol
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, self.minimumTLSVersion.nwTLSProtocolVersion)
} else {
sec_protocol_options_set_tls_min_version(options.securityProtocolOptions, self.minimumTLSVersion.sslProtocol)
}

// maximum TLS protocol
if let maximumTLSVersion = self.maximumTLSVersion {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
sec_protocol_options_set_max_tls_protocol_version(options.securityProtocolOptions, maximumTLSVersion.nwTLSProtocolVersion)
} else {
sec_protocol_options_set_tls_max_version(options.securityProtocolOptions, maximumTLSVersion.sslProtocol)
}
}

// application protocols
for applicationProtocol in self.applicationProtocols {
applicationProtocol.withCString { buffer in
sec_protocol_options_add_tls_application_protocol(options.securityProtocolOptions, buffer)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is also reasonably do-able, if we configure a custom trust challenge. For now, though, let's leave it as-is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this would be done with sec_protocol_options_set_verify_block. There doesn't seem to be any documentation on what the default verify block would involve, outside of evaluating the trust (Does it also verify server address?). So wasn't keen on setting one up outside the very specific case of having no certificate verification.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. You start out with a trust that's appropriately configured (it's passed in, I believe) and you can modify it as needed.


// the certificate chain
if self.certificateChain.count > 0 {
preconditionFailure("TLSConfiguration.certificateChain is not supported")
}

// cipher suites
if self.cipherSuites.count > 0 {
// TODO: Requires NIOSSL to provide list of cipher values before we can continue
// https://github.com/apple/swift-nio-ssl/issues/207
}

// key log callback
if self.keyLogCallback != nil {
preconditionFailure("TLSConfiguration.keyLogCallback is not supported")
}

// private key
if self.privateKey != nil {
preconditionFailure("TLSConfiguration.privateKey is not supported")
}

// renegotiation support key is unsupported

// trust roots
if let trustRoots = self.trustRoots {
guard case .default = trustRoots else {
preconditionFailure("TLSConfiguration.trustRoots != .default is not supported")
}
}

switch self.certificateVerification {
case .none:
// add verify block to control certificate verification
sec_protocol_options_set_verify_block(
options.securityProtocolOptions,
{ _, _, sec_protocol_verify_complete in
sec_protocol_verify_complete(true)
}, TLSConfiguration.tlsDispatchQueue
)

case .noHostnameVerification:
precondition(self.certificateVerification != .noHostnameVerification, "TLSConfiguration.certificateVerification = .noHostnameVerification is not supported")

case .fullVerification:
break
}

return options
}
}

#endif
Loading