Skip to content

Support UNIX Domain Sockets #151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 27, 2020
29 changes: 18 additions & 11 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -281,15 +281,22 @@ public class HTTPClient {
bootstrap = bootstrap.connectTimeout(timeout)
}

let address = self.resolveAddress(request: request, proxy: self.configuration.proxy)
bootstrap.connect(host: address.host, port: address.port)
.map { channel in
task.setChannel(channel)
}
.flatMap { channel in
channel.writeAndFlush(request)
}
.cascadeFailure(to: task.promise)
let eventLoopChannel: EventLoopFuture<Channel>
if request.kind == .unixSocket, let baseURL = request.url.baseURL {
eventLoopChannel = bootstrap.connect(unixDomainSocketPath: baseURL.path)
} else {
let address = self.resolveAddress(request: request, proxy: self.configuration.proxy)
eventLoopChannel = bootstrap.connect(host: address.host, port: address.port)
}

eventLoopChannel.map { channel in
task.setChannel(channel)
}
.flatMap { channel in
channel.writeAndFlush(request)
}
.cascadeFailure(to: task.promise)

return task
}

Expand Down Expand Up @@ -481,12 +488,12 @@ private extension ChannelPipeline {
func addProxyHandler(for request: HTTPClient.Request, decoder: ByteToMessageHandler<HTTPResponseDecoder>, encoder: HTTPRequestEncoder, tlsConfiguration: TLSConfiguration?, proxy: HTTPClient.Configuration.Proxy?) -> EventLoopFuture<Void> {
let handler = HTTPClientProxyHandler(host: request.host, port: request.port, authorization: proxy?.authorization, onConnect: { channel in
channel.pipeline.removeHandler(decoder).flatMap {
return channel.pipeline.addHandler(
channel.pipeline.addHandler(
ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .forwardBytes)),
position: .after(encoder)
)
}.flatMap {
return channel.pipeline.addSSLHandlerIfNeeded(for: request, tlsConfiguration: tlsConfiguration)
channel.pipeline.addSSLHandlerIfNeeded(for: request, tlsConfiguration: tlsConfiguration)
}
})
return self.addHandler(handler)
Expand Down
71 changes: 51 additions & 20 deletions Sources/AsyncHTTPClient/HTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,54 @@ extension HTTPClient {

/// Represent HTTP request.
public struct Request {
/// Represent kind of Request
enum Kind {
/// Remote host request.
case host
/// UNIX Domain Socket HTTP request.
case unixSocket

private static var hostSchemes = ["http", "https"]
private static var unixSchemes = ["unix"]

init(forScheme scheme: String) throws {
if Kind.host.supports(scheme: scheme) {
self = .host
} else if Kind.unixSocket.supports(scheme: scheme) {
self = .unixSocket
} else {
throw HTTPClientError.unsupportedScheme(scheme)
}
}

func hostFromURL(_ url: URL) throws -> String {
switch self {
case .host:
guard let host = url.host else {
throw HTTPClientError.emptyHost
}
return host
case .unixSocket:
return ""
}
}

func supports(scheme: String) -> Bool {
switch self {
case .host:
return Kind.hostSchemes.contains(scheme)
case .unixSocket:
return Kind.unixSchemes.contains(scheme)
}
}
}

/// Request HTTP method, defaults to `GET`.
public let method: HTTPMethod
public var method: HTTPMethod
/// Remote URL.
public let url: URL
public var url: URL
/// Remote HTTP scheme, resolved from `URL`.
public let scheme: String
public var scheme: String
/// Remote host, resolved from `URL`.
public let host: String
/// Request custom HTTP Headers, defaults to no headers.
Expand All @@ -107,6 +149,7 @@ extension HTTPClient {
}

var redirectState: RedirectState?
let kind: Kind

/// Create HTTP request.
///
Expand All @@ -133,7 +176,6 @@ extension HTTPClient {
///
/// - parameters:
/// - url: Remote `URL`.
/// - version: HTTP version.
/// - method: HTTP method.
/// - headers: Custom HTTP headers.
/// - body: Request body.
Expand All @@ -146,22 +188,15 @@ extension HTTPClient {
throw HTTPClientError.emptyScheme
}

guard Request.isSchemeSupported(scheme: scheme) else {
throw HTTPClientError.unsupportedScheme(scheme)
}

guard let host = url.host else {
throw HTTPClientError.emptyHost
}
self.kind = try Kind(forScheme: scheme)
self.host = try self.kind.hostFromURL(url)

self.method = method
self.redirectState = nil
self.url = url
self.method = method
self.scheme = scheme
self.host = host
self.headers = headers
self.body = body

self.redirectState = nil
}

/// Whether request will be executed using secure socket.
Expand All @@ -173,10 +208,6 @@ extension HTTPClient {
public var port: Int {
return self.url.port ?? (self.useTLS ? 443 : 80)
}

static func isSchemeSupported(scheme: String) -> Bool {
return scheme == "http" || scheme == "https"
}
}

/// Represent HTTP response.
Expand Down Expand Up @@ -812,7 +843,7 @@ internal struct RedirectHandler<ResponseType> {
return nil
}

guard HTTPClient.Request.isSchemeSupported(scheme: self.request.scheme) else {
guard self.request.kind.supports(scheme: self.request.scheme) else {
return nil
}

Expand Down
12 changes: 10 additions & 2 deletions Tests/AsyncHTTPClientTests/HTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,21 @@ class HTTPClientTests: XCTestCase {

let request2 = try Request(url: "https://someserver.com")
XCTAssertEqual(request2.url.path, "")

let request3 = try Request(url: "unix:///tmp/file")
XCTAssertNil(request3.url.host)
XCTAssertEqual(request3.host, "")
XCTAssertEqual(request3.url.path, "/tmp/file")
XCTAssertEqual(request3.port, 80)
XCTAssertFalse(request3.useTLS)
}

func testBadRequestURI() throws {
XCTAssertThrowsError(try Request(url: "some/path"), "should throw") { error in
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.emptyScheme)
}
XCTAssertThrowsError(try Request(url: "file://somewhere/some/path?foo=bar"), "should throw") { error in
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.unsupportedScheme("file"))
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)
Expand All @@ -63,6 +70,7 @@ class HTTPClientTests: XCTestCase {

func testSchemaCasing() throws {
XCTAssertNoThrow(try Request(url: "hTTpS://someserver.com:8888/some/path?foo=bar"))
XCTAssertNoThrow(try Request(url: "uNIx:///some/path"))
}

func testGet() throws {
Expand Down