Skip to content

Commit

Permalink
Support Unix domain sockets (#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pushkar N Kulkarni authored and djones6 committed Apr 2, 2019
1 parent a46018d commit d403683
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 46 deletions.
33 changes: 24 additions & 9 deletions Sources/KituraNet/ClientRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ public class ClientRequest {
/// A semaphore used to make ClientRequest.end() synchronous
let waitSemaphore = DispatchSemaphore(value: 0)

// Socket path for Unix domain sockets
var unixDomainSocketPath: String?

/**
Client request options enum. This allows the client to specify certain parameteres such as HTTP headers, HTTP methods, host names, and SSL credentials.
Expand Down Expand Up @@ -292,9 +295,11 @@ public class ClientRequest {
/// Initializes a `ClientRequest` instance
///
/// - Parameter options: An array of `Options' describing the request
/// - Parameter unixDomainSocketPath: Specifies a path of a Unix domain socket that the client should connect to.
/// - Parameter callback: The closure of type `Callback` to be used for the callback.
init(options: [Options], callback: @escaping Callback) {
init(options: [Options], unixDomainSocketPath: String? = nil, callback: @escaping Callback) {

self.unixDomainSocketPath = unixDomainSocketPath
self.callback = callback

var theSchema = "http://"
Expand Down Expand Up @@ -558,9 +563,15 @@ public class ClientRequest {

do {
guard let bootstrap = bootstrap else { return }
channel = try bootstrap.connect(host: hostName, port: Int(self.port!)).wait()
if let unixDomainSocketPath = self.unixDomainSocketPath {
channel = try bootstrap.connect(unixDomainSocketPath: unixDomainSocketPath).wait()
} else {
channel = try bootstrap.connect(host: hostName, port: Int(self.port!)).wait()
}
} catch let error {
Log.error("Connection to \(hostName):\(self.port ?? 80) failed with error: \(error)")
let target = self.unixDomainSocketPath ?? "\(self.port ?? 80)"
print("Connection to \(hostName): \(target) failed with error: \(error)")
Log.error("Connection to \(hostName): \(target) failed with error: \(error)")
callback(nil)
return
}
Expand Down Expand Up @@ -759,13 +770,17 @@ class HTTPClientHandler: ChannelInboundHandler {
}
if url.starts(with: "/") {
let scheme = URL(string: clientRequest.url)?.scheme
let port = clientRequest.port.map { UInt16($0) }.map { $0.toInt16() }!
let request = ClientRequest(options: [.schema(scheme!),
.hostname(clientRequest.hostName!),
.port(port),
.path(url)],
callback: clientRequest.callback)
var options: [ClientRequest.Options] = [.schema(scheme!), .hostname(clientRequest.hostName!), .path(url)]
let request: ClientRequest
if let socketPath = self.clientRequest.unixDomainSocketPath {
request = ClientRequest(options: options, unixDomainSocketPath: socketPath, callback: clientRequest.callback)
} else {
let port = clientRequest.port.map { UInt16($0) }.map { $0.toInt16() }!
options.append(.port(port))
request = ClientRequest(options: options, callback: clientRequest.callback)
}
request.maxRedirects = self.clientRequest.maxRedirects - 1

// The next request can be asynchronously moved to a DispatchQueue.
// ClientRequest.end() calls connect().wait(), so we better move this to a dispatch queue.
// Because ClientRequest.end() is blocking, we mark the current task complete after the new task also completes.
Expand Down
12 changes: 7 additions & 5 deletions Sources/KituraNet/HTTP/HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,20 @@ public class HTTP {
Create a new `ClientRequest` using a list of options.
- Parameter options: a list of `ClientRequest.Options`.
- Parameter callback: closure to run after the request.
- Parameter unixDomainSocketPath: the path of a Unix domain socket that this client should connect to (defaults to `nil`).
- Parameter callback: The closure to run after the request completes. The `ClientResponse?` parameter allows access to the response from the server.
- Returns: a `ClientRequest` instance
### Usage Example: ###
````swift
let request = HTTP.request([ClientRequest.Options]) {response in
...
let myOptions: [ClientRequest.Options] = [.hostname("localhost"), .port("8080")]
let request = HTTP.request(myOptions) { response in
// Process the ClientResponse
}
````
*/
public static func request(_ options: [ClientRequest.Options], callback: @escaping ClientRequest.Callback) -> ClientRequest {
return ClientRequest(options: options, callback: callback)
public static func request(_ options: [ClientRequest.Options], unixDomainSocketPath: String? = nil, callback: @escaping ClientRequest.Callback) -> ClientRequest {
return ClientRequest(options: options, unixDomainSocketPath: unixDomainSocketPath, callback: callback)
}

/**
Expand Down
1 change: 0 additions & 1 deletion Sources/KituraNet/HTTP/HTTPRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ internal class HTTPRequestHandler: ChannelInboundHandler {

public func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
let request = self.unwrapInboundIn(data)

// If an error response was already sent, we'd want to spare running through this for now.
// If an upgrade to WebSocket fails, both `errorCaught` and `channelRead` are triggered.
// We'd want to return the error via `errorCaught`.
Expand Down
97 changes: 83 additions & 14 deletions Sources/KituraNet/HTTP/HTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ import LoggerAPI
import NIOWebSocket
import CLinuxHelpers

#if os(Linux)
import Glibc
#else
import Darwin
#endif

// MARK: HTTPServer
/**
An HTTP server that listens for connections on a socket.
Expand All @@ -49,16 +55,12 @@ public class HTTPServer: Server {
*/
public var delegate: ServerDelegate?

/**
Port number for listening for new connections.
### Usage Example: ###
````swift
httpServer.port = 8080
````
*/
/// The TCP port on which this server listens for new connections. If `nil`, this server does not listen on a TCP socket.
public private(set) var port: Int?

/// The Unix domain socket path on which this server listens for new connections. If `nil`, this server does not listen on a Unix socket.
public private(set) var unixDomainSocketPath: String?

private var _state: ServerState = .unknown

private let syncQ = DispatchQueue(label: "HTTPServer.syncQ")
Expand Down Expand Up @@ -225,8 +227,31 @@ public class HTTPServer: Server {
return nil
}

// Sockets could either be TCP/IP sockets or Unix domain sockets
private enum SocketType {
// An TCP/IP socket has an associated port number
case tcp(Int)
// A unix domain socket has an associated filename
case unix(String)
}

/**
Listens for connections on a socket.
Listens for connections on a Unix socket.
### Usage Example: ###
````swift
try server.listen(unixDomainSocketPath: "/my/path")
````
- Parameter unixDomainSocketPath: Unix socket path for new connections, eg. "/my/path"
*/
public func listen(unixDomainSocketPath: String) throws {
self.unixDomainSocketPath = unixDomainSocketPath
try listen(.unix(unixDomainSocketPath))
}

/**
Listens for connections on a TCP socket.
### Usage Example: ###
````swift
Expand All @@ -237,6 +262,10 @@ public class HTTPServer: Server {
*/
public func listen(on port: Int) throws {
self.port = port
try listen(.tcp(port))
}

private func listen(_ socket: SocketType) throws {

if let tlsConfig = tlsConfig {
do {
Expand Down Expand Up @@ -275,20 +304,40 @@ public class HTTPServer: Server {
}
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)

let listenerDescription: String
do {
serverChannel = try bootstrap.bind(host: "0.0.0.0", port: port).wait()
self.port = serverChannel?.localAddress?.port.map { Int($0) }
switch socket {
case SocketType.tcp(let port):
serverChannel = try bootstrap.bind(host: "0.0.0.0", port: port).wait()
self.port = serverChannel?.localAddress?.port.map { Int($0) }
listenerDescription = "port \(self.port ?? port)"
case SocketType.unix(let unixDomainSocketPath):
// Ensure the path doesn't exist...
#if os(Linux)
_ = Glibc.unlink(unixDomainSocketPath)
#else
_ = Darwin.unlink(unixDomainSocketPath)
#endif
serverChannel = try bootstrap.bind(unixDomainSocketPath: unixDomainSocketPath).wait()
self.unixDomainSocketPath = unixDomainSocketPath
listenerDescription = "path \(unixDomainSocketPath)"
}
self.state = .started
self.lifecycleListener.performStartCallbacks()
} catch let error {
self.state = .failed
self.lifecycleListener.performFailCallbacks(with: error)
Log.error("Error trying to bind to \(port): \(error)")
switch socket {
case .tcp(let port):
Log.error("Error trying to bind to \(port): \(error)")
case .unix(let socketPath):
Log.error("Error trying to bind to \(socketPath): \(error)")
}
throw error
}

Log.info("Listening on port \(self.port!)")
Log.verbose("Options for port \(self.port!): maxPendingConnections: \(maxPendingConnections), allowPortReuse: \(self.allowPortReuse)")
Log.info("Listening on \(listenerDescription)")
Log.verbose("Options for \(listenerDescription): maxPendingConnections: \(maxPendingConnections), allowPortReuse: \(self.allowPortReuse)")

let queuedBlock = DispatchWorkItem(block: {
guard let serverChannel = self.serverChannel else { return }
Expand Down Expand Up @@ -323,6 +372,26 @@ public class HTTPServer: Server {
return server
}

/**
Static method to create a new HTTP server and have it listen for connections on a Unix domain socket.
### Usage Example: ###
````swift
let server = HTTPServer.listen(unixDomainSocketPath: "/my/path", delegate: self)
````
- Parameter unixDomainSocketPath: The path of the Unix domain socket that this server should listen on.
- Parameter delegate: The delegate handler for HTTP connections.
- Returns: A new instance of a `HTTPServer`.
*/
public static func listen(unixDomainSocketPath: String, delegate: ServerDelegate?) throws -> HTTPServer {
let server = HTTP.createServer()
server.delegate = delegate
try server.listen(unixDomainSocketPath: unixDomainSocketPath)
return server
}

/**
Listen for connections on a socket.
Expand Down
2 changes: 1 addition & 1 deletion Sources/KituraNet/HTTP/HTTPServerRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ public class HTTPServerRequest: ServerRequest {
case .v6(let addr):
return addr.host
case .unixDomainSocket:
return "n/a"
return "uds"
}
}

Expand Down
4 changes: 2 additions & 2 deletions Tests/KituraNetTests/ClientE2ETests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ class ClientE2ETests: KituraNetTest {

func testUrlURL() {
let delegate = TestURLDelegate()
performServerTest(delegate) { expectation in
performServerTest(delegate, socketType: .tcp) { expectation in
delegate.port = self.port
self.performRequest("post", path: ClientE2ETests.urlPath, callback: {response in
XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "Status code wasn't .Ok was \(String(describing: response?.statusCode))")
Expand Down Expand Up @@ -344,7 +344,7 @@ class ClientE2ETests: KituraNetTest {
}

class TestServerDelegate: ServerDelegate {
let remoteAddress = ["127.0.0.1", "::1", "::ffff:127.0.0.1"]
let remoteAddress = ["127.0.0.1", "::1", "::ffff:127.0.0.1", "uds"]

func handle(request: ServerRequest, response: ServerResponse) {
XCTAssertTrue(remoteAddress.contains(request.remoteAddress), "Remote address wasn't ::1 or 127.0.0.1 or ::ffff:127.0.0.1, it was \(request.remoteAddress)")
Expand Down
8 changes: 8 additions & 0 deletions Tests/KituraNetTests/HTTPResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ class HTTPResponseTests: KituraNetTest {
]
}

override func setUp() {
doSetUp()
}

override func tearDown() {
doTearDown()
}

func testContentTypeHeaders() {
let headers = HeadersContainer()

Expand Down
Loading

0 comments on commit d403683

Please sign in to comment.