Skip to content

Commit 29b0dde

Browse files
RudraniWdjones6
authored andcommitted
feat: Ability to limit request size and connection count (#221)
1 parent 52b4235 commit 29b0dde

File tree

8 files changed

+328
-23
lines changed

8 files changed

+328
-23
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ let package = Package(
4242
dependencies: []),
4343
.target(
4444
name: "KituraNet",
45-
dependencies: ["NIO", "NIOFoundationCompat", "NIOHTTP1", "NIOSSL", "SSLService", "LoggerAPI", "NIOWebSocket", "CLinuxHelpers", "NIOExtras"]),
45+
dependencies: ["NIO", "NIOFoundationCompat", "NIOHTTP1", "NIOSSL", "SSLService", "LoggerAPI", "NIOWebSocket", "CLinuxHelpers", "NIOConcurrencyHelpers", "NIOExtras"]),
4646
.testTarget(
4747
name: "KituraNetTests",
4848
dependencies: ["KituraNet"])

Sources/KituraNet/HTTP/HTTPRequestHandler.swift

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ internal class HTTPRequestHandler: ChannelInboundHandler, RemovableChannelHandle
2626
/// The HTTPServer instance on which this handler is installed
2727
var server: HTTPServer
2828

29+
var requestSize: Int = 0
30+
2931
/// The serverRequest related to this handler instance
3032
var serverRequest: HTTPServerRequest?
3133

@@ -66,7 +68,6 @@ internal class HTTPRequestHandler: ChannelInboundHandler, RemovableChannelHandle
6668
self.enableSSLVerification = true
6769
}
6870
}
69-
7071
public typealias InboundIn = HTTPServerRequestPart
7172
public typealias OutboundOut = HTTPServerResponsePart
7273

