Skip to content

Commit

Permalink
Add an option disable_keepalive
Browse files Browse the repository at this point in the history
By default HTTP server sets a keep-alive option in connections to make
it persistent. However sometimes keeping connections alive is not
required and user may want to disable it for a certain connections.

Patch adds a new option 'disable_keepalive' that accepts a table with
user agent names. HTTP server will disable keep-alive for connections
established by user agents listed in a table.

Note: nginx web-server has a similar option, see [1].

Unfortunately HTTP client [1] builtin into Tarantool supports only HTTP
protocol version 1.1, so version 1.0 is not covered in regression tests.

Example:

local httpd = http_server.new('127.0.0.1', 8080, {
    disable_keepalive = { 'curl/7.68.0' },
})

1. https://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_disable
2. https://www.tarantool.io/en/doc/latest/reference/reference_lua/http/

Part of #137

Reviewed-by: Alexander Turenko <alexander.turenko@tarantool.org>
  • Loading branch information
ligurio committed Jul 9, 2022
1 parent ff5298d commit cb3b99a
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Allow to use a non-standard socket (for example, `sslsocket` with TLS
support).

### Added

- Add option to control keepalive connection state (#137).

## [1.2.0] - 2021-11-10

### Changed
Expand Down
14 changes: 14 additions & 0 deletions README.md
Expand Up @@ -132,6 +132,20 @@ httpd = require('http.server').new(host, port[, { options } ])

By default uses `log.info` function for requests logging.
* `log_errors` - same as the `log_requests` option but is used for error messages logging. By default uses `log.error()` function.
* `disable_keepalive` - disables keep-alive connections with misbehaving
clients. Parameter accept a table that contains a user agents names.
By default table is empty.

Example:

```lua
local httpd = http_server.new('127.0.0.1', 8080, {
log_requests = true,
log_errors = true,
disable_keepalive = { 'curl/7.68.0' }
})
```


## Using routes

Expand Down
30 changes: 30 additions & 0 deletions http/server.lua
Expand Up @@ -23,6 +23,16 @@ local function sprintf(fmt, ...)
return string.format(fmt, ...)
end

-- Converts a table to a map, values becomes keys with value 'true'.
-- { 'a', 'b', 'c' } -> { 'a' = true, 'b' == 'true', 'c' = true }
local function tomap(tbl)
local map = {}
for _, v in pairs(tbl) do
map[v] = true
end
return map
end

local function valid_cookie_value_byte(byte)
-- https://tools.ietf.org/html/rfc6265#section-4.1.1
-- US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon,
Expand Down Expand Up @@ -891,6 +901,11 @@ local function process_client(self, s, peer)
end
end

local useragent = p.headers['user-agent']
if self.disable_keepalive[useragent] == true then
hdrs.connection = 'close'
end

local response = {
"HTTP/1.1 ";
status;
Expand Down Expand Up @@ -1259,7 +1274,9 @@ local function httpd_start(self)
local server = self.tcp_server_f(self.host, self.port, {
name = 'http',
handler = function(...)
self.internal.preprocess_client_handler()
process_client(self, ...)
self.internal.postprocess_client_handler()
end,
http_server = self,
})
Expand All @@ -1285,6 +1302,11 @@ local exports = {
if type(options) ~= 'table' then
errorf("options must be table not '%s'", type(options))
end
local disable_keepalive = options.disable_keepalive or {}
if type(disable_keepalive) ~= 'table' then
error('Option disable_keepalive must be a table.')
end

local default = {
max_header_size = 4096,
header_timeout = 100,
Expand All @@ -1297,6 +1319,7 @@ local exports = {
log_requests = true,
log_errors = true,
display_errors = false,
disable_keepalive = {},
}

local self = {
Expand Down Expand Up @@ -1330,6 +1353,13 @@ local exports = {
ctx = {},
static = {},
},

disable_keepalive = tomap(disable_keepalive),

internal = {
preprocess_client_handler = function() end,
postprocess_client_handler = function() end,
}
}

return self
Expand Down
89 changes: 89 additions & 0 deletions test/integration/http_server_options_test.lua
@@ -0,0 +1,89 @@
local t = require('luatest')
local http_client = require('http.client')

local helpers = require('test.helpers')

local g = t.group()

g.after_each(function()
helpers.teardown(g.httpd)
end)

g.before_test('test_keepalive_is_allowed', function()
g.httpd = helpers.cfgserv()
g.httpd:start()
end)

g.test_keepalive_is_allowed = function()
local conn_is_opened = false
local conn_is_closed = false
g.httpd.internal.preprocess_client_handler = function() conn_is_opened = true end
g.httpd.internal.postprocess_client_handler = function() conn_is_closed = true end

-- Set HTTP keepalive headers: Connection:Keep-Alive and
-- Keep-Alive:timeout=<keepalive_idle>. Otherwise HTTP client will send
-- "Connection:close".
local opts = {
keepalive_idle = 3600,
keepalive_interval = 3600,
}
local r = http_client.new():request('GET', helpers.base_uri .. '/test', nil, opts)
t.assert_equals(r.status, 200)
t.assert_equals(r.headers.connection, 'keep-alive')
t.assert_equals(conn_is_opened, true)
t.assert_equals(conn_is_closed, false) -- Connection is alive.
end

g.before_test('test_keepalive_is_disallowed', function()
g.useragent = 'Mozilla/4.0'
g.httpd = helpers.cfgserv({
disable_keepalive = { g.useragent },
})
g.httpd:start()
end)

g.test_keepalive_is_disallowed = function()
local conn_is_opened = false
local conn_is_closed = false
g.httpd.internal.preprocess_client_handler = function() conn_is_opened = true end
g.httpd.internal.postprocess_client_handler = function() conn_is_closed = true end

-- Set HTTP keepalive headers: Connection:Keep-Alive and
-- Keep-Alive:timeout=<keepalive_idle>. Otherwise HTTP client will send
-- "Connection:close".
local opts = {
headers = {
['user-agent'] = g.useragent,
},
keepalive_idle = 3600,
keepalive_interval = 3600,
}
local r = http_client.new():request('GET', helpers.base_uri .. '/test', nil, opts)
t.assert_equals(r.status, 200)
t.assert_equals(r.headers.connection, 'close')
t.assert_equals(conn_is_opened, true)
t.assert_equals(conn_is_closed, true) -- Connection is closed.
end

g.before_test('test_disable_keepalive_is_set', function()
g.useragent = 'Mozilla/5.0'
g.httpd = helpers.cfgserv({
disable_keepalive = { g.useragent },
})
g.httpd:start()
end)

g.test_disable_keepalive_is_set = function(g)
local httpd = g.httpd
t.assert_equals(httpd.disable_keepalive[g.useragent], true)
end

g.before_test('test_disable_keepalive_default', function()
g.httpd = helpers.cfgserv()
g.httpd:start()
end)

g.test_disable_keepalive_default = function(g)
local httpd = g.httpd
t.assert_equals(table.getn(httpd.options.disable_keepalive), 0)
end

0 comments on commit cb3b99a

Please sign in to comment.