-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
roles: introduce role for http servers
This patch adds `roles.http`. This role allows to configurate one or more HTTP servers. Those servers could be reused by several other roles. Each server is assigned with unique ID. Servers could be accessed by this ID or by their names (from the config). `get_default_server` method returns default server (or `nil`). The server is default, if it has `default_server_name` as a name. Closes #196
- Loading branch information
Showing
3 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
local urilib = require("uri") | ||
local http_server = require('http.server') | ||
|
||
local M = { | ||
default_server_name = 'default', | ||
} | ||
local servers = {} | ||
local name_by_id = {} | ||
local current_id = 1 -- To match Lua 1-indexing. | ||
|
||
local function parse_listen(listen) | ||
if listen == nil then | ||
return nil, nil, "must exist" | ||
end | ||
if type(listen) ~= "string" and type(listen) ~= "number" then | ||
return nil, nil, "must be a string or a number, got " .. type(listen) | ||
end | ||
|
||
local host | ||
local port | ||
if type(listen) == "string" then | ||
local uri, err = urilib.parse(listen) | ||
if err ~= nil then | ||
return nil, nil, "failed to parse URI: " .. err | ||
end | ||
|
||
if uri.scheme ~= nil then | ||
if uri.scheme == "unix" then | ||
uri.unix = uri.path | ||
else | ||
return nil, nil, "URI scheme is not supported" | ||
end | ||
end | ||
|
||
if uri.login ~= nil or uri.password then | ||
return nil, nil, "URI login and password are not supported" | ||
end | ||
|
||
if uri.query ~= nil then | ||
return nil, nil, "URI query component is not supported" | ||
end | ||
|
||
if uri.unix ~= nil then | ||
host = "unix/" | ||
port = uri.unix | ||
else | ||
if uri.service == nil then | ||
return nil, nil, "URI must contain a port" | ||
end | ||
|
||
port = tonumber(uri.service) | ||
if port == nil then | ||
return nil, nil, "URI port must be a number" | ||
end | ||
if uri.host ~= nil then | ||
host = uri.host | ||
elseif uri.ipv4 ~= nil then | ||
host = uri.ipv4 | ||
elseif uri.ipv6 ~= nil then | ||
host = uri.ipv6 | ||
else | ||
host = "0.0.0.0" | ||
end | ||
end | ||
elseif type(listen) == "number" then | ||
host = "0.0.0.0" | ||
port = listen | ||
end | ||
|
||
if type(port) == "number" and (port < 1 or port > 65535) then | ||
return nil, nil, "port must be in the range [1, 65535]" | ||
end | ||
return host, port, nil | ||
end | ||
|
||
local function apply_http(name, node) | ||
local enabled = false | ||
local host, port, err = parse_listen(node.listen) | ||
if err ~= nil then | ||
error("failed to parse URI: " .. err, 2) | ||
end | ||
|
||
enabled = true | ||
|
||
if servers[name] == nil then | ||
local httpd = http_server.new(host, port) | ||
httpd:start() | ||
servers[name] = { | ||
httpd = httpd, | ||
routes = {}, | ||
} | ||
end | ||
|
||
if not enabled then | ||
servers[name].httpd:stop() | ||
servers[name] = nil | ||
else | ||
name_by_id[current_id] = name | ||
current_id = current_id + 1 | ||
end | ||
end | ||
|
||
M.validate = function(conf) | ||
if conf ~= nil and type(conf) ~= "table" then | ||
error("configuration must be a table, got " .. type(conf)) | ||
end | ||
conf = conf or {} | ||
|
||
for name, node in pairs(conf) do | ||
if type(name) ~= 'string' then | ||
error("name of the server must be a string") | ||
end | ||
|
||
local _, _, err = parse_listen(node.listen) | ||
if err ~= nil then | ||
error("failed to parse http 'listen' param: " .. err) | ||
end | ||
end | ||
end | ||
|
||
M.apply = function(conf) | ||
-- This should be called on the role's lifecycle, but it's better to give | ||
-- a meaningful error if something goes wrong. | ||
M.validate(conf) | ||
|
||
for name, node in pairs(conf or {}) do | ||
apply_http(name, node) | ||
end | ||
end | ||
|
||
M.stop = function() | ||
for _, server in pairs(servers) do | ||
server.httpd:stop() | ||
end | ||
servers = {} | ||
name_by_id = {} | ||
end | ||
|
||
M.get_default_server = function() | ||
return servers[M.default_server_name] | ||
end | ||
|
||
M.get_server = function(id) | ||
if type(id) == 'string' then | ||
return servers[id] | ||
elseif type(id) == 'number' then | ||
return servers[name_by_id[id]] | ||
end | ||
|
||
error('expected string or number, got ' .. type(id)) | ||
end | ||
|
||
return M |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
local t = require('luatest') | ||
local g = t.group() | ||
|
||
local http_role = require('roles.http') | ||
|
||
local validation_cases = { | ||
["not_table"] = { | ||
cfg = 42, | ||
err = "configuration must be a table, got number", | ||
}, | ||
["name_not_string"] = { | ||
cfg = { | ||
[42] = { | ||
listen = 8081, | ||
}, | ||
}, | ||
err = "name of the server must be a string", | ||
}, | ||
["listen_not_exist"] = { | ||
cfg = { | ||
server = { | ||
listen = nil, | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: must exist", | ||
}, | ||
["listen_not_string_and_not_number"] = { | ||
cfg = { | ||
server = { | ||
listen = {}, | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: must be a string or a number, got table", | ||
}, | ||
["listen_port_too_small"] = { | ||
cfg = { | ||
server = { | ||
listen = 0, | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: port must be in the range [1, 65535]", | ||
}, | ||
["listen_port_in_range"] = { | ||
cfg = { | ||
server = { | ||
listen = 8081, | ||
}, | ||
}, | ||
}, | ||
["listen_port_too_big"] = { | ||
cfg = { | ||
server = { | ||
listen = 65536, | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: port must be in the range [1, 65535]", | ||
}, | ||
["listen_uri_no_port"] = { | ||
cfg = { | ||
server = { | ||
listen = "localhost", | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: URI must contain a port", | ||
}, | ||
["listen_uri_port_too_small"] = { | ||
cfg = { | ||
server = { | ||
listen = "localhost:0", | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: port must be in the range [1, 65535]", | ||
}, | ||
["listen_uri_with_port_in_range"] = { | ||
cfg = { | ||
server = { | ||
listen = "localhost:8081", | ||
}, | ||
}, | ||
}, | ||
["listen_uri_port_too_big"] = { | ||
cfg = { | ||
server = { | ||
listen = "localhost:65536", | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: port must be in the range [1, 65535]", | ||
}, | ||
["listen_uri_port_not_number"] = { | ||
cfg = { | ||
server = { | ||
listen = "localhost:foo", | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: URI port must be a number", | ||
}, | ||
["listen_uri_non_unix_scheme"] = { | ||
cfg = { | ||
server = { | ||
listen = "http://localhost:123", | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: URI scheme is not supported", | ||
}, | ||
["listen_uri_login_password"] = { | ||
cfg = { | ||
server = { | ||
listen = "login:password@localhost:123", | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: URI login and password are not supported", | ||
}, | ||
["listen_uri_query"] = { | ||
cfg = { | ||
server = { | ||
listen = "localhost:123/?foo=bar", | ||
}, | ||
}, | ||
err = "failed to parse http 'listen' param: URI query component is not supported", | ||
}, | ||
} | ||
|
||
for name, case in pairs(validation_cases) do | ||
local test_name = ('test_validate_http_%s%s'):format( | ||
(case.err ~= nil) and 'fails_on_' or 'success_for_', | ||
name | ||
) | ||
|
||
g[test_name] = function() | ||
local ok, res = pcall(http_role.validate, case.cfg) | ||
|
||
if case.err ~= nil then | ||
t.assert_not(ok) | ||
t.assert_str_contains(res, case.err) | ||
else | ||
t.assert(ok) | ||
t.assert_is(res, nil) | ||
end | ||
end | ||
end | ||
|
||
g['test_get_default_without_apply'] = function() | ||
local result = http_role.get_default_server() | ||
t.assert_is(result, nil) | ||
end | ||
|
||
g['test_get_default_no_default'] = function() | ||
local cfg = { | ||
not_a_default = { | ||
listen = 13000, | ||
}, | ||
} | ||
|
||
http_role.apply(cfg) | ||
|
||
local result = http_role.get_default_server() | ||
t.assert_is(result, nil) | ||
end | ||
|
||
g['test_get_default'] = function() | ||
local cfg = { | ||
[http_role.default_server_name] = { | ||
listen = 13001, | ||
}, | ||
} | ||
|
||
http_role.apply(cfg) | ||
|
||
local result = http_role.get_default_server() | ||
t.assert(result) | ||
end | ||
|
||
g['test_get_server_bad_type'] = function() | ||
local ok, res = pcall(http_role.get_server, {}) | ||
|
||
t.assert_not(ok) | ||
t.assert_str_contains(res, 'expected string or number') | ||
end |