Skip to content

Commit

Permalink
roles: add tls support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
themilchenko committed Nov 13, 2024
1 parent 5bfaaf8 commit 4a064fb
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions http/server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down
23 changes: 21 additions & 2 deletions roles/httpd.lua
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,28 @@ 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
error("failed to parse URI: " .. err)
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,
Expand All @@ -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

Expand Down
56 changes: 45 additions & 11 deletions test/integration/httpd_role_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions test/mocks/mock_role.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
87 changes: 87 additions & 0 deletions test/unit/httpd_role_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 4a064fb

Please sign in to comment.