Files
async-http-client/Sources/AsyncHTTPClient/Utils.swift
T
Adam Fowler 9cdf8a01e5 Generate trust roots SecCertificate for Transport Services (#350)
This PR is a result of another #321.

In that PR I provided an alternative structure to TLSConfiguration for when connecting with Transport Services.

In this one I construct the NWProtocolTLS.Options from TLSConfiguration. It does mean a little more work for whenever we make a connection, but having spoken to @weissi he doesn't seem to think that is an issue.

Also there is no method to create a SecIdentity at the moment. We need to generate a pkcs#12 from the certificate chain and private key, which can then be used to create the SecIdentity.

This should resolve #292
2021-05-13 13:59:18 +01:00

283 lines
13 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2018-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
//
//===----------------------------------------------------------------------===//
import Foundation
#if canImport(Network)
import Network
#endif
import Logging
import NIO
import NIOHTTP1
import NIOHTTPCompression
import NIOSSL
import NIOTransportServices
internal extension String {
var isIPAddress: Bool {
var ipv4Addr = in_addr()
var ipv6Addr = in6_addr()
return self.withCString { ptr in
inet_pton(AF_INET, ptr, &ipv4Addr) == 1 ||
inet_pton(AF_INET6, ptr, &ipv6Addr) == 1
}
}
}
public final class HTTPClientCopyingDelegate: HTTPClientResponseDelegate {
public typealias Response = Void
let chunkHandler: (ByteBuffer) -> EventLoopFuture<Void>
public init(chunkHandler: @escaping (ByteBuffer) -> EventLoopFuture<Void>) {
self.chunkHandler = chunkHandler
}
public func didReceiveBodyPart(task: HTTPClient.Task<Void>, _ buffer: ByteBuffer) -> EventLoopFuture<Void> {
return self.chunkHandler(buffer)
}
public func didFinishRequest(task: HTTPClient.Task<Void>) throws {
return ()
}
}
extension NIOClientTCPBootstrap {
static func makeHTTP1Channel(destination: ConnectionPool.Key,
eventLoop: EventLoop,
configuration: HTTPClient.Configuration,
sslContextCache: SSLContextCache,
preference: HTTPClient.EventLoopPreference,
logger: Logger) -> EventLoopFuture<Channel> {
let channelEventLoop = preference.bestEventLoop ?? eventLoop
let key = destination
let requiresTLS = key.scheme.requiresTLS
let sslContext: EventLoopFuture<NIOSSLContext?>
if key.scheme.requiresTLS, configuration.proxy != nil {
// If we use a proxy & also require TLS, then we always use NIOSSL (and not Network.framework TLS because
// it can't be added later) and therefore require a `NIOSSLContext`.
// In this case, `makeAndConfigureBootstrap` will not create another `NIOSSLContext`.
//
// Note that TLS proxies are not supported at the moment. This means that we will always speak
// plaintext to the proxy but we do support sending HTTPS traffic through the proxy.
sslContext = sslContextCache.sslContext(tlsConfiguration: configuration.tlsConfiguration ?? .forClient(),
eventLoop: eventLoop,
logger: logger).map { $0 }
} else {
sslContext = eventLoop.makeSucceededFuture(nil)
}
let bootstrap = NIOClientTCPBootstrap.makeAndConfigureBootstrap(on: channelEventLoop,
host: key.host,
port: key.port,
requiresTLS: requiresTLS,
configuration: configuration,
sslContextCache: sslContextCache,
logger: logger)
return bootstrap.flatMap { bootstrap -> EventLoopFuture<Channel> in
let channel: EventLoopFuture<Channel>
switch key.scheme {
case .http, .https:
let address = HTTPClient.resolveAddress(host: key.host, port: key.port, proxy: configuration.proxy)
channel = bootstrap.connect(host: address.host, port: address.port)
case .unix, .http_unix, .https_unix:
channel = bootstrap.connect(unixDomainSocketPath: key.unixPath)
}
return channel.flatMap { channel -> EventLoopFuture<(Channel, NIOSSLContext?)> in
sslContext.map { sslContext -> (Channel, NIOSSLContext?) in
(channel, sslContext)
}
}.flatMap { channel, sslContext in
configureChannelPipeline(channel,
isNIOTS: bootstrap.isNIOTS,
sslContext: sslContext,
configuration: configuration,
key: key)
}.flatMapErrorThrowing { error in
if bootstrap.isNIOTS {
throw HTTPClient.NWErrorHandler.translateError(error)
} else {
throw error
}
}
}
}
/// Creates and configures a bootstrap given the `eventLoop`, if TLS/a proxy is being used.
private static func makeAndConfigureBootstrap(
on eventLoop: EventLoop,
host: String,
port: Int,
requiresTLS: Bool,
configuration: HTTPClient.Configuration,
sslContextCache: SSLContextCache,
logger: Logger
) -> EventLoopFuture<NIOClientTCPBootstrap> {
return self.makeBestBootstrap(host: host,
eventLoop: eventLoop,
requiresTLS: requiresTLS,
sslContextCache: sslContextCache,
tlsConfiguration: configuration.tlsConfiguration ?? .forClient(),
useProxy: configuration.proxy != nil,
logger: logger)
.map { bootstrap -> NIOClientTCPBootstrap in
var bootstrap = bootstrap
if let timeout = configuration.timeout.connect {
bootstrap = bootstrap.connectTimeout(timeout)
}
// Don't enable TLS if we have a proxy, this will be enabled later on (outside of this method).
if requiresTLS, configuration.proxy == nil {
bootstrap = bootstrap.enableTLS()
}
return bootstrap.channelInitializer { channel in
do {
if let proxy = configuration.proxy {
try channel.pipeline.syncAddProxyHandler(host: host,
port: port,
authorization: proxy.authorization)
} else if requiresTLS {
// We only add the handshake verifier if we need TLS and we're not going through a proxy.
// If we're going through a proxy we add it later (outside of this method).
let completionPromise = channel.eventLoop.makePromise(of: Void.self)
try channel.pipeline.syncOperations.addHandler(TLSEventsHandler(completionPromise: completionPromise),
name: TLSEventsHandler.handlerName)
}
return channel.eventLoop.makeSucceededVoidFuture()
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
}
}
}
/// Creates the best-suited bootstrap given an `EventLoop` and pairs it with an appropriate TLS provider.
private static func makeBestBootstrap(
host: String,
eventLoop: EventLoop,
requiresTLS: Bool,
sslContextCache: SSLContextCache,
tlsConfiguration: TLSConfiguration,
useProxy: Bool,
logger: Logger
) -> EventLoopFuture<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) {
// create NIOClientTCPBootstrap with NIOTS TLS provider
return tlsConfiguration.getNWProtocolTLSOptions(on: eventLoop)
.map { parameters in
let tlsProvider = NIOTSClientTLSProvider(tlsOptions: parameters)
return NIOClientTCPBootstrap(tsBootstrap, tls: tlsProvider)
}
}
#endif
if let clientBootstrap = ClientBootstrap(validatingGroup: eventLoop) {
// If there is a proxy don't create TLS provider as it will be added at a later point.
if !requiresTLS || useProxy {
return eventLoop.makeSucceededFuture(NIOClientTCPBootstrap(clientBootstrap,
tls: NIOInsecureNoTLS()))
} else {
return sslContextCache.sslContext(tlsConfiguration: tlsConfiguration,
eventLoop: eventLoop,
logger: logger)
.flatMapThrowing { sslContext in
let hostname = (host.isIPAddress || host.isEmpty) ? nil : host
let tlsProvider = try NIOSSLClientTLSProvider<ClientBootstrap>(context: sslContext, serverHostname: hostname)
return NIOClientTCPBootstrap(clientBootstrap, tls: tlsProvider)
}
}
}
preconditionFailure("Cannot create bootstrap for event loop \(eventLoop)")
}
}
private func configureChannelPipeline(_ channel: Channel,
isNIOTS: Bool,
sslContext: NIOSSLContext?,
configuration: HTTPClient.Configuration,
key: ConnectionPool.Key) -> EventLoopFuture<Channel> {
let requiresTLS = key.scheme.requiresTLS
let handshakeFuture: EventLoopFuture<Void>
if requiresTLS, configuration.proxy != nil {
let handshakePromise = channel.eventLoop.makePromise(of: Void.self)
channel.pipeline.syncAddLateSSLHandlerIfNeeded(for: key,
sslContext: sslContext!,
handshakePromise: handshakePromise)
handshakeFuture = handshakePromise.futureResult
} else if requiresTLS {
do {
handshakeFuture = try channel.pipeline.syncOperations.handler(type: TLSEventsHandler.self).completionPromise.futureResult
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
} else {
handshakeFuture = channel.eventLoop.makeSucceededVoidFuture()
}
return handshakeFuture.flatMapThrowing {
let syncOperations = channel.pipeline.syncOperations
// If we got here and we had a TLSEventsHandler in the pipeline, we can remove it ow.
if requiresTLS {
channel.pipeline.removeHandler(name: TLSEventsHandler.handlerName, promise: nil)
}
try syncOperations.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
if isNIOTS {
try syncOperations.addHandler(HTTPClient.NWErrorHandler(), position: .first)
}
switch configuration.decompression {
case .disabled:
()
case .enabled(let limit):
let decompressHandler = NIOHTTPResponseDecompressor(limit: limit)
try syncOperations.addHandler(decompressHandler)
}
return channel
}
}
extension Connection {
func removeHandler<Handler: RemovableChannelHandler>(_ type: Handler.Type) -> EventLoopFuture<Void> {
return self.channel.pipeline.handler(type: type).flatMap { handler in
self.channel.pipeline.removeHandler(handler)
}.recover { _ in }
}
}
extension NIOClientTCPBootstrap {
var isNIOTS: Bool {
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
return self.underlyingBootstrap is NIOTSConnectionBootstrap
} else {
return false
}
#else
return false
#endif
}
}