Skip to content

Commit a9c76da

Browse files
committed
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
1 parent 0b471d8 commit a9c76da

File tree

3 files changed

+329
-0
lines changed

3 files changed

+329
-0
lines changed

http-scm-1.rockspec

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ build = {
3131
['http.version'] = 'http/version.lua',
3232
['http.mime_types'] = 'http/mime_types.lua',
3333
['http.codes'] = 'http/codes.lua',
34+
['roles.http'] = 'roles/http.lua',
3435
}
3536
}
3637

roles/http.lua

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
local urilib = require("uri")
2+
local http_server = require('http.server')
3+
4+
local M = {
5+
default_server_name = 'default',
6+
}
7+
local servers = {}
8+
local name_by_id = {}
9+
local current_id = 1 -- To match Lua 1-indexing.
10+
11+
local function parse_listen(listen)
12+
if listen == nil then
13+
return nil, nil, "must exist"
14+
end
15+
if type(listen) ~= "string" and type(listen) ~= "number" then
16+
return nil, nil, "must be a string or a number, got " .. type(listen)
17+
end
18+
19+
local host
20+
local port
21+
if type(listen) == "string" then
22+
local uri, err = urilib.parse(listen)
23+
if err ~= nil then
24+
return nil, nil, "failed to parse URI: " .. err
25+
end
26+
27+
if uri.scheme ~= nil then
28+
if uri.scheme == "unix" then
29+
uri.unix = uri.path
30+
else
31+
return nil, nil, "URI scheme is not supported"
32+
end
33+
end
34+
35+
if uri.login ~= nil or uri.password then
36+
return nil, nil, "URI login and password are not supported"
37+
end
38+
39+
if uri.query ~= nil then
40+
return nil, nil, "URI query component is not supported"
41+
end
42+
43+
if uri.unix ~= nil then
44+
host = "unix/"
45+
port = uri.unix
46+
else
47+
if uri.service == nil then
48+
return nil, nil, "URI must contain a port"
49+
end
50+
51+
port = tonumber(uri.service)
52+
if port == nil then
53+
return nil, nil, "URI port must be a number"
54+
end
55+
if uri.host ~= nil then
56+
host = uri.host
57+
elseif uri.ipv4 ~= nil then
58+
host = uri.ipv4
59+
elseif uri.ipv6 ~= nil then
60+
host = uri.ipv6
61+
else
62+
host = "0.0.0.0"
63+
end
64+
end
65+
elseif type(listen) == "number" then
66+
host = "0.0.0.0"
67+
port = listen
68+
end
69+
70+
if type(port) == "number" and (port < 1 or port > 65535) then
71+
return nil, nil, "port must be in the range [1, 65535]"
72+
end
73+
return host, port, nil
74+
end
75+
76+
local function apply_http(name, node)
77+
local host, port, err = parse_listen(node.listen)
78+
if err ~= nil then
79+
error("failed to parse URI: " .. err)
80+
end
81+
82+
if servers[name] == nil then
83+
local httpd = http_server.new(host, port)
84+
httpd:start()
85+
servers[name] = {
86+
httpd = httpd,
87+
routes = {},
88+
}
89+
90+
name_by_id[current_id] = name
91+
current_id = current_id + 1
92+
end
93+
end
94+
95+
M.validate = function(conf)
96+
if conf ~= nil and type(conf) ~= "table" then
97+
error("configuration must be a table, got " .. type(conf))
98+
end
99+
conf = conf or {}
100+
101+
for name, node in pairs(conf) do
102+
if type(name) ~= 'string' then
103+
error("name of the server must be a string")
104+
end
105+
106+
local _, _, err = parse_listen(node.listen)
107+
if err ~= nil then
108+
error("failed to parse http 'listen' param: " .. err)
109+
end
110+
end
111+
end
112+
113+
M.apply = function(conf)
114+
-- This should be called on the role's lifecycle, but it's better to give
115+
-- a meaningful error if something goes wrong.
116+
M.validate(conf)
117+
118+
for name, node in pairs(conf or {}) do
119+
apply_http(name, node)
120+
end
121+
end
122+
123+
M.stop = function()
124+
for _, server in pairs(servers) do
125+
server.httpd:stop()
126+
end
127+
servers = {}
128+
name_by_id = {}
129+
current_id = 0
130+
end
131+
132+
M.get_default_server = function()
133+
return servers[M.default_server_name]
134+
end
135+
136+
M.get_server = function(id)
137+
if type(id) == 'string' then
138+
return servers[id]
139+
elseif type(id) == 'number' then
140+
return servers[name_by_id[id]]
141+
end
142+
143+
error('expected string or number, got ' .. type(id))
144+
end
145+
146+
return M

