Skip to content
Permalink
Browse files

feat: Ability to limit request size and connection count (#1481)

  • Loading branch information
djones6 committed Oct 9, 2019
1 parent 5c3589f commit 728acab53615e662cac6cd5a98391a471907ed16
@@ -22,9 +22,9 @@ import Foundation
var kituraNetPackage: Package.Dependency

if ProcessInfo.processInfo.environment["KITURA_NIO"] != nil {
kituraNetPackage = .package(url: "https://github.com/IBM-Swift/Kitura-NIO.git", from: "2.1.0")
kituraNetPackage = .package(url: "https://github.com/IBM-Swift/Kitura-NIO.git", from: "2.3.0")
} else {
kituraNetPackage = .package(url: "https://github.com/IBM-Swift/Kitura-net.git", from: "2.3.0")
kituraNetPackage = .package(url: "https://github.com/IBM-Swift/Kitura-net.git", from: "2.4.0")
}

let package = Package(
@@ -30,7 +30,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/IBM-Swift/LoggerAPI.git", from: "1.9.0"),
.package(url: "https://github.com/apple/swift-log.git", Version("0.0.0") ..< Version("2.0.0")),
.package(url: "https://github.com/IBM-Swift/Kitura-net.git", from: "2.3.0"),
.package(url: "https://github.com/IBM-Swift/Kitura-net.git", from: "2.4.0"),
.package(url: "https://github.com/IBM-Swift/Kitura-TemplateEngine.git", from: "2.0.0"),
.package(url: "https://github.com/IBM-Swift/KituraContracts.git", from: "1.0.0"),
.package(url: "https://github.com/IBM-Swift/TypeDecoder.git", from: "1.3.0"),
@@ -30,7 +30,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/IBM-Swift/LoggerAPI.git", from: "1.9.0"),
.package(url: "https://github.com/apple/swift-log.git", Version("0.0.0") ..< Version("2.0.0")),
.package(url: "https://github.com/IBM-Swift/Kitura-net.git", from: "2.3.0"),
.package(url: "https://github.com/IBM-Swift/Kitura-net.git", from: "2.4.0"),
.package(url: "https://github.com/IBM-Swift/Kitura-TemplateEngine.git", from: "2.0.0"),
.package(url: "https://github.com/IBM-Swift/KituraContracts.git", from: "1.0.0"),
.package(url: "https://github.com/IBM-Swift/TypeDecoder.git", from: "1.3.0"),
@@ -30,7 +30,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/IBM-Swift/LoggerAPI.git", from: "1.9.0"),
.package(url: "https://github.com/apple/swift-log.git", Version("0.0.0") ..< Version("2.0.0")),
.package(url: "https://github.com/IBM-Swift/Kitura-net.git", from: "2.3.0"),
.package(url: "https://github.com/IBM-Swift/Kitura-net.git", from: "2.4.0"),
.package(url: "https://github.com/IBM-Swift/Kitura-TemplateEngine.git", from: "2.0.0"),
.package(url: "https://github.com/IBM-Swift/KituraContracts.git", from: "1.0.0"),
.package(url: "https://github.com/IBM-Swift/TypeDecoder.git", from: "1.3.0"),
@@ -65,16 +65,18 @@ public class Kitura {
/// - Parameter withSSL: The `sslConfig` to use.
/// - Parameter keepAlive: The maximum number of additional requests to permit per Keep-Alive connection. Defaults to `.unlimited`. If set to `.disabled`, Keep-Alive will not be permitted.
/// - Parameter allowPortReuse: Determines whether the listener port may be shared with other Kitura instances (`SO_REUSEPORT`). Defaults to `false`. If the specified port is already in use by another listener that has not allowed sharing, the server will fail to start.
/// - Parameter options: Allows customization of default policies for this server.
/// - Returns: The created `HTTPServer`.
@discardableResult
public class func addHTTPServer(onPort port: Int,
onAddress address: String? = nil,
with delegate: ServerDelegate,
withSSL sslConfig: SSLConfig?=nil,
keepAlive keepAliveState: KeepAliveState = .unlimited,
allowPortReuse: Bool = false) -> HTTPServer {
allowPortReuse: Bool = false,
options: ServerOptions? = nil) -> HTTPServer {
return Kitura._addHTTPServer(on: .inet(port, address), with: delegate, withSSL: sslConfig,
keepAlive: keepAliveState, allowPortReuse: allowPortReuse)
keepAlive: keepAliveState, allowPortReuse: allowPortReuse, options: options)
}

/// Add an HTTPServer on a Unix domain socket path with a delegate.
@@ -96,20 +98,26 @@ public class Kitura {
public class func addHTTPServer(onUnixDomainSocket socketPath: String,
with delegate: ServerDelegate,
withSSL sslConfig: SSLConfig?=nil,
keepAlive keepAliveState: KeepAliveState = .unlimited) -> HTTPServer {
return Kitura._addHTTPServer(on: .unix(socketPath), with: delegate, withSSL: sslConfig, keepAlive: keepAliveState)
keepAlive keepAliveState: KeepAliveState = .unlimited,
options: ServerOptions? = nil) -> HTTPServer {

return Kitura._addHTTPServer(on: .unix(socketPath), with: delegate, withSSL: sslConfig, keepAlive: keepAliveState, options: options)
}

private class func _addHTTPServer(on listenType: ListenerType,
with delegate: ServerDelegate,
withSSL sslConfig: SSLConfig?=nil,
keepAlive keepAliveState: KeepAliveState = .unlimited,
allowPortReuse: Bool = false) -> HTTPServer {
allowPortReuse: Bool = false,
options: ServerOptions?) -> HTTPServer {
let server = HTTP.createServer()
server.delegate = delegate
server.sslConfig = sslConfig?.config
server.keepAliveState = keepAliveState
server.allowPortReuse = allowPortReuse
if let options = options {
server.options = options
}
serverLock.lock()
switch listenType {
case .inet(let port, let address):
@@ -300,3 +308,30 @@ public class Kitura {
internal private(set) static var httpServersAndUnixSocketPaths = [(server: HTTPServer, socketPath: String)]()
internal private(set) static var fastCGIServersAndPorts = [(server: FastCGIServer, port: Port, address: String?)]()
}

// MARK: ServerOptions
/**
Bridge [ServerOptions](http://ibm-swift.github.io/Kitura-net/Structs/ServerOptions.html) from [KituraNet](http://ibm-swift.github.io/Kitura-net) so that you only need to import `Kitura` to access it.
ServerOptions allows customization of default connection policies, including:
- `requestSizeLimit`: Defines the maximum size of an incoming request body, in bytes. If requests are received that are larger than this limit, they will be rejected and the connection will be closed. A value of `nil` means no limit.
- `connectionLimit`: Defines the maximum number of concurrent connections that a server should accept. Clients attempting to connect when this limit has been reached will be rejected. A value of `nil` means no limit.
The server can optionally respond to the client with a message in either of these cases. This message can be customized by defining `requestSizeResponseGenerator` and `connectionResponseGenerator`.
Example usage:
```
let port = 8080
let router = Router()
let connectionResponse: (Int, String) -> (HTTPStatusCode, String)? = { (limit, client) in
Log.debug("Rejecting request from \(client): Connection limit \(limit) reached")
return (.serviceUnavailable, "Service busy - please try again later.\r\n")
}
let serverOptions = ServerOptions(requestSizeLimit: 1000, connectionLimit: 10, connectionResponseGenerator: connectionResponse)
Kitura.addHTTPServer(onPort: port, with: router, options: serverOptions)
```
*/
public typealias ServerOptions = KituraNet.ServerOptions

@@ -62,6 +62,9 @@ class KituraTest: XCTestCase {
// A singleton Kitura server listening on HTTPS on a Unix socket
static private(set) var httpsUnixServer: HTTPServer?

// A short-lived Kitura server created for a specific test
private var serverWithOptions: HTTPServer?

// The port of the server returned by startServer().
private(set) var port = -1
// Whether the server used by doPerformServerTest should use SSL.
@@ -106,24 +109,24 @@ class KituraTest: XCTestCase {
return ServerTestBuilder(test: self, router: router, sslOption: sslOption, socketTypeOption: socketTypeOption, timeout: timeout, line: line)
}

func performServerTest(_ router: ServerDelegate, sslOption: SSLOption = SSLOption.both, socketTypeOption: SocketTypeOption = SocketTypeOption.both, timeout: TimeInterval = 10,
func performServerTest(_ router: ServerDelegate, options: ServerOptions? = nil, sslOption: SSLOption = SSLOption.both, socketTypeOption: SocketTypeOption = SocketTypeOption.both, timeout: TimeInterval = 10,
line: Int = #line, asyncTasks: (XCTestExpectation) -> Void...) {
performServerTest(router, sslOption: sslOption, socketTypeOption: socketTypeOption, timeout: timeout, line: line, asyncTasks: asyncTasks)
performServerTest(router, options: options, sslOption: sslOption, socketTypeOption: socketTypeOption, timeout: timeout, line: line, asyncTasks: asyncTasks)
}

func performServerTest(_ router: ServerDelegate, sslOption: SSLOption = SSLOption.both, socketTypeOption: SocketTypeOption = SocketTypeOption.both, timeout: TimeInterval = 10,
func performServerTest(_ router: ServerDelegate, options: ServerOptions? = nil, sslOption: SSLOption = SSLOption.both, socketTypeOption: SocketTypeOption = SocketTypeOption.both, timeout: TimeInterval = 10,
line: Int = #line, asyncTasks: [(XCTestExpectation) -> Void]) {
if sslOption != SSLOption.httpsOnly {
self.useSSL = false
if socketTypeOption != SocketTypeOption.unix {
self.useUnixSocket = false
doPerformServerTest(router: router, timeout: timeout, line: line, asyncTasks: asyncTasks)
doPerformServerTest(router: router, options: options, timeout: timeout, line: line, asyncTasks: asyncTasks)
}
#if !SKIP_UNIX_SOCKETS
setUp()
if socketTypeOption != SocketTypeOption.inet {
self.useUnixSocket = true
doPerformServerTest(router: router, timeout: timeout, line: line, asyncTasks: asyncTasks)
doPerformServerTest(router: router, options: options, timeout: timeout, line: line, asyncTasks: asyncTasks)
}
#endif
}
@@ -135,27 +138,27 @@ class KituraTest: XCTestCase {
self.useSSL = true
if socketTypeOption != SocketTypeOption.unix {
self.useUnixSocket = false
doPerformServerTest(router: router, timeout: timeout, line: line, asyncTasks: asyncTasks)
doPerformServerTest(router: router, options: options, timeout: timeout, line: line, asyncTasks: asyncTasks)
}
#if !SKIP_UNIX_SOCKETS
setUp()
if socketTypeOption != SocketTypeOption.inet {
self.useUnixSocket = true
doPerformServerTest(router: router, timeout: timeout, line: line, asyncTasks: asyncTasks)
doPerformServerTest(router: router, options: options, timeout: timeout, line: line, asyncTasks: asyncTasks)
}
#endif
}
}

func doPerformServerTest(router: ServerDelegate, timeout: TimeInterval, line: Int, asyncTasks: [(XCTestExpectation) -> Void]) {
func doPerformServerTest(router: ServerDelegate, options: ServerOptions?, timeout: TimeInterval, line: Int, asyncTasks: [(XCTestExpectation) -> Void]) {

if self.useUnixSocket {
guard let socketPath = startUnixSocketServer(router: router) else {
guard let socketPath = startUnixSocketServer(router: router, options: options) else {
return XCTFail("Error starting server. useSSL:\(self.useSSL), useUnixSocket:\(self.useUnixSocket)")
}
XCTAssertEqual(socketPath, self.socketFilePath, "Server is listening on the wrong path")
} else {
guard let port = startServer(router: router) else {
guard let port = startServer(router: router, options: options) else {
return XCTFail("Error starting server. useSSL:\(self.useSSL), useUnixSocket:\(self.useUnixSocket)")
}
self.port = port
@@ -172,9 +175,45 @@ class KituraTest: XCTestCase {
waitForExpectations(timeout: timeout) { error in
XCTAssertNil(error)
}

// If we created a short-lived server for specific ServerOptions, shut it down now
serverWithOptions?.stop()
}

// Start a server. If a non-nil `options` is provided, then the server is stored
// as the `serverWithOptions` property, and will be shut down at the end of the test.
private func doStartServer(router: ServerDelegate, options: ServerOptions?) -> HTTPServer? {
let server = HTTP.createServer()
server.delegate = router

if useSSL {
server.sslConfig = KituraTest.sslConfig.config
}

if let options = options {
server.options = options
serverWithOptions = server
}

do {
try server.listen(on: 0, address: "localhost")
return server
} catch {
XCTFail("Error starting server: \(error)")
return nil
}
}

private func startServer(router: ServerDelegate) -> Int? {
// Start a server on an inet socket. If nil `options` are specified, this is
// a generic server that can be reused between tests, and will be stored as a
// static property.
private func startServer(router: ServerDelegate, options: ServerOptions?) -> Int? {
// Servers with options (live for duration of one test)
if options != nil {
let server = doStartServer(router: router, options: options)
return server?.port
}
// Generic servers that can be long-lived (for a whole test class)
if useSSL {
if let server = KituraTest.httpsInetServer {
server.delegate = router
@@ -187,28 +226,54 @@ class KituraTest: XCTestCase {
}
}

let server = doStartServer(router: router, options: nil)

if useSSL {
KituraTest.httpsInetServer = server
} else {
KituraTest.httpInetServer = server
}
return server?.port
}

// Start a server. If a non-nil `options` is provided, then the server is stored
// as the `serverWithOptions` property, and will be shut down at the end of the test.
private func doStartUnixSocketServer(router: ServerDelegate, options: ServerOptions?) -> HTTPServer? {
let server = HTTP.createServer()
server.delegate = router

if useSSL {
server.sslConfig = KituraTest.sslConfig.config
}

do {
try server.listen(on: 0, address: "localhost")
if let options = options {
server.options = options
serverWithOptions = server
}

if useSSL {
KituraTest.httpsInetServer = server
} else {
KituraTest.httpInetServer = server
}
return server.port
// Create a temporary path for Unix domain socket
let socketPath = uniqueTemporaryFilePath()
self.socketFilePath = socketPath

do {
try server.listen(unixDomainSocketPath: socketPath)
return server
} catch {
XCTFail("Error starting server: \(error)")
return nil
}
}

private func startUnixSocketServer(router: ServerDelegate) -> String? {
// Start a server on a unix domain socket. If nil `options` are specified, this is
// a generic server that can be reused between tests, and will be stored as a
// static property.
private func startUnixSocketServer(router: ServerDelegate, options: ServerOptions?) -> String? {
// Servers with options (live for duration of one test)
if (options != nil) {
let server = doStartUnixSocketServer(router: router, options: options)
return server?.unixDomainSocketPath
}
// Generic servers that can be long-lived (for a whole test class)
if useSSL {
if let server = KituraTest.httpsUnixServer {
server.delegate = router
@@ -230,29 +295,15 @@ class KituraTest: XCTestCase {
return server.unixDomainSocketPath
}
}
let server = HTTP.createServer()
server.delegate = router
if useSSL {
server.sslConfig = KituraTest.sslConfig.config
}

// Create a temporary path for Unix domain socket
let socketPath = uniqueTemporaryFilePath()
self.socketFilePath = socketPath

do {
try server.listen(unixDomainSocketPath: socketPath)
let server = doStartUnixSocketServer(router: router, options: nil)

if useSSL {
KituraTest.httpsUnixServer = server
} else {
KituraTest.httpUnixServer = server
}
return server.unixDomainSocketPath
} catch {
XCTFail("Error starting server: \(error)")
return nil
if useSSL {
KituraTest.httpsUnixServer = server
} else {
KituraTest.httpUnixServer = server
}
return server?.unixDomainSocketPath
}

func stopServer() {
@@ -663,7 +663,7 @@ final class TestCodableRouter: KituraTest, KituraTestSuite {
}

func testCodableGetSingleQueryParameters() {
let date: Date = Coder().dateFormatter.date(from: Coder().dateFormatter.string(from: Date()))!
let date: Date = Coder.defaultDateFormatter.date(from: Coder.defaultDateFormatter.string(from: Date()))!

let expectedQuery = MyQuery(intField: 23, optionalIntField: 282, stringField: "a string", intArray: [1, 2, 3], dateField: date, optionalDateField: date, nested: Nested(nestedIntField: 333, nestedStringField: "nested string"))

@@ -715,7 +715,7 @@ final class TestCodableRouter: KituraTest, KituraTestSuite {
func testCodableGetArrayQueryParameters() {
/// Currently the milliseconds are cut off by our date formatter
/// This synchronizes it for testing with the codable route
let date: Date = Coder().dateFormatter.date(from: Coder().dateFormatter.string(from: Date()))!
let date: Date = Coder.defaultDateFormatter.date(from: Coder.defaultDateFormatter.string(from: Date()))!

let expectedQuery = MyQuery(intField: 23, optionalIntField: 282, stringField: "a string", intArray: [1, 2, 3], dateField: date, optionalDateField: date, nested: Nested(nestedIntField: 333, nestedStringField: "nested string"))

@@ -767,7 +767,7 @@ final class TestCodableRouter: KituraTest, KituraTestSuite {
func testCodableDeleteQueryParameters() {
/// Currently the milliseconds are cut off by our date formatter
/// This synchronizes it for testing with the codable route
let date: Date = Coder().dateFormatter.date(from: Coder().dateFormatter.string(from: Date()))!
let date: Date = Coder.defaultDateFormatter.date(from: Coder.defaultDateFormatter.string(from: Date()))!

let expectedQuery = MyQuery(intField: 23, optionalIntField: 282, stringField: "a string", intArray: [1, 2, 3], dateField: date, optionalDateField: date, nested: Nested(nestedIntField: 333, nestedStringField: "nested string"))

0 comments on commit 728acab

Please sign in to comment.
You can’t perform that action at this time.