Skip to content

Commit

Permalink
roles: introduce role for http servers
Browse files Browse the repository at this point in the history
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
DerekBum committed Sep 5, 2024
1 parent 0b471d8 commit a9c76da
Show file tree
Hide file tree
Showing 3 changed files with 329 additions and 0 deletions.
1 change: 1 addition & 0 deletions http-scm-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ build = {
['http.version'] = 'http/version.lua',
['http.mime_types'] = 'http/mime_types.lua',
['http.codes'] = 'http/codes.lua',
['roles.http'] = 'roles/http.lua',
}
}

Expand Down
146 changes: 146 additions & 0 deletions roles/http.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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 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)
httpd:start()
servers[name] = {
httpd = httpd,
routes = {},
}

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 = {}
current_id = 0
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
182 changes: 182 additions & 0 deletions test/unit/http_role_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
local t = require('luatest')
local g = t.group()

local http_role = require('roles.http')

g.after_each(function()
http_role.stop()
end)

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

0 comments on commit a9c76da

Please sign in to comment.