test/unit/http_role_test.lua

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
local t = require('luatest')
2+
local g = t.group()
3+
4+
local http_role = require('roles.http')
5+
6+
g.after_each(function()
7+
http_role.stop()
8+
end)
9+
10+
local validation_cases = {
11+
["not_table"] = {
12+
cfg = 42,
13+
err = "configuration must be a table, got number",
14+
},
15+
["name_not_string"] = {
16+
cfg = {
17+
[42] = {
18+
listen = 8081,
19+
},
20+
},
21+
err = "name of the server must be a string",
22+
},
23+
["listen_not_exist"] = {
24+
cfg = {
25+
server = {
26+
listen = nil,
27+
},
28+
},
29+
err = "failed to parse http 'listen' param: must exist",
30+
},
31+
["listen_not_string_and_not_number"] = {
32+
cfg = {
33+
server = {
34+
listen = {},
35+
},
36+
},
37+
err = "failed to parse http 'listen' param: must be a string or a number, got table",
38+
},
39+
["listen_port_too_small"] = {
40+
cfg = {
41+
server = {
42+
listen = 0,
43+
},
44+
},
45+
err = "failed to parse http 'listen' param: port must be in the range [1, 65535]",
46+
},
47+
["listen_port_in_range"] = {
48+
cfg = {
49+
server = {
50+
listen = 8081,
51+
},
52+
},
53+
},
54+
["listen_port_too_big"] = {
55+
cfg = {
56+
server = {
57+
listen = 65536,
58+
},
59+
},
60+
err = "failed to parse http 'listen' param: port must be in the range [1, 65535]",
61+
},
62+
["listen_uri_no_port"] = {
63+
cfg = {
64+
server = {
65+
listen = "localhost",
66+
},
67+
},
68+
err = "failed to parse http 'listen' param: URI must contain a port",
69+
},
70+
["listen_uri_port_too_small"] = {
71+
cfg = {
72+
server = {
73+
listen = "localhost:0",
74+
},
75+
},
76+
err = "failed to parse http 'listen' param: port must be in the range [1, 65535]",
77+
},
78+
["listen_uri_with_port_in_range"] = {
79+
cfg = {
80+
server = {
81+
listen = "localhost:8081",
82+
},
83+
},
84+
},
85+
["listen_uri_port_too_big"] = {
86+
cfg = {
87+
server = {
88+
listen = "localhost:65536",
89+
},
90+
},
91+
err = "failed to parse http 'listen' param: port must be in the range [1, 65535]",
92+
},
93+
["listen_uri_port_not_number"] = {
94+
cfg = {
95+
server = {
96+
listen = "localhost:foo",
97+
},
98+
},
99+
err = "failed to parse http 'listen' param: URI port must be a number",
100+
},
101+
["listen_uri_non_unix_scheme"] = {
102+
cfg = {
103+
server = {
104+
listen = "http://localhost:123",
105+
},
106+
},
107+
err = "failed to parse http 'listen' param: URI scheme is not supported",
108+
},
109+
["listen_uri_login_password"] = {
110+
cfg = {
111+
server = {
112+
listen = "login:password@localhost:123",
113+
},
114+
},
115+
err = "failed to parse http 'listen' param: URI login and password are not supported",
116+
},
117+
["listen_uri_query"] = {
118+
cfg = {
119+
server = {
120+
listen = "localhost:123/?foo=bar",
121+
},
122+
},
123+
err = "failed to parse http 'listen' param: URI query component is not supported",
124+
},
125+
}
126+
127+
for name, case in pairs(validation_cases) do
128+
local test_name = ('test_validate_http_%s%s'):format(
129+
(case.err ~= nil) and 'fails_on_' or 'success_for_',
130+
name
131+
)
132+
133+
g[test_name] = function()
134+
local ok, res = pcall(http_role.validate, case.cfg)
135+
136+
if case.err ~= nil then
137+
t.assert_not(ok)
138+
t.assert_str_contains(res, case.err)
139+
else
140+
t.assert(ok)
141+
t.assert_is(res, nil)
142+
end
143+
end
144+
end
145+
146+
g['test_get_default_without_apply'] = function()
147+
local result = http_role.get_default_server()
148+
t.assert_is(result, nil)
149+
end
150+
151+
g['test_get_default_no_default'] = function()
152+
local cfg = {
153+
not_a_default = {
154+
listen = 13000,
155+
},
156+
}
157+
158+
http_role.apply(cfg)
159+
160+
local result = http_role.get_default_server()
161+
t.assert_is(result, nil)
162+
end
163+
164+
g['test_get_default'] = function()
165+
local cfg = {
166+
[http_role.default_server_name] = {
167+
listen = 13001,
168+
},
169+
}
170+
171+
http_role.apply(cfg)
172+
173+
local result = http_role.get_default_server()
174+
t.assert(result)
175+
end
176+
177+
g['test_get_server_bad_type'] = function()
178+
local ok, res = pcall(http_role.get_server, {})
179+
180+
t.assert_not(ok)
181+
t.assert_str_contains(res, 'expected string or number')
182+
end

0 commit comments

Comments
 (0)