diff --git a/Sources/KituraNet/FastCGI/FastCGIServer.swift b/Sources/KituraNet/FastCGI/FastCGIServer.swift index c2f4b5d..2b4f6fe 100644 --- a/Sources/KituraNet/FastCGI/FastCGIServer.swift +++ b/Sources/KituraNet/FastCGI/FastCGIServer.swift @@ -50,6 +50,9 @@ public class FastCGIServer: Server { /// TCP socket used for listening for new connections private var listenSocket: Socket? + + /// Whether or not this server allows port reuse (default: disallowed) + public var allowPortReuse: Bool = false fileprivate let lifecycleListener = ServerLifecycleListener() @@ -74,8 +77,9 @@ public class FastCGIServer: Server { let socket = try Socket.create() self.listenSocket = socket - try socket.listen(on: port, maxBacklogSize: maxPendingConnections) + try socket.listen(on: port, maxBacklogSize: maxPendingConnections, allowPortReuse: self.allowPortReuse) Log.info("Listening on port \(port)") + Log.verbose("Options for port \(port): maxPendingConnections: \(maxPendingConnections), allowPortReuse: \(self.allowPortReuse)") // set synchronously to avoid contention in back to back server start/stop calls self.state = .started diff --git a/Sources/KituraNet/HTTP/HTTPServer.swift b/Sources/KituraNet/HTTP/HTTPServer.swift index b933e38..df80e1c 100644 --- a/Sources/KituraNet/HTTP/HTTPServer.swift +++ b/Sources/KituraNet/HTTP/HTTPServer.swift @@ -43,6 +43,9 @@ public class HTTPServer: Server { /// TCP socket used for listening for new connections private var listenSocket: Socket? + /// Whether or not this server allows port reuse (default: disallowed) + public var allowPortReuse: Bool = false + /// Maximum number of pending connections private let maxPendingConnections = 100 @@ -92,7 +95,7 @@ public class HTTPServer: Server { socket.delegate = try SSLService(usingConfiguration: sslConfig); } - try socket.listen(on: port, maxBacklogSize: maxPendingConnections) + try socket.listen(on: port, maxBacklogSize: maxPendingConnections, allowPortReuse: self.allowPortReuse) let socketManager = IncomingSocketManager() self.socketManager = socketManager @@ -116,8 +119,10 @@ public class HTTPServer: Server { #endif Log.info("Listening on port \(self.port!) (delegate: \(delegate))") + Log.verbose("Options for port \(self.port!): delegate: \(delegate), maxPendingConnections: \(maxPendingConnections), allowPortReuse: \(self.allowPortReuse)") } else { Log.info("Listening on port \(self.port!)") + Log.verbose("Options for port \(self.port!): maxPendingConnections: \(maxPendingConnections), allowPortReuse: \(self.allowPortReuse)") } // set synchronously to avoid contention in back to back server start/stop calls diff --git a/Tests/KituraNetTests/KituraNetTest.swift b/Tests/KituraNetTests/KituraNetTest.swift index 72d62ad..7e3953e 100644 --- a/Tests/KituraNetTests/KituraNetTest.swift +++ b/Tests/KituraNetTests/KituraNetTest.swift @@ -26,6 +26,7 @@ class KituraNetTest: XCTestCase { static let useSSLDefault = true static let portDefault = 8080 + static let portReuseDefault = false var useSSL = useSSLDefault var port = portDefault @@ -56,28 +57,26 @@ class KituraNetTest: XCTestCase { func doTearDown() { } - func startServer(_ delegate: ServerDelegate?, port: Int = portDefault, useSSL: Bool = useSSLDefault) throws -> HTTPServer { + func startServer(_ delegate: ServerDelegate?, port: Int = portDefault, useSSL: Bool = useSSLDefault, allowPortReuse: Bool = portReuseDefault) throws -> HTTPServer { - let server: HTTPServer + let server = HTTP.createServer() + server.delegate = delegate + server.allowPortReuse = allowPortReuse if useSSL { - server = HTTP.createServer() - server.delegate = delegate server.sslConfig = KituraNetTest.sslConfig - try server.listen(on: port) - } else { - server = try HTTPServer.listen(on: port, delegate: delegate) } + try server.listen(on: port) return server } - func performServerTest(_ delegate: ServerDelegate?, port: Int = portDefault, useSSL: Bool = useSSLDefault, + func performServerTest(_ delegate: ServerDelegate?, port: Int = portDefault, useSSL: Bool = useSSLDefault, allowPortReuse: Bool = portReuseDefault, line: Int = #line, asyncTasks: (XCTestExpectation) -> Void...) { do { self.useSSL = useSSL self.port = port - let server: HTTPServer = try startServer(delegate, port: port, useSSL: useSSL) + let server: HTTPServer = try startServer(delegate, port: port, useSSL: useSSL, allowPortReuse: allowPortReuse) defer { server.stop() } @@ -99,13 +98,14 @@ class KituraNetTest: XCTestCase { } } - func performFastCGIServerTest(_ delegate: ServerDelegate?, port: Int = portDefault, + func performFastCGIServerTest(_ delegate: ServerDelegate?, port: Int = portDefault, allowPortReuse: Bool = portReuseDefault, line: Int = #line, asyncTasks: (XCTestExpectation) -> Void...) { do { self.port = port let server = try FastCGIServer.listen(on: port, delegate: delegate) + server.allowPortReuse = allowPortReuse defer { server.stop() } diff --git a/Tests/KituraNetTests/RegressionTests.swift b/Tests/KituraNetTests/RegressionTests.swift index 5d51f71..3a9172b 100644 --- a/Tests/KituraNetTests/RegressionTests.swift +++ b/Tests/KituraNetTests/RegressionTests.swift @@ -148,4 +148,63 @@ class RegressionTests: KituraNetTest { } } + + /// Tests that attempting to start a second HTTPServer on the same port fails. + func testServersCollidingOnPort() { + do { + let server: HTTPServer = try startServer(nil, port: 0, useSSL: false) + defer { + server.stop() + } + + guard let serverPort = server.port else { + XCTFail("Server port was not initialized") + return + } + XCTAssertTrue(serverPort != 0, "Ephemeral server port not set") + + do { + let collidingServer: HTTPServer = try startServer(nil, port: serverPort, useSSL: false) + defer { + collidingServer.stop() + } + XCTFail("Server unexpectedly succeeded in listening on a port already in use") + } catch { + XCTAssert(error is Socket.Error, "Expected a Socket.Error, received: \(error)") + } + + } catch { + XCTFail("Error: \(error)") + } + } + + /// Tests that attempting to start a second HTTPServer on the same port with + /// SO_REUSEPORT enabled is successful. + func testServersSharingPort() { + do { + let server: HTTPServer = try startServer(nil, port: 0, useSSL: false, allowPortReuse: true) + defer { + server.stop() + } + + guard let serverPort = server.port else { + XCTFail("Server port was not initialized") + return + } + XCTAssertTrue(serverPort != 0, "Ephemeral server port not set") + + do { + let sharingServer: HTTPServer = try startServer(nil, port: serverPort, useSSL: false, allowPortReuse: true) + defer { + sharingServer.stop() + } + } catch { + XCTFail("Second server could not share listener port, received: \(error)") + } + + } catch { + XCTFail("Error: \(error)") + } + } + }