diff --git a/Package.swift b/Package.swift index f1e14fa34..7a79e52fa 100644 --- a/Package.swift +++ b/Package.swift @@ -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", diff --git a/Sources/AsyncHTTPClient/ConnectionPool.swift b/Sources/AsyncHTTPClient/ConnectionPool.swift index dfb7d5f4f..aae2f9748 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool.swift @@ -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 /// @@ -373,9 +374,15 @@ final class ConnectionPool { private func makeConnection(on eventLoop: EventLoop) -> EventLoopFuture { 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 switch self.key.scheme { @@ -386,9 +393,17 @@ final class ConnectionPool { } return channel.flatMap { channel -> EventLoopFuture 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 @@ -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) diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index eb9833774..6c6dfa21a 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -19,6 +19,7 @@ import NIOHTTP1 import NIOHTTPCompression import NIOSSL import NIOTLS +import NIOTransportServices /// HTTPClient class provides API for request execution. /// @@ -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) @@ -672,19 +681,24 @@ extension ChannelPipeline { return addHandlers([encoder, decoder, handler]) } - func addSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, handshakePromise: EventLoopPromise) { + func addSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, addSSLClient: Bool, handshakePromise: EventLoopPromise) { 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) diff --git a/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift b/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift new file mode 100644 index 000000000..1f9dceb88 --- /dev/null +++ b/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift @@ -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 + 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 diff --git a/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift b/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift new file mode 100644 index 000000000..e1003dd93 --- /dev/null +++ b/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift @@ -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) + } + } + + // 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 diff --git a/Sources/AsyncHTTPClient/Utils.swift b/Sources/AsyncHTTPClient/Utils.swift index 6e2fedf53..95d85c958 100644 --- a/Sources/AsyncHTTPClient/Utils.swift +++ b/Sources/AsyncHTTPClient/Utils.swift @@ -2,7 +2,7 @@ // // This source file is part of the AsyncHTTPClient open source project // -// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors +// Copyright (c) 2018-2020 Apple Inc. and the AsyncHTTPClient project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,9 +12,15 @@ // //===----------------------------------------------------------------------===// +import Foundation +#if canImport(Network) + import Network +#endif import NIO import NIOHTTP1 import NIOHTTPCompression +import NIOSSL +import NIOTransportServices internal extension String { var isIPAddress: Bool { @@ -47,21 +53,84 @@ public final class HTTPClientCopyingDelegate: HTTPClientResponseDelegate { } extension ClientBootstrap { - static func makeHTTPClientBootstrapBase(group: EventLoopGroup, host: String, port: Int, configuration: HTTPClient.Configuration, channelInitializer: ((Channel) -> EventLoopFuture)? = nil) -> ClientBootstrap { - return ClientBootstrap(group: group) - .channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1) + fileprivate func makeClientTCPBootstrap( + host: String, + requiresTLS: Bool, + configuration: HTTPClient.Configuration + ) throws -> NIOClientTCPBootstrap { + // if there is a proxy don't create TLS provider as it will be added at a later point + if configuration.proxy != nil { + return NIOClientTCPBootstrap(self, tls: NIOInsecureNoTLS()) + } else { + let tlsConfiguration = configuration.tlsConfiguration ?? TLSConfiguration.forClient() + let sslContext = try NIOSSLContext(configuration: tlsConfiguration) + let hostname = (!requiresTLS || host.isIPAddress) ? nil : host + let tlsProvider = try NIOSSLClientTLSProvider(context: sslContext, serverHostname: hostname) + return NIOClientTCPBootstrap(self, tls: tlsProvider) + } + } +} + +extension NIOClientTCPBootstrap { + /// create a TCP Bootstrap based off what type of `EventLoop` has been passed to the function. + fileprivate static func makeBootstrap( + on eventLoop: EventLoop, + host: String, + requiresTLS: Bool, + configuration: HTTPClient.Configuration + ) throws -> NIOClientTCPBootstrap { + let bootstrap: NIOClientTCPBootstrap + #if canImport(Network) + // if eventLoop is compatible with NIOTransportServices create a NIOTSConnectionBootstrap + if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) { + // if there is a proxy don't create TLS provider as it will be added at a later point + if configuration.proxy != nil { + bootstrap = NIOClientTCPBootstrap(tsBootstrap, tls: NIOInsecureNoTLS()) + } else { + // create NIOClientTCPBootstrap with NIOTS TLS provider + let tlsConfiguration = configuration.tlsConfiguration ?? TLSConfiguration.forClient() + let parameters = tlsConfiguration.getNWProtocolTLSOptions() + let tlsProvider = NIOTSClientTLSProvider(tlsOptions: parameters) + bootstrap = NIOClientTCPBootstrap(tsBootstrap, tls: tlsProvider) + } + } else if let clientBootstrap = ClientBootstrap(validatingGroup: eventLoop) { + bootstrap = try clientBootstrap.makeClientTCPBootstrap(host: host, requiresTLS: requiresTLS, configuration: configuration) + } else { + preconditionFailure("Cannot create bootstrap for the supplied EventLoop") + } + #else + if let clientBootstrap = ClientBootstrap(validatingGroup: eventLoop) { + bootstrap = try clientBootstrap.makeClientTCPBootstrap(host: host, requiresTLS: requiresTLS, configuration: configuration) + } else { + preconditionFailure("Cannot create bootstrap for the supplied EventLoop") + } + #endif + // don't enable TLS if we have a proxy, this will be enabled later on + if requiresTLS, configuration.proxy == nil { + return bootstrap.enableTLS() + } + return bootstrap + } + + static func makeHTTPClientBootstrapBase( + on eventLoop: EventLoop, + host: String, + port: Int, + requiresTLS: Bool, + configuration: HTTPClient.Configuration + ) throws -> NIOClientTCPBootstrap { + return try self.makeBootstrap(on: eventLoop, host: host, requiresTLS: requiresTLS, configuration: configuration) + .channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1) .channelInitializer { channel in let channelAddedFuture: EventLoopFuture switch configuration.proxy { case .none: - channelAddedFuture = group.next().makeSucceededFuture(()) + channelAddedFuture = eventLoop.makeSucceededFuture(()) case .some: channelAddedFuture = channel.pipeline.addProxyHandler(host: host, port: port, authorization: configuration.proxy?.authorization) } - return channelAddedFuture.flatMap { (_: Void) -> EventLoopFuture in - channelInitializer?(channel) ?? group.next().makeSucceededFuture(()) - } + return channelAddedFuture } } } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift index f175246c0..556499d86 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift @@ -23,6 +23,19 @@ class HTTPClientInternalTests: XCTestCase { typealias Request = HTTPClient.Request typealias Task = HTTPClient.Task + var clientGroup: EventLoopGroup! + + override func setUp() { + XCTAssertNil(self.clientGroup) + self.clientGroup = getDefaultEventLoopGroup(numberOfThreads: 1) + } + + override func tearDown() { + XCTAssertNotNil(self.clientGroup) + XCTAssertNoThrow(try self.clientGroup.syncShutdownGracefully()) + self.clientGroup = nil + } + func testHTTPPartsHandler() throws { let channel = EmbeddedChannel() let recorder = RecordingHandler() @@ -105,7 +118,7 @@ class HTTPClientInternalTests: XCTestCase { func testProxyStreaming() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) XCTAssertNoThrow(try httpBin.shutdown()) @@ -135,7 +148,7 @@ class HTTPClientInternalTests: XCTestCase { func testProxyStreamingFailure() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) XCTAssertNoThrow(try httpBin.shutdown()) @@ -217,13 +230,16 @@ class HTTPClientInternalTests: XCTestCase { func didFinishRequest(task: HTTPClient.Task) throws {} } - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + // cannot test with NIOTS as `maxMessagesPerRead` is not supported + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup)) let promise = httpClient.eventLoopGroup.next().makePromise(of: Channel.self) let httpBin = HTTPBin(channelPromise: promise) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) XCTAssertNoThrow(try httpBin.shutdown()) + XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } let request = try Request(url: "http://localhost:\(httpBin.port)/custom") @@ -354,7 +370,7 @@ class HTTPClientInternalTests: XCTestCase { } } - let group = MultiThreadedEventLoopGroup(numberOfThreads: 3) + let group = getDefaultEventLoopGroup(numberOfThreads: 3) defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) } @@ -443,7 +459,7 @@ class HTTPClientInternalTests: XCTestCase { func testResponseConnectionCloseGet() throws { let httpBin = HTTPBin(ssl: false) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -547,7 +563,7 @@ class HTTPClientInternalTests: XCTestCase { } let url = "http://127.0.0.1:\(server.localAddress!.port!)" - let client = HTTPClient(eventLoopGroupProvider: .shared(group)) + let client = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try client.syncShutdown()) } @@ -588,7 +604,7 @@ class HTTPClientInternalTests: XCTestCase { func testWeTolerateConnectionsGoingAwayWhilstPoolIsShuttingDown() { struct NoChannelError: Error {} - let client = HTTPClient(eventLoopGroupProvider: .createNew) + let client = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) var maybeServersAndChannels: [(HTTPBin, Channel)]? XCTAssertNoThrow(maybeServersAndChannels = try (0..<10).map { _ in let web = HTTPBin() @@ -670,7 +686,7 @@ class HTTPClientInternalTests: XCTestCase { XCTAssertNoThrow(try web.shutdown()) } - let client = HTTPClient(eventLoopGroupProvider: .createNew) + let client = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try client.syncShutdown()) } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests+XCTest.swift new file mode 100644 index 000000000..c0d86085f --- /dev/null +++ b/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests+XCTest.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2018-2019 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 +// +//===----------------------------------------------------------------------===// +// +// HTTPClientNIOTSTests+XCTest.swift +// +import XCTest + +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension HTTPClientNIOTSTests { + static var allTests: [(String, (HTTPClientNIOTSTests) -> () throws -> Void)] { + return [ + ("testCorrectEventLoopGroup", testCorrectEventLoopGroup), + ("testTLSFailError", testTLSFailError), + ("testConnectionFailError", testConnectionFailError), + ("testTLSVersionError", testTLSVersionError), + ] + } +} diff --git a/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift new file mode 100644 index 000000000..776a8e686 --- /dev/null +++ b/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift @@ -0,0 +1,115 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2018-2019 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 +// +//===----------------------------------------------------------------------===// + +@testable import AsyncHTTPClient +#if canImport(Network) + import Network +#endif +import NIO +import NIOSSL +import NIOTransportServices +import XCTest + +class HTTPClientNIOTSTests: XCTestCase { + var clientGroup: EventLoopGroup! + + override func setUp() { + XCTAssertNil(self.clientGroup) + self.clientGroup = getDefaultEventLoopGroup(numberOfThreads: 3) + } + + override func tearDown() { + XCTAssertNotNil(self.clientGroup) + XCTAssertNoThrow(try self.clientGroup.syncShutdownGracefully()) + self.clientGroup = nil + } + + func testCorrectEventLoopGroup() { + let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown()) + } + #if canImport(Network) + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + XCTAssertTrue(httpClient.eventLoopGroup is NIOTSEventLoopGroup) + return + } + #endif + XCTAssertTrue(httpClient.eventLoopGroup is MultiThreadedEventLoopGroup) + } + + func testTLSFailError() { + guard isTestingNIOTS() else { return } + #if canImport(Network) + let httpBin = HTTPBin(ssl: true) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) + XCTAssertNoThrow(try httpBin.shutdown()) + } + + do { + _ = try httpClient.get(url: "https://localhost:\(httpBin.port)/get").wait() + XCTFail("This should have failed") + } catch let error as HTTPClient.NWTLSError { + XCTAssertEqual(error.status, errSSLHandshakeFail) + } catch { + XCTFail("Error should have been NWTLSError not \(type(of: error))") + } + #endif + } + + func testConnectionFailError() { + guard isTestingNIOTS() else { return } + let httpBin = HTTPBin(ssl: true) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) + } + let port = httpBin.port + XCTAssertNoThrow(try httpBin.shutdown()) + + do { + _ = try httpClient.get(url: "https://localhost:\(port)/get").wait() + XCTFail("This should have failed") + } catch ChannelError.connectTimeout { + } catch { + XCTFail("Error should have been ChannelError.connectTimeout not \(type(of: error))") + } + } + + func testTLSVersionError() { + guard isTestingNIOTS() else { return } + #if canImport(Network) + let httpBin = HTTPBin(ssl: true) + let httpClient = HTTPClient( + eventLoopGroupProvider: .shared(self.clientGroup), + configuration: .init(tlsConfiguration: TLSConfiguration.forClient(minimumTLSVersion: .tlsv11, maximumTLSVersion: .tlsv1, certificateVerification: .none)) + ) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) + XCTAssertNoThrow(try httpBin.shutdown()) + } + + do { + _ = try httpClient.get(url: "https://localhost:\(httpBin.port)/get").wait() + XCTFail("This should have failed") + } catch let error as HTTPClient.NWTLSError { + XCTAssertEqual(error.status, errSSLHandshakeFail) + } catch { + XCTFail("Error should have been NWTLSError not \(type(of: error))") + } + #endif + } +} diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index 765839f93..25fafefa0 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -19,6 +19,26 @@ import NIOConcurrencyHelpers import NIOHTTP1 import NIOHTTPCompression import NIOSSL +import NIOTransportServices + +/// Are we testing NIO Transport services +func isTestingNIOTS() -> Bool { + #if canImport(Network) + return ProcessInfo.processInfo.environment["DISABLE_TS_TESTS"] != "true" + #else + return false + #endif +} + +func getDefaultEventLoopGroup(numberOfThreads: Int) -> EventLoopGroup { + #if canImport(Network) + if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), + isTestingNIOTS() { + return NIOTSEventLoopGroup(loopCount: numberOfThreads, defaultQoS: .default) + } + #endif + return MultiThreadedEventLoopGroup(numberOfThreads: numberOfThreads) +} class TestHTTPDelegate: HTTPClientResponseDelegate { typealias Response = Void diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index f58170205..63c9d1397 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -13,6 +13,9 @@ //===----------------------------------------------------------------------===// @testable import AsyncHTTPClient +#if canImport(Network) + import Network +#endif import NIO import NIOConcurrencyHelpers import NIOFoundationCompat @@ -20,22 +23,29 @@ import NIOHTTP1 import NIOHTTPCompression import NIOSSL import NIOTestUtils +import NIOTransportServices import XCTest class HTTPClientTests: XCTestCase { typealias Request = HTTPClient.Request - var group: EventLoopGroup! + var clientGroup: EventLoopGroup! + var serverGroup: EventLoopGroup! override func setUp() { - XCTAssertNil(self.group) - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + XCTAssertNil(self.clientGroup) + XCTAssertNil(self.serverGroup) + self.clientGroup = getDefaultEventLoopGroup(numberOfThreads: 1) + self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) } override func tearDown() { - XCTAssertNotNil(self.group) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - self.group = nil + XCTAssertNotNil(self.clientGroup) + XCTAssertNoThrow(try self.clientGroup.syncShutdownGracefully()) + self.clientGroup = nil + XCTAssertNotNil(self.serverGroup) + XCTAssertNoThrow(try self.serverGroup.syncShutdownGracefully()) + self.serverGroup = nil } func testRequestURI() throws { @@ -76,7 +86,7 @@ class HTTPClientTests: XCTestCase { func testGet() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) XCTAssertNoThrow(try httpBin.shutdown()) @@ -88,23 +98,20 @@ class HTTPClientTests: XCTestCase { func testGetWithDifferentEventLoopBackpressure() throws { let httpBin = HTTPBin() - let loopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let external = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let httpClient = HTTPClient(eventLoopGroupProvider: .shared(loopGroup)) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) - XCTAssertNoThrow(try loopGroup.syncShutdownGracefully()) XCTAssertNoThrow(try httpBin.shutdown()) } let request = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)/events/10/1") - let delegate = TestHTTPDelegate(backpressureEventLoop: external.next()) + let delegate = TestHTTPDelegate(backpressureEventLoop: self.serverGroup.next()) let task = httpClient.execute(request: request, delegate: delegate) try task.wait() } func testPost() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) XCTAssertNoThrow(try httpBin.shutdown()) @@ -120,7 +127,7 @@ class HTTPClientTests: XCTestCase { func testGetHttps() throws { let httpBin = HTTPBin(ssl: true) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -133,7 +140,7 @@ class HTTPClientTests: XCTestCase { func testGetHttpsWithIP() throws { let httpBin = HTTPBin(ssl: true) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -146,7 +153,7 @@ class HTTPClientTests: XCTestCase { func testPostHttps() throws { let httpBin = HTTPBin(ssl: true) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -166,7 +173,7 @@ class HTTPClientTests: XCTestCase { func testHttpRedirect() throws { let httpBin = HTTPBin(ssl: false) let httpsBin = HTTPBin(ssl: true) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none, redirectConfiguration: .follow(max: 10, allowCycles: true))) defer { @@ -184,7 +191,7 @@ class HTTPClientTests: XCTestCase { func testHttpHostRedirect() throws { let httpBin = HTTPBin(ssl: false) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none, redirectConfiguration: .follow(max: 10, allowCycles: true))) defer { @@ -208,7 +215,7 @@ class HTTPClientTests: XCTestCase { func testPercentEncoded() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) XCTAssertNoThrow(try httpBin.shutdown()) @@ -231,7 +238,7 @@ class HTTPClientTests: XCTestCase { } func testMultipleContentLengthHeaders() throws { - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) } @@ -252,7 +259,7 @@ class HTTPClientTests: XCTestCase { func testStreaming() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) XCTAssertNoThrow(try httpBin.shutdown()) @@ -269,7 +276,7 @@ class HTTPClientTests: XCTestCase { func testRemoteClose() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -285,7 +292,7 @@ class HTTPClientTests: XCTestCase { func testReadTimeout() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: HTTPClient.Configuration(timeout: HTTPClient.Configuration.Timeout(read: .milliseconds(150)))) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(timeout: HTTPClient.Configuration.Timeout(read: .milliseconds(150)))) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -301,7 +308,7 @@ class HTTPClientTests: XCTestCase { func testDeadline() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -317,7 +324,7 @@ class HTTPClientTests: XCTestCase { func testCancel() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -341,7 +348,7 @@ class HTTPClientTests: XCTestCase { func testStressCancel() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -380,7 +387,7 @@ class HTTPClientTests: XCTestCase { func testProxyPlaintext() throws { let httpBin = HTTPBin(simulateProxy: .plaintext) let httpClient = HTTPClient( - eventLoopGroupProvider: .createNew, + eventLoopGroupProvider: .shared(self.clientGroup), configuration: .init(proxy: .server(host: "localhost", port: httpBin.port)) ) defer { @@ -394,7 +401,7 @@ class HTTPClientTests: XCTestCase { func testProxyTLS() throws { let httpBin = HTTPBin(simulateProxy: .tls) let httpClient = HTTPClient( - eventLoopGroupProvider: .createNew, + eventLoopGroupProvider: .shared(self.clientGroup), configuration: .init( certificateVerification: .none, proxy: .server(host: "localhost", port: httpBin.port) @@ -411,7 +418,7 @@ class HTTPClientTests: XCTestCase { func testProxyPlaintextWithCorrectlyAuthorization() throws { let httpBin = HTTPBin(simulateProxy: .plaintext) let httpClient = HTTPClient( - eventLoopGroupProvider: .createNew, + eventLoopGroupProvider: .shared(self.clientGroup), configuration: .init(proxy: .server(host: "localhost", port: httpBin.port, authorization: .basic(username: "aladdin", password: "opensesame"))) ) defer { @@ -425,7 +432,7 @@ class HTTPClientTests: XCTestCase { func testProxyPlaintextWithIncorrectlyAuthorization() throws { let httpBin = HTTPBin(simulateProxy: .plaintext) let httpClient = HTTPClient( - eventLoopGroupProvider: .createNew, + eventLoopGroupProvider: .shared(self.clientGroup), configuration: .init(proxy: .server(host: "localhost", port: httpBin.port, authorization: .basic(username: "aladdin", password: "opensesamefoo"))) ) defer { @@ -441,7 +448,7 @@ class HTTPClientTests: XCTestCase { func testUploadStreaming() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) XCTAssertNoThrow(try httpBin.shutdown()) @@ -464,8 +471,11 @@ class HTTPClientTests: XCTestCase { } func testNoContentLengthForSSLUncleanShutdown() throws { + // NIOTS deals with ssl unclean shutdown internally + guard !isTestingNIOTS() else { return } + let httpBin = HttpBinForSSLUncleanShutdown() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { @@ -481,8 +491,11 @@ class HTTPClientTests: XCTestCase { } func testNoContentLengthWithIgnoreErrorForSSLUncleanShutdown() throws { + // NIOTS deals with ssl unclean shutdown internally + guard !isTestingNIOTS() else { return } + let httpBin = HttpBinForSSLUncleanShutdown() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none, ignoreUncleanSSLShutdown: true)) defer { @@ -499,8 +512,11 @@ class HTTPClientTests: XCTestCase { } func testCorrectContentLengthForSSLUncleanShutdown() throws { + // NIOTS deals with ssl unclean shutdown internally + guard !isTestingNIOTS() else { return } + let httpBin = HttpBinForSSLUncleanShutdown() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { @@ -517,8 +533,11 @@ class HTTPClientTests: XCTestCase { } func testNoContentForSSLUncleanShutdown() throws { + // NIOTS deals with ssl unclean shutdown internally + guard !isTestingNIOTS() else { return } + let httpBin = HttpBinForSSLUncleanShutdown() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { @@ -533,8 +552,11 @@ class HTTPClientTests: XCTestCase { } func testNoResponseForSSLUncleanShutdown() throws { + // NIOTS deals with ssl unclean shutdown internally + guard !isTestingNIOTS() else { return } + let httpBin = HttpBinForSSLUncleanShutdown() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { @@ -543,15 +565,18 @@ class HTTPClientTests: XCTestCase { } XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/noresponse").wait(), "Should fail") { error in - guard case let error = error as? NIOSSLError, error == .uncleanShutdown else { + guard case let sslError = error as? NIOSSLError, sslError == .uncleanShutdown else { return XCTFail("Should fail with NIOSSLError.uncleanShutdown") } } } func testNoResponseWithIgnoreErrorForSSLUncleanShutdown() throws { + // NIOTS deals with ssl unclean shutdown internally + guard !isTestingNIOTS() else { return } + let httpBin = HttpBinForSSLUncleanShutdown() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none, ignoreUncleanSSLShutdown: true)) defer { @@ -560,15 +585,18 @@ class HTTPClientTests: XCTestCase { } XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/noresponse").wait(), "Should fail") { error in - guard case let error = error as? NIOSSLError, error == .uncleanShutdown else { + guard case let sslError = error as? NIOSSLError, sslError == .uncleanShutdown else { return XCTFail("Should fail with NIOSSLError.uncleanShutdown") } } } func testWrongContentLengthForSSLUncleanShutdown() throws { + // NIOTS deals with ssl unclean shutdown internally + guard !isTestingNIOTS() else { return } + let httpBin = HttpBinForSSLUncleanShutdown() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { @@ -577,15 +605,18 @@ class HTTPClientTests: XCTestCase { } XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/wrongcontentlength").wait(), "Should fail") { error in - guard case let error = error as? NIOSSLError, error == .uncleanShutdown else { + guard case let sslError = error as? NIOSSLError, sslError == .uncleanShutdown else { return XCTFail("Should fail with NIOSSLError.uncleanShutdown") } } } func testWrongContentLengthWithIgnoreErrorForSSLUncleanShutdown() throws { + // NIOTS deals with ssl unclean shutdown internally + guard !isTestingNIOTS() else { return } + let httpBin = HttpBinForSSLUncleanShutdown() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none, ignoreUncleanSSLShutdown: true)) defer { @@ -602,12 +633,10 @@ class HTTPClientTests: XCTestCase { func testEventLoopArgument() throws { let httpBin = HTTPBin() - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 5) - let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup), + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(redirectConfiguration: .follow(max: 10, allowCycles: true))) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) XCTAssertNoThrow(try httpBin.shutdown()) } @@ -631,7 +660,7 @@ class HTTPClientTests: XCTestCase { } } - let eventLoop = eventLoopGroup.next() + let eventLoop = self.clientGroup.next() let delegate = EventLoopValidatingDelegate(eventLoop: eventLoop) var request = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)/get") var response = try httpClient.execute(request: request, delegate: delegate, eventLoop: .delegate(on: eventLoop)).wait() @@ -644,7 +673,10 @@ class HTTPClientTests: XCTestCase { } func testResponseFutureIsOnCorrectEL() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 4) + let group = getDefaultEventLoopGroup(numberOfThreads: 4) + defer { + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } let client = HTTPClient(eventLoopGroupProvider: .shared(group)) let httpBin = HTTPBin() defer { @@ -669,7 +701,7 @@ class HTTPClientTests: XCTestCase { func testDecompression() throws { let httpBin = HTTPBin(compress: true) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: .init(decompression: .enabled(limit: .none))) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: .init(decompression: .enabled(limit: .none))) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -705,7 +737,7 @@ class HTTPClientTests: XCTestCase { func testDecompressionLimit() throws { let httpBin = HTTPBin(compress: true) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: .init(decompression: .enabled(limit: .ratio(10)))) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: .init(decompression: .enabled(limit: .ratio(10)))) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -733,7 +765,7 @@ class HTTPClientTests: XCTestCase { func testLoopDetectionRedirectLimit() throws { let httpBin = HTTPBin(ssl: true) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none, redirectConfiguration: .follow(max: 5, allowCycles: false))) defer { @@ -742,13 +774,13 @@ class HTTPClientTests: XCTestCase { } XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/redirect/infinite1").wait(), "Should fail with redirect limit") { error in - XCTAssertEqual(error as! HTTPClientError, HTTPClientError.redirectCycleDetected) + XCTAssertEqual(error as? HTTPClientError, HTTPClientError.redirectCycleDetected) } } func testCountRedirectLimit() throws { let httpBin = HTTPBin(ssl: true) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none, redirectConfiguration: .follow(max: 1000, allowCycles: true))) defer { @@ -757,7 +789,7 @@ class HTTPClientTests: XCTestCase { } XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/redirect/infinite1").wait(), "Should fail with redirect limit") { error in - XCTAssertEqual(error as! HTTPClientError, HTTPClientError.redirectLimitReached) + XCTAssertEqual(error as? HTTPClientError, HTTPClientError.redirectLimitReached) } } @@ -801,7 +833,7 @@ class HTTPClientTests: XCTestCase { XCTAssertNoThrow(try server?.close().wait()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .shared(group)) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) } @@ -828,12 +860,12 @@ class HTTPClientTests: XCTestCase { } func testWorksWith500Error() { - let web = NIOHTTP1TestServer(group: self.group) + let web = NIOHTTP1TestServer(group: self.serverGroup) defer { XCTAssertNoThrow(try web.stop()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) } @@ -857,12 +889,12 @@ class HTTPClientTests: XCTestCase { } func testWorksWithHTTP10Response() { - let web = NIOHTTP1TestServer(group: self.group) + let web = NIOHTTP1TestServer(group: self.serverGroup) defer { XCTAssertNoThrow(try web.stop()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) } @@ -886,9 +918,9 @@ class HTTPClientTests: XCTestCase { } func testWorksWhenServerClosesConnectionAfterReceivingRequest() { - let web = NIOHTTP1TestServer(group: self.group) + let web = NIOHTTP1TestServer(group: self.serverGroup) - let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) } @@ -909,12 +941,12 @@ class HTTPClientTests: XCTestCase { } func testSubsequentRequestsWorkWithServerSendingConnectionClose() { - let web = NIOHTTP1TestServer(group: self.group) + let web = NIOHTTP1TestServer(group: self.serverGroup) defer { XCTAssertNoThrow(try web.stop()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) } @@ -942,12 +974,12 @@ class HTTPClientTests: XCTestCase { } func testSubsequentRequestsWorkWithServerAlternatingBetweenKeepAliveAndClose() { - let web = NIOHTTP1TestServer(group: self.group) + let web = NIOHTTP1TestServer(group: self.serverGroup) defer { XCTAssertNoThrow(try web.stop()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) } @@ -977,7 +1009,7 @@ class HTTPClientTests: XCTestCase { func testStressGetHttps() throws { let httpBin = HTTPBin(ssl: true) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -996,7 +1028,7 @@ class HTTPClientTests: XCTestCase { func testStressGetHttpsSSLError() throws { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -1016,9 +1048,19 @@ class HTTPClientTests: XCTestCase { XCTFail("Shouldn't succeed") continue case .failure(let error): - guard let clientError = error as? NIOSSLError, case NIOSSLError.handshakeFailed = clientError else { - XCTFail("Unexpected error: \(error)") - continue + if isTestingNIOTS() { + #if canImport(Network) + guard let clientError = error as? HTTPClient.NWTLSError else { + XCTFail("Unexpected error: \(error)") + continue + } + XCTAssertEqual(clientError.status, errSSLHandshakeFail) + #endif + } else { + guard let clientError = error as? NIOSSLError, case NIOSSLError.handshakeFailed = clientError else { + XCTFail("Unexpected error: \(error)") + continue + } } } } @@ -1030,7 +1072,7 @@ class HTTPClientTests: XCTestCase { XCTAssertNoThrow(try httpBin.shutdown()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) _ = httpClient.get(url: "http://localhost:\(httpBin.port)/wait") do { try httpClient.syncShutdown(requiresCleanClose: true) @@ -1045,7 +1087,7 @@ class HTTPClientTests: XCTestCase { func testFailingConnectionIsReleased() { let httpBin = HTTPBin(refusesConnections: true) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) XCTAssertNoThrow(try httpBin.shutdown()) @@ -1063,7 +1105,7 @@ class HTTPClientTests: XCTestCase { func testResponseDelayGet() throws { let httpBin = HTTPBin(ssl: false) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -1079,7 +1121,7 @@ class HTTPClientTests: XCTestCase { func testIdleTimeoutNoReuse() throws { let httpBin = HTTPBin(ssl: false) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -1094,7 +1136,7 @@ class HTTPClientTests: XCTestCase { func testStressGetClose() throws { let httpBin = HTTPBin(ssl: false) - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: HTTPClient.Configuration(certificateVerification: .none)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -1122,7 +1164,7 @@ class HTTPClientTests: XCTestCase { defer { XCTAssertNoThrow(try httpBin.shutdown()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) } @@ -1158,12 +1200,12 @@ class HTTPClientTests: XCTestCase { } func testRepeatedRequestsWorkWhenServerAlwaysCloses() { - let web = NIOHTTP1TestServer(group: self.group) + let web = NIOHTTP1TestServer(group: self.serverGroup) defer { XCTAssertNoThrow(try web.stop()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) } @@ -1191,20 +1233,18 @@ class HTTPClientTests: XCTestCase { func testShutdownBeforeTasksCompletion() throws { let httpBin = HTTPBin() - let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let client = HTTPClient(eventLoopGroupProvider: .shared(elg)) + let client = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) let req = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)/get", method: .GET, headers: ["X-internal-delay": "500"]) let res = client.execute(request: req) XCTAssertNoThrow(try client.syncShutdown(requiresCleanClose: false)) _ = try? res.timeout(after: .seconds(2)).wait() try httpBin.shutdown() - try elg.syncShutdownGracefully() } /// This test would cause an assertion failure on `HTTPClient` deinit if client doesn't actually shutdown func testUncleanShutdownActuallyShutsDown() throws { let httpBin = HTTPBin() - let client = HTTPClient(eventLoopGroupProvider: .createNew) + let client = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) let req = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)/get", method: .GET, headers: ["X-internal-delay": "200"]) _ = client.execute(request: req) try? client.syncShutdown(requiresCleanClose: true) @@ -1213,12 +1253,10 @@ class HTTPClientTests: XCTestCase { func testUncleanShutdownCancelsTasks() throws { let httpBin = HTTPBin() - let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let client = HTTPClient(eventLoopGroupProvider: .shared(elg)) + let client = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpBin.shutdown()) - XCTAssertNoThrow(try elg.syncShutdownGracefully()) } let responses = (1...100).map { _ in @@ -1227,7 +1265,7 @@ class HTTPClientTests: XCTestCase { try client.syncShutdown(requiresCleanClose: false) - let results = try EventLoopFuture.whenAllComplete(responses, on: elg.next()).timeout(after: .seconds(100)).wait() + let results = try EventLoopFuture.whenAllComplete(responses, on: self.clientGroup.next()).timeout(after: .seconds(100)).wait() for result in results { switch result { @@ -1244,7 +1282,7 @@ class HTTPClientTests: XCTestCase { } func testDoubleShutdown() { - let client = HTTPClient(eventLoopGroupProvider: .createNew) + let client = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) XCTAssertNoThrow(try client.syncShutdown()) do { try client.syncShutdown() @@ -1258,11 +1296,7 @@ class HTTPClientTests: XCTestCase { } func testTaskFailsWhenClientIsShutdown() { - let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - XCTAssertNoThrow(try elg.syncShutdownGracefully()) - } - let client = HTTPClient(eventLoopGroupProvider: .shared(elg)) + let client = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) XCTAssertNoThrow(try client.syncShutdown(requiresCleanClose: true)) do { _ = try client.get(url: "http://localhost/").wait() @@ -1286,7 +1320,7 @@ class HTTPClientTests: XCTestCase { defer { XCTAssertNoThrow(try httpBin.shutdown()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertThrowsError(try httpClient.syncShutdown()) { error in XCTAssertEqual(.alreadyShutdown, error as? HTTPClientError) @@ -1336,7 +1370,7 @@ class HTTPClientTests: XCTestCase { } func testVaryingLoopPreference() throws { - let elg = MultiThreadedEventLoopGroup(numberOfThreads: 2) + let elg = getDefaultEventLoopGroup(numberOfThreads: 2) let first = elg.next() let second = elg.next() XCTAssert(first !== second) @@ -1374,13 +1408,9 @@ class HTTPClientTests: XCTestCase { } func testMakeSecondRequestDuringCancelledCallout() { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) // needs to be 1 thread! - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - let el = group.next() + let el = self.clientGroup.next() - let web = NIOHTTP1TestServer(group: el) + let web = NIOHTTP1TestServer(group: self.serverGroup.next()) defer { // This will throw as we've started the request but haven't fulfilled it. XCTAssertThrowsError(try web.stop()) @@ -1424,11 +1454,7 @@ class HTTPClientTests: XCTestCase { } func testMakeSecondRequestDuringSuccessCallout() { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) // needs to be 1 thread! - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - let el = group.next() + let el = self.clientGroup.next() let web = HTTPBin() defer { @@ -1451,12 +1477,12 @@ class HTTPClientTests: XCTestCase { } func testMakeSecondRequestWhilstFirstIsOngoing() { - let web = NIOHTTP1TestServer(group: self.group) + let web = NIOHTTP1TestServer(group: self.serverGroup) defer { XCTAssertNoThrow(try web.stop()) } - let client = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + let client = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try client.syncShutdown()) } @@ -1493,7 +1519,7 @@ class HTTPClientTests: XCTestCase { defer { XCTAssertNoThrow(try httpBin.shutdown()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) } @@ -1514,7 +1540,7 @@ class HTTPClientTests: XCTestCase { defer { XCTAssertNoThrow(try httpBin.shutdown()) } - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) } @@ -1531,7 +1557,7 @@ class HTTPClientTests: XCTestCase { func testUseExistingConnectionOnDifferentEL() throws { let threadCount = 16 - let elg = MultiThreadedEventLoopGroup(numberOfThreads: threadCount) + let elg = getDefaultEventLoopGroup(numberOfThreads: threadCount) let httpBin = HTTPBin() let httpClient = HTTPClient(eventLoopGroupProvider: .shared(elg)) defer { @@ -1598,7 +1624,7 @@ class HTTPClientTests: XCTestCase { let sharedStateServerHandler = ServerThatAcceptsThenRejects(requestNumber: requestNumber, connectionNumber: connectionNumber) var maybeServer: Channel? - XCTAssertNoThrow(maybeServer = try ServerBootstrap(group: self.group) + XCTAssertNoThrow(maybeServer = try ServerBootstrap(group: self.serverGroup) .serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1) .childChannelInitializer { channel in channel.pipeline.configureHTTPServerPipeline().flatMap { @@ -1618,7 +1644,7 @@ class HTTPClientTests: XCTestCase { } let url = "http://127.0.0.1:\(server.localAddress!.port!)" - let client = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + let client = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) defer { XCTAssertNoThrow(try client.syncShutdown()) } @@ -1640,7 +1666,7 @@ class HTTPClientTests: XCTestCase { func testPoolClosesIdleConnections() { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: .init(maximumAllowedIdleTimeInConnectionPool: .milliseconds(100))) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: .init(maximumAllowedIdleTimeInConnectionPool: .milliseconds(100))) defer { XCTAssertNoThrow(try httpBin.shutdown()) XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -1652,7 +1678,7 @@ class HTTPClientTests: XCTestCase { func testRacePoolIdleConnectionsAndGet() { let httpBin = HTTPBin() - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: .init(maximumAllowedIdleTimeInConnectionPool: .milliseconds(10))) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: .init(maximumAllowedIdleTimeInConnectionPool: .milliseconds(10))) defer { XCTAssertNoThrow(try httpBin.shutdown()) XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) @@ -1664,7 +1690,7 @@ class HTTPClientTests: XCTestCase { } func testAvoidLeakingTLSHandshakeCompletionPromise() { - let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) let httpBin = HTTPBin() let port = httpBin.port XCTAssertNoThrow(try httpBin.shutdown()) @@ -1673,18 +1699,24 @@ class HTTPClientTests: XCTestCase { } XCTAssertThrowsError(try httpClient.get(url: "http://localhost:\(port)").wait()) { error in - guard error is NIOConnectionError else { - XCTFail("Unexpected error: \(error)") - return + if isTestingNIOTS() { + guard case ChannelError.connectTimeout = error else { + XCTFail("Unexpected error: \(error)") + return + } + } else { + guard error is NIOConnectionError else { + XCTFail("Unexpected error: \(error)") + return + } } } } func testAsyncShutdown() { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup)) - let promise = eventLoopGroup.next().makePromise(of: Void.self) - eventLoopGroup.next().execute { + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup)) + let promise = self.clientGroup.next().makePromise(of: Void.self) + self.clientGroup.next().execute { httpClient.shutdown(queue: DispatchQueue(label: "testAsyncShutdown")) { error in XCTAssertNil(error) promise.succeed(()) diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 93e594a06..de6695f92 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -28,6 +28,7 @@ import XCTest XCTMain([ testCase(HTTPClientCookieTests.allTests), testCase(HTTPClientInternalTests.allTests), + testCase(HTTPClientNIOTSTests.allTests), testCase(HTTPClientTests.allTests), testCase(RequestValidationTests.allTests), ])