diff --git a/README.md b/README.md index a47e2572a..0474f6b17 100644 --- a/README.md +++ b/README.md @@ -157,3 +157,22 @@ httpClient.execute(request: request, delegate: delegate).futureResult.whenSucces print(count) } ``` + +### Unix Domain Socket Paths +Connecting to servers bound to socket paths is easy: +```swift +let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) +httpClient.execute(.GET, socketPath: "/tmp/myServer.socket", urlPath: "/path/to/resource").whenComplete (...) +``` + +Connecting over TLS to a unix domain socket path is possible as well: +```swift +let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) +httpClient.execute(.POST, secureSocketPath: "/tmp/myServer.socket", urlPath: "/path/to/resource", body: .string("hello")).whenComplete (...) +``` + +Direct URLs can easily be contructed to be executed in other scenarios: +```swift +let socketPathBasedURL = URL(httpURLWithSocketPath: "/tmp/myServer.socket", uri: "/path/to/resource") +let secureSocketPathBasedURL = URL(httpsURLWithSocketPath: "/tmp/myServer.socket", uri: "/path/to/resource") +``` diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index faa31d06d..1910157dc 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -237,12 +237,7 @@ public class HTTPClient { /// - deadline: Point in time by which the request must complete. /// - logger: The logger to use for this request. public func get(url: String, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture { - do { - let request = try Request(url: url, method: .GET) - return self.execute(request: request, deadline: deadline, logger: logger) - } catch { - return self.eventLoopGroup.next().makeFailedFuture(error) - } + return self.execute(.GET, url: url, deadline: deadline, logger: logger) } /// Execute `POST` request using specified URL. @@ -263,12 +258,7 @@ public class HTTPClient { /// - deadline: Point in time by which the request must complete. /// - logger: The logger to use for this request. public func post(url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture { - do { - let request = try HTTPClient.Request(url: url, method: .POST, body: body) - return self.execute(request: request, deadline: deadline, logger: logger) - } catch { - return self.eventLoopGroup.next().makeFailedFuture(error) - } + return self.execute(.POST, url: url, body: body, deadline: deadline, logger: logger) } /// Execute `PATCH` request using specified URL. @@ -277,9 +267,8 @@ public class HTTPClient { /// - url: Remote URL. /// - body: Request body. /// - deadline: Point in time by which the request must complete. - /// - logger: The logger to use for this request. public func patch(url: String, body: Body? = nil, deadline: NIODeadline? = nil) -> EventLoopFuture { - return self.post(url: url, body: body, deadline: deadline, logger: HTTPClient.loggingDisabled) + return self.patch(url: url, body: body, deadline: deadline, logger: HTTPClient.loggingDisabled) } /// Execute `PATCH` request using specified URL. @@ -290,12 +279,7 @@ public class HTTPClient { /// - deadline: Point in time by which the request must complete. /// - logger: The logger to use for this request. public func patch(url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture { - do { - let request = try HTTPClient.Request(url: url, method: .PATCH, body: body) - return self.execute(request: request, deadline: deadline, logger: logger) - } catch { - return self.eventLoopGroup.next().makeFailedFuture(error) - } + return self.execute(.PATCH, url: url, body: body, deadline: deadline, logger: logger) } /// Execute `PUT` request using specified URL. @@ -316,12 +300,7 @@ public class HTTPClient { /// - deadline: Point in time by which the request must complete. /// - logger: The logger to use for this request. public func put(url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture { - do { - let request = try HTTPClient.Request(url: url, method: .PUT, body: body) - return self.execute(request: request, deadline: deadline, logger: logger) - } catch { - return self.eventLoopGroup.next().makeFailedFuture(error) - } + return self.execute(.PUT, url: url, body: body, deadline: deadline, logger: logger) } /// Execute `DELETE` request using specified URL. @@ -338,10 +317,65 @@ public class HTTPClient { /// - parameters: /// - url: Remote URL. /// - deadline: The time when the request must have been completed by. + /// - logger: The logger to use for this request. public func delete(url: String, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture { + return self.execute(.DELETE, url: url, deadline: deadline, logger: logger) + } + + /// Execute arbitrary HTTP request using specified URL. + /// + /// - parameters: + /// - method: Request method. + /// - url: Request url. + /// - body: Request body. + /// - deadline: Point in time by which the request must complete. + /// - logger: The logger to use for this request. + public func execute(_ method: HTTPMethod = .GET, url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger? = nil) -> EventLoopFuture { do { - let request = try Request(url: url, method: .DELETE) - return self.execute(request: request, deadline: deadline, logger: logger) + let request = try Request(url: url, method: method, body: body) + return self.execute(request: request, deadline: deadline, logger: logger ?? HTTPClient.loggingDisabled) + } catch { + return self.eventLoopGroup.next().makeFailedFuture(error) + } + } + + /// Execute arbitrary HTTP+UNIX request to a unix domain socket path, using the specified URL as the request to send to the server. + /// + /// - parameters: + /// - method: Request method. + /// - socketPath: The path to the unix domain socket to connect to. + /// - urlPath: The URL path and query that will be sent to the server. + /// - body: Request body. + /// - deadline: Point in time by which the request must complete. + /// - 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 { + throw HTTPClientError.invalidURL + } + let request = try Request(url: url, method: method, body: body) + return self.execute(request: request, deadline: deadline, logger: logger ?? HTTPClient.loggingDisabled) + } catch { + return self.eventLoopGroup.next().makeFailedFuture(error) + } + } + + /// Execute arbitrary HTTPS+UNIX request to a unix domain socket path over TLS, using the specified URL as the request to send to the server. + /// + /// - parameters: + /// - method: Request method. + /// - secureSocketPath: The path to the unix domain socket to connect to. + /// - urlPath: The URL path and query that will be sent to the server. + /// - body: Request body. + /// - deadline: Point in time by which the request must complete. + /// - 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 { + throw HTTPClientError.invalidURL + } + let request = try Request(url: url, method: method, body: body) + return self.execute(request: request, deadline: deadline, logger: logger ?? HTTPClient.loggingDisabled) } catch { return self.eventLoopGroup.next().makeFailedFuture(error) } diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 945f063b3..fb32f482c 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -95,8 +95,8 @@ extension HTTPClient { /// Represent HTTP request. public struct Request { /// Represent kind of Request - enum Kind { - enum UnixScheme { + enum Kind: Equatable { + enum UnixScheme: Equatable { case baseURL case http_unix case https_unix @@ -516,6 +516,36 @@ extension URL { func hasTheSameOrigin(as other: URL) -> Bool { return self.host == other.host && self.scheme == other.scheme && self.port == other.port } + + /// 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. + /// - 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 } + var urlString: String + if uri.hasPrefix("/") { + urlString = "http+unix://\(host)\(uri)" + } else { + urlString = "http+unix://\(host)/\(uri)" + } + self.init(string: 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. + /// - 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 } + var urlString: String + if uri.hasPrefix("/") { + urlString = "https+unix://\(host)\(uri)" + } else { + urlString = "https+unix://\(host)/\(uri)" + } + self.init(string: urlString) + } } extension HTTPClient { diff --git a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests+XCTest.swift index 9a3e11b50..8a89eb740 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests+XCTest.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests+XCTest.swift @@ -43,6 +43,7 @@ extension HTTPClientInternalTests { ("testUploadStreamingIsCalledOnTaskEL", testUploadStreamingIsCalledOnTaskEL), ("testWeCanActuallyExactlySetTheEventLoops", testWeCanActuallyExactlySetTheEventLoops), ("testTaskPromiseBoundToEL", testTaskPromiseBoundToEL), + ("testInternalRequestURI", testInternalRequestURI), ] } } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift index e5a6ad411..39d0c6f3a 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift @@ -959,4 +959,31 @@ class HTTPClientInternalTests: XCTestCase { XCTAssertTrue(task.futureResult.eventLoop === el2) XCTAssertNoThrow(try task.wait()) } + + func testInternalRequestURI() throws { + let request1 = try Request(url: "https://someserver.com:8888/some/path?foo=bar") + XCTAssertEqual(request1.kind, .host) + XCTAssertEqual(request1.socketPath, "") + XCTAssertEqual(request1.uri, "/some/path?foo=bar") + + let request2 = try Request(url: "https://someserver.com") + XCTAssertEqual(request2.kind, .host) + XCTAssertEqual(request2.socketPath, "") + XCTAssertEqual(request2.uri, "/") + + let request3 = try Request(url: "unix:///tmp/file") + XCTAssertEqual(request3.kind, .unixSocket(.baseURL)) + XCTAssertEqual(request3.socketPath, "/tmp/file") + XCTAssertEqual(request3.uri, "/") + + let request4 = try Request(url: "http+unix://%2Ftmp%2Ffile/file/path") + XCTAssertEqual(request4.kind, .unixSocket(.http_unix)) + XCTAssertEqual(request4.socketPath, "/tmp/file") + XCTAssertEqual(request4.uri, "/file/path") + + let request5 = try Request(url: "https+unix://%2Ftmp%2Ffile/file/path") + XCTAssertEqual(request5.kind, .unixSocket(.https_unix)) + XCTAssertEqual(request5.socketPath, "/tmp/file") + XCTAssertEqual(request5.uri, "/file/path") + } } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index ed65fd7b0..e5df9cfc5 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -439,6 +439,11 @@ internal final class HttpBinHandler: ChannelInboundHandler { headers.add(name: "X-Calling-URI", value: req.uri) self.resps.append(HTTPResponseBuilder(status: .ok, headers: headers)) return + case "/echo-method": + var headers = self.responseHeaders + headers.add(name: "X-Method-Used", value: req.method.rawValue) + self.resps.append(HTTPResponseBuilder(status: .ok, headers: headers)) + return case "/ok": self.resps.append(HTTPResponseBuilder(status: .ok)) return diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift index 5d8ba873f..83efb6f0d 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift @@ -28,6 +28,10 @@ extension HTTPClientTests { ("testRequestURI", testRequestURI), ("testBadRequestURI", testBadRequestURI), ("testSchemaCasing", testSchemaCasing), + ("testURLSocketPathInitializers", testURLSocketPathInitializers), + ("testConvenienceExecuteMethods", testConvenienceExecuteMethods), + ("testConvenienceExecuteMethodsOverSocket", testConvenienceExecuteMethodsOverSocket), + ("testConvenienceExecuteMethodsOverSecureSocket", testConvenienceExecuteMethodsOverSecureSocket), ("testGet", testGet), ("testGetWithDifferentEventLoopBackpressure", testGetWithDifferentEventLoopBackpressure), ("testPost", testPost), diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index 511b3b1c6..ec8eb92fe 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -98,6 +98,18 @@ class HTTPClientTests: XCTestCase { XCTAssertEqual(request3.url.path, "/tmp/file") XCTAssertEqual(request3.port, 80) XCTAssertFalse(request3.useTLS) + + let request4 = try Request(url: "http+unix://%2Ftmp%2Ffile/file/path") + XCTAssertEqual(request4.host, "") + XCTAssertEqual(request4.url.host, "/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.path, "/file/path") + XCTAssertTrue(request5.useTLS) } func testBadRequestURI() throws { @@ -110,11 +122,148 @@ class HTTPClientTests: XCTestCase { XCTAssertThrowsError(try Request(url: "https:/foo"), "should throw") { error in XCTAssertEqual(error as! HTTPClientError, HTTPClientError.emptyHost) } + XCTAssertThrowsError(try Request(url: "http+unix:///path"), "should throw") { error in + XCTAssertEqual(error as! HTTPClientError, HTTPClientError.missingSocketPath) + } } func testSchemaCasing() throws { XCTAssertNoThrow(try Request(url: "hTTpS://someserver.com:8888/some/path?foo=bar")) XCTAssertNoThrow(try Request(url: "uNIx:///some/path")) + XCTAssertNoThrow(try Request(url: "hTtP+uNIx://%2Fsome%2Fpath/")) + XCTAssertNoThrow(try Request(url: "hTtPS+uNIx://%2Fsome%2Fpath/")) + } + + func testURLSocketPathInitializers() throws { + let url1 = URL(httpURLWithSocketPath: "/tmp/file") + XCTAssertNotNil(url1) + if let url = url1 { + XCTAssertEqual(url.scheme, "http+unix") + XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.path, "/") + XCTAssertEqual(url.absoluteString, "http+unix://%2Ftmp%2Ffile/") + } + + let url2 = URL(httpURLWithSocketPath: "/tmp/file", uri: "/file/path") + XCTAssertNotNil(url2) + if let url = url2 { + XCTAssertEqual(url.scheme, "http+unix") + XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.path, "/file/path") + XCTAssertEqual(url.absoluteString, "http+unix://%2Ftmp%2Ffile/file/path") + } + + let url3 = URL(httpURLWithSocketPath: "/tmp/file", uri: "file/path") + XCTAssertNotNil(url3) + if let url = url3 { + XCTAssertEqual(url.scheme, "http+unix") + XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.path, "/file/path") + XCTAssertEqual(url.absoluteString, "http+unix://%2Ftmp%2Ffile/file/path") + } + + let url4 = URL(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.path, "/file/path") + XCTAssertEqual(url.absoluteString, "http+unix://%2Ftmp%2Ffile%20with%20spaces%E3%81%A8%E6%BC%A2%E5%AD%97/file/path") + } + + let url5 = URL(httpsURLWithSocketPath: "/tmp/file") + XCTAssertNotNil(url5) + if let url = url5 { + XCTAssertEqual(url.scheme, "https+unix") + XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.path, "/") + XCTAssertEqual(url.absoluteString, "https+unix://%2Ftmp%2Ffile/") + } + + let url6 = URL(httpsURLWithSocketPath: "/tmp/file", uri: "/file/path") + XCTAssertNotNil(url6) + if let url = url6 { + XCTAssertEqual(url.scheme, "https+unix") + XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.path, "/file/path") + XCTAssertEqual(url.absoluteString, "https+unix://%2Ftmp%2Ffile/file/path") + } + + let url7 = URL(httpsURLWithSocketPath: "/tmp/file", uri: "file/path") + XCTAssertNotNil(url7) + if let url = url7 { + XCTAssertEqual(url.scheme, "https+unix") + XCTAssertEqual(url.host, "/tmp/file") + XCTAssertEqual(url.path, "/file/path") + XCTAssertEqual(url.absoluteString, "https+unix://%2Ftmp%2Ffile/file/path") + } + + let url8 = URL(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.path, "/file/path") + XCTAssertEqual(url.absoluteString, "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) + } + + func testConvenienceExecuteMethods() throws { + XCTAssertNoThrow(XCTAssertEqual(["GET"[...]], + try self.defaultClient.get(url: self.defaultHTTPBinURLPrefix + "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + XCTAssertNoThrow(XCTAssertEqual(["POST"[...]], + try self.defaultClient.post(url: self.defaultHTTPBinURLPrefix + "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + XCTAssertNoThrow(XCTAssertEqual(["PATCH"[...]], + try self.defaultClient.patch(url: self.defaultHTTPBinURLPrefix + "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + XCTAssertNoThrow(XCTAssertEqual(["PUT"[...]], + try self.defaultClient.put(url: self.defaultHTTPBinURLPrefix + "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + XCTAssertNoThrow(XCTAssertEqual(["DELETE"[...]], + try self.defaultClient.delete(url: self.defaultHTTPBinURLPrefix + "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + XCTAssertNoThrow(XCTAssertEqual(["GET"[...]], + try self.defaultClient.execute(url: self.defaultHTTPBinURLPrefix + "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + XCTAssertNoThrow(XCTAssertEqual(["CHECKOUT"[...]], + try self.defaultClient.execute(.CHECKOUT, url: self.defaultHTTPBinURLPrefix + "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + } + + func testConvenienceExecuteMethodsOverSocket() throws { + XCTAssertNoThrow(try TemporaryFileHelpers.withTemporaryUnixDomainSocketPathName { path in + let localSocketPathHTTPBin = HTTPBin(bindTarget: .unixDomainSocket(path)) + defer { + XCTAssertNoThrow(try localSocketPathHTTPBin.shutdown()) + } + + XCTAssertNoThrow(XCTAssertEqual(["GET"[...]], + try self.defaultClient.execute(socketPath: path, urlPath: "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + XCTAssertNoThrow(XCTAssertEqual(["GET"[...]], + try self.defaultClient.execute(.GET, socketPath: path, urlPath: "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + XCTAssertNoThrow(XCTAssertEqual(["POST"[...]], + try self.defaultClient.execute(.POST, socketPath: path, urlPath: "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + }) + } + + func testConvenienceExecuteMethodsOverSecureSocket() throws { + XCTAssertNoThrow(try TemporaryFileHelpers.withTemporaryUnixDomainSocketPathName { path in + let localSocketPathHTTPBin = HTTPBin(ssl: true, bindTarget: .unixDomainSocket(path)) + let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), + configuration: HTTPClient.Configuration(certificateVerification: .none)) + defer { + XCTAssertNoThrow(try localClient.syncShutdown()) + XCTAssertNoThrow(try localSocketPathHTTPBin.shutdown()) + } + + XCTAssertNoThrow(XCTAssertEqual(["GET"[...]], + try localClient.execute(secureSocketPath: path, urlPath: "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + XCTAssertNoThrow(XCTAssertEqual(["GET"[...]], + try localClient.execute(.GET, secureSocketPath: path, urlPath: "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + XCTAssertNoThrow(XCTAssertEqual(["POST"[...]], + try localClient.execute(.POST, secureSocketPath: path, urlPath: "echo-method").wait().headers[canonicalForm: "X-Method-Used"])) + }) } func testGet() throws { @@ -1351,7 +1500,7 @@ class HTTPClientTests: XCTestCase { defer { XCTAssertNoThrow(try localHTTPBin.shutdown()) } - guard let target = URL(string: "http+unix://\(path.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!)/echo-uri"), + guard let target = URL(httpURLWithSocketPath: path, uri: "/echo-uri"), let request = try? Request(url: target) else { XCTFail("couldn't build URL for request") return @@ -1371,7 +1520,7 @@ class HTTPClientTests: XCTestCase { XCTAssertNoThrow(try localClient.syncShutdown()) XCTAssertNoThrow(try localHTTPBin.shutdown()) } - guard let target = URL(string: "https+unix://\(path.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!)/echo-uri"), + guard let target = URL(httpsURLWithSocketPath: path, uri: "/echo-uri"), let request = try? Request(url: target) else { XCTFail("couldn't build URL for request") return @@ -1942,7 +2091,70 @@ class HTTPClientTests: XCTestCase { logger: logger).wait()) XCTAssertEqual(0, logStore.allEntries.count) + // === Synthesized Request + XCTAssertNoThrow(try self.defaultClient.execute(.GET, + url: self.defaultHTTPBinURLPrefix + "get", + body: nil, + deadline: nil, + logger: logger).wait()) + XCTAssertEqual(0, logStore.allEntries.count) + XCTAssertEqual(0, self.backgroundLogStore.allEntries.count) + + // === Synthesized Socket Path Request + XCTAssertNoThrow(try TemporaryFileHelpers.withTemporaryUnixDomainSocketPathName { path in + let backgroundLogStore = CollectEverythingLogHandler.LogStore() + var backgroundLogger = Logger(label: "\(#function)", factory: { _ in + CollectEverythingLogHandler(logStore: backgroundLogStore) + }) + backgroundLogger.logLevel = .trace + + let localSocketPathHTTPBin = HTTPBin(bindTarget: .unixDomainSocket(path)) + let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), + backgroundActivityLogger: backgroundLogger) + defer { + XCTAssertNoThrow(try localClient.syncShutdown()) + XCTAssertNoThrow(try localSocketPathHTTPBin.shutdown()) + } + + XCTAssertNoThrow(try localClient.execute(.GET, + socketPath: path, + urlPath: "get", + body: nil, + deadline: nil, + logger: logger).wait()) + XCTAssertEqual(0, logStore.allEntries.count) + + XCTAssertEqual(0, backgroundLogStore.allEntries.count) + }) + + // === Synthesized Secure Socket Path Request + XCTAssertNoThrow(try TemporaryFileHelpers.withTemporaryUnixDomainSocketPathName { path in + let backgroundLogStore = CollectEverythingLogHandler.LogStore() + var backgroundLogger = Logger(label: "\(#function)", factory: { _ in + CollectEverythingLogHandler(logStore: backgroundLogStore) + }) + backgroundLogger.logLevel = .trace + + let localSocketPathHTTPBin = HTTPBin(ssl: true, bindTarget: .unixDomainSocket(path)) + let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), + configuration: HTTPClient.Configuration(certificateVerification: .none), + backgroundActivityLogger: backgroundLogger) + defer { + XCTAssertNoThrow(try localClient.syncShutdown()) + XCTAssertNoThrow(try localSocketPathHTTPBin.shutdown()) + } + + XCTAssertNoThrow(try localClient.execute(.GET, + secureSocketPath: path, + urlPath: "get", + body: nil, + deadline: nil, + logger: logger).wait()) + XCTAssertEqual(0, logStore.allEntries.count) + + XCTAssertEqual(0, backgroundLogStore.allEntries.count) + }) } func testAllMethodsLog() { @@ -1955,7 +2167,7 @@ class HTTPClientTests: XCTestCase { logger.logLevel = .trace logger[metadataKey: "req"] = "yo-\(type)" - let url = self.defaultHTTPBinURLPrefix + "not-found/request/\(type))" + let url = "not-found/request/\(type))" let result = try body(logger, url) XCTAssertGreaterThan(logStore.allEntries.count, 0) @@ -1967,27 +2179,78 @@ class HTTPClientTests: XCTestCase { } XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "GET") { logger, url in - try self.defaultClient.get(url: url, logger: logger).wait() + try self.defaultClient.get(url: self.defaultHTTPBinURLPrefix + url, logger: logger).wait() }.status)) XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "PUT") { logger, url in - try self.defaultClient.put(url: url, logger: logger).wait() + try self.defaultClient.put(url: self.defaultHTTPBinURLPrefix + url, logger: logger).wait() }.status)) XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "POST") { logger, url in - try self.defaultClient.post(url: url, logger: logger).wait() + try self.defaultClient.post(url: self.defaultHTTPBinURLPrefix + url, logger: logger).wait() }.status)) XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "DELETE") { logger, url in - try self.defaultClient.delete(url: url, logger: logger).wait() + try self.defaultClient.delete(url: self.defaultHTTPBinURLPrefix + url, logger: logger).wait() }.status)) XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "PATCH") { logger, url in - try self.defaultClient.patch(url: url, logger: logger).wait() + try self.defaultClient.patch(url: self.defaultHTTPBinURLPrefix + url, logger: logger).wait() + }.status)) + + XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "CHECKOUT") { logger, url in + try self.defaultClient.execute(.CHECKOUT, url: self.defaultHTTPBinURLPrefix + url, logger: logger).wait() }.status)) // No background activity expected here. XCTAssertEqual(0, self.backgroundLogStore.allEntries.count) + + XCTAssertNoThrow(try TemporaryFileHelpers.withTemporaryUnixDomainSocketPathName { path in + let backgroundLogStore = CollectEverythingLogHandler.LogStore() + var backgroundLogger = Logger(label: "\(#function)", factory: { _ in + CollectEverythingLogHandler(logStore: backgroundLogStore) + }) + backgroundLogger.logLevel = .trace + + let localSocketPathHTTPBin = HTTPBin(bindTarget: .unixDomainSocket(path)) + let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), + backgroundActivityLogger: backgroundLogger) + defer { + XCTAssertNoThrow(try localClient.syncShutdown()) + XCTAssertNoThrow(try localSocketPathHTTPBin.shutdown()) + } + + XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "GET") { logger, url in + try localClient.execute(socketPath: path, urlPath: url, logger: logger).wait() + }.status)) + + // No background activity expected here. + XCTAssertEqual(0, backgroundLogStore.allEntries.count) + }) + + XCTAssertNoThrow(try TemporaryFileHelpers.withTemporaryUnixDomainSocketPathName { path in + let backgroundLogStore = CollectEverythingLogHandler.LogStore() + var backgroundLogger = Logger(label: "\(#function)", factory: { _ in + CollectEverythingLogHandler(logStore: backgroundLogStore) + }) + backgroundLogger.logLevel = .trace + + let localSocketPathHTTPBin = HTTPBin(ssl: true, bindTarget: .unixDomainSocket(path)) + let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), + configuration: HTTPClient.Configuration(certificateVerification: .none), + backgroundActivityLogger: backgroundLogger) + defer { + XCTAssertNoThrow(try localClient.syncShutdown()) + XCTAssertNoThrow(try localSocketPathHTTPBin.shutdown()) + } + + XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "GET") { logger, url in + try localClient.execute(secureSocketPath: path, urlPath: url, logger: logger).wait() + }.status)) + + // No background activity expected here. + XCTAssertEqual(0, backgroundLogStore.allEntries.count) + }) } func testClosingIdleConnectionsInPoolLogsInTheBackground() {