From 82868a8780b2f9722a9c57d3ea1fdad42b6a96f6 Mon Sep 17 00:00:00 2001 From: Karl <5254025+karwa@users.noreply.github.com> Date: Fri, 19 Nov 2021 18:27:12 +0100 Subject: [PATCH] Port to WebURL --- Package.swift | 3 + .../AsyncHTTPClient/ConnectionTarget.swift | 30 +++-- .../AsyncHTTPClient/DeconstructedURL.swift | 29 ++-- Sources/AsyncHTTPClient/HTTPClient.swift | 13 +- Sources/AsyncHTTPClient/HTTPHandler.swift | 77 ++++++----- .../RequestBag+StateMachine.swift | 6 +- .../HTTPClientInternalTests.swift | 56 +++++--- .../HTTPClientTests+XCTest.swift | 2 - .../HTTPClientTests.swift | 124 ++++++++---------- .../HTTPConnectionPoolTests.swift | 3 +- 10 files changed, 176 insertions(+), 167 deletions(-) diff --git a/Package.swift b/Package.swift index b96f4df0d..961306a80 100644 --- a/Package.swift +++ b/Package.swift @@ -27,6 +27,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.10.0"), .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.11.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"), + .package(url: "https://github.com/karwa/swift-url.git", .upToNextMinor(from: "0.2.0")), ], targets: [ .target( @@ -44,6 +45,7 @@ let package = Package( .product(name: "NIOSOCKS", package: "swift-nio-extras"), .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), .product(name: "Logging", package: "swift-log"), + .product(name: "WebURL", package: "swift-url"), ] ), .testTarget( @@ -59,6 +61,7 @@ let package = Package( .product(name: "NIOHTTP2", package: "swift-nio-http2"), .product(name: "NIOSOCKS", package: "swift-nio-extras"), .product(name: "Logging", package: "swift-log"), + .product(name: "WebURL", package: "swift-url"), ] ), ] diff --git a/Sources/AsyncHTTPClient/ConnectionTarget.swift b/Sources/AsyncHTTPClient/ConnectionTarget.swift index 3b4dfd465..1fcc87353 100644 --- a/Sources/AsyncHTTPClient/ConnectionTarget.swift +++ b/Sources/AsyncHTTPClient/ConnectionTarget.swift @@ -13,6 +13,8 @@ //===----------------------------------------------------------------------===// import enum NIOCore.SocketAddress +import struct NIOCore.ByteBuffer +import WebURL enum ConnectionTarget: Equatable, Hashable { // We keep the IP address serialization precisely as it is in the URL. @@ -24,19 +26,23 @@ enum ConnectionTarget: Equatable, Hashable { case domain(name: String, port: Int) case unixSocket(path: String) - init(remoteHost: String, port: Int) { - if let addr = try? SocketAddress(ipAddress: remoteHost, port: port) { - switch addr { - case .v6: - self = .ipAddress(serialization: "[\(remoteHost)]", address: addr) - case .v4: - self = .ipAddress(serialization: remoteHost, address: addr) - case .unixDomainSocket: - fatalError("Expected a remote host") + init(remoteHost: WebURL.Host, port: Int) { + switch remoteHost { + case .ipv6Address(let address): + let socketAddr = withUnsafeBytes(of: address.octets) { + try! SocketAddress(packedIPAddress: ByteBuffer(bytes: $0), port: port) } - } else { - precondition(!remoteHost.isEmpty, "HTTPClient.Request should already reject empty remote hostnames") - self = .domain(name: remoteHost, port: port) + self = .ipAddress(serialization: remoteHost.serialized, address: socketAddr) + case .ipv4Address(let address): + let socketAddr = withUnsafeBytes(of: address.octets) { + try! SocketAddress(packedIPAddress: ByteBuffer(bytes: $0), port: port) + } + self = .ipAddress(serialization: remoteHost.serialized, address: socketAddr) + case .domain(let name): + assert(!name.isEmpty, "WebURL ensures domains cannot be empty.") + self = .domain(name: name, port: port) + case .empty, .opaque: + fatalError("WebURL ensures these cannot occur for HTTP(S) URLs.") } } } diff --git a/Sources/AsyncHTTPClient/DeconstructedURL.swift b/Sources/AsyncHTTPClient/DeconstructedURL.swift index a4ab658c8..6e57b3332 100644 --- a/Sources/AsyncHTTPClient/DeconstructedURL.swift +++ b/Sources/AsyncHTTPClient/DeconstructedURL.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import struct Foundation.URL +import WebURL struct DeconstructedURL { var scheme: Scheme @@ -31,45 +31,40 @@ struct DeconstructedURL { } 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) + init(url: WebURL) throws { + + guard let scheme = Scheme(rawValue: url.scheme) else { + throw HTTPClientError.unsupportedScheme(url.scheme) } switch scheme { case .http, .https: - guard let host = url.host, !host.isEmpty else { - throw HTTPClientError.emptyHost - } + let host = url.host! self.init( scheme: scheme, - connectionTarget: .init(remoteHost: host, port: url.port ?? scheme.defaultPort), - uri: url.uri + connectionTarget: .init(remoteHost: host, port: url.portOrKnownDefault!), + uri: url.originFormRequestTarget ) case .httpUnix, .httpsUnix: - guard let socketPath = url.host, !socketPath.isEmpty else { + guard let socketPath = url.hostname?.percentDecoded(), !socketPath.isEmpty else { throw HTTPClientError.missingSocketPath } self.init( scheme: scheme, connectionTarget: .unixSocket(path: socketPath), - uri: url.uri + uri: url.originFormRequestTarget ) case .unix: - let socketPath = url.baseURL?.path ?? url.path - let uri = url.baseURL != nil ? url.uri : "/" + let socketPath = url.path.percentDecoded() guard !socketPath.isEmpty else { throw HTTPClientError.missingSocketPath } self.init( scheme: scheme, connectionTarget: .unixSocket(path: socketPath), - uri: uri + uri: "/" ) } } diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 1cc9b3bc4..554a18c52 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import Foundation +import Dispatch import Logging import NIOConcurrencyHelpers import NIOCore @@ -22,6 +22,7 @@ import NIOPosix import NIOSSL import NIOTLS import NIOTransportServices +import WebURL extension Logger { private func requestInfo(_ request: HTTPClient.Request) -> Logger.Metadata.Value { @@ -373,7 +374,7 @@ public class HTTPClient { /// - logger: The logger to use for this request. public func execute(_ method: HTTPMethod = .GET, socketPath: String, urlPath: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger? = nil) -> EventLoopFuture { do { - guard let url = URL(httpURLWithSocketPath: socketPath, uri: urlPath) else { + guard let url = WebURL(httpURLWithSocketPath: socketPath, uri: urlPath) else { throw HTTPClientError.invalidURL } let request = try Request(url: url, method: method, body: body) @@ -394,7 +395,7 @@ public class HTTPClient { /// - logger: The logger to use for this request. public func execute(_ method: HTTPMethod = .GET, secureSocketPath: String, urlPath: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger? = nil) -> EventLoopFuture { do { - guard let url = URL(httpsURLWithSocketPath: secureSocketPath, uri: urlPath) else { + guard let url = WebURL(httpsURLWithSocketPath: secureSocketPath, uri: urlPath) else { throw HTTPClientError.invalidURL } let request = try Request(url: url, method: method, body: body) @@ -874,10 +875,8 @@ extension HTTPClient.Configuration { public struct HTTPClientError: Error, Equatable, CustomStringConvertible { private enum Code: Equatable { case invalidURL - case emptyHost case missingSocketPath case alreadyShutdown - case emptyScheme case unsupportedScheme(String) case readTimeout case remoteConnectionClosed @@ -920,14 +919,10 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible { /// URL provided is invalid. public static let invalidURL = HTTPClientError(code: .invalidURL) - /// URL does not contain host. - public static let emptyHost = HTTPClientError(code: .emptyHost) /// URL does not contain a socketPath as a host for http(s)+unix shemes. public static let missingSocketPath = HTTPClientError(code: .missingSocketPath) /// Client is shutdown and cannot be used for new requests. public static let alreadyShutdown = HTTPClientError(code: .alreadyShutdown) - /// URL does not contain scheme. - public static let emptyScheme = HTTPClientError(code: .emptyScheme) /// Provided URL scheme is not supported, supported schemes are: `http` and `https` public static func unsupportedScheme(_ scheme: String) -> HTTPClientError { return HTTPClientError(code: .unsupportedScheme(scheme)) } /// Request timed out. diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 2cf805fcd..0a59b9ba8 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -12,12 +12,13 @@ // //===----------------------------------------------------------------------===// -import Foundation +import struct Foundation.Data import Logging import NIOConcurrencyHelpers import NIOCore import NIOHTTP1 import NIOSSL +import WebURL extension HTTPClient { /// Represent request body. @@ -95,7 +96,7 @@ extension HTTPClient { /// Request HTTP method, defaults to `GET`. public let method: HTTPMethod /// Remote URL. - public let url: URL + public let url: WebURL /// Remote HTTP scheme, resolved from `URL`. public var scheme: String { @@ -111,7 +112,7 @@ extension HTTPClient { struct RedirectState { var count: Int - var visited: Set? + var visited: Set? } /// Parsed, validated and deconstructed URL. @@ -151,7 +152,7 @@ extension HTTPClient { /// - `unsupportedScheme` if URL does contains unsupported HTTP scheme. /// - `emptyHost` if URL does not contains a host. public init(url: String, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws { - guard let url = URL(string: url) else { + guard let url = WebURL(url) else { throw HTTPClientError.invalidURL } @@ -170,7 +171,7 @@ extension HTTPClient { /// - `unsupportedScheme` if URL does contains unsupported HTTP scheme. /// - `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) throws { + public init(url: WebURL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil) throws { try self.init(url: url, method: method, headers: headers, body: body, tlsConfiguration: nil) } @@ -187,7 +188,7 @@ extension HTTPClient { /// - `unsupportedScheme` if URL does contains unsupported HTTP scheme. /// - `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 { + public init(url: WebURL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws { self.deconstructedURL = try DeconstructedURL(url: url) self.redirectState = nil @@ -518,56 +519,60 @@ extension HTTPClientResponseDelegate { public func didReceiveError(task: HTTPClient.Task, _: Error) {} } -extension URL { - var percentEncodedPath: String { - if self.path.isEmpty { - return "/" - } - return URLComponents(url: self, resolvingAgainstBaseURL: false)?.percentEncodedPath ?? self.path - } - - var uri: String { - var uri = self.percentEncodedPath +extension WebURL { - if let query = self.query { - uri += "?" + query + /// The origin-form request target, as defined by [RFC 7230](https://httpwg.org/specs/rfc7230.html#origin-form) + var originFormRequestTarget: String { + let pathCodeUnits = utf8.path + guard !pathCodeUnits.isEmpty else { + guard let query = utf8.query else { return "/" } + return "/?" + String(decoding: query, as: UTF8.self) } - - return uri - } - - func hasTheSameOrigin(as other: URL) -> Bool { - return self.host == other.host && self.scheme == other.scheme && self.port == other.port + return String(decoding: utf8[pathCodeUnits.startIndex..<(utf8.query?.endIndex ?? pathCodeUnits.endIndex)], as: UTF8.self) } - /// Initializes a newly created HTTP URL connecting to a unix domain socket path. The socket path is encoded as the URL's host, replacing percent encoding invalid path characters, and will use the "http+unix" scheme. + /// Initializes a URL connecting to a unix domain socket path over HTTP. + /// + /// The new URL will use the "http+unix" scheme, and its hostname will be the percent-encoded socket path. + /// The new URL's path and query will be derived from the given URI (or [origin-form request target][RFC7230]), + /// and will be lexically simplified as is required by the URL Standard (e.g. "/a/b/.." -> "/a/"). + /// + /// [RFC7230]: https://httpwg.org/specs/rfc7230.html#origin-form + /// /// - Parameters: /// - socketPath: The path to the unix domain socket to connect to. /// - uri: The URI path and query that will be sent to the server. public init?(httpURLWithSocketPath socketPath: String, uri: String = "/") { - guard let host = socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil } + let host = socketPath.percentEncoded(using: .urlComponentSet) var urlString: String if uri.hasPrefix("/") { urlString = "http+unix://\(host)\(uri)" } else { urlString = "http+unix://\(host)/\(uri)" } - self.init(string: urlString) + self.init(urlString) } - /// Initializes a newly created HTTPS URL connecting to a unix domain socket path over TLS. The socket path is encoded as the URL's host, replacing percent encoding invalid path characters, and will use the "https+unix" scheme. + /// Initializes a URL connecting to a unix domain socket path over HTTPS. + /// + /// The new URL will use the "https+unix" scheme, and its hostname will be the percent-encoded socket path. + /// The new URL's path and query will be derived from the given URI (or [origin-form request target][RFC7230]), + /// and will be lexically simplified as is required by the URL Standard (e.g. "/a/b/.." -> "/a/"). + /// + /// [RFC7230]: https://httpwg.org/specs/rfc7230.html#origin-form + /// /// - Parameters: /// - socketPath: The path to the unix domain socket to connect to. /// - uri: The URI path and query that will be sent to the server. public init?(httpsURLWithSocketPath socketPath: String, uri: String = "/") { - guard let host = socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil } + let host = socketPath.percentEncoded(using: .urlComponentSet) var urlString: String if uri.hasPrefix("/") { urlString = "https+unix://\(host)\(uri)" } else { urlString = "https+unix://\(host)/\(uri)" } - self.init(string: urlString) + self.init(urlString) } } @@ -659,7 +664,7 @@ internal struct RedirectHandler { let request: HTTPClient.Request let execute: (HTTPClient.Request) -> HTTPClient.Task - func redirectTarget(status: HTTPResponseStatus, headers: HTTPHeaders) -> URL? { + func redirectTarget(status: HTTPResponseStatus, headers: HTTPHeaders) -> WebURL? { switch status { case .movedPermanently, .found, .seeOther, .notModified, .useProxy, .temporaryRedirect, .permanentRedirect: break @@ -671,7 +676,7 @@ internal struct RedirectHandler { return nil } - guard let url = URL(string: location, relativeTo: request.url) else { + guard let url = request.url.resolve(location) else { return nil } @@ -679,14 +684,14 @@ internal struct RedirectHandler { return nil } - if url.isFileURL { + if url.scheme == "file" { return nil } - return url.absoluteURL + return url } - func redirect(status: HTTPResponseStatus, to redirectURL: URL, promise: EventLoopPromise) { + func redirect(status: HTTPResponseStatus, to redirectURL: WebURL, promise: EventLoopPromise) { var nextState: HTTPClient.Request.RedirectState? if var state = request.redirectState { guard state.count > 0 else { @@ -727,7 +732,7 @@ internal struct RedirectHandler { headers.remove(name: "Content-Type") } - if !originalRequest.url.hasTheSameOrigin(as: redirectURL) { + if originalRequest.url.origin != redirectURL.origin { headers.remove(name: "Origin") headers.remove(name: "Cookie") headers.remove(name: "Authorization") diff --git a/Sources/AsyncHTTPClient/RequestBag+StateMachine.swift b/Sources/AsyncHTTPClient/RequestBag+StateMachine.swift index c20d5e211..97c54dae4 100644 --- a/Sources/AsyncHTTPClient/RequestBag+StateMachine.swift +++ b/Sources/AsyncHTTPClient/RequestBag+StateMachine.swift @@ -12,9 +12,9 @@ // //===----------------------------------------------------------------------===// -import struct Foundation.URL import NIOCore import NIOHTTP1 +import WebURL extension RequestBag { struct StateMachine { @@ -23,7 +23,7 @@ extension RequestBag { case queued(HTTPRequestScheduler) case executing(HTTPRequestExecutor, RequestStreamState, ResponseStreamState) case finished(error: Error?) - case redirected(HTTPResponseHead, URL) + case redirected(HTTPResponseHead, WebURL) case modifying } @@ -325,7 +325,7 @@ extension RequestBag.StateMachine { enum ReceiveResponseEndAction { case consume(ByteBuffer) - case redirect(RedirectHandler, HTTPResponseHead, URL) + case redirect(RedirectHandler, HTTPResponseHead, WebURL) case succeedRequest case none } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift index 349f44e20..bb2e98725 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift @@ -106,40 +106,52 @@ class HTTPClientInternalTests: XCTestCase { func testRequestURITrailingSlash() throws { let request1 = try Request(url: "https://someserver.com:8888/some/path?foo=bar#ref") - XCTAssertEqual(request1.url.uri, "/some/path?foo=bar") + XCTAssertEqual(request1.url.originFormRequestTarget, "/some/path?foo=bar") let request2 = try Request(url: "https://someserver.com:8888/some/path/?foo=bar#ref") - XCTAssertEqual(request2.url.uri, "/some/path/?foo=bar") + XCTAssertEqual(request2.url.originFormRequestTarget, "/some/path/?foo=bar") let request3 = try Request(url: "https://someserver.com:8888?foo=bar#ref") - XCTAssertEqual(request3.url.uri, "/?foo=bar") + XCTAssertEqual(request3.url.originFormRequestTarget, "/?foo=bar") let request4 = try Request(url: "https://someserver.com:8888/?foo=bar#ref") - XCTAssertEqual(request4.url.uri, "/?foo=bar") + XCTAssertEqual(request4.url.originFormRequestTarget, "/?foo=bar") let request5 = try Request(url: "https://someserver.com:8888/some/path") - XCTAssertEqual(request5.url.uri, "/some/path") + XCTAssertEqual(request5.url.originFormRequestTarget, "/some/path") let request6 = try Request(url: "https://someserver.com:8888/some/path/") - XCTAssertEqual(request6.url.uri, "/some/path/") + XCTAssertEqual(request6.url.originFormRequestTarget, "/some/path/") let request7 = try Request(url: "https://someserver.com:8888") - XCTAssertEqual(request7.url.uri, "/") + XCTAssertEqual(request7.url.originFormRequestTarget, "/") let request8 = try Request(url: "https://someserver.com:8888/") - XCTAssertEqual(request8.url.uri, "/") + XCTAssertEqual(request8.url.originFormRequestTarget, "/") let request9 = try Request(url: "https://someserver.com:8888#ref") - XCTAssertEqual(request9.url.uri, "/") + XCTAssertEqual(request9.url.originFormRequestTarget, "/") let request10 = try Request(url: "https://someserver.com:8888/#ref") - XCTAssertEqual(request10.url.uri, "/") + XCTAssertEqual(request10.url.originFormRequestTarget, "/") let request11 = try Request(url: "https://someserver.com/some%20path") - XCTAssertEqual(request11.url.uri, "/some%20path") + XCTAssertEqual(request11.url.originFormRequestTarget, "/some%20path") let request12 = try Request(url: "https://someserver.com/some%2Fpathsegment1/pathsegment2") - XCTAssertEqual(request12.url.uri, "/some%2Fpathsegment1/pathsegment2") + XCTAssertEqual(request12.url.originFormRequestTarget, "/some%2Fpathsegment1/pathsegment2") + + let request13 = try Request(url: "https+unix://some%2Fsocket%2Fpath") + XCTAssertEqual(request13.url.originFormRequestTarget, "/") + + let request14 = try Request(url: "https+unix://some%2Fsocket%2Fpath?someQuery") + XCTAssertEqual(request14.url.originFormRequestTarget, "/?someQuery") + + let request15 = try Request(url: "https+unix://some%2Fsocket%2Fpath/request/path") + XCTAssertEqual(request15.url.originFormRequestTarget, "/request/path") + + let request16 = try Request(url: "https+unix://some%2Fsocket%2Fpath/request/path?someQuery") + XCTAssertEqual(request16.url.originFormRequestTarget, "/request/path?someQuery") } func testChannelAndDelegateOnDifferentEventLoops() throws { @@ -257,7 +269,7 @@ class HTTPClientInternalTests: XCTestCase { switch sentMessages.dropFirst(0).first { case .some(.sentRequestHead(let head)): - XCTAssertEqual(request.url.uri, head.uri) + XCTAssertEqual(request.url.originFormRequestTarget, head.uri) default: XCTFail("wrong message") } @@ -480,7 +492,10 @@ class HTTPClientInternalTests: XCTestCase { let request7 = try Request(url: "https://0x7F.1:9999") XCTAssertEqual(request7.deconstructedURL.scheme, .https) - XCTAssertEqual(request7.deconstructedURL.connectionTarget, .domain(name: "0x7F.1", port: 9999)) + XCTAssertEqual(request7.deconstructedURL.connectionTarget, .ipAddress( + serialization: "127.0.0.1", + address: try SocketAddress(ipAddress: "127.0.0.1", port: 9999) + )) XCTAssertEqual(request7.deconstructedURL.uri, "/") let request8 = try Request(url: "http://[::1]") @@ -494,15 +509,14 @@ class HTTPClientInternalTests: XCTestCase { let request9 = try Request(url: "http://[763e:61d9::6ACA:3100:6274]:4242/foo/bar?baz") XCTAssertEqual(request9.deconstructedURL.scheme, .http) XCTAssertEqual(request9.deconstructedURL.connectionTarget, .ipAddress( - serialization: "[763e:61d9::6ACA:3100:6274]", + serialization: "[763e:61d9::6aca:3100:6274]", address: try! SocketAddress(ipAddress: "763e:61d9::6aca:3100:6274", port: 4242) )) 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. + // Ensure WebURL normalizes these the same way on every system. + // Some platforms have quirks in their versions of 'ntop' which produce different + // serializations of certain IPv6 addresses. let request10 = try Request(url: "http://[::c0a8:1]:4242/foo/bar?baz") XCTAssertEqual(request10.deconstructedURL.scheme, .http) XCTAssertEqual(request10.deconstructedURL.connectionTarget, .ipAddress( @@ -514,8 +528,8 @@ class HTTPClientInternalTests: XCTestCase { let request11 = try Request(url: "http://[::192.168.0.1]:4242/foo/bar?baz") 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) + serialization: "[::c0a8:1]", + address: try! SocketAddress(ipAddress: "::c0a8:1", port: 4242) )) XCTAssertEqual(request11.deconstructedURL.uri, "/foo/bar?baz") } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift index 22659d32c..bbdd97074 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift @@ -29,7 +29,6 @@ extension HTTPClientTests { ("testBadRequestURI", testBadRequestURI), ("testSchemaCasing", testSchemaCasing), ("testURLSocketPathInitializers", testURLSocketPathInitializers), - ("testBadUnixWithBaseURL", testBadUnixWithBaseURL), ("testConvenienceExecuteMethods", testConvenienceExecuteMethods), ("testConvenienceExecuteMethodsOverSocket", testConvenienceExecuteMethodsOverSocket), ("testConvenienceExecuteMethodsOverSecureSocket", testConvenienceExecuteMethodsOverSecureSocket), @@ -92,7 +91,6 @@ extension HTTPClientTests { ("testMakeSecondRequestDuringSuccessCallout", testMakeSecondRequestDuringSuccessCallout), ("testMakeSecondRequestWhilstFirstIsOngoing", testMakeSecondRequestWhilstFirstIsOngoing), ("testUDSBasic", testUDSBasic), - ("testUDSSocketAndPath", testUDSSocketAndPath), ("testHTTPPlusUNIX", testHTTPPlusUNIX), ("testHTTPSPlusUNIX", testHTTPSPlusUNIX), ("testUseExistingConnectionOnDifferentEL", testUseExistingConnectionOnDifferentEL), diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index e7ba9d510..3e62016e4 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -27,6 +27,7 @@ import NIOSSL import NIOTestUtils import NIOTransportServices import XCTest +import WebURL class HTTPClientTests: XCTestCase { typealias Request = HTTPClient.Request @@ -84,17 +85,17 @@ class HTTPClientTests: XCTestCase { func testRequestURI() throws { let request1 = try Request(url: "https://someserver.com:8888/some/path?foo=bar") - XCTAssertEqual(request1.url.host, "someserver.com") + XCTAssertEqual(request1.url.hostname, "someserver.com") XCTAssertEqual(request1.url.path, "/some/path") - XCTAssertEqual(request1.url.query!, "foo=bar") + XCTAssertEqual(request1.url.query, "foo=bar") XCTAssertEqual(request1.port, 8888) XCTAssertTrue(request1.useTLS) let request2 = try Request(url: "https://someserver.com") - XCTAssertEqual(request2.url.path, "") + XCTAssertEqual(request2.url.path, "/") let request3 = try Request(url: "unix:///tmp/file") - XCTAssertNil(request3.url.host) + XCTAssertEqual(request3.url.hostname, "") XCTAssertEqual(request3.host, "") XCTAssertEqual(request3.url.path, "/tmp/file") XCTAssertEqual(request3.port, 80) @@ -102,26 +103,33 @@ class HTTPClientTests: XCTestCase { let request4 = try Request(url: "http+unix://%2Ftmp%2Ffile/file/path") XCTAssertEqual(request4.host, "") - XCTAssertEqual(request4.url.host, "/tmp/file") + XCTAssertEqual(request4.url.hostname?.percentDecoded(), "/tmp/file") XCTAssertEqual(request4.url.path, "/file/path") XCTAssertFalse(request4.useTLS) let request5 = try Request(url: "https+unix://%2Ftmp%2Ffile/file/path") XCTAssertEqual(request5.host, "") - XCTAssertEqual(request5.url.host, "/tmp/file") + XCTAssertEqual(request5.url.hostname?.percentDecoded(), "/tmp/file") XCTAssertEqual(request5.url.path, "/file/path") XCTAssertTrue(request5.useTLS) + + // This may seem strange, but it's what the URL standard says should happen. + let request6 = try Request(url: "https:/foo") + XCTAssertEqual(request6.url.hostname, "foo") + XCTAssertEqual(request6.url.path, "/") + XCTAssertEqual(request6.url.serialized(), "https://foo/") + XCTAssertTrue(request6.useTLS) } func testBadRequestURI() throws { XCTAssertThrowsError(try Request(url: "some/path"), "should throw") { error in - XCTAssertEqual(error as! HTTPClientError, HTTPClientError.emptyScheme) + XCTAssertEqual(error as! HTTPClientError, HTTPClientError.invalidURL) } XCTAssertThrowsError(try Request(url: "app://somewhere/some/path?foo=bar"), "should throw") { error in XCTAssertEqual(error as! HTTPClientError, HTTPClientError.unsupportedScheme("app")) } - XCTAssertThrowsError(try Request(url: "https:/foo"), "should throw") { error in - XCTAssertEqual(error as! HTTPClientError, HTTPClientError.emptyHost) + XCTAssertThrowsError(try Request(url: "unix:"), "should throw") { error in + XCTAssertEqual(error as! HTTPClientError, HTTPClientError.missingSocketPath) } XCTAssertThrowsError(try Request(url: "http+unix:///path"), "should throw") { error in XCTAssertEqual(error as! HTTPClientError, HTTPClientError.missingSocketPath) @@ -136,90 +144,94 @@ class HTTPClientTests: XCTestCase { } func testURLSocketPathInitializers() throws { - let url1 = URL(httpURLWithSocketPath: "/tmp/file") + let url1 = WebURL(httpURLWithSocketPath: "/tmp/file") XCTAssertNotNil(url1) if let url = url1 { XCTAssertEqual(url.scheme, "http+unix") - XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.hostname?.percentDecoded(), "/tmp/file") XCTAssertEqual(url.path, "/") - XCTAssertEqual(url.absoluteString, "http+unix://%2Ftmp%2Ffile/") + XCTAssertEqual(url.serialized(), "http+unix://%2Ftmp%2Ffile/") } - let url2 = URL(httpURLWithSocketPath: "/tmp/file", uri: "/file/path") + let url2 = WebURL(httpURLWithSocketPath: "/tmp/file", uri: "/file/path") XCTAssertNotNil(url2) if let url = url2 { XCTAssertEqual(url.scheme, "http+unix") - XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.hostname?.percentDecoded(), "/tmp/file") XCTAssertEqual(url.path, "/file/path") - XCTAssertEqual(url.absoluteString, "http+unix://%2Ftmp%2Ffile/file/path") + XCTAssertEqual(url.serialized(), "http+unix://%2Ftmp%2Ffile/file/path") } - let url3 = URL(httpURLWithSocketPath: "/tmp/file", uri: "file/path") + let url3 = WebURL(httpURLWithSocketPath: "/tmp/file", uri: "file/path") XCTAssertNotNil(url3) if let url = url3 { XCTAssertEqual(url.scheme, "http+unix") - XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.hostname?.percentDecoded(), "/tmp/file") XCTAssertEqual(url.path, "/file/path") - XCTAssertEqual(url.absoluteString, "http+unix://%2Ftmp%2Ffile/file/path") + XCTAssertEqual(url.serialized(), "http+unix://%2Ftmp%2Ffile/file/path") } - let url4 = URL(httpURLWithSocketPath: "/tmp/file with spacesと漢字", uri: "file/path") + let url4 = WebURL(httpURLWithSocketPath: "/tmp/file with spacesと漢字", uri: "file/path") XCTAssertNotNil(url4) if let url = url4 { XCTAssertEqual(url.scheme, "http+unix") - XCTAssertEqual(url.host, "/tmp/file with spacesと漢字") + XCTAssertEqual(url.hostname?.percentDecoded(), "/tmp/file with spacesと漢字") XCTAssertEqual(url.path, "/file/path") - XCTAssertEqual(url.absoluteString, "http+unix://%2Ftmp%2Ffile%20with%20spaces%E3%81%A8%E6%BC%A2%E5%AD%97/file/path") + XCTAssertEqual(url.serialized(), "http+unix://%2Ftmp%2Ffile%20with%20spaces%E3%81%A8%E6%BC%A2%E5%AD%97/file/path") } - let url5 = URL(httpsURLWithSocketPath: "/tmp/file") + let url5 = WebURL(httpsURLWithSocketPath: "/tmp/file") XCTAssertNotNil(url5) if let url = url5 { XCTAssertEqual(url.scheme, "https+unix") - XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.hostname?.percentDecoded(), "/tmp/file") XCTAssertEqual(url.path, "/") - XCTAssertEqual(url.absoluteString, "https+unix://%2Ftmp%2Ffile/") + XCTAssertEqual(url.serialized(), "https+unix://%2Ftmp%2Ffile/") } - let url6 = URL(httpsURLWithSocketPath: "/tmp/file", uri: "/file/path") + let url6 = WebURL(httpsURLWithSocketPath: "/tmp/file", uri: "/file/path") XCTAssertNotNil(url6) if let url = url6 { XCTAssertEqual(url.scheme, "https+unix") - XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.hostname?.percentDecoded(), "/tmp/file") XCTAssertEqual(url.path, "/file/path") - XCTAssertEqual(url.absoluteString, "https+unix://%2Ftmp%2Ffile/file/path") + XCTAssertEqual(url.serialized(), "https+unix://%2Ftmp%2Ffile/file/path") } - let url7 = URL(httpsURLWithSocketPath: "/tmp/file", uri: "file/path") + let url7 = WebURL(httpsURLWithSocketPath: "/tmp/file", uri: "file/path") XCTAssertNotNil(url7) if let url = url7 { XCTAssertEqual(url.scheme, "https+unix") - XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.hostname?.percentDecoded(), "/tmp/file") XCTAssertEqual(url.path, "/file/path") - XCTAssertEqual(url.absoluteString, "https+unix://%2Ftmp%2Ffile/file/path") + XCTAssertEqual(url.serialized(), "https+unix://%2Ftmp%2Ffile/file/path") } - let url8 = URL(httpsURLWithSocketPath: "/tmp/file with spacesと漢字", uri: "file/path") + let url8 = WebURL(httpsURLWithSocketPath: "/tmp/file with spacesと漢字", uri: "file/path") XCTAssertNotNil(url8) if let url = url8 { XCTAssertEqual(url.scheme, "https+unix") - XCTAssertEqual(url.host, "/tmp/file with spacesと漢字") + XCTAssertEqual(url.hostname?.percentDecoded(), "/tmp/file with spacesと漢字") XCTAssertEqual(url.path, "/file/path") - XCTAssertEqual(url.absoluteString, "https+unix://%2Ftmp%2Ffile%20with%20spaces%E3%81%A8%E6%BC%A2%E5%AD%97/file/path") + XCTAssertEqual(url.serialized(), "https+unix://%2Ftmp%2Ffile%20with%20spaces%E3%81%A8%E6%BC%A2%E5%AD%97/file/path") } - let url9 = URL(httpURLWithSocketPath: "/tmp/file", uri: " ") - XCTAssertNil(url9) - - let url10 = URL(httpsURLWithSocketPath: "/tmp/file", uri: " ") - XCTAssertNil(url10) - } + let url9 = WebURL(httpURLWithSocketPath: "/tmp/file", uri: " ") + XCTAssertNotNil(url9) + if let url = url9 { + XCTAssertEqual(url.scheme, "http+unix") + XCTAssertEqual(url.hostname?.percentDecoded(), "/tmp/file") + XCTAssertEqual(url.path, "/") + XCTAssertEqual(url.serialized(), "http+unix://%2Ftmp%2Ffile/") + } - func testBadUnixWithBaseURL() { - let badUnixBaseURL = URL(string: "/foo", relativeTo: URL(string: "unix:")!)! - XCTAssertEqual(badUnixBaseURL.baseURL?.path, "") - XCTAssertThrowsError(try Request(url: badUnixBaseURL)) { error in - XCTAssertEqual(error as! HTTPClientError, HTTPClientError.missingSocketPath) + let url10 = WebURL(httpsURLWithSocketPath: "/tmp/file", uri: " ") + XCTAssertNotNil(url10) + if let url = url9 { + XCTAssertEqual(url.scheme, "http+unix") + XCTAssertEqual(url.hostname?.percentDecoded(), "/tmp/file") + XCTAssertEqual(url.path, "/") + XCTAssertEqual(url.serialized(), "http+unix://%2Ftmp%2Ffile/") } } @@ -1536,26 +1548,6 @@ class HTTPClientTests: XCTestCase { }) } - func testUDSSocketAndPath() { - // Here, we're testing a URL that's encoding two different paths: - // - // 1. a "base path" which is the path to the UNIX domain socket - // 2. an actual path which is the normal path in a regular URL like https://example.com/this/is/the/path - XCTAssertNoThrow(try TemporaryFileHelpers.withTemporaryUnixDomainSocketPathName { path in - let localHTTPBin = HTTPBin(bindTarget: .unixDomainSocket(path)) - defer { - XCTAssertNoThrow(try localHTTPBin.shutdown()) - } - guard let target = URL(string: "/echo-uri", relativeTo: URL(string: "unix://\(path)")), - let request = try? Request(url: target) else { - XCTFail("couldn't build URL for request") - return - } - XCTAssertEqual(["/echo-uri"[...]], - try self.defaultClient.execute(request: request).wait().headers[canonicalForm: "X-Calling-URI"]) - }) - } - func testHTTPPlusUNIX() { // Here, we're testing a URL where the UNIX domain socket is encoded as the host name XCTAssertNoThrow(try TemporaryFileHelpers.withTemporaryUnixDomainSocketPathName { path in @@ -1563,7 +1555,7 @@ class HTTPClientTests: XCTestCase { defer { XCTAssertNoThrow(try localHTTPBin.shutdown()) } - guard let target = URL(httpURLWithSocketPath: path, uri: "/echo-uri"), + guard let target = WebURL(httpURLWithSocketPath: path, uri: "/echo-uri"), let request = try? Request(url: target) else { XCTFail("couldn't build URL for request") return @@ -1583,7 +1575,7 @@ class HTTPClientTests: XCTestCase { XCTAssertNoThrow(try localClient.syncShutdown()) XCTAssertNoThrow(try localHTTPBin.shutdown()) } - guard let target = URL(httpsURLWithSocketPath: path, uri: "/echo-uri"), + guard let target = WebURL(httpsURLWithSocketPath: path, uri: "/echo-uri"), let request = try? Request(url: target) else { XCTFail("couldn't build URL for request") return diff --git a/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift b/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift index 60e5077ee..4ce37a499 100644 --- a/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift @@ -125,7 +125,8 @@ class HTTPConnectionPoolTests: XCTestCase { } } - func testConnectionPoolGrowsToMaxConcurrentConnections() { + func testConnectionPoolGrowsToMaxConcurrentConnections() throws { + try XCTSkipIf(true, "Flaky test: https://github.com/swift-server/async-http-client/issues/508") let httpBin = HTTPBin() let maxConnections = 8 defer { XCTAssertNoThrow(try httpBin.shutdown()) }