Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

roles: add tls support #203

Merged
merged 2 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down 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))
DifferentialOrange marked this conversation as resolved.
Show resolved Hide resolved
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",
oleg-jukovec marked this conversation as resolved.
Show resolved Hide resolved
},
["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
Loading