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 26 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(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 = 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
172 changes: 172 additions & 0 deletions Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//===----------------------------------------------------------------------===//
//
// 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

/// Enum containing all the DNS errors
public enum NWDNSError: Error {
case noError
case unknown /* 0xFFFE FFFF */
case noSuchName
case noMemory
case badParam
case badReference
case badState
case badFlags
case unsupported
case notInitialized
case alreadyRegistered
case nameConflict
case invalid
case firewall
case incompatible /* client library incompatible with daemon */
case badInterfaceIndex
case refused
case noSuchRecord
case noAuth
case noSuchKey
case NATTraversal
case doubleNAT
case badTime /* Codes up to here existed in Tiger */
case badSig
case badKey
case transient
case serviceNotRunning /* Background daemon not running */
case NATPortMappingUnsupported /* NAT doesn't support PCP, NAT-PMP or UPnP */
case NATPortMappingDisabled /* NAT supports PCP, NAT-PMP or UPnP, but it's disabled by the administrator */
case noRouter /* No router currently configured (probably no network connectivity) */
case pollingMode
case timeout
case defunctConnection /* Connection to daemon returned a SO_ISDEFUNCT error result */
case other(DNSServiceErrorType)
}

extension NWDNSError {
/// Initialize NWDNSError from DNSServiceErrorType
/// - Parameter error: error type
public init(from error: DNSServiceErrorType) {
let errorInt = Int(error)
switch errorInt {
case kDNSServiceErr_NoError: self = .noError
case kDNSServiceErr_Unknown: self = .unknown
case kDNSServiceErr_NoSuchName: self = .noSuchName
case kDNSServiceErr_NoMemory: self = .noMemory
case kDNSServiceErr_BadParam: self = .badParam
case kDNSServiceErr_BadReference: self = .badReference
case kDNSServiceErr_BadState: self = .badState
case kDNSServiceErr_BadFlags: self = .badFlags
case kDNSServiceErr_Unsupported: self = .unsupported
case kDNSServiceErr_NotInitialized: self = .notInitialized
case kDNSServiceErr_AlreadyRegistered: self = .alreadyRegistered
case kDNSServiceErr_NameConflict: self = .nameConflict
case kDNSServiceErr_Invalid: self = .invalid
case kDNSServiceErr_Firewall: self = .firewall
case kDNSServiceErr_Incompatible: self = .incompatible
case kDNSServiceErr_BadInterfaceIndex: self = .badInterfaceIndex
case kDNSServiceErr_Refused: self = .refused
case kDNSServiceErr_NoSuchRecord: self = .noSuchRecord
case kDNSServiceErr_NoAuth: self = .noAuth
case kDNSServiceErr_NoSuchKey: self = .noSuchKey
case kDNSServiceErr_NATTraversal: self = .NATTraversal
case kDNSServiceErr_DoubleNAT: self = .doubleNAT
case kDNSServiceErr_BadTime: self = .badTime
case kDNSServiceErr_BadSig: self = .badSig
case kDNSServiceErr_BadKey: self = .badKey
case kDNSServiceErr_Transient: self = .transient
case kDNSServiceErr_ServiceNotRunning: self = .serviceNotRunning
case kDNSServiceErr_NATPortMappingUnsupported: self = .NATPortMappingUnsupported
case kDNSServiceErr_NATPortMappingDisabled: self = .NATPortMappingDisabled
case kDNSServiceErr_NoRouter: self = .noRouter
case kDNSServiceErr_PollingMode: self = .pollingMode
case kDNSServiceErr_Timeout: self = .timeout
case kDNSServiceErr_DefunctConnection: self = .defunctConnection
default:
self = .other(error)
}
}
}

public struct NWPOSIXError: Error {
/// 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
}
}

extension NWPOSIXError: CustomStringConvertible {
public var description: String { return self.reason }
}

public struct NWTLSError: Error {
/// 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
}
}

extension NWTLSError: CustomStringConvertible {
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 .dns(let errorType):
return NWDNSError(from: errorType)
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
Loading