diff --git a/Sources/AsyncHTTPClient/ConnectionPool.swift b/Sources/AsyncHTTPClient/ConnectionPool.swift index 291068c55..cd884fd26 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool.swift @@ -24,52 +24,13 @@ enum ConnectionPool { private var tlsConfiguration: BestEffortHashableTLSConfiguration? init(_ request: HTTPClient.Request) { - self.connectionTarget = request.connectionTarget - switch request.scheme { - case "http": - self.scheme = .http - case "https": - self.scheme = .https - case "unix": - self.scheme = .unix - case "http+unix": - self.scheme = .http_unix - case "https+unix": - self.scheme = .https_unix - default: - fatalError("HTTPClient.Request scheme should already be a valid one") - } + self.scheme = request.deconstructedURL.scheme + self.connectionTarget = request.deconstructedURL.connectionTarget if let tls = request.tlsConfiguration { self.tlsConfiguration = BestEffortHashableTLSConfiguration(wrapping: tls) } } - enum Scheme: Hashable { - case http - case https - case unix - case http_unix - case https_unix - - var requiresTLS: Bool { - switch self { - case .https, .https_unix: - return true - default: - return false - } - } - } - - /// Returns a key-specific `HTTPClient.Configuration` by overriding the properties of `base` - func config(overriding base: HTTPClient.Configuration) -> HTTPClient.Configuration { - var config = base - if let tlsConfiguration = self.tlsConfiguration { - config.tlsConfiguration = tlsConfiguration.base - } - return config - } - var description: String { var hasher = Hasher() self.tlsConfiguration?.hash(into: &hasher) diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift index 8b803f510..4a3338697 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift @@ -185,9 +185,9 @@ extension HTTPConnectionPool.ConnectionFactory { logger: Logger ) -> EventLoopFuture { switch self.key.scheme { - case .http, .http_unix, .unix: + case .http, .httpUnix, .unix: return self.makePlainChannel(deadline: deadline, eventLoop: eventLoop).map { .http1_1($0) } - case .https, .https_unix: + case .https, .httpsUnix: return self.makeTLSChannel(deadline: deadline, eventLoop: eventLoop, logger: logger).flatMapThrowing { channel, negotiated in @@ -197,7 +197,7 @@ extension HTTPConnectionPool.ConnectionFactory { } private func makePlainChannel(deadline: NIODeadline, eventLoop: EventLoop) -> EventLoopFuture { - precondition(!self.key.scheme.requiresTLS, "Unexpected scheme") + precondition(!self.key.scheme.usesTLS, "Unexpected scheme") return self.makePlainBootstrap(deadline: deadline, eventLoop: eventLoop).connect(target: self.key.connectionTarget) } @@ -283,7 +283,7 @@ extension HTTPConnectionPool.ConnectionFactory { logger: Logger ) -> EventLoopFuture { switch self.key.scheme { - case .unix, .http_unix, .https_unix: + case .unix, .httpUnix, .httpsUnix: preconditionFailure("Unexpected scheme. Not supported for proxy!") case .http: return channel.eventLoop.makeSucceededFuture(.http1_1(channel)) @@ -356,7 +356,7 @@ extension HTTPConnectionPool.ConnectionFactory { } private func makeTLSChannel(deadline: NIODeadline, eventLoop: EventLoop, logger: Logger) -> EventLoopFuture<(Channel, String?)> { - precondition(self.key.scheme.requiresTLS, "Unexpected scheme") + precondition(self.key.scheme.usesTLS, "Unexpected scheme") let bootstrapFuture = self.makeTLSBootstrap( deadline: deadline, eventLoop: eventLoop, @@ -470,12 +470,12 @@ extension HTTPConnectionPool.ConnectionFactory { } } -extension ConnectionPool.Key.Scheme { +extension Scheme { var isProxyable: Bool { switch self { case .http, .https: return true - case .unix, .http_unix, .https_unix: + case .unix, .httpUnix, .httpsUnix: return false } } diff --git a/Sources/AsyncHTTPClient/DeconstructedURL.swift b/Sources/AsyncHTTPClient/DeconstructedURL.swift new file mode 100644 index 000000000..a4ab658c8 --- /dev/null +++ b/Sources/AsyncHTTPClient/DeconstructedURL.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import struct Foundation.URL + +struct DeconstructedURL { + var scheme: Scheme + var connectionTarget: ConnectionTarget + var uri: String + + init( + scheme: Scheme, + connectionTarget: ConnectionTarget, + uri: String + ) { + self.scheme = scheme + self.connectionTarget = connectionTarget + self.uri = uri + } +} + +extension DeconstructedURL { + init(url: URL) throws { + guard let schemeString = url.scheme else { + throw HTTPClientError.emptyScheme + } + guard let scheme = Scheme(rawValue: schemeString.lowercased()) else { + throw HTTPClientError.unsupportedScheme(schemeString) + } + + switch scheme { + case .http, .https: + guard let host = url.host, !host.isEmpty else { + throw HTTPClientError.emptyHost + } + self.init( + scheme: scheme, + connectionTarget: .init(remoteHost: host, port: url.port ?? scheme.defaultPort), + uri: url.uri + ) + + case .httpUnix, .httpsUnix: + guard let socketPath = url.host, !socketPath.isEmpty else { + throw HTTPClientError.missingSocketPath + } + self.init( + scheme: scheme, + connectionTarget: .unixSocket(path: socketPath), + uri: url.uri + ) + + case .unix: + let socketPath = url.baseURL?.path ?? url.path + let uri = url.baseURL != nil ? url.uri : "/" + guard !socketPath.isEmpty else { + throw HTTPClientError.missingSocketPath + } + self.init( + scheme: scheme, + connectionTarget: .unixSocket(path: socketPath), + uri: uri + ) + } + } +} diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 576e0776c..2cf805fcd 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -92,82 +92,16 @@ extension HTTPClient { /// Represent HTTP request. public struct Request { - /// Represent kind of Request - enum Kind: Equatable { - enum UnixScheme: Equatable { - case baseURL - case http_unix - case https_unix - } - - /// Remote host request. - case host - /// UNIX Domain Socket HTTP request. - case unixSocket(_ scheme: UnixScheme) - - private static var hostRestrictedSchemes: Set = ["http", "https"] - private static var allSupportedSchemes: Set = ["http", "https", "unix", "http+unix", "https+unix"] - - func supportsRedirects(to scheme: String?) -> Bool { - guard let scheme = scheme?.lowercased() else { return false } - - switch self { - case .host: - return Kind.hostRestrictedSchemes.contains(scheme) - case .unixSocket: - return Kind.allSupportedSchemes.contains(scheme) - } - } - } - - static func useTLS(_ scheme: String) -> Bool { - return scheme == "https" || scheme == "https+unix" - } - - static func deconstructURL( - _ url: URL - ) throws -> (kind: Kind, scheme: String, connectionTarget: ConnectionTarget, uri: String) { - guard let scheme = url.scheme?.lowercased() else { - throw HTTPClientError.emptyScheme - } - switch scheme { - case "http", "https": - guard let host = url.host, !host.isEmpty else { - throw HTTPClientError.emptyHost - } - let defaultPort = self.useTLS(scheme) ? 443 : 80 - let hostTarget = ConnectionTarget(remoteHost: host, port: url.port ?? defaultPort) - return (.host, scheme, hostTarget, url.uri) - case "http+unix", "https+unix": - guard let socketPath = url.host, !socketPath.isEmpty else { - throw HTTPClientError.missingSocketPath - } - let socketTarget = ConnectionTarget.unixSocket(path: socketPath) - let kind = self.useTLS(scheme) ? Kind.UnixScheme.https_unix : .http_unix - return (.unixSocket(kind), scheme, socketTarget, url.uri) - case "unix": - let socketPath = url.baseURL?.path ?? url.path - let uri = url.baseURL != nil ? url.uri : "/" - guard !socketPath.isEmpty else { - throw HTTPClientError.missingSocketPath - } - let socketTarget = ConnectionTarget.unixSocket(path: socketPath) - return (.unixSocket(.baseURL), scheme, socketTarget, uri) - default: - throw HTTPClientError.unsupportedScheme(url.scheme!) - } - } - /// Request HTTP method, defaults to `GET`. public let method: HTTPMethod /// Remote URL. public let url: URL + /// Remote HTTP scheme, resolved from `URL`. - public let scheme: String - /// The connection target, resolved from `URL`. - let connectionTarget: ConnectionTarget - /// URI composed of the path and query, resolved from `URL`. - let uri: String + public var scheme: String { + self.deconstructedURL.scheme.rawValue + } + /// Request custom HTTP Headers, defaults to no headers. public var headers: HTTPHeaders /// Request body, defaults to no body. @@ -180,8 +114,10 @@ extension HTTPClient { var visited: Set? } + /// Parsed, validated and deconstructed URL. + let deconstructedURL: DeconstructedURL + var redirectState: RedirectState? - let kind: Kind /// Create HTTP request. /// @@ -252,7 +188,8 @@ extension HTTPClient { /// - `emptyHost` if URL does not contains a host. /// - `missingSocketPath` if URL does not contains a socketPath as an encoded host. public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws { - (self.kind, self.scheme, self.connectionTarget, self.uri) = try Request.deconstructURL(url) + self.deconstructedURL = try DeconstructedURL(url: url) + self.redirectState = nil self.url = url self.method = method @@ -261,14 +198,9 @@ extension HTTPClient { self.tlsConfiguration = tlsConfiguration } - /// Whether request will be executed using secure socket. - public var useTLS: Bool { - return Request.useTLS(self.scheme) - } - /// Remote host, resolved from `URL`. public var host: String { - switch self.connectionTarget { + switch self.deconstructedURL.connectionTarget { case .ipAddress(let serialization, _): return serialization case .domain(let name, _): return name case .unixSocket: return "" @@ -277,25 +209,28 @@ extension HTTPClient { /// Resolved port. public var port: Int { - switch self.connectionTarget { + switch self.deconstructedURL.connectionTarget { case .ipAddress(_, let address): return address.port! case .domain(_, let port): return port - case .unixSocket: return Request.useTLS(self.scheme) ? 443 : 80 + case .unixSocket: return self.deconstructedURL.scheme.defaultPort } } + /// Whether request will be executed using secure socket. + public var useTLS: Bool { self.deconstructedURL.scheme.usesTLS } + func createRequestHead() throws -> (HTTPRequestHead, RequestFramingMetadata) { var head = HTTPRequestHead( version: .http1_1, method: self.method, - uri: self.uri, + uri: self.deconstructedURL.uri, headers: self.headers ) if !head.headers.contains(name: "host") { let port = self.port var host = self.host - if !(port == 80 && self.scheme == "http"), !(port == 443 && self.scheme == "https") { + if !(port == 80 && self.deconstructedURL.scheme == .http), !(port == 443 && self.deconstructedURL.scheme == .https) { host += ":\(port)" } head.headers.add(name: "host", value: host) @@ -740,7 +675,7 @@ internal struct RedirectHandler { return nil } - guard self.request.kind.supportsRedirects(to: url.scheme) else { + guard self.request.deconstructedURL.scheme.supportsRedirects(to: url.scheme) else { return nil } diff --git a/Sources/AsyncHTTPClient/Scheme.swift b/Sources/AsyncHTTPClient/Scheme.swift new file mode 100644 index 000000000..16065a3c1 --- /dev/null +++ b/Sources/AsyncHTTPClient/Scheme.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// List of schemes `HTTPClient` currently supports +enum Scheme: String { + case http + case https + case unix + case httpUnix = "http+unix" + case httpsUnix = "https+unix" +} + +extension Scheme { + var usesTLS: Bool { + switch self { + case .http, .httpUnix, .unix: + return false + case .https, .httpsUnix: + return true + } + } + + var defaultPort: Int { + self.usesTLS ? 443 : 80 + } +} + +extension Scheme { + func supportsRedirects(to destinationScheme: String?) -> Bool { + guard + let destinationSchemeString = destinationScheme?.lowercased(), + let destinationScheme = Self(rawValue: destinationSchemeString) + else { + return false + } + return self.supportsRedirects(to: destinationScheme) + } + + func supportsRedirects(to destinationScheme: Self) -> Bool { + switch self { + case .http, .https: + switch destinationScheme { + case .http, .https: + return true + case .unix, .httpUnix, .httpsUnix: + return false + } + case .unix, .httpUnix, .httpsUnix: + return true + } + } +} diff --git a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift index 20081a04d..349f44e20 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift @@ -446,77 +446,77 @@ class HTTPClientInternalTests: XCTestCase { func testInternalRequestURI() throws { let request1 = try Request(url: "https://someserver.com:8888/some/path?foo=bar") - XCTAssertEqual(request1.kind, .host) - XCTAssertEqual(request1.connectionTarget, .domain(name: "someserver.com", port: 8888)) - XCTAssertEqual(request1.uri, "/some/path?foo=bar") + XCTAssertEqual(request1.deconstructedURL.scheme, .https) + XCTAssertEqual(request1.deconstructedURL.connectionTarget, .domain(name: "someserver.com", port: 8888)) + XCTAssertEqual(request1.deconstructedURL.uri, "/some/path?foo=bar") let request2 = try Request(url: "https://someserver.com") - XCTAssertEqual(request2.kind, .host) - XCTAssertEqual(request2.connectionTarget, .domain(name: "someserver.com", port: 443)) - XCTAssertEqual(request2.uri, "/") + XCTAssertEqual(request2.deconstructedURL.scheme, .https) + XCTAssertEqual(request2.deconstructedURL.connectionTarget, .domain(name: "someserver.com", port: 443)) + XCTAssertEqual(request2.deconstructedURL.uri, "/") let request3 = try Request(url: "unix:///tmp/file") - XCTAssertEqual(request3.kind, .unixSocket(.baseURL)) - XCTAssertEqual(request3.connectionTarget, .unixSocket(path: "/tmp/file")) - XCTAssertEqual(request3.uri, "/") + XCTAssertEqual(request3.deconstructedURL.scheme, .unix) + XCTAssertEqual(request3.deconstructedURL.connectionTarget, .unixSocket(path: "/tmp/file")) + XCTAssertEqual(request3.deconstructedURL.uri, "/") let request4 = try Request(url: "http+unix://%2Ftmp%2Ffile/file/path") - XCTAssertEqual(request4.kind, .unixSocket(.http_unix)) - XCTAssertEqual(request4.connectionTarget, .unixSocket(path: "/tmp/file")) - XCTAssertEqual(request4.uri, "/file/path") + XCTAssertEqual(request4.deconstructedURL.scheme, .httpUnix) + XCTAssertEqual(request4.deconstructedURL.connectionTarget, .unixSocket(path: "/tmp/file")) + XCTAssertEqual(request4.deconstructedURL.uri, "/file/path") let request5 = try Request(url: "https+unix://%2Ftmp%2Ffile/file/path") - XCTAssertEqual(request5.kind, .unixSocket(.https_unix)) - XCTAssertEqual(request5.connectionTarget, .unixSocket(path: "/tmp/file")) - XCTAssertEqual(request5.uri, "/file/path") + XCTAssertEqual(request5.deconstructedURL.scheme, .httpsUnix) + XCTAssertEqual(request5.deconstructedURL.connectionTarget, .unixSocket(path: "/tmp/file")) + XCTAssertEqual(request5.deconstructedURL.uri, "/file/path") let request6 = try Request(url: "https://127.0.0.1") - XCTAssertEqual(request6.kind, .host) - XCTAssertEqual(request6.connectionTarget, .ipAddress( + XCTAssertEqual(request6.deconstructedURL.scheme, .https) + XCTAssertEqual(request6.deconstructedURL.connectionTarget, .ipAddress( serialization: "127.0.0.1", address: try! SocketAddress(ipAddress: "127.0.0.1", port: 443) )) - XCTAssertEqual(request6.uri, "/") + XCTAssertEqual(request6.deconstructedURL.uri, "/") let request7 = try Request(url: "https://0x7F.1:9999") - XCTAssertEqual(request7.kind, .host) - XCTAssertEqual(request7.connectionTarget, .domain(name: "0x7F.1", port: 9999)) - XCTAssertEqual(request7.uri, "/") + XCTAssertEqual(request7.deconstructedURL.scheme, .https) + XCTAssertEqual(request7.deconstructedURL.connectionTarget, .domain(name: "0x7F.1", port: 9999)) + XCTAssertEqual(request7.deconstructedURL.uri, "/") let request8 = try Request(url: "http://[::1]") - XCTAssertEqual(request8.kind, .host) - XCTAssertEqual(request8.connectionTarget, .ipAddress( + XCTAssertEqual(request8.deconstructedURL.scheme, .http) + XCTAssertEqual(request8.deconstructedURL.connectionTarget, .ipAddress( serialization: "[::1]", address: try! SocketAddress(ipAddress: "::1", port: 80) )) - XCTAssertEqual(request8.uri, "/") + XCTAssertEqual(request8.deconstructedURL.uri, "/") let request9 = try Request(url: "http://[763e:61d9::6ACA:3100:6274]:4242/foo/bar?baz") - XCTAssertEqual(request9.kind, .host) - XCTAssertEqual(request9.connectionTarget, .ipAddress( + XCTAssertEqual(request9.deconstructedURL.scheme, .http) + XCTAssertEqual(request9.deconstructedURL.connectionTarget, .ipAddress( serialization: "[763e:61d9::6ACA:3100:6274]", address: try! SocketAddress(ipAddress: "763e:61d9::6aca:3100:6274", port: 4242) )) - XCTAssertEqual(request9.uri, "/foo/bar?baz") + XCTAssertEqual(request9.deconstructedURL.uri, "/foo/bar?baz") // Some systems have quirks in their implementations of 'ntop' which cause them to write // certain IPv6 addresses with embedded IPv4 parts (e.g. "::192.168.0.1" vs "::c0a8:1"). // We want to make sure that our request formatting doesn't depend on the platform's quirks, // so the serialization must be kept verbatim as it was given in the request. let request10 = try Request(url: "http://[::c0a8:1]:4242/foo/bar?baz") - XCTAssertEqual(request10.kind, .host) - XCTAssertEqual(request10.connectionTarget, .ipAddress( + XCTAssertEqual(request10.deconstructedURL.scheme, .http) + XCTAssertEqual(request10.deconstructedURL.connectionTarget, .ipAddress( serialization: "[::c0a8:1]", address: try! SocketAddress(ipAddress: "::c0a8:1", port: 4242) )) - XCTAssertEqual(request9.uri, "/foo/bar?baz") + XCTAssertEqual(request10.deconstructedURL.uri, "/foo/bar?baz") let request11 = try Request(url: "http://[::192.168.0.1]:4242/foo/bar?baz") - XCTAssertEqual(request11.kind, .host) - XCTAssertEqual(request11.connectionTarget, .ipAddress( + XCTAssertEqual(request11.deconstructedURL.scheme, .http) + XCTAssertEqual(request11.deconstructedURL.connectionTarget, .ipAddress( serialization: "[::192.168.0.1]", address: try! SocketAddress(ipAddress: "::192.168.0.1", port: 4242) )) - XCTAssertEqual(request11.uri, "/foo/bar?baz") + XCTAssertEqual(request11.deconstructedURL.uri, "/foo/bar?baz") } }