From 9475480c68ac93ed49a62a6a182d4407e9b5ae38 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Fri, 29 Oct 2021 11:06:53 +0300 Subject: [PATCH] Add an option to control keep-alive connections By default http module set keep-alive option in connections to make it persistent. However sometimes we need persistent connection and close it instead. It is possible with option keepalive_disable. Option accepts a map where key is a user agent name with assigned non-nil value. 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 untested. Example: local httpd = http_server.new('127.0.0.1', 8080, { log_requests = true, log_errors = true, keepalive_disable = { ['curl/7.68.0'] = true }, }) 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 --- CHANGELOG.md | 4 + README.md | 16 ++++ http/server.lua | 23 ++++++ test/integration/http_server_options_test.lua | 75 +++++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100644 test/integration/http_server_options_test.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 0545c07..23fd2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Add option to control keepalive connection state (#137). + ## [1.2.0] - 2021-11-10 ### Changed diff --git a/README.md b/README.md index 0f96907..2c47606 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,22 @@ 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. +* `keepalive_disable` - disables keep-alive connections with misbehaving + clients. Parameter accept a map that contains a user agents with non-nil + value. By default map is empty. + + Example: + + ```lua + local httpd = http_server.new('127.0.0.1', 8080, { + log_requests = true, + log_errors = true, + keepalive_disable = { + ['curl/7.68.0'] = true + }, + }) + ``` + ## Using routes diff --git a/http/server.lua b/http/server.lua index 17c1b66..eb61879 100644 --- a/http/server.lua +++ b/http/server.lua @@ -23,6 +23,17 @@ local function sprintf(fmt, ...) return string.format(fmt, ...) end +local function is_no_keepalive(disallowed_map, useragent) + if useragent == nil then + return false + end + if disallowed_map[useragent] == true then + return true + end + + return false +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, @@ -891,6 +902,12 @@ local function process_client(self, s, peer) end end + local no_keepalive = is_no_keepalive(self.options.keepalive_disable, + p.headers['user-agent']) + if no_keepalive == true then + hdrs.connection = 'close' + end + local response = { "HTTP/1.1 "; status; @@ -1282,6 +1299,11 @@ local exports = { if type(options) ~= 'table' then errorf("options must be table not '%s'", type(options)) end + options.keepalive_disable = options.keepalive_disable or {} + if type(options.keepalive_disable) ~= 'table' then + error('Option keepalive_disable must be a table.') + end + local default = { max_header_size = 4096, header_timeout = 100, @@ -1294,6 +1316,7 @@ local exports = { log_requests = true, log_errors = true, display_errors = false, + keepalive_disable = {}, } local self = { diff --git a/test/integration/http_server_options_test.lua b/test/integration/http_server_options_test.lua new file mode 100644 index 0000000..3cb2a59 --- /dev/null +++ b/test/integration/http_server_options_test.lua @@ -0,0 +1,75 @@ +local t = require('luatest') +local http_client = require('http.client') + +local helpers = require('test.helpers') + +local g = t.group() + +g.before_test('test_keepalive_default', function() + g.httpd = helpers.cfgserv() + g.httpd:start() +end) + +g.before_test('test_keepalive_change_in_runtime', function() + g.httpd = helpers.cfgserv() + g.httpd:start() +end) + +g.before_test('test_keepalive_allowed', function() + g.httpd = helpers.cfgserv() + g.httpd:start() +end) + +g.before_test('test_keepalive_disallowed', function() + local useragent = 'Mozilla/4.0' + g.httpd = helpers.cfgserv({ + keepalive_disable = { + [useragent] = true, + } + }) + g.httpd:start() +end) + +g.after_each(function() + helpers.teardown(g.httpd) +end) + +g.test_keepalive_default = function(g) + local httpd = g.httpd + t.assert_equals(httpd.options.keepalive_disable, {}) +end + +g.test_keepalive_allowed = function() + local useragent = 'Mozilla/4.0' + local r = http_client.request('GET', helpers.base_uri .. '/test', nil, { + headers = { + ['user-agent'] = useragent, + connection = 'keep-alive' + }, + }) + t.assert_equals(r.status, 200) + t.assert_equals(r.headers.connection, 'keep-alive') +end + +g.test_keepalive_disallowed = function() + local useragent = 'Mozilla/4.0' + local r = http_client.request('GET', helpers.base_uri .. '/test', nil, { + headers = { + ['user-agent'] = useragent, + connection = 'keep-alive' + }, + }) + t.assert_equals(r.status, 200) + t.assert_equals(r.headers.connection, 'close') +end + +g.test_keepalive_change_in_runtime = function(g) + local httpd = g.httpd + t.assert_equals(httpd.options.keepalive_disable, {}) + httpd.options.keepalive_disable = { + ['Mozilla/4.0'] = true + } + t.assert_equals(httpd.options.keepalive_disable, { + ["Mozilla/4.0"] = true + }) +end