Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: Kitura/Kitura-NIO
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 6a7f631cde520edd77ebddd58adddb0d3e505022
Choose a base ref
..
head repository: Kitura/Kitura-NIO
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 2537bf1964d820035f88c5074e75a7338132bf5b
Choose a head ref
Showing with 4,457 additions and 1,788 deletions.
  1. +1 −1 Package.swift
  2. +53 −2 Sources/KituraNet/HTTP/HTTPRequestHandler.swift
  3. +20 −5 Sources/KituraNet/HTTP/HTTPServer.swift
  4. +120 −0 Sources/KituraNet/HTTP/ServerOptions.swift
  5. +27 −0 Tests/KituraNetTests/ClientE2ETests.swift
  6. +93 −0 Tests/KituraNetTests/ConnectionLimitTests.swift
  7. +16 −12 Tests/KituraNetTests/KituraNIOTest.swift
  8. +5 −5 Tests/KituraNetTests/KituraNetWebSocketUpgrade.swift
  9. +6 −3 Tests/KituraNetTests/RegressionTests.swift
  10. +16 −12 docs/Classes.html
  11. +22 −19 docs/Classes/BufferList.html
  12. +29 −26 docs/Classes/ClientRequest.html
  13. +38 −75 docs/Classes/ClientRequest/Options.html
  14. +22 −15 docs/Classes/ClientResponse.html
  15. +6 −3 docs/Classes/FastCGI.html
  16. +97 −34 docs/Classes/FastCGIServer.html
  17. +26 −21 docs/Classes/FastCGIServerRequest.html
  18. +18 −35 docs/Classes/FastCGIServerRequest/FastCGIParserErrorType.html
  19. +17 −14 docs/Classes/FastCGIServerResponse.html
  20. +35 −19 docs/Classes/HTTP.html
  21. +277 −48 docs/Classes/HTTPServer.html
  22. +17 −12 docs/Classes/HTTPServerRequest.html
  23. +15 −12 docs/Classes/HTTPServerResponse.html
  24. +27 −24 docs/Classes/HeadersContainer.html
  25. +8 −5 docs/Classes/ListenerGroup.html
  26. +10 −7 docs/Classes/SPIUtils.html
  27. +21 −18 docs/Classes/URLParser.html
  28. +8 −4 docs/Enums.html
  29. +174 −212 docs/Enums/HTTPStatusCode.html
  30. +18 −35 docs/Enums/HTTPStatusCode/Class.html
  31. +13 −18 docs/Enums/KeepAliveState.html
  32. +14 −23 docs/Enums/ServerState.html
  33. +10 −6 docs/Protocols.html
  34. +15 −12 docs/Protocols/ProtocolHandlerFactory.html
  35. +216 −31 docs/Protocols/Server.html
  36. +9 −6 docs/Protocols/ServerDelegate.html
  37. +10 −7 docs/Protocols/ServerMonitor.html
  38. +22 −17 docs/Protocols/ServerRequest.html
  39. +15 −12 docs/Protocols/ServerResponse.html
  40. +51 −3 docs/Structs.html
  41. +8 −5 docs/Structs/ConnectionUpgrader.html
  42. +6 −3 docs/Structs/Monitor.html
  43. +542 −0 docs/Structs/ServerOptions.html
  44. +2 −2 docs/badge.svg
  45. +7 −3 docs/css/jazzy.css
  46. +16 −12 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes.html
  47. +22 −19 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/BufferList.html
  48. +29 −26 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/ClientRequest.html
  49. +38 −75 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/ClientRequest/Options.html
  50. +22 −15 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/ClientResponse.html
  51. +6 −3 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/FastCGI.html
  52. +97 −34 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/FastCGIServer.html
  53. +26 −21 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/FastCGIServerRequest.html
  54. +18 −35 ...aNet.docset/Contents/Resources/Documents/Classes/FastCGIServerRequest/FastCGIParserErrorType.html
  55. +17 −14 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/FastCGIServerResponse.html
  56. +35 −19 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/HTTP.html
  57. +277 −48 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/HTTPServer.html
  58. +17 −12 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/HTTPServerRequest.html
  59. +15 −12 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/HTTPServerResponse.html
  60. +27 −24 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/HeadersContainer.html
  61. +8 −5 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/ListenerGroup.html
  62. +10 −7 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/SPIUtils.html
  63. +21 −18 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Classes/URLParser.html
  64. +8 −4 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Enums.html
  65. +174 −212 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Enums/HTTPStatusCode.html
  66. +18 −35 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Enums/HTTPStatusCode/Class.html
  67. +13 −18 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Enums/KeepAliveState.html
  68. +14 −23 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Enums/ServerState.html
  69. +10 −6 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Protocols.html
  70. +15 −12 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Protocols/ProtocolHandlerFactory.html
  71. +216 −31 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Protocols/Server.html
  72. +9 −6 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Protocols/ServerDelegate.html
  73. +10 −7 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Protocols/ServerMonitor.html
  74. +22 −17 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Protocols/ServerRequest.html
  75. +15 −12 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Protocols/ServerResponse.html
  76. +51 −3 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Structs.html
  77. +8 −5 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Structs/ConnectionUpgrader.html
  78. +6 −3 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Structs/Monitor.html
  79. +542 −0 docs/docsets/KituraNet.docset/Contents/Resources/Documents/Structs/ServerOptions.html
  80. +7 −3 docs/docsets/KituraNet.docset/Contents/Resources/Documents/css/jazzy.css
  81. +14 −10 docs/docsets/KituraNet.docset/Contents/Resources/Documents/index.html
  82. +0 −6 docs/docsets/KituraNet.docset/Contents/Resources/Documents/js/jazzy.js
  83. +17 −9 docs/docsets/KituraNet.docset/Contents/Resources/Documents/js/jazzy.search.js
  84. +2 −4 docs/docsets/KituraNet.docset/Contents/Resources/Documents/js/jquery.min.js
  85. +1 −6 docs/docsets/KituraNet.docset/Contents/Resources/Documents/js/lunr.min.js
  86. +182 −46 docs/docsets/KituraNet.docset/Contents/Resources/Documents/js/typeahead.jquery.js
  87. +1 −1 docs/docsets/KituraNet.docset/Contents/Resources/Documents/search.json
  88. BIN docs/docsets/KituraNet.docset/Contents/Resources/docSet.dsidx
  89. BIN docs/docsets/KituraNet.tgz
  90. +14 −10 docs/index.html
  91. +0 −6 docs/js/jazzy.js
  92. +17 −9 docs/js/jazzy.search.js
  93. +2 −4 docs/js/jquery.min.js
  94. +1 −6 docs/js/lunr.min.js
  95. +182 −46 docs/js/typeahead.jquery.js
  96. +1 −1 docs/search.json
  97. +2 −2 docs/undocumented.json
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ let package = Package(
dependencies: []),
.target(
name: "KituraNet",
dependencies: ["NIO", "NIOFoundationCompat", "NIOHTTP1", "NIOSSL", "SSLService", "LoggerAPI", "NIOWebSocket", "CLinuxHelpers", "NIOExtras"]),
dependencies: ["NIO", "NIOFoundationCompat", "NIOHTTP1", "NIOSSL", "SSLService", "LoggerAPI", "NIOWebSocket", "CLinuxHelpers", "NIOConcurrencyHelpers", "NIOExtras"]),
.testTarget(
name: "KituraNetTests",
dependencies: ["KituraNet"])
55 changes: 53 additions & 2 deletions Sources/KituraNet/HTTP/HTTPRequestHandler.swift
Original file line number Diff line number Diff line change
@@ -26,6 +26,8 @@ internal class HTTPRequestHandler: ChannelInboundHandler, RemovableChannelHandle
/// The HTTPServer instance on which this handler is installed
var server: HTTPServer