@@ -76,12 +77,42 @@ internal class HTTPRequestHandler: ChannelInboundHandler, RemovableChannelHandle
7677
// If an upgrade to WebSocket fails, both `errorCaught` and `channelRead` are triggered.
7778
// We'd want to return the error via `errorCaught`.
7879
if errorResponseSent { return }
79-
8080
switch request {
8181
case .head(let header):
82+
serverRequest = HTTPServerRequest(channel: context.channel, requestHead: header, enableSSL: enableSSLVerification)
83+
if let requestSizeLimit = server.options.requestSizeLimit,
84+
let contentLength = header.headers["Content-Length"].first,
85+
let contentLengthValue = Int(contentLength) {
86+
if contentLengthValue > requestSizeLimit {
87+
do {
88+
if let (httpStatus, response) = server.options.requestSizeResponseGenerator(requestSizeLimit, serverRequest?.remoteAddress ?? "") {
89+
serverResponse = HTTPServerResponse(channel: context.channel, handler: self)
90+
errorResponseSent = true
91+
try serverResponse?.end(with: httpStatus, message: response)
92+
}
93+
} catch {
94+
Log.error("Failed to send error response")
95+
}
96+
context.close()
97+
}
98+
}
8299
serverRequest = HTTPServerRequest(channel: context.channel, requestHead: header, enableSSL: enableSSLVerification)
83100
self.clientRequestedKeepAlive = header.isKeepAlive
84101
case .body(var buffer):
102+
requestSize += buffer.readableBytes
103+
if let requestSizeLimit = server.options.requestSizeLimit {
104+
if requestSize > requestSizeLimit {
105+
do {
106+
if let (httpStatus, response) = server.options.requestSizeResponseGenerator(requestSizeLimit,serverRequest?.remoteAddress ?? "") {
107+
serverResponse = HTTPServerResponse(channel: context.channel, handler: self)
108+
errorResponseSent = true
109+
try serverResponse?.end(with: httpStatus, message: response)
110+
}
111+
} catch {
112+
Log.error("Failed to send error response")
113+
}
114+
}
115+
}
85116
guard let serverRequest = serverRequest else {
86117
Log.error("No ServerRequest available")
87118
return
@@ -91,7 +122,23 @@ internal class HTTPRequestHandler: ChannelInboundHandler, RemovableChannelHandle
91122
} else {
92123
serverRequest.buffer!.byteBuffer.writeBuffer(&buffer)
93124
}
125+
94126
case .end:
127+
requestSize = 0
128+
server.connectionCount.add(1)
129+
if let connectionLimit = server.options.connectionLimit {
130+
if server.connectionCount.load() > connectionLimit {
131+
do {
132+
if let (httpStatus, response) = server.options.connectionResponseGenerator(connectionLimit,serverRequest?.remoteAddress ?? "") {
133+
serverResponse = HTTPServerResponse(channel: context.channel, handler: self)
134+
errorResponseSent = true
135+
try serverResponse?.end(with: httpStatus, message: response)
136+
}
137+
} catch {
138+
Log.error("Failed to send error response")
139+
}
140+
}
141+
}
95142
serverResponse = HTTPServerResponse(channel: context.channel, handler: self)
96143
//Make sure we use the latest delegate registered with the server
97144
DispatchQueue.global().async {
@@ -152,4 +199,8 @@ internal class HTTPRequestHandler: ChannelInboundHandler, RemovableChannelHandle
152199
func updateKeepAliveState() {
153200
keepAliveState.decrement()
154201
}
202+
203+
func channelInactive(context: ChannelHandlerContext, httpServer: HTTPServer) {
204+
httpServer.connectionCount.sub(1)
205+
}
155206
}

Sources/KituraNet/HTTP/HTTPServer.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import SSLService
2222
import LoggerAPI
2323
import NIOWebSocket
2424
import CLinuxHelpers
25+
import Foundation
2526
import NIOExtras
27+
import NIOConcurrencyHelpers
2628

2729
#if os(Linux)
2830
import Glibc
@@ -127,22 +129,30 @@ public class HTTPServer: Server {
127129

128130
var quiescingHelper: ServerQuiescingHelper?
129131

132+
/// server configuration
133+
public var options: ServerOptions = ServerOptions()
134+
135+
//counter for no of connections
136+
var connectionCount = Atomic(value: 0)
137+
130138
/**
131139
Creates an HTTP server object.
132140

133141
### Usage Example: ###
134142
````swift
135-
let server = HTTPServer()
143+
let config =HTTPServerConfiguration(requestSize: 1000, coonectionLimit: 100)
144+
let server = HTTPServer(serverconfig: config)
136145
server.listen(on: 8080)
137146
````
138147
*/
139-
public init() {
148+
public init(options: ServerOptions = ServerOptions()) {
140149
#if os(Linux)
141150
let numberOfCores = Int(linux_sched_getaffinity())
142151
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: numberOfCores > 0 ? numberOfCores : System.coreCount)
143152
#else
144153
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
145154
#endif
155+
self.options = options
146156
}
147157

148158
/**
@@ -309,7 +319,7 @@ public class HTTPServer: Server {
309319
}
310320
.childChannelInitializer { channel in
311321
let httpHandler = HTTPRequestHandler(for: self)
312-
let config: NIOHTTPServerUpgradeConfiguration = (upgraders: upgraders, completionHandler: { _ in
322+
let config: HTTPUpgradeConfiguration = (upgraders: upgraders, completionHandler: {_ in
313323
_ = channel.pipeline.removeHandler(httpHandler)
314324
})
315325
return channel.pipeline.configureHTTPServerPipeline(withServerUpgrade: config, withErrorHandling: true).flatMap {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright IBM Corporation 2019
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Foundation
18+
import LoggerAPI
19+
20+
/**
21+
ServerOptions allows customization of default server policies, including:
22+
23+
- `requestSizeLimit`: Defines the maximum size for the body of an incoming request, in bytes. If a request body is larger than this limit, it will be rejected and the connection will be closed. A value of `nil` means no limit.
24+
- `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.
25+
26+
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`.
27+
28+
Example usage:
29+
```
30+
let server = HTTP.createServer()
31+
server.options = ServerOptions(requestSizeLimit: 1000, connectionLimit: 10)
32+
```
33+
*/
34+
public struct ServerOptions {
35+
36+
/// A default limit of 100mb on the size of the request body that a server should accept.
37+
public static let defaultRequestSizeLimit = 104857600
38+
39+
/// A default limit of 10,000 on the number of concurrent connections that a server should accept.
40+
public static let defaultConnectionLimit = 10000
41+
42+
/// Defines a default response to an over-sized request of HTTP 413: Request Too Long. A message is also
43+
/// logged at debug level.
44+
public static let defaultRequestSizeResponseGenerator: (Int, String) -> (HTTPStatusCode, String)? = { (limit, clientSource) in
45+
Log.debug("Request from \(clientSource) exceeds size limit of \(limit) bytes. Connection will be closed.")
46+
return (.requestTooLong, "")
47+
}
48+
49+
/// Defines a default response when refusing a new connection of HTTP 503: Service Unavailable. A message is
50+
/// also logged at debug level.
51+
public static let defaultConnectionResponseGenerator: (Int, String) -> (HTTPStatusCode, String)? = { (limit, clientSource) in
52+
Log.debug("Rejected connection from \(clientSource): Maximum connection limit of \(limit) reached.")
53+
return (.serviceUnavailable, "")
54+
}
55+
56+
/// Defines the maximum size for the body of an incoming request, in bytes. If a request body is larger
57+
/// than this limit, it will be rejected and the connection will be closed.
58+
///
59+
/// A value of `nil` means no limit.
60+
public let requestSizeLimit: Int?
61+
62+
/// Defines the maximum number of concurrent connections that a server should accept. Clients attempting
63+
/// to connect when this limit has been reached will be rejected.
64+
public let connectionLimit: Int?
65+
66+
/**
67+
Determines the response message and HTTP status code used to respond to clients whose request exceeds
68+
the `requestSizeLimit`. The current limit and client's address are provided as parameters to enable a
69+
message to be logged, and/or a response to be provided back to the client.
70+
71+
The returned tuple indicates the HTTP status code and response body to send to the client. If `nil` is
72+
returned, then no response will be sent.
73+
74+
Example usage:
75+
```
76+
let oversizeResponse: (Int, String) -> (HTTPStatusCode, String)? = { (limit, client) in
77+
Log.debug("Rejecting request from \(client): Exceeds limit of \(limit) bytes")
78+
return (.requestTooLong, "Your request exceeds the limit of \(limit) bytes.\r\n")
79+
}
80+
```
81+
*/
82+
public let requestSizeResponseGenerator: (Int, String) -> (HTTPStatusCode, String)?
83+
84+
/**
85+
Determines the response message and HTTP status code used to respond to clients that attempt to connect
86+
while the server is already servicing the maximum number of connections, as defined by `connectionLimit`.
87+
The current limit and client's address are provided as parameters to enable a message to be logged,
88+
and/or a response to be provided back to the client.
89+
90+
The returned tuple indicates the HTTP status code and response body to send to the client. If `nil` is
91+
returned, then no response will be sent.
92+
93+
Example usage:
94+
```
95+
let connectionResponse: (Int, String) -> (HTTPStatusCode, String)? = { (limit, client) in
96+
Log.debug("Rejecting request from \(client): Connection limit \(limit) reached")
97+
return (.serviceUnavailable, "Service busy - please try again later.\r\n")
98+
}
99+
```
100+
*/
101+
public let connectionResponseGenerator: (Int, String) -> (HTTPStatusCode, String)?
102+
103+
/// Create a `ServerOptions` to determine the behaviour of a `Server`.
104+
///
105+
/// - parameter requestSizeLimit: The maximum size of an incoming request body. Defaults to `ServerOptions.defaultRequestSizeLimit`.
106+
/// - parameter connectionLimit: The maximum number of concurrent connections. Defaults to `ServerOptions.defaultConnectionLimit`.
107+
/// - parameter requestSizeResponseGenerator: A closure producing a response to send to a client when an over-sized request is rejected. Defaults to `ServerOptions.defaultRequestSizeResponseGenerator`.
108+
/// - parameter defaultConnectionResponseGenerator: A closure producing a response to send to a client when a the server is busy and new connections are not being accepted. Defaults to `ServerOptions.defaultConnectionResponseGenerator`.
109+
public init(requestSizeLimit: Int? = ServerOptions.defaultRequestSizeLimit,
110+
connectionLimit: Int? = ServerOptions.defaultConnectionLimit,
111+
requestSizeResponseGenerator: @escaping (Int, String) -> (HTTPStatusCode, String)? = ServerOptions.defaultRequestSizeResponseGenerator,
112+
connectionResponseGenerator: @escaping (Int, String) -> (HTTPStatusCode, String)? = ServerOptions.defaultConnectionResponseGenerator)
113+
{
114+
self.requestSizeLimit = requestSizeLimit
115+
self.connectionLimit = connectionLimit
116+
self.requestSizeResponseGenerator = requestSizeResponseGenerator
117+
self.connectionResponseGenerator = connectionResponseGenerator
118+
}
119+
120+
}

Tests/KituraNetTests/ClientE2ETests.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class ClientE2ETests: KituraNetTest {
3737
("testQueryParameters", testQueryParameters),
3838
("testRedirect", testRedirect),
3939
("testPercentEncodedQuery", testPercentEncodedQuery),
40+
("testRequestSize",testRequestSize),
4041
]
4142
}
4243

@@ -52,6 +53,32 @@ class ClientE2ETests: KituraNetTest {
5253

5354
let delegate = TestServerDelegate()
5455

56+
func testRequestSize() {
57+
performServerTest(serverConfig: ServerOptions(requestSizeLimit: 10000, connectionLimit: 100),delegate, useSSL: false, asyncTasks: { expectation in
58+
let payload = "[" + contentTypesString + "," + contentTypesString + contentTypesString + "," + contentTypesString + "]"
59+
self.performRequest("post", path: "/largepost", callback: {response in
60+
XCTAssertEqual(response?.statusCode, HTTPStatusCode.requestTooLong)
61+
do {
62+
let expectedResult = ""
63+
var data = Data()
64+
let count = try response?.readAllData(into: &data)
65+
XCTAssertEqual(count, expectedResult.count, "Result should have been \(expectedResult.count) bytes, was \(String(describing: count)) bytes")
66+
let postValue = String(data: data, encoding: .utf8)
67+
if let postValue = postValue {
68+
XCTAssertEqual(postValue, expectedResult)
69+
} else {
70+
XCTFail("postValue's value wasn't an UTF8 string")
71+
}
72+
} catch {
73+
XCTFail("Failed reading the body of the response")
74+
}
75+
expectation.fulfill()
76+
}) {request in
77+
request.write(from: payload)
78+
}
79+
})
80+
}
81+
5582
func testHeadRequests() {
5683
performServerTest(delegate) { expectation in
5784
self.performRequest("head", path: "/headtest", callback: {response in

0 commit comments

Comments
 (0)