|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the AsyncHTTPClient open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors |
| 6 | +// Licensed under Apache License v2.0 |
| 7 | +// |
| 8 | +// See LICENSE.txt for license information |
| 9 | +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors |
| 10 | +// |
| 11 | +// SPDX-License-Identifier: Apache-2.0 |
| 12 | +// |
| 13 | +//===----------------------------------------------------------------------===// |
| 14 | + |
| 15 | +import Logging |
| 16 | +import NIO |
| 17 | +import NIOHTTP1 |
| 18 | +import NIOSSL |
| 19 | +import NIOTLS |
| 20 | +#if canImport(Network) |
| 21 | + import NIOTransportServices |
| 22 | +#endif |
| 23 | + |
| 24 | +extension HTTPConnectionPool { |
| 25 | + enum NegotiatedProtocol { |
| 26 | + case http1_1(Channel) |
| 27 | + case http2_0(Channel) |
| 28 | + } |
| 29 | + |
| 30 | + final class ConnectionFactory { |
| 31 | + let key: ConnectionPool.Key |
| 32 | + let clientConfiguration: HTTPClient.Configuration |
| 33 | + let tlsConfiguration: TLSConfiguration |
| 34 | + let sslContextCache: SSLContextCache |
| 35 | + |
| 36 | + init(key: ConnectionPool.Key, |
| 37 | + tlsConfiguration: TLSConfiguration?, |
| 38 | + clientConfiguration: HTTPClient.Configuration, |
| 39 | + sslContextCache: SSLContextCache) { |
| 40 | + self.key = key |
| 41 | + self.clientConfiguration = clientConfiguration |
| 42 | + self.sslContextCache = sslContextCache |
| 43 | + self.tlsConfiguration = tlsConfiguration ?? clientConfiguration.tlsConfiguration ?? .forClient() |
| 44 | + } |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +extension HTTPConnectionPool.ConnectionFactory { |
| 49 | + func makeBestChannel(connectionID: HTTPConnectionPool.Connection.ID, eventLoop: EventLoop, logger: Logger) -> EventLoopFuture<(Channel, HTTPVersion)> { |
| 50 | + if self.key.scheme.isProxyable, let proxy = self.clientConfiguration.proxy { |
| 51 | + return self.makeHTTPProxyChannel(proxy, connectionID: connectionID, eventLoop: eventLoop, logger: logger) |
| 52 | + } else { |
| 53 | + return self.makeChannel(eventLoop: eventLoop, logger: logger) |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + private func makeChannel(eventLoop: EventLoop, logger: Logger) -> EventLoopFuture<(Channel, HTTPVersion)> { |
| 58 | + switch self.key.scheme { |
| 59 | + case .http, .http_unix, .unix: |
| 60 | + return self.makePlainChannel(eventLoop: eventLoop).map { ($0, .http1_1) } |
| 61 | + case .https, .https_unix: |
| 62 | + return self.makeTLSChannel(eventLoop: eventLoop, logger: logger).map { |
| 63 | + (channel, negotiated) -> (Channel, HTTPVersion) in |
| 64 | + let version = negotiated == "h2" ? HTTPVersion.http2 : HTTPVersion.http1_1 |
| 65 | + return (channel, version) |
| 66 | + } |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + private func makePlainChannel(eventLoop: EventLoop) -> EventLoopFuture<Channel> { |
| 71 | + let bootstrap = self.makePlainBootstrap(eventLoop: eventLoop) |
| 72 | + |
| 73 | + switch self.key.scheme { |
| 74 | + case .http: |
| 75 | + return bootstrap.connect(host: self.key.host, port: self.key.port) |
| 76 | + case .http_unix, .unix: |
| 77 | + return bootstrap.connect(unixDomainSocketPath: self.key.unixPath) |
| 78 | + case .https, .https_unix: |
| 79 | + preconditionFailure("Unexpected schema") |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + private func makeHTTPProxyChannel( |
| 84 | + _ proxy: HTTPClient.Configuration.Proxy, |
| 85 | + connectionID: HTTPConnectionPool.Connection.ID, |
| 86 | + eventLoop: EventLoop, |
| 87 | + logger: Logger |
| 88 | + ) -> EventLoopFuture<(Channel, HTTPVersion)> { |
| 89 | + // A proxy connection starts with a plain text connection to the proxy server. After |
| 90 | + // the connection has been established with the proxy server, the connection might be |
| 91 | + // upgraded to TLS before we send our first request. |
| 92 | + let bootstrap = self.makePlainBootstrap(eventLoop: eventLoop) |
| 93 | + return bootstrap.connect(host: proxy.host, port: proxy.port).flatMap { channel in |
| 94 | + let connectPromise = channel.eventLoop.makePromise(of: Void.self) |
| 95 | + |
| 96 | + let encoder = HTTPRequestEncoder() |
| 97 | + let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes)) |
| 98 | + let proxyHandler = HTTP1ProxyConnectHandler( |
| 99 | + targetHost: self.key.host, |
| 100 | + targetPort: self.key.port, |
| 101 | + proxyAuthorization: proxy.authorization, |
| 102 | + connectPromise: connectPromise |
| 103 | + ) |
| 104 | + |
| 105 | + do { |
| 106 | + try channel.pipeline.syncOperations.addHandler(encoder) |
| 107 | + try channel.pipeline.syncOperations.addHandler(decoder) |
| 108 | + try channel.pipeline.syncOperations.addHandler(proxyHandler) |
| 109 | + } catch { |
| 110 | + return channel.eventLoop.makeFailedFuture(error) |
| 111 | + } |
| 112 | + |
| 113 | + return connectPromise.futureResult.flatMap { |
| 114 | + channel.pipeline.removeHandler(proxyHandler).flatMap { |
| 115 | + channel.pipeline.removeHandler(decoder).flatMap { |
| 116 | + channel.pipeline.removeHandler(encoder) |
| 117 | + } |
| 118 | + } |
| 119 | + }.flatMap { () -> EventLoopFuture<(Channel, HTTPVersion)> in |
| 120 | + switch self.key.scheme { |
| 121 | + case .unix, .http_unix, .https_unix: |
| 122 | + preconditionFailure("Unexpected scheme. Not supported for proxy!") |
| 123 | + case .http: |
| 124 | + return channel.eventLoop.makeSucceededFuture((channel, .http1_1)) |
| 125 | + case .https: |
| 126 | + var tlsConfig = self.tlsConfiguration |
| 127 | + // since we can support h2, we need to advertise this in alpn |
| 128 | + tlsConfig.applicationProtocols = ["http/1.1" /* , "h2" */ ] |
| 129 | + let tlsEventHandler = TLSEventsHandler() |
| 130 | + |
| 131 | + let sslContextFuture = self.sslContextCache.sslContext( |
| 132 | + tlsConfiguration: tlsConfig, |
| 133 | + eventLoop: channel.eventLoop, |
| 134 | + logger: logger |
| 135 | + ) |
| 136 | + |
| 137 | + return sslContextFuture.flatMap { sslContext -> EventLoopFuture<String?> in |
| 138 | + do { |
| 139 | + let sslHandler = try NIOSSLClientHandler( |
| 140 | + context: sslContext, |
| 141 | + serverHostname: self.key.host |
| 142 | + ) |
| 143 | + try channel.pipeline.syncOperations.addHandler(sslHandler) |
| 144 | + try channel.pipeline.syncOperations.addHandler(tlsEventHandler) |
| 145 | + return tlsEventHandler.tlsEstablishedFuture |
| 146 | + } catch { |
| 147 | + return channel.eventLoop.makeFailedFuture(error) |
| 148 | + } |
| 149 | + }.flatMap { negotiated -> EventLoopFuture<(Channel, HTTPVersion)> in |
| 150 | + channel.pipeline.removeHandler(tlsEventHandler).map { |
| 151 | + switch negotiated { |
| 152 | + case "h2": |
| 153 | + return (channel, .http2) |
| 154 | + default: |
| 155 | + return (channel, .http1_1) |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + } |
| 160 | + } |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + private func makePlainBootstrap(eventLoop: EventLoop) -> NIOClientTCPBootstrapProtocol { |
| 165 | + #if canImport(Network) |
| 166 | + if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) { |
| 167 | + return tsBootstrap |
| 168 | + .addTimeoutIfNeeded(self.clientConfiguration.timeout) |
| 169 | + .channelInitializer { channel in |
| 170 | + do { |
| 171 | + try channel.pipeline.syncOperations.addHandler(HTTPClient.NWErrorHandler()) |
| 172 | + return channel.eventLoop.makeSucceededFuture(()) |
| 173 | + } catch { |
| 174 | + return channel.eventLoop.makeFailedFuture(error) |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + #endif |
| 179 | + |
| 180 | + if let nioBootstrap = ClientBootstrap(validatingGroup: eventLoop) { |
| 181 | + return nioBootstrap |
| 182 | + .addTimeoutIfNeeded(self.clientConfiguration.timeout) |
| 183 | + } |
| 184 | + |
| 185 | + preconditionFailure("No matching bootstrap found") |
| 186 | + } |
| 187 | + |
| 188 | + private func makeTLSChannel(eventLoop: EventLoop, logger: Logger) -> EventLoopFuture<(Channel, String?)> { |
| 189 | + let bootstrapFuture = self.makeTLSBootstrap( |
| 190 | + eventLoop: eventLoop, |
| 191 | + logger: logger |
| 192 | + ) |
| 193 | + |
| 194 | + var channelFuture = bootstrapFuture.flatMap { bootstrap -> EventLoopFuture<Channel> in |
| 195 | + switch self.key.scheme { |
| 196 | + case .https: |
| 197 | + return bootstrap.connect(host: self.key.host, port: self.key.port) |
| 198 | + case .https_unix: |
| 199 | + return bootstrap.connect(unixDomainSocketPath: self.key.unixPath) |
| 200 | + case .http, .http_unix, .unix: |
| 201 | + preconditionFailure("Unexpected schema") |
| 202 | + } |
| 203 | + }.flatMap { channel -> EventLoopFuture<(Channel, String?)> in |
| 204 | + let tlsEventHandler = try! channel.pipeline.syncOperations.handler(type: TLSEventsHandler.self) |
| 205 | + return tlsEventHandler.tlsEstablishedFuture.flatMap { negotiated in |
| 206 | + channel.pipeline.removeHandler(tlsEventHandler).map { (channel, negotiated) } |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + #if canImport(Network) |
| 211 | + // If NIOTransportSecurity is used, we want to map NWErrors into NWPOsixErrors or NWTLSError. |
| 212 | + channelFuture = channelFuture.flatMapErrorThrowing { error in |
| 213 | + throw HTTPClient.NWErrorHandler.translateError(error) |
| 214 | + } |
| 215 | + #endif |
| 216 | + |
| 217 | + return channelFuture |
| 218 | + } |
| 219 | + |
| 220 | + private func makeTLSBootstrap(eventLoop: EventLoop, logger: Logger) |
| 221 | + -> EventLoopFuture<NIOClientTCPBootstrapProtocol> { |
| 222 | + // since we can support h2, we need to advertise this in alpn |
| 223 | + var tlsConfig = self.tlsConfiguration |
| 224 | + tlsConfig.applicationProtocols = ["http/1.1" /* , "h2" */ ] |
| 225 | + |
| 226 | + #if canImport(Network) |
| 227 | + if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) { |
| 228 | + // create NIOClientTCPBootstrap with NIOTS TLS provider |
| 229 | + let bootstrapFuture = tlsConfig.getNWProtocolTLSOptions(on: eventLoop).map { |
| 230 | + options -> NIOClientTCPBootstrapProtocol in |
| 231 | + |
| 232 | + tsBootstrap |
| 233 | + .addTimeoutIfNeeded(self.clientConfiguration.timeout) |
| 234 | + .tlsOptions(options) |
| 235 | + .channelInitializer { channel in |
| 236 | + do { |
| 237 | + try channel.pipeline.syncOperations.addHandler(HTTPClient.NWErrorHandler()) |
| 238 | + try channel.pipeline.syncOperations.addHandler(TLSEventsHandler()) |
| 239 | + return channel.eventLoop.makeSucceededFuture(()) |
| 240 | + } catch { |
| 241 | + return channel.eventLoop.makeFailedFuture(error) |
| 242 | + } |
| 243 | + } as NIOClientTCPBootstrapProtocol |
| 244 | + } |
| 245 | + return bootstrapFuture |
| 246 | + } |
| 247 | + #endif |
| 248 | + |
| 249 | + let host = self.key.host |
| 250 | + let hostname = (host.isIPAddress || host.isEmpty) ? nil : host |
| 251 | + |
| 252 | + let sslContextFuture = sslContextCache.sslContext( |
| 253 | + tlsConfiguration: tlsConfig, |
| 254 | + eventLoop: eventLoop, |
| 255 | + logger: logger |
| 256 | + ) |
| 257 | + |
| 258 | + let bootstrap = ClientBootstrap(group: eventLoop) |
| 259 | + .addTimeoutIfNeeded(self.clientConfiguration.timeout) |
| 260 | + .channelInitializer { channel in |
| 261 | + sslContextFuture.flatMap { (sslContext) -> EventLoopFuture<Void> in |
| 262 | + let sync = channel.pipeline.syncOperations |
| 263 | + |
| 264 | + do { |
| 265 | + let sslHandler = try NIOSSLClientHandler( |
| 266 | + context: sslContext, |
| 267 | + serverHostname: hostname |
| 268 | + ) |
| 269 | + let tlsEventHandler = TLSEventsHandler() |
| 270 | + |
| 271 | + try sync.addHandler(sslHandler) |
| 272 | + try sync.addHandler(tlsEventHandler) |
| 273 | + return channel.eventLoop.makeSucceededFuture(()) |
| 274 | + } catch { |
| 275 | + return channel.eventLoop.makeFailedFuture(error) |
| 276 | + } |
| 277 | + } |
| 278 | + } |
| 279 | + |
| 280 | + return eventLoop.makeSucceededFuture(bootstrap) |
| 281 | + } |
| 282 | +} |
| 283 | + |
| 284 | +extension ConnectionPool.Key.Scheme { |
| 285 | + var isProxyable: Bool { |
| 286 | + switch self { |
| 287 | + case .http, .https: |
| 288 | + return true |
| 289 | + case .unix, .http_unix, .https_unix: |
| 290 | + return false |
| 291 | + } |
| 292 | + } |
| 293 | +} |
| 294 | + |
| 295 | +extension NIOClientTCPBootstrapProtocol { |
| 296 | + func addTimeoutIfNeeded(_ config: HTTPClient.Configuration.Timeout?) -> Self { |
| 297 | + guard let connectTimeamount = config?.connect else { |
| 298 | + return self |
| 299 | + } |
| 300 | + return self.connectTimeout(connectTimeamount) |
| 301 | + } |
| 302 | +} |
| 303 | + |
| 304 | +private extension String { |
| 305 | + var isIPAddress: Bool { |
| 306 | + var ipv4Addr = in_addr() |
| 307 | + var ipv6Addr = in6_addr() |
| 308 | + |
| 309 | + return self.withCString { ptr in |
| 310 | + inet_pton(AF_INET, ptr, &ipv4Addr) == 1 || |
| 311 | + inet_pton(AF_INET6, ptr, &ipv6Addr) == 1 |
| 312 | + } |
| 313 | + } |
| 314 | +} |
0 commit comments