var requestSize: Int = 0

/// The serverRequest related to this handler instance
var serverRequest: HTTPServerRequest?

@@ -66,7 +68,6 @@ internal class HTTPRequestHandler: ChannelInboundHandler, RemovableChannelHandle
self.enableSSLVerification = true
}
}

public typealias InboundIn = HTTPServerRequestPart
public typealias OutboundOut = HTTPServerResponsePart

@@ -76,12 +77,42 @@ internal class HTTPRequestHandler: ChannelInboundHandler, RemovableChannelHandle
// If an upgrade to WebSocket fails, both `errorCaught` and `channelRead` are triggered.
// We'd want to return the error via `errorCaught`.
if errorResponseSent { return }

switch request {
case .head(let header):
serverRequest = HTTPServerRequest(channel: context.channel, requestHead: header, enableSSL: enableSSLVerification)
if let requestSizeLimit = server.options.requestSizeLimit,
let contentLength = header.headers["Content-Length"].first,
let contentLengthValue = Int(contentLength) {
if contentLengthValue > requestSizeLimit {
do {
if let (httpStatus, response) = server.options.requestSizeResponseGenerator(requestSizeLimit, serverRequest?.remoteAddress ?? "") {
serverResponse = HTTPServerResponse(channel: context.channel, handler: self)
errorResponseSent = true
try serverResponse?.end(with: httpStatus, message: response)
}
} catch {
Log.error("Failed to send error response")
}
context.close()
}
}
serverRequest = HTTPServerRequest(channel: context.channel, requestHead: header, enableSSL: enableSSLVerification)
self.clientRequestedKeepAlive = header.isKeepAlive
case .body(var buffer):
requestSize += buffer.readableBytes
if let requestSizeLimit = server.options.requestSizeLimit {
if requestSize > requestSizeLimit {
do {
if let (httpStatus, response) = server.options.requestSizeResponseGenerator(requestSizeLimit,serverRequest?.remoteAddress ?? "") {
serverResponse = HTTPServerResponse(channel: context.channel, handler: self)
errorResponseSent = true
try serverResponse?.end(with: httpStatus, message: response)
}
} catch {
Log.error("Failed to send error response")
}
}
}
guard let serverRequest = serverRequest else {
Log.error("No ServerRequest available")
return
@@ -91,7 +122,23 @@ internal class HTTPRequestHandler: ChannelInboundHandler, RemovableChannelHandle
} else {
serverRequest.buffer!.byteBuffer.writeBuffer(&buffer)
}

case .end:
requestSize = 0
server.connectionCount.add(1)
if let connectionLimit = server.options.connectionLimit {
if server.connectionCount.load() > connectionLimit {
do {
if let (httpStatus, response) = server.options.connectionResponseGenerator(connectionLimit,serverRequest?.remoteAddress ?? "") {
serverResponse = HTTPServerResponse(channel: context.channel, handler: self)
errorResponseSent = true
try serverResponse?.end(with: httpStatus, message: response)
}
} catch {
Log.error("Failed to send error response")
}
}
}
serverResponse = HTTPServerResponse(channel: context.channel, handler: self)
//Make sure we use the latest delegate registered with the server
DispatchQueue.global().async {
@@ -152,4 +199,8 @@ internal class HTTPRequestHandler: ChannelInboundHandler, RemovableChannelHandle
func updateKeepAliveState() {
keepAliveState.decrement()
}

func channelInactive(context: ChannelHandlerContext, httpServer: HTTPServer) {
httpServer.connectionCount.sub(1)
}
}
25 changes: 20 additions & 5 deletions Sources/KituraNet/HTTP/HTTPServer.swift
Original file line number Diff line number Diff line change
@@ -22,7 +22,9 @@ import SSLService
import LoggerAPI
import NIOWebSocket
import CLinuxHelpers
import Foundation
import NIOExtras
import NIOConcurrencyHelpers

#if os(Linux)
import Glibc
@@ -89,8 +91,8 @@ public class HTTPServer: Server {
````
*/
public private(set) var state: ServerState {
get {
return self.syncQ.sync {
get {
return self.syncQ.sync {
return self._state
}
}
@@ -135,6 +137,11 @@ public class HTTPServer: Server {

/// The EventLoopGroup used by this HTTPServer. This property may be assigned
/// once and once only, by calling `setEventLoopGroup(value:)` before `listen()` is called.
/// Server runs on `eventLoopGroup` which it is initialized to i.e. when user explicitly provides `eventLoopGroup` for server,
/// public variable `eventLoopGroup` will return value stored private variable `_eventLoopGroup` when `ServerBootstrap` is called in `listen()`
/// making the server run of userdefined EventLoopGroup. If the `setEventLoopGroup(value:)` is not called, `nil` in variable `_eventLoopGroup` forces
/// Server to run in `globalELG` since value of `eventLoopGroup` in `ServerBootstrap(group: eventLoopGroup)` gets initialzed to value `globalELG`
/// if `setEventLoopGroup(value:)` is not called before `listen()`
public var eventLoopGroup: EventLoopGroup {
if let value = self._eventLoopGroup { return value }
let value = globalELG
@@ -144,16 +151,24 @@ public class HTTPServer: Server {

var quiescingHelper: ServerQuiescingHelper?

/// server configuration
public var options: ServerOptions = ServerOptions()

//counter for no of connections
var connectionCount = Atomic(value: 0)

/**
Creates an HTTP server object.

### Usage Example: ###
````swift
let server = HTTPServer()
let config =HTTPServerConfiguration(requestSize: 1000, coonectionLimit: 100)
let server = HTTPServer(serverconfig: config)
server.listen(on: 8080)
````
*/
public init() {
public init(options: ServerOptions = ServerOptions()) {
self.options = options
}

/**
@@ -330,7 +345,7 @@ public class HTTPServer: Server {
}
.childChannelInitializer { channel in
let httpHandler = HTTPRequestHandler(for: self)
let config: NIOHTTPServerUpgradeConfiguration = (upgraders: upgraders, completionHandler: { _ in
let config: HTTPUpgradeConfiguration = (upgraders: upgraders, completionHandler: {_ in
_ = channel.pipeline.removeHandler(httpHandler)
})
return channel.pipeline.configureHTTPServerPipeline(withServerUpgrade: config, withErrorHandling: true).flatMap {
120 changes: 120 additions & 0 deletions Sources/KituraNet/HTTP/ServerOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright IBM Corporation 2019
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation
import LoggerAPI

/**
ServerOptions allows customization of default server policies, including:

- `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.
- `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 server = HTTP.createServer()
server.options = ServerOptions(requestSizeLimit: 1000, connectionLimit: 10)
```
*/
public struct ServerOptions {

/// A default limit of 100mb on the size of the request body that a server should accept.
public static let defaultRequestSizeLimit = 104857600

/// A default limit of 10,000 on the number of concurrent connections that a server should accept.
public static let defaultConnectionLimit = 10000

/// Defines a default response to an over-sized request of HTTP 413: Request Too Long. A message is also
/// logged at debug level.
public static let defaultRequestSizeResponseGenerator: (Int, String) -> (HTTPStatusCode, String)? = { (limit, clientSource) in
Log.debug("Request from \(clientSource) exceeds size limit of \(limit) bytes. Connection will be closed.")
return (.requestTooLong, "")
}

/// Defines a default response when refusing a new connection of HTTP 503: Service Unavailable. A message is
/// also logged at debug level.
public static let defaultConnectionResponseGenerator: (Int, String) -> (HTTPStatusCode, String)? = { (limit, clientSource) in
Log.debug("Rejected connection from \(clientSource): Maximum connection limit of \(limit) reached.")
return (.serviceUnavailable, "")
}

/// 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.
public let requestSizeLimit: Int?

/// 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.
public let connectionLimit: Int?

/**
Determines the response message and HTTP status code used to respond to clients whose request exceeds
the `requestSizeLimit`. The current limit and client's address are provided as parameters to enable a
message to be logged, and/or a response to be provided back to the client.

The returned tuple indicates the HTTP status code and response body to send to the client. If `nil` is
returned, then no response will be sent.

Example usage:
```
let oversizeResponse: (Int, String) -> (HTTPStatusCode, String)? = { (limit, client) in
Log.debug("Rejecting request from \(client): Exceeds limit of \(limit) bytes")
return (.requestTooLong, "Your request exceeds the limit of \(limit) bytes.\r\n")
}
```
*/
public let requestSizeResponseGenerator: (Int, String) -> (HTTPStatusCode, String)?

/**
Determines the response message and HTTP status code used to respond to clients that attempt to connect
while the server is already servicing the maximum number of connections, as defined by `connectionLimit`.
The current limit and client's address are provided as parameters to enable a message to be logged,
and/or a response to be provided back to the client.

The returned tuple indicates the HTTP status code and response body to send to the client. If `nil` is
returned, then no response will be sent.

Example usage:
```
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")
}
```
*/
public let connectionResponseGenerator: (Int, String) -> (HTTPStatusCode, String)?

/// Create a `ServerOptions` to determine the behaviour of a `Server`.
///
/// - parameter requestSizeLimit: The maximum size of an incoming request body. Defaults to `ServerOptions.defaultRequestSizeLimit`.
/// - parameter connectionLimit: The maximum number of concurrent connections. Defaults to `ServerOptions.defaultConnectionLimit`.
/// - parameter requestSizeResponseGenerator: A closure producing a response to send to a client when an over-sized request is rejected. Defaults to `ServerOptions.defaultRequestSizeResponseGenerator`.
/// - 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`.
public init(requestSizeLimit: Int? = ServerOptions.defaultRequestSizeLimit,
connectionLimit: Int? = ServerOptions.defaultConnectionLimit,
requestSizeResponseGenerator: @escaping (Int, String) -> (HTTPStatusCode, String)? = ServerOptions.defaultRequestSizeResponseGenerator,
connectionResponseGenerator: @escaping (Int, String) -> (HTTPStatusCode, String)? = ServerOptions.defaultConnectionResponseGenerator)
{
self.requestSizeLimit = requestSizeLimit
self.connectionLimit = connectionLimit
self.requestSizeResponseGenerator = requestSizeResponseGenerator
self.connectionResponseGenerator = connectionResponseGenerator
}

}
27 changes: 27 additions & 0 deletions Tests/KituraNetTests/ClientE2ETests.swift
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@ class ClientE2ETests: KituraNetTest {
("testQueryParameters", testQueryParameters),
("testRedirect", testRedirect),
("testPercentEncodedQuery", testPercentEncodedQuery),
("testRequestSize",testRequestSize),
]
}

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

let delegate = TestServerDelegate()

func testRequestSize() {
performServerTest(serverConfig: ServerOptions(requestSizeLimit: 10000, connectionLimit: 100),delegate, useSSL: false, asyncTasks: { expectation in
let payload = "[" + contentTypesString + "," + contentTypesString + contentTypesString + "," + contentTypesString + "]"
self.performRequest("post", path: "/largepost", callback: {response in
XCTAssertEqual(response?.statusCode, HTTPStatusCode.requestTooLong)
do {
let expectedResult = ""
var data = Data()
let count = try response?.readAllData(into: &data)
XCTAssertEqual(count, expectedResult.count, "Result should have been \(expectedResult.count) bytes, was \(String(describing: count)) bytes")
let postValue = String(data: data, encoding: .utf8)
if let postValue = postValue {
XCTAssertEqual(postValue, expectedResult)
} else {
XCTFail("postValue's value wasn't an UTF8 string")
}
} catch {
XCTFail("Failed reading the body of the response")
}
expectation.fulfill()
}) {request in
request.write(from: payload)
}
})
}

func testHeadRequests() {
performServerTest(delegate) { expectation in
self.performRequest("head", path: "/headtest", callback: {response in
Loading