From 4a064fb65d4eb88533ece13d0a2ce1318c9b964c Mon Sep 17 00:00:00 2001 From: themilchenko Date: Tue, 12 Nov 2024 18:41:35 +0300 Subject: [PATCH] roles: add tls support Since we support TLS in this module, we need to add it into roles too. After the patch TLS params was added. It can be configurated like the following: ```yaml server_name: listen: "localhost:3013" ssl_key_file: "path/to/key/file" ssl_cert_file: "path/to/key/file" ssl_ca_file: "path/to/key/file" ssl_ciphers: "cipher1:cipher2" ssl_password: "password" ssl_password_file: "path/to/ssl/password" ``` Closes #199 --- CHANGELOG.md | 1 + README.md | 16 +++++ http/server.lua | 2 + roles/httpd.lua | 23 +++++++- test/integration/httpd_role_test.lua | 56 ++++++++++++++---- test/mocks/mock_role.lua | 6 ++ test/unit/httpd_role_test.lua | 87 ++++++++++++++++++++++++++++ 7 files changed, 178 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e2e48..a59c94f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - SSL support (#35). +- SSL support into roles (#199). ### Changed diff --git a/README.md b/README.md index 8b7d3dd..7ae7032 100644 --- a/README.md +++ b/README.md @@ -581,6 +581,22 @@ end return M ``` +To enable TLS, provide the following params into roles config (for proper work +it's enough to provide only `ssl_key_file` and `ssl_cert_file`): + +```yaml +roles_cfg: + roles.httpd: + default: + listen: 8081 + ssl_key_file: "path/to/key/file" + ssl_cert_file: "path/to/key/file" + ssl_ca_file: "path/to/key/file" + ssl_ciphers: "cipher1:cipher2" + ssl_password: "password" + ssl_password_file: "path/to/ssl/password" +``` + This role accepts a server by name from a config and creates a route to return `Hello, world!` to every request by this route. diff --git a/http/server.lua b/http/server.lua index 7403159..1296b3c 100644 --- a/http/server.lua +++ b/http/server.lua @@ -1404,6 +1404,8 @@ local exports = { _VERSION = require('http.version'), DETACHED = DETACHED, + -- Since TLS support this function uses in roles's validate section to check + -- TLS options. new = function(host, port, options) if options == nil then options = {} diff --git a/roles/httpd.lua b/roles/httpd.lua index 5e7f2f8..6cf990b 100644 --- a/roles/httpd.lua +++ b/roles/httpd.lua @@ -72,6 +72,19 @@ local function parse_listen(listen) return host, port, nil end +-- parse_params returns table with set options from config to pass +-- it into new() function. +local function parse_params(node) + return { + ssl_cert_file = node.ssl_cert_file, + ssl_key_file = node.ssl_key_file, + ssl_password = node.ssl_password, + ssl_password_file = node.ssl_password_file, + ssl_ca_file = node.ssl_ca_file, + ssl_ciphers = node.ssl_ciphers, + } +end + local function apply_http(name, node) local host, port, err = parse_listen(node.listen) if err ~= nil then @@ -79,7 +92,8 @@ local function apply_http(name, node) end if servers[name] == nil then - local httpd = http_server.new(host, port) + local httpd = http_server.new(host, port, parse_params(node)) + httpd:start() servers[name] = { httpd = httpd, @@ -99,10 +113,15 @@ M.validate = function(conf) error("name of the server must be a string") end - local _, _, err = parse_listen(node.listen) + local host, port, err = parse_listen(node.listen) if err ~= nil then error("failed to parse http 'listen' param: " .. err) end + + local ok, err = pcall(http_server.new, host, port, parse_params(node)) + if not ok then + error("failed to parse params in " .. name .. " server: " .. tostring(err)) + end end end diff --git a/test/integration/httpd_role_test.lua b/test/integration/httpd_role_test.lua index 7b99ff7..8dd4046 100644 --- a/test/integration/httpd_role_test.lua +++ b/test/integration/httpd_role_test.lua @@ -3,10 +3,15 @@ local treegen = require('luatest.treegen') local server = require('luatest.server') local fun = require('fun') local yaml = require('yaml') +local fio = require('fio') +local http_client = require('http.client').new() + local helpers = require('test.helpers') -local g = t.group() +local g = t.group(nil, t.helpers.matrix({use_tls = {true, false}})) + +local ssl_data_dir = fio.abspath(fio.pathjoin(helpers.get_testdir_path(), "ssl_data")) local config = { credentials = { @@ -55,29 +60,58 @@ local config = { }, } -g.before_each(function() +local tls_config = table.deepcopy(config) +tls_config.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default + .ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt') + +tls_config.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default + .ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.enc.key') + +tls_config.groups['group-001'].replicasets['replicaset-001'].roles_cfg['roles.httpd'].default + .ssl_password_file = fio.pathjoin(ssl_data_dir, 'passwords') + +g.before_each(function(cg) helpers.skip_if_not_tarantool3() local dir = treegen.prepare_directory({}, {}) + local cfg = config + if cg.params.use_tls then + cfg = tls_config + end + local config_file = treegen.write_file(dir, 'config.yaml', - yaml.encode(config)) + yaml.encode(cfg)) local opts = {config_file = config_file, chdir = dir} - g.server = server:new(fun.chain(opts, {alias = 'instance-001'}):tomap()) - helpers.update_lua_env_variables(g.server) + cg.server = server:new(fun.chain(opts, {alias = 'instance-001'}):tomap()) + helpers.update_lua_env_variables(cg.server) - g.server:start() + cg.server:start() end) -g.after_each(function() - g.server:stop() +g.after_each(function(cg) + helpers.teardown(cg.server) end) -g.test_httpd_role_usage = function() - t.assert_equals(g.server:eval( +g.test_httpd_role_usage = function(cg) + if cg.params.use_tls then + local resp = http_client:get('https://localhost:13000/ping', { + ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt') + }) + t.assert_equals(resp.status, 200, 'response not 200') + t.assert_equals(resp.body, 'pong') + end + + -- We can use https only for one endpoind due to we haven't publish separate + -- certificates for it. + local resp = http_client:get('http://localhost:13001/ping') + t.assert_equals(resp.status, 200, 'response not 200') + t.assert_equals(resp.body, 'pong') + + t.assert_equals(cg.server:eval( 'return require("test.mocks.mock_role").get_server_port(1)' ), 13000) - t.assert_equals(g.server:eval( + t.assert_equals(cg.server:eval( 'return require("test.mocks.mock_role").get_server_port(2)' ), 13001) end diff --git a/test/mocks/mock_role.lua b/test/mocks/mock_role.lua index 01118c8..8753c21 100644 --- a/test/mocks/mock_role.lua +++ b/test/mocks/mock_role.lua @@ -7,6 +7,12 @@ M.validate = function() end M.apply = function(conf) for _, server in pairs(conf) do servers[server.id] = require('roles.httpd').get_server(server.name) + + servers[server.id]:route({ + path = '/ping', + }, function(tx) + return tx:render({text = 'pong'}) + end) end end diff --git a/test/unit/httpd_role_test.lua b/test/unit/httpd_role_test.lua index d4e6872..d8e90f3 100644 --- a/test/unit/httpd_role_test.lua +++ b/test/unit/httpd_role_test.lua @@ -2,6 +2,10 @@ local t = require('luatest') local g = t.group() local httpd_role = require('roles.httpd') +local helpers = require('test.helpers') +local fio = require('fio') + +local ssl_data_dir = fio.abspath(fio.pathjoin(helpers.get_testdir_path(), "ssl_data")) g.after_each(function() httpd_role.stop() @@ -122,6 +126,89 @@ local validation_cases = { }, err = "failed to parse http 'listen' param: URI query component is not supported", }, + ["ssl_ok_minimal"] = { + cfg = { + server = { + listen = "localhost:123", + ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), + ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt') + }, + }, + }, + ["ssl_ok_full"] = { + cfg = { + server = { + listen = 123, + ssl_key_file = fio.pathjoin(ssl_data_dir, 'server.key'), + ssl_cert_file = fio.pathjoin(ssl_data_dir, 'server.crt'), + ssl_ca_file = fio.pathjoin(ssl_data_dir, 'ca.crt'), + ssl_ciphers = "ECDHE-RSA-AES256-GCM-SHA384", + }, + }, + }, + ["ssl_key_file_not_string"] = { + cfg = { + server = { + listen = "localhost:123", + ssl_key_file = 123, + }, + }, + err = "ssl_key_file option must be a string", + }, + ["ssl_cert_file_not_string"] = { + cfg = { + server = { + listen = "localhost:123", + ssl_cert_file = 123, + }, + }, + err = "ssl_cert_file option must be a string", + }, + ["ssl_password_not_string"] = { + cfg = { + server = { + listen = "localhost:123", + ssl_password = 123, + }, + }, + err = "ssl_password option must be a string", + }, + ["ssl_password_file_not_string"] = { + cfg = { + server = { + listen = "localhost:123", + ssl_password_file = 123, + }, + }, + err = "ssl_password_file option must be a string", + }, + ["ssl_ca_file_not_string"] = { + cfg = { + server = { + listen = "localhost:123", + ssl_ca_file = 123, + }, + }, + err = "ssl_ca_file option must be a string", + }, + ["ssl_ciphers_not_string"] = { + cfg = { + server = { + listen = "localhost:123", + ssl_ciphers = 123, + }, + }, + err = "ssl_ciphers option must be a string", + }, + ["ssl_key_and_cert_must_exist"] = { + cfg = { + server = { + listen = "localhost:123", + ssl_ciphers = 'cipher1:cipher2', + }, + }, + err = "ssl_key_file and ssl_cert_file must be set to enable TLS", + }, } for name, case in pairs(validation_cases) do