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..95c2ea4 100644 --- a/README.md +++ b/README.md @@ -533,9 +533,9 @@ Example of the configuration: roles_cfg: roles.httpd: default: - - listen: 8081 + listen: 8081 additional: - - listen: '127.0.0.1:8082' + listen: '127.0.0.1:8082' ``` Server address should be provided either as a URI or as a single port @@ -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