diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift new file mode 100644 index 000000000..8eacb4f47 --- /dev/null +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2021 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 compiler(>=5.5) && canImport(_Concurrency) +import NIOHTTP1 + +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +extension HTTPClientRequest { + struct Prepared { + var poolKey: ConnectionPool.Key + var requestFramingMetadata: RequestFramingMetadata + var head: HTTPRequestHead + var body: Body? + } +} + +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +extension HTTPClientRequest.Prepared { + init(_ request: HTTPClientRequest) throws { + let url = try DeconstructedURL(url: request.url) + + var headers = request.headers + headers.addHostIfNeeded(for: url) + let metadata = try headers.validateAndSetTransportFraming( + method: request.method, + bodyLength: .init(request.body) + ) + + self.init( + poolKey: .init(url: url, tlsConfiguration: nil), + requestFramingMetadata: metadata, + head: .init( + version: .http1_1, + method: request.method, + uri: url.uri, + headers: headers + ), + body: request.body + ) + } +} + +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +extension RequestBodyLength { + init(_ body: HTTPClientRequest.Body?) { + switch body?.mode { + case .none: + self = .fixed(length: 0) + case .byteBuffer(let buffer): + self = .fixed(length: buffer.readableBytes) + case .sequence(nil, _), .asyncSequence(nil, _): + self = .dynamic + case .sequence(.some(let length), _), .asyncSequence(.some(let length), _): + self = .fixed(length: length) + } + } +} + +#endif diff --git a/Sources/AsyncHTTPClient/ConnectionPool.swift b/Sources/AsyncHTTPClient/ConnectionPool.swift index cd884fd26..0dac50e5f 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import NIOSSL + enum ConnectionPool { /// Used by the `ConnectionPool` to index its `HTTP1ConnectionProvider`s /// @@ -23,12 +25,14 @@ enum ConnectionPool { var connectionTarget: ConnectionTarget private var tlsConfiguration: BestEffortHashableTLSConfiguration? - init(_ request: HTTPClient.Request) { - self.scheme = request.deconstructedURL.scheme - self.connectionTarget = request.deconstructedURL.connectionTarget - if let tls = request.tlsConfiguration { - self.tlsConfiguration = BestEffortHashableTLSConfiguration(wrapping: tls) - } + init( + scheme: Scheme, + connectionTarget: ConnectionTarget, + tlsConfiguration: BestEffortHashableTLSConfiguration? = nil + ) { + self.scheme = scheme + self.connectionTarget = connectionTarget + self.tlsConfiguration = tlsConfiguration } var description: String { @@ -48,3 +52,22 @@ enum ConnectionPool { } } } + +extension ConnectionPool.Key { + init(url: DeconstructedURL, tlsConfiguration: TLSConfiguration?) { + self.init( + scheme: url.scheme, + connectionTarget: url.connectionTarget, + tlsConfiguration: tlsConfiguration.map { + BestEffortHashableTLSConfiguration(wrapping: $0) + } + ) + } + + init(_ request: HTTPClient.Request) { + self.init( + url: request.deconstructedURL, + tlsConfiguration: request.tlsConfiguration + ) + } +} diff --git a/Sources/AsyncHTTPClient/ConnectionTarget.swift b/Sources/AsyncHTTPClient/ConnectionTarget.swift index 3b4dfd465..207a6e1bb 100644 --- a/Sources/AsyncHTTPClient/ConnectionTarget.swift +++ b/Sources/AsyncHTTPClient/ConnectionTarget.swift @@ -40,3 +40,25 @@ enum ConnectionTarget: Equatable, Hashable { } } } + +extension ConnectionTarget { + /// The host name which will be send as an HTTP `Host` header. + /// Only returns nil if the `self` is a `unixSocket`. + var host: String? { + switch self { + case .ipAddress(let serialization, _): return serialization + case .domain(let name, _): return name + case .unixSocket: return nil + } + } + + /// The host name which will be send as an HTTP host header. + /// Only returns nil if the `self` is a `unixSocket`. + var port: Int? { + switch self { + case .ipAddress(_, let address): return address.port! + case .domain(_, let port): return port + case .unixSocket: return nil + } + } +} diff --git a/Sources/AsyncHTTPClient/DeconstructedURL.swift b/Sources/AsyncHTTPClient/DeconstructedURL.swift index a4ab658c8..020c17455 100644 --- a/Sources/AsyncHTTPClient/DeconstructedURL.swift +++ b/Sources/AsyncHTTPClient/DeconstructedURL.swift @@ -31,6 +31,13 @@ struct DeconstructedURL { } extension DeconstructedURL { + init(url: String) throws { + guard let url = URL(string: url) else { + throw HTTPClientError.invalidURL + } + try self.init(url: url) + } + init(url: URL) throws { guard let schemeString = url.scheme else { throw HTTPClientError.emptyScheme diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 2cf805fcd..e745726cb 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -200,20 +200,12 @@ extension HTTPClient { /// Remote host, resolved from `URL`. public var host: String { - switch self.deconstructedURL.connectionTarget { - case .ipAddress(let serialization, _): return serialization - case .domain(let name, _): return name - case .unixSocket: return "" - } + self.deconstructedURL.connectionTarget.host ?? "" } /// Resolved port. public var port: Int { - switch self.deconstructedURL.connectionTarget { - case .ipAddress(_, let address): return address.port! - case .domain(_, let port): return port - case .unixSocket: return self.deconstructedURL.scheme.defaultPort - } + self.deconstructedURL.connectionTarget.port ?? self.deconstructedURL.scheme.defaultPort } /// Whether request will be executed using secure socket. @@ -227,14 +219,7 @@ extension HTTPClient { headers: self.headers ) - if !head.headers.contains(name: "host") { - let port = self.port - var host = self.host - if !(port == 80 && self.deconstructedURL.scheme == .http), !(port == 443 && self.deconstructedURL.scheme == .https) { - host += ":\(port)" - } - head.headers.add(name: "host", value: host) - } + head.headers.addHostIfNeeded(for: self.deconstructedURL) let metadata = try head.headers.validateAndSetTransportFraming(method: self.method, bodyLength: .init(self.body)) diff --git a/Sources/AsyncHTTPClient/RequestValidation.swift b/Sources/AsyncHTTPClient/RequestValidation.swift index 359b1496a..ea0370484 100644 --- a/Sources/AsyncHTTPClient/RequestValidation.swift +++ b/Sources/AsyncHTTPClient/RequestValidation.swift @@ -110,3 +110,20 @@ extension HTTPHeaders { } } } + +extension HTTPHeaders { + mutating func addHostIfNeeded(for url: DeconstructedURL) { + // if no host header was set, let's use the url host + guard !self.contains(name: "host"), + var host = url.connectionTarget.host + else { + return + } + // if the request uses a non-default port, we need to add it after the host + if let port = url.connectionTarget.port, + port != url.scheme.defaultPort { + host += ":\(port)" + } + self.add(name: "host", value: host) + } +}