mirror of
https://git.lapiole.org/dani/ansible-roles.git
synced 2025-08-04 15:47:32 +02:00
Update to 2024-07-22 23:00
This commit is contained in:
280
roles/jitsi/files/prosody/modules/mod_firewall/actions.lib.lua
Normal file
280
roles/jitsi/files/prosody/modules/mod_firewall/actions.lib.lua
Normal file
@@ -0,0 +1,280 @@
|
||||
local unpack = table.unpack or unpack;
|
||||
|
||||
local interpolation = require "util.interpolation";
|
||||
local template = interpolation.new("%b$$", function (s) return ("%q"):format(s) end);
|
||||
|
||||
--luacheck: globals meta idsafe
|
||||
local action_handlers = {};
|
||||
|
||||
|
||||
-- Takes an XML string and returns a code string that builds that stanza
|
||||
-- using st.stanza()
|
||||
local function compile_xml(data)
|
||||
local code = {};
|
||||
local first, short_close = true, nil;
|
||||
for tagline, text in data:gmatch("<([^>]+)>([^<]*)") do
|
||||
if tagline:sub(-1,-1) == "/" then
|
||||
tagline = tagline:sub(1, -2);
|
||||
short_close = true;
|
||||
end
|
||||
if tagline:sub(1,1) == "/" then
|
||||
code[#code+1] = (":up()");
|
||||
else
|
||||
local name, attr = tagline:match("^(%S*)%s*(.*)$");
|
||||
local attr_str = {};
|
||||
for k, _, v in attr:gmatch("(%S+)=([\"'])([^%2]-)%2") do
|
||||
if #attr_str == 0 then
|
||||
table.insert(attr_str, ", { ");
|
||||
else
|
||||
table.insert(attr_str, ", ");
|
||||
end
|
||||
if k:find("^%a%w*$") then
|
||||
table.insert(attr_str, string.format("%s = %q", k, v));
|
||||
else
|
||||
table.insert(attr_str, string.format("[%q] = %q", k, v));
|
||||
end
|
||||
end
|
||||
if #attr_str > 0 then
|
||||
table.insert(attr_str, " }");
|
||||
end
|
||||
if first then
|
||||
code[#code+1] = (string.format("st.stanza(%q %s)", name, #attr_str>0 and table.concat(attr_str) or ", nil"));
|
||||
first = nil;
|
||||
else
|
||||
code[#code+1] = (string.format(":tag(%q%s)", name, table.concat(attr_str)));
|
||||
end
|
||||
end
|
||||
if text and text:find("%S") then
|
||||
code[#code+1] = (string.format(":text(%q)", text));
|
||||
elseif short_close then
|
||||
short_close = nil;
|
||||
code[#code+1] = (":up()");
|
||||
end
|
||||
end
|
||||
return table.concat(code, "");
|
||||
end
|
||||
|
||||
function action_handlers.PASS()
|
||||
return "do return pass_return end"
|
||||
end
|
||||
|
||||
function action_handlers.DROP()
|
||||
return "do return true end";
|
||||
end
|
||||
|
||||
function action_handlers.DEFAULT()
|
||||
return "do return false end";
|
||||
end
|
||||
|
||||
function action_handlers.RETURN()
|
||||
return "do return end"
|
||||
end
|
||||
|
||||
function action_handlers.STRIP(tag_desc)
|
||||
local code = {};
|
||||
local name, xmlns = tag_desc:match("^(%S+) (.+)$");
|
||||
if not name then
|
||||
name, xmlns = tag_desc, nil;
|
||||
end
|
||||
if name == "*" then
|
||||
name = nil;
|
||||
end
|
||||
code[#code+1] = ("local stanza_xmlns = stanza.attr.xmlns; ");
|
||||
code[#code+1] = "stanza:maptags(function (tag) if ";
|
||||
if name then
|
||||
code[#code+1] = ("tag.name == %q and "):format(name);
|
||||
end
|
||||
if xmlns then
|
||||
code[#code+1] = ("(tag.attr.xmlns or stanza_xmlns) == %q "):format(xmlns);
|
||||
else
|
||||
code[#code+1] = ("tag.attr.xmlns == stanza_xmlns ");
|
||||
end
|
||||
code[#code+1] = "then return nil; end return tag; end );";
|
||||
return table.concat(code);
|
||||
end
|
||||
|
||||
function action_handlers.INJECT(tag)
|
||||
return "stanza:add_child("..compile_xml(tag)..")", { "st" };
|
||||
end
|
||||
|
||||
local error_types = {
|
||||
["bad-request"] = "modify";
|
||||
["conflict"] = "cancel";
|
||||
["feature-not-implemented"] = "cancel";
|
||||
["forbidden"] = "auth";
|
||||
["gone"] = "cancel";
|
||||
["internal-server-error"] = "cancel";
|
||||
["item-not-found"] = "cancel";
|
||||
["jid-malformed"] = "modify";
|
||||
["not-acceptable"] = "modify";
|
||||
["not-allowed"] = "cancel";
|
||||
["not-authorized"] = "auth";
|
||||
["payment-required"] = "auth";
|
||||
["policy-violation"] = "modify";
|
||||
["recipient-unavailable"] = "wait";
|
||||
["redirect"] = "modify";
|
||||
["registration-required"] = "auth";
|
||||
["remote-server-not-found"] = "cancel";
|
||||
["remote-server-timeout"] = "wait";
|
||||
["resource-constraint"] = "wait";
|
||||
["service-unavailable"] = "cancel";
|
||||
["subscription-required"] = "auth";
|
||||
["undefined-condition"] = "cancel";
|
||||
["unexpected-request"] = "wait";
|
||||
};
|
||||
|
||||
|
||||
local function route_modify(make_new, to, drop)
|
||||
local reroute, deps = "session.send(newstanza)", { "st" };
|
||||
if to then
|
||||
reroute = ("newstanza.attr.to = %q; core_post_stanza(session, newstanza)"):format(to);
|
||||
deps[#deps+1] = "core_post_stanza";
|
||||
end
|
||||
return ([[do local newstanza = st.%s; %s;%s end]])
|
||||
:format(make_new, reroute, drop and " return true" or ""), deps;
|
||||
end
|
||||
|
||||
function action_handlers.BOUNCE(with)
|
||||
local error = with and with:match("^%S+") or "service-unavailable";
|
||||
local error_type = error:match(":(%S+)");
|
||||
if not error_type then
|
||||
error_type = error_types[error] or "cancel";
|
||||
else
|
||||
error = error:match("^[^:]+");
|
||||
end
|
||||
error, error_type = string.format("%q", error), string.format("%q", error_type);
|
||||
local text = with and with:match(" %((.+)%)$");
|
||||
if text then
|
||||
text = string.format("%q", text);
|
||||
else
|
||||
text = "nil";
|
||||
end
|
||||
local route_modify_code, deps = route_modify(("error_reply(stanza, %s, %s, %s)"):format(error_type, error, text), nil, true);
|
||||
deps[#deps+1] = "type";
|
||||
deps[#deps+1] = "name";
|
||||
return [[if type == "error" or (name == "iq" and type == "result") then return true; end -- Don't reply to 'error' stanzas, or iq results
|
||||
]]..route_modify_code, deps;
|
||||
end
|
||||
|
||||
function action_handlers.REDIRECT(where)
|
||||
return route_modify("clone(stanza)", where, true);
|
||||
end
|
||||
|
||||
function action_handlers.COPY(where)
|
||||
return route_modify("clone(stanza)", where, false);
|
||||
end
|
||||
|
||||
function action_handlers.REPLY(with)
|
||||
return route_modify(("reply(stanza):body(%q)"):format(with));
|
||||
end
|
||||
|
||||
function action_handlers.FORWARD(where)
|
||||
local code = [[
|
||||
local newstanza = st.stanza("message", { to = %q, from = current_host }):tag("forwarded", { xmlns = "urn:xmpp:forward:0" });
|
||||
local tmp_stanza = st.clone(stanza); tmp_stanza.attr.xmlns = "jabber:client"; newstanza:add_child(tmp_stanza);
|
||||
core_post_stanza(session, newstanza);
|
||||
]];
|
||||
return code:format(where), { "core_post_stanza", "current_host" };
|
||||
end
|
||||
|
||||
function action_handlers.LOG(string)
|
||||
local level = string:match("^%[(%a+)%]") or "info";
|
||||
string = string:gsub("^%[%a+%] ?", "");
|
||||
local meta_deps = {};
|
||||
local code = meta(("(session.log or log)(%q, '%%s', %q);"):format(level, string), meta_deps);
|
||||
return code, meta_deps;
|
||||
end
|
||||
|
||||
function action_handlers.RULEDEP(dep)
|
||||
return "", { dep };
|
||||
end
|
||||
|
||||
function action_handlers.EVENT(name)
|
||||
return ("fire_event(%q, event)"):format(name);
|
||||
end
|
||||
|
||||
function action_handlers.JUMP_EVENT(name)
|
||||
return ("do return fire_event(%q, event); end"):format(name);
|
||||
end
|
||||
|
||||
function action_handlers.JUMP_CHAIN(name)
|
||||
return template([[do
|
||||
local ret = fire_event($chain_event$, event);
|
||||
if ret ~= nil then
|
||||
if ret == false then
|
||||
log("debug", "Chain %q accepted stanza (ret %s)", $chain_name$, tostring(ret));
|
||||
return pass_return;
|
||||
end
|
||||
log("debug", "Chain %q rejected stanza (ret %s)", $chain_name$, tostring(ret));
|
||||
return ret;
|
||||
end
|
||||
end]], { chain_event = "firewall/chains/"..name, chain_name = name });
|
||||
end
|
||||
|
||||
function action_handlers.MARK_ORIGIN(name)
|
||||
return [[session.firewall_marked_]]..idsafe(name)..[[ = current_timestamp;]], { "timestamp" };
|
||||
end
|
||||
|
||||
function action_handlers.UNMARK_ORIGIN(name)
|
||||
return [[session.firewall_marked_]]..idsafe(name)..[[ = nil;]]
|
||||
end
|
||||
|
||||
function action_handlers.MARK_USER(name)
|
||||
return ([[if session.username and session.host == current_host then
|
||||
fire_event("firewall/marked/user", {
|
||||
username = session.username;
|
||||
mark = %q;
|
||||
timestamp = current_timestamp;
|
||||
});
|
||||
else
|
||||
log("warn", "Attempt to MARK a remote user - only local users may be marked");
|
||||
end]]):format(assert(idsafe(name), "Invalid characters in mark name: "..name)), {
|
||||
"current_host";
|
||||
"timestamp";
|
||||
};
|
||||
end
|
||||
|
||||
function action_handlers.UNMARK_USER(name)
|
||||
return ([[if session.username and session.host == current_host then
|
||||
fire_event("firewall/unmarked/user", {
|
||||
username = session.username;
|
||||
mark = %q;
|
||||
});
|
||||
else
|
||||
log("warn", "Attempt to UNMARK a remote user - only local users may be marked");
|
||||
end]]):format(assert(idsafe(name), "Invalid characters in mark name: "..name));
|
||||
end
|
||||
|
||||
function action_handlers.ADD_TO(spec)
|
||||
local list_name, value = spec:match("(%S+) (.+)");
|
||||
local meta_deps = {};
|
||||
value = meta(("%q"):format(value), meta_deps);
|
||||
return ("list_%s:add(%s);"):format(list_name, value), { "list:"..list_name, unpack(meta_deps) };
|
||||
end
|
||||
|
||||
function action_handlers.UNSUBSCRIBE_SENDER()
|
||||
return "rostermanager.unsubscribed(to_node, to_host, bare_from);\
|
||||
rostermanager.roster_push(to_node, to_host, bare_from);\
|
||||
core_post_stanza(session, st.presence({ from = bare_to, to = bare_from, type = \"unsubscribed\" }));",
|
||||
{ "rostermanager", "core_post_stanza", "st", "split_to", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
function action_handlers.REPORT_TO(spec)
|
||||
local where, reason, text = spec:match("^%s*(%S+) *(%S*) *(.*)$");
|
||||
if reason == "spam" then
|
||||
reason = "urn:xmpp:reporting:spam";
|
||||
elseif reason == "abuse" or not reason then
|
||||
reason = "urn:xmpp:reporting:abuse";
|
||||
end
|
||||
local code = [[
|
||||
local newstanza = st.stanza("message", { to = %q, from = current_host }):tag("forwarded", { xmlns = "urn:xmpp:forward:0" });
|
||||
local tmp_stanza = st.clone(stanza); tmp_stanza.attr.xmlns = "jabber:client"; newstanza:add_child(tmp_stanza):up();
|
||||
newstanza:tag("report", { xmlns = "urn:xmpp:reporting:1", reason = %q })
|
||||
do local text = %q; if text ~= "" then newstanza:text_tag("text", text); end end
|
||||
newstanza:up();
|
||||
core_post_stanza(session, newstanza);
|
||||
]];
|
||||
return code:format(where, reason, text), { "core_post_stanza", "current_host", "st" };
|
||||
end
|
||||
|
||||
return action_handlers;
|
@@ -0,0 +1,384 @@
|
||||
--luacheck: globals meta idsafe
|
||||
local condition_handlers = {};
|
||||
|
||||
local jid = require "util.jid";
|
||||
local unpack = table.unpack or unpack;
|
||||
|
||||
-- Helper to convert user-input strings (yes/true//no/false) to a bool
|
||||
local function string_to_boolean(s)
|
||||
s = s:lower();
|
||||
return s == "yes" or s == "true";
|
||||
end
|
||||
|
||||
-- Return a code string for a condition that checks whether the contents
|
||||
-- of variable with the name 'name' matches any of the values in the
|
||||
-- comma/space/pipe delimited list 'values'.
|
||||
local function compile_comparison_list(name, values)
|
||||
local conditions = {};
|
||||
for value in values:gmatch("[^%s,|]+") do
|
||||
table.insert(conditions, ("%s == %q"):format(name, value));
|
||||
end
|
||||
return table.concat(conditions, " or ");
|
||||
end
|
||||
|
||||
function condition_handlers.KIND(kind)
|
||||
assert(kind, "Expected stanza kind to match against");
|
||||
return compile_comparison_list("name", kind), { "name" };
|
||||
end
|
||||
|
||||
local wildcard_equivs = { ["*"] = ".*", ["?"] = "." };
|
||||
|
||||
local function compile_jid_match_part(part, match)
|
||||
if not match then
|
||||
return part.." == nil";
|
||||
end
|
||||
local pattern = match:match("^<(.*)>$");
|
||||
if pattern then
|
||||
if pattern == "*" then
|
||||
return part;
|
||||
end
|
||||
if pattern:find("^<.*>$") then
|
||||
pattern = pattern:match("^<(.*)>$");
|
||||
else
|
||||
pattern = pattern:gsub("%p", "%%%0"):gsub("%%(%p)", wildcard_equivs);
|
||||
end
|
||||
return ("(%s and %s:find(%q))"):format(part, part, "^"..pattern.."$");
|
||||
else
|
||||
return ("%s == %q"):format(part, match);
|
||||
end
|
||||
end
|
||||
|
||||
local function compile_jid_match(which, match_jid)
|
||||
local match_node, match_host, match_resource = jid.split(match_jid);
|
||||
local conditions = {};
|
||||
conditions[#conditions+1] = compile_jid_match_part(which.."_node", match_node);
|
||||
conditions[#conditions+1] = compile_jid_match_part(which.."_host", match_host);
|
||||
if match_resource then
|
||||
conditions[#conditions+1] = compile_jid_match_part(which.."_resource", match_resource);
|
||||
end
|
||||
return table.concat(conditions, " and ");
|
||||
end
|
||||
|
||||
function condition_handlers.TO(to)
|
||||
return compile_jid_match("to", to), { "split_to" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM(from)
|
||||
return compile_jid_match("from", from), { "split_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_FULL_JID()
|
||||
return "not "..compile_jid_match_part("from_resource", nil), { "split_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_EXACTLY(from)
|
||||
local metadeps = {};
|
||||
return ("from == %s"):format(metaq(from, metadeps)), { "from", unpack(metadeps) };
|
||||
end
|
||||
|
||||
function condition_handlers.TO_EXACTLY(to)
|
||||
local metadeps = {};
|
||||
return ("to == %s"):format(metaq(to, metadeps)), { "to", unpack(metadeps) };
|
||||
end
|
||||
|
||||
function condition_handlers.TO_SELF()
|
||||
-- Intentionally not using 'to' here, as that defaults to bare JID when nil
|
||||
return ("stanza.attr.to == nil");
|
||||
end
|
||||
|
||||
function condition_handlers.TYPE(type)
|
||||
assert(type, "Expected 'type' value to match against");
|
||||
return compile_comparison_list("(type or (name == 'message' and 'normal') or (name == 'presence' and 'available'))", type), { "type", "name" };
|
||||
end
|
||||
|
||||
local function zone_check(zone, which)
|
||||
local zone_var = zone;
|
||||
if zone == "$local" then zone_var = "_local" end
|
||||
local which_not = which == "from" and "to" or "from";
|
||||
return ("(zone_%s[%s_host] or zone_%s[%s] or zone_%s[bare_%s]) "
|
||||
.."and not(zone_%s[%s_host] or zone_%s[%s] or zone_%s[bare_%s])"
|
||||
)
|
||||
:format(zone_var, which, zone_var, which, zone_var, which,
|
||||
zone_var, which_not, zone_var, which_not, zone_var, which_not), {
|
||||
"split_to", "split_from", "bare_to", "bare_from", "zone:"..zone
|
||||
};
|
||||
end
|
||||
|
||||
function condition_handlers.ENTERING(zone)
|
||||
return zone_check(zone, "to");
|
||||
end
|
||||
|
||||
function condition_handlers.LEAVING(zone)
|
||||
return zone_check(zone, "from");
|
||||
end
|
||||
|
||||
-- IN ROSTER? (parameter is deprecated)
|
||||
function condition_handlers.IN_ROSTER(yes_no)
|
||||
local in_roster_requirement = string_to_boolean(yes_no or "yes"); -- COMPAT w/ older scripts
|
||||
return "not "..(in_roster_requirement and "not" or "").." roster_entry", { "roster_entry" };
|
||||
end
|
||||
|
||||
function condition_handlers.IN_ROSTER_GROUP(group)
|
||||
return ("not not (roster_entry and roster_entry.groups[%q])"):format(group), { "roster_entry" };
|
||||
end
|
||||
|
||||
function condition_handlers.SUBSCRIBED()
|
||||
return "(bare_to == bare_from or to_node and rostermanager.is_contact_subscribed(to_node, to_host, bare_from))",
|
||||
{ "rostermanager", "split_to", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.PENDING_SUBSCRIPTION_FROM_SENDER()
|
||||
return "(bare_to == bare_from or to_node and rostermanager.is_contact_pending_in(to_node, to_host, bare_from))",
|
||||
{ "rostermanager", "split_to", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.PAYLOAD(payload_ns)
|
||||
return ("stanza:get_child(nil, %q)"):format(payload_ns);
|
||||
end
|
||||
|
||||
function condition_handlers.INSPECT(path)
|
||||
if path:find("=") then
|
||||
local query, match_type, value = path:match("(.-)([~/$]*)=(.*)");
|
||||
if not(query:match("#$") or query:match("@[^/]+")) then
|
||||
error("Stanza path does not return a string (append # for text content or @name for value of named attribute)", 0);
|
||||
end
|
||||
local meta_deps = {};
|
||||
local quoted_value = ("%q"):format(value);
|
||||
if match_type:find("$", 1, true) then
|
||||
match_type = match_type:gsub("%$", "");
|
||||
quoted_value = meta(quoted_value, meta_deps);
|
||||
end
|
||||
if match_type == "~" then -- Lua pattern match
|
||||
return ("(stanza:find(%q) or ''):match(%s)"):format(query, quoted_value), meta_deps;
|
||||
elseif match_type == "/" then -- find literal substring
|
||||
return ("(stanza:find(%q) or ''):find(%s, 1, true)"):format(query, quoted_value), meta_deps;
|
||||
elseif match_type == "" then -- exact match
|
||||
return ("stanza:find(%q) == %s"):format(query, quoted_value), meta_deps;
|
||||
else
|
||||
error("Unrecognised comparison '"..match_type.."='", 0);
|
||||
end
|
||||
end
|
||||
return ("stanza:find(%q)"):format(path);
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_GROUP(group_name)
|
||||
return ("group_contains(%q, bare_from)"):format(group_name), { "group_contains", "bare_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.TO_GROUP(group_name)
|
||||
return ("group_contains(%q, bare_to)"):format(group_name), { "group_contains", "bare_to" };
|
||||
end
|
||||
|
||||
function condition_handlers.CROSSING_GROUPS(group_names)
|
||||
local code = {};
|
||||
for group_name in group_names:gmatch("([^, ][^,]+)") do
|
||||
group_name = group_name:match("^%s*(.-)%s*$"); -- Trim leading/trailing whitespace
|
||||
-- Just check that's it is crossing from outside group to inside group
|
||||
table.insert(code, ("(group_contains(%q, bare_to) and group_contains(%q, bare_from))"):format(group_name, group_name))
|
||||
end
|
||||
return "not "..table.concat(code, " or "), { "group_contains", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.FROM_ADMIN_OF(host)
|
||||
return ("is_admin(bare_from, %s)"):format(host ~= "*" and metaq(host) or nil), { "is_admin", "bare_from" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.TO_ADMIN_OF(host)
|
||||
return ("is_admin(bare_to, %s)"):format(host ~= "*" and metaq(host) or nil), { "is_admin", "bare_to" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.FROM_ADMIN()
|
||||
return ("is_admin(bare_from, current_host)"), { "is_admin", "bare_from", "current_host" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.TO_ADMIN()
|
||||
return ("is_admin(bare_to, current_host)"), { "is_admin", "bare_to", "current_host" };
|
||||
end
|
||||
|
||||
-- MAY: permission_to_check
|
||||
function condition_handlers.MAY(permission_to_check)
|
||||
return ("module:may(%q, event)"):format(permission_to_check);
|
||||
end
|
||||
|
||||
function condition_handlers.TO_ROLE(role_name)
|
||||
return ("get_jid_role(bare_to, current_host) == %q"):format(role_name), { "get_jid_role", "current_host", "bare_to" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_ROLE(role_name)
|
||||
return ("get_jid_role(bare_from, current_host) == %q"):format(role_name), { "get_jid_role", "current_host", "bare_from" };
|
||||
end
|
||||
|
||||
local day_numbers = { sun = 0, mon = 2, tue = 3, wed = 4, thu = 5, fri = 6, sat = 7 };
|
||||
|
||||
local function current_time_check(op, hour, minute)
|
||||
hour, minute = tonumber(hour), tonumber(minute);
|
||||
local adj_op = op == "<" and "<" or ">="; -- Start time inclusive, end time exclusive
|
||||
if minute == 0 then
|
||||
return "(current_hour"..adj_op..hour..")";
|
||||
else
|
||||
return "((current_hour"..op..hour..") or (current_hour == "..hour.." and current_minute"..adj_op..minute.."))";
|
||||
end
|
||||
end
|
||||
|
||||
local function resolve_day_number(day_name)
|
||||
return assert(day_numbers[day_name:sub(1,3):lower()], "Unknown day name: "..day_name);
|
||||
end
|
||||
|
||||
function condition_handlers.DAY(days)
|
||||
local conditions = {};
|
||||
for day_range in days:gmatch("[^,]+") do
|
||||
local day_start, day_end = day_range:match("(%a+)%s*%-%s*(%a+)");
|
||||
if day_start and day_end then
|
||||
local day_start_num, day_end_num = resolve_day_number(day_start), resolve_day_number(day_end);
|
||||
local op = "and";
|
||||
if day_end_num < day_start_num then
|
||||
op = "or";
|
||||
end
|
||||
table.insert(conditions, ("current_day >= %d %s current_day <= %d"):format(day_start_num, op, day_end_num));
|
||||
elseif day_range:find("%a") then
|
||||
local day = resolve_day_number(day_range:match("%a+"));
|
||||
table.insert(conditions, "current_day == "..day);
|
||||
else
|
||||
error("Unable to parse day/day range: "..day_range);
|
||||
end
|
||||
end
|
||||
assert(#conditions>0, "Expected a list of days or day ranges");
|
||||
return "("..table.concat(conditions, ") or (")..")", { "time:day" };
|
||||
end
|
||||
|
||||
function condition_handlers.TIME(ranges)
|
||||
local conditions = {};
|
||||
for range in ranges:gmatch("([^,]+)") do
|
||||
local clause = {};
|
||||
range = range:lower()
|
||||
:gsub("(%d+):?(%d*) *am", function (h, m) return tostring(tonumber(h)%12)..":"..(tonumber(m) or "00"); end)
|
||||
:gsub("(%d+):?(%d*) *pm", function (h, m) return tostring(tonumber(h)%12+12)..":"..(tonumber(m) or "00"); end);
|
||||
local start_hour, start_minute = range:match("(%d+):(%d+) *%-");
|
||||
local end_hour, end_minute = range:match("%- *(%d+):(%d+)");
|
||||
local op = tonumber(start_hour) > tonumber(end_hour) and " or " or " and ";
|
||||
if start_hour and end_hour then
|
||||
table.insert(clause, current_time_check(">", start_hour, start_minute));
|
||||
table.insert(clause, current_time_check("<", end_hour, end_minute));
|
||||
end
|
||||
if #clause == 0 then
|
||||
error("Unable to parse time range: "..range);
|
||||
end
|
||||
table.insert(conditions, "("..table.concat(clause, " "..op.." ")..")");
|
||||
end
|
||||
return table.concat(conditions, " or "), { "time:hour,min" };
|
||||
end
|
||||
|
||||
function condition_handlers.LIMIT(spec)
|
||||
local name, param = spec:match("^(%w+) on (.+)$");
|
||||
local meta_deps = {};
|
||||
|
||||
if not name then
|
||||
name = spec:match("^%w+$");
|
||||
if not name then
|
||||
error("Unable to parse LIMIT specification");
|
||||
end
|
||||
else
|
||||
param = meta(("%q"):format(param), meta_deps);
|
||||
end
|
||||
|
||||
if not param then
|
||||
return ("not global_throttle_%s:poll(1)"):format(name), { "globalthrottle:"..name, unpack(meta_deps) };
|
||||
end
|
||||
return ("not multi_throttle_%s:poll_on(%s, 1)"):format(name, param), { "multithrottle:"..name, unpack(meta_deps) };
|
||||
end
|
||||
|
||||
function condition_handlers.ORIGIN_MARKED(name_and_time)
|
||||
local name, time = name_and_time:match("^%s*([%w_]+)%s+%(([^)]+)s%)%s*$");
|
||||
if not name then
|
||||
name = name_and_time:match("^%s*([%w_]+)%s*$");
|
||||
end
|
||||
if not name then
|
||||
error("Error parsing mark name, see documentation for usage examples");
|
||||
end
|
||||
if time then
|
||||
return ("(current_timestamp - (session.firewall_marked_%s or 0)) < %d"):format(idsafe(name), tonumber(time)), { "timestamp" };
|
||||
end
|
||||
return ("not not session.firewall_marked_"..idsafe(name));
|
||||
end
|
||||
|
||||
function condition_handlers.USER_MARKED(name_and_time)
|
||||
local name, time = name_and_time:match("^%s*([%w_]+)%s+%(([^)]+)s%)%s*$");
|
||||
if not name then
|
||||
name = name_and_time:match("^%s*([%w_]+)%s*$");
|
||||
end
|
||||
if not name then
|
||||
error("Error parsing mark name, see documentation for usage examples");
|
||||
end
|
||||
if time then
|
||||
return ([[(
|
||||
current_timestamp - (session.firewall_marks and session.firewall_marks.%s or 0)
|
||||
) < %d]]):format(idsafe(name), tonumber(time)), { "timestamp" };
|
||||
end
|
||||
return ("not not (session.firewall_marks and session.firewall_marks."..idsafe(name)..")");
|
||||
end
|
||||
|
||||
function condition_handlers.SENT_DIRECTED_PRESENCE_TO_SENDER()
|
||||
return "not not (session.directed and session.directed[from])", { "from" };
|
||||
end
|
||||
|
||||
-- TO FULL JID?
|
||||
function condition_handlers.TO_FULL_JID()
|
||||
return "not not full_sessions[to]", { "to", "full_sessions" };
|
||||
end
|
||||
|
||||
-- CHECK LIST: spammers contains $<@from>
|
||||
function condition_handlers.CHECK_LIST(list_condition)
|
||||
local list_name, expr = list_condition:match("(%S+) contains (.+)$");
|
||||
if not (list_name and expr) then
|
||||
error("Error parsing list check, syntax: LISTNAME contains EXPRESSION");
|
||||
end
|
||||
local meta_deps = {};
|
||||
expr = meta(("%q"):format(expr), meta_deps);
|
||||
return ("list_%s:contains(%s) == true"):format(list_name, expr), { "list:"..list_name, unpack(meta_deps) };
|
||||
end
|
||||
|
||||
-- SCAN: body for word in badwords
|
||||
function condition_handlers.SCAN(scan_expression)
|
||||
local search_name, pattern_name, list_name = scan_expression:match("(%S+) for (%S+) in (%S+)$");
|
||||
if not (search_name) then
|
||||
error("Error parsing SCAN expression, syntax: SEARCH for PATTERN in LIST");
|
||||
end
|
||||
return ("scan_list(list_%s, %s)"):format(
|
||||
list_name,
|
||||
"tokens_"..search_name.."_"..pattern_name
|
||||
), {
|
||||
"scan_list",
|
||||
"tokens:"..search_name.."-"..pattern_name, "list:"..list_name
|
||||
};
|
||||
end
|
||||
|
||||
-- COUNT: lines in body < 10
|
||||
local valid_comp_ops = { [">"] = ">", ["<"] = "<", ["="] = "==", ["=="] = "==", ["<="] = "<=", [">="] = ">=" };
|
||||
function condition_handlers.COUNT(count_expression)
|
||||
local pattern_name, search_name, comparator_expression = count_expression:match("(%S+) in (%S+) (.+)$");
|
||||
if not (pattern_name) then
|
||||
error("Error parsing COUNT expression, syntax: PATTERN in SEARCH COMPARATOR");
|
||||
end
|
||||
local value;
|
||||
comparator_expression = comparator_expression:gsub("%d+", function (value_string)
|
||||
value = tonumber(value_string);
|
||||
return "";
|
||||
end);
|
||||
if not value then
|
||||
error("Error parsing COUNT expression, expected value");
|
||||
end
|
||||
local comp_op = comparator_expression:gsub("%s+", "");
|
||||
assert(valid_comp_ops[comp_op], "Error parsing COUNT expression, unknown comparison operator: "..comp_op);
|
||||
return ("it_count(search_%s:gmatch(pattern_%s)) %s %d"):format(
|
||||
search_name, pattern_name, comp_op, value
|
||||
), {
|
||||
"it_count",
|
||||
"search:"..search_name, "pattern:"..pattern_name
|
||||
};
|
||||
end
|
||||
|
||||
return condition_handlers;
|
@@ -0,0 +1,335 @@
|
||||
|
||||
-- Name arguments are unused here
|
||||
-- luacheck: ignore 212
|
||||
|
||||
local definition_handlers = {};
|
||||
|
||||
local http = require "net.http";
|
||||
local timer = require "util.timer";
|
||||
local set = require"util.set";
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local hashes = require "util.hashes";
|
||||
local jid = require "util.jid";
|
||||
local lfs = require "lfs";
|
||||
|
||||
local multirate_cache_size = module:get_option_number("firewall_multirate_cache_limit", 1000);
|
||||
|
||||
function definition_handlers.ZONE(zone_name, zone_members)
|
||||
local zone_member_list = {};
|
||||
for member in zone_members:gmatch("[^, ]+") do
|
||||
zone_member_list[#zone_member_list+1] = member;
|
||||
end
|
||||
return set.new(zone_member_list)._items;
|
||||
end
|
||||
|
||||
-- Helper function used by RATE handler
|
||||
local function evict_only_unthrottled(name, throttle)
|
||||
throttle:update();
|
||||
-- Check whether the throttle is at max balance (i.e. totally safe to forget about it)
|
||||
if throttle.balance < throttle.max then
|
||||
-- Not safe to forget
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
function definition_handlers.RATE(name, line)
|
||||
local rate = assert(tonumber(line:match("([%d.]+)")), "Unable to parse rate");
|
||||
local burst = tonumber(line:match("%(%s*burst%s+([%d.]+)%s*%)")) or 1;
|
||||
local max_throttles = tonumber(line:match("%(%s*entries%s+([%d]+)%s*%)")) or multirate_cache_size;
|
||||
local deny_when_full = not line:match("%(allow overflow%)");
|
||||
return {
|
||||
single = function ()
|
||||
return new_throttle(rate*burst, burst);
|
||||
end;
|
||||
|
||||
multi = function ()
|
||||
local cache = require "util.cache".new(max_throttles, deny_when_full and evict_only_unthrottled or nil);
|
||||
return {
|
||||
poll_on = function (_, key, amount)
|
||||
assert(key, "no key");
|
||||
local throttle = cache:get(key);
|
||||
if not throttle then
|
||||
throttle = new_throttle(rate*burst, burst);
|
||||
if not cache:set(key, throttle) then
|
||||
module:log("warn", "Multirate '%s' has hit its maximum number of active throttles (%d), denying new events", name, max_throttles);
|
||||
return false;
|
||||
end
|
||||
end
|
||||
return throttle:poll(amount);
|
||||
end;
|
||||
}
|
||||
end;
|
||||
};
|
||||
end
|
||||
|
||||
local list_backends = {
|
||||
-- %LIST name: memory (limit: number)
|
||||
memory = {
|
||||
init = function (self, type, opts)
|
||||
if opts.limit then
|
||||
local have_cache_lib, cache_lib = pcall(require, "util.cache");
|
||||
if not have_cache_lib then
|
||||
error("In-memory lists with a size limit require Prosody 0.10");
|
||||
end
|
||||
self.cache = cache_lib.new((assert(tonumber(opts.limit), "Invalid list limit")));
|
||||
if not self.cache.table then
|
||||
error("In-memory lists with a size limit require a newer version of Prosody 0.10");
|
||||
end
|
||||
self.items = self.cache:table();
|
||||
else
|
||||
self.items = {};
|
||||
end
|
||||
end;
|
||||
add = function (self, item)
|
||||
self.items[item] = true;
|
||||
end;
|
||||
remove = function (self, item)
|
||||
self.items[item] = nil;
|
||||
end;
|
||||
contains = function (self, item)
|
||||
return self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
|
||||
-- %LIST name: http://example.com/ (ttl: number, pattern: pat, hash: sha1)
|
||||
http = {
|
||||
init = function (self, url, opts)
|
||||
local poll_interval = assert(tonumber(opts.ttl or "3600"), "invalid ttl for <"..url.."> (expected number of seconds)");
|
||||
local pattern = opts.pattern or "([^\r\n]+)\r?\n";
|
||||
assert(pcall(string.match, "", pattern), "invalid pattern for <"..url..">");
|
||||
if opts.hash then
|
||||
assert(opts.hash:match("^%w+$") and type(hashes[opts.hash]) == "function", "invalid hash function: "..opts.hash);
|
||||
self.hash_function = hashes[opts.hash];
|
||||
end
|
||||
local etag;
|
||||
local failure_count = 0;
|
||||
local retry_intervals = { 60, 120, 300 };
|
||||
-- By default only check the certificate if net.http supports SNI
|
||||
local sni_supported = http.feature and http.features.sni;
|
||||
local insecure = false;
|
||||
if opts.checkcert == "never" then
|
||||
insecure = true;
|
||||
elseif (opts.checkcert == nil or opts.checkcert == "when-sni") and not sni_supported then
|
||||
insecure = false;
|
||||
end
|
||||
local function update_list()
|
||||
http.request(url, {
|
||||
insecure = insecure;
|
||||
headers = {
|
||||
["If-None-Match"] = etag;
|
||||
};
|
||||
}, function (body, code, response)
|
||||
local next_poll = poll_interval;
|
||||
if code == 200 and body then
|
||||
etag = response.headers.etag;
|
||||
local items = {};
|
||||
for entry in body:gmatch(pattern) do
|
||||
items[entry] = true;
|
||||
end
|
||||
self.items = items;
|
||||
module:log("debug", "Fetched updated list from <%s>", url);
|
||||
elseif code == 304 then
|
||||
module:log("debug", "List at <%s> is unchanged", url);
|
||||
elseif code == 0 or (code >= 400 and code <=599) then
|
||||
module:log("warn", "Failed to fetch list from <%s>: %d %s", url, code, tostring(body));
|
||||
failure_count = failure_count + 1;
|
||||
next_poll = retry_intervals[failure_count] or retry_intervals[#retry_intervals];
|
||||
end
|
||||
if next_poll > 0 then
|
||||
timer.add_task(next_poll+math.random(0, 60), update_list);
|
||||
end
|
||||
end);
|
||||
end
|
||||
update_list();
|
||||
end;
|
||||
add = function ()
|
||||
end;
|
||||
remove = function ()
|
||||
end;
|
||||
contains = function (self, item)
|
||||
if self.hash_function then
|
||||
item = self.hash_function(item);
|
||||
end
|
||||
return self.items and self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
|
||||
-- %LIST: file:/path/to/file
|
||||
file = {
|
||||
init = function (self, file_spec, opts)
|
||||
local n, items = 0, {};
|
||||
self.items = items;
|
||||
local filename = file_spec:gsub("^file:", "");
|
||||
if opts.missing == "ignore" and not lfs.attributes(filename, "mode") then
|
||||
module:log("debug", "Ignoring missing list file: %s", filename);
|
||||
return;
|
||||
end
|
||||
local file, err = io.open(filename);
|
||||
if not file then
|
||||
module:log("warn", "Failed to open list from %s: %s", filename, err);
|
||||
return;
|
||||
else
|
||||
for line in file:lines() do
|
||||
if not items[line] then
|
||||
n = n + 1;
|
||||
items[line] = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
module:log("debug", "Loaded %d items from %s", n, filename);
|
||||
end;
|
||||
add = function (self, item)
|
||||
self.items[item] = true;
|
||||
end;
|
||||
remove = function (self, item)
|
||||
self.items[item] = nil;
|
||||
end;
|
||||
contains = function (self, item)
|
||||
return self.items and self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
|
||||
-- %LIST: pubsub:pubsub.example.com/node
|
||||
-- TODO or the actual URI scheme? Bit overkill maybe?
|
||||
-- TODO Publish items back to the service?
|
||||
-- Step 1: Receiving pubsub events and storing them in the list
|
||||
-- We'll start by using only the item id.
|
||||
-- TODO Invent some custom schema for this? Needed for just a set of strings?
|
||||
pubsubitemid = {
|
||||
init = function(self, pubsub_spec, opts)
|
||||
local service_addr, node = pubsub_spec:match("^pubsubitemid:([^/]*)/(.*)");
|
||||
if not service_addr then
|
||||
module:log("warn", "Invalid list specification (expected 'pubsubitemid:<service>/<node>', got: '%s')", pubsub_spec);
|
||||
return;
|
||||
end
|
||||
module:depends("pubsub_subscription");
|
||||
module:add_item("pubsub-subscription", {
|
||||
service = service_addr;
|
||||
node = node;
|
||||
on_subscribed = function ()
|
||||
self.items = {};
|
||||
end;
|
||||
on_item = function (event)
|
||||
self:add(event.item.attr.id);
|
||||
end;
|
||||
on_retract = function (event)
|
||||
self:remove(event.item.attr.id);
|
||||
end;
|
||||
on_purge = function ()
|
||||
self.items = {};
|
||||
end;
|
||||
on_unsubscribed = function ()
|
||||
self.items = nil;
|
||||
end;
|
||||
on_delete= function ()
|
||||
self.items = nil;
|
||||
end;
|
||||
});
|
||||
-- TODO Initial fetch? Or should mod_pubsub_subscription do this?
|
||||
end;
|
||||
add = function (self, item)
|
||||
if self.items then
|
||||
self.items[item] = true;
|
||||
end
|
||||
end;
|
||||
remove = function (self, item)
|
||||
if self.items then
|
||||
self.items[item] = nil;
|
||||
end
|
||||
end;
|
||||
contains = function (self, item)
|
||||
return self.items and self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
};
|
||||
list_backends.https = list_backends.http;
|
||||
|
||||
local normalize_functions = {
|
||||
upper = string.upper, lower = string.lower;
|
||||
md5 = hashes.md5, sha1 = hashes.sha1, sha256 = hashes.sha256;
|
||||
prep = jid.prep, bare = jid.bare;
|
||||
};
|
||||
|
||||
local function wrap_list_method(list_method, filter)
|
||||
return function (self, item)
|
||||
return list_method(self, filter(item));
|
||||
end
|
||||
end
|
||||
|
||||
local function create_list(list_backend, list_def, opts)
|
||||
if not list_backends[list_backend] then
|
||||
error("Unknown list type '"..list_backend.."'", 0);
|
||||
end
|
||||
local list = setmetatable({}, { __index = list_backends[list_backend] });
|
||||
if list.init then
|
||||
list:init(list_def, opts);
|
||||
end
|
||||
if opts.filter then
|
||||
local filters = {};
|
||||
for func_name in opts.filter:gmatch("[%w_]+") do
|
||||
if func_name == "log" then
|
||||
table.insert(filters, function (s)
|
||||
--print("&&&&&", s);
|
||||
module:log("debug", "Checking list <%s> for: %s", list_def, s);
|
||||
return s;
|
||||
end);
|
||||
else
|
||||
assert(normalize_functions[func_name], "Unknown list filter: "..func_name);
|
||||
table.insert(filters, normalize_functions[func_name]);
|
||||
end
|
||||
end
|
||||
|
||||
local filter;
|
||||
local n = #filters;
|
||||
if n == 1 then
|
||||
filter = filters[1];
|
||||
else
|
||||
function filter(s)
|
||||
for i = 1, n do
|
||||
s = filters[i](s or "");
|
||||
end
|
||||
return s;
|
||||
end
|
||||
end
|
||||
|
||||
list.add = wrap_list_method(list.add, filter);
|
||||
list.remove = wrap_list_method(list.remove, filter);
|
||||
list.contains = wrap_list_method(list.contains, filter);
|
||||
end
|
||||
return list;
|
||||
end
|
||||
|
||||
--[[
|
||||
%LIST spammers: memory (source: /etc/spammers.txt)
|
||||
|
||||
%LIST spammers: memory (source: /etc/spammers.txt)
|
||||
|
||||
|
||||
%LIST spammers: http://example.com/blacklist.txt
|
||||
]]
|
||||
|
||||
function definition_handlers.LIST(list_name, list_definition)
|
||||
local list_backend = list_definition:match("^%w+");
|
||||
local opts = {};
|
||||
local opt_string = list_definition:match("^%S+%s+%((.+)%)");
|
||||
if opt_string then
|
||||
for opt_k, opt_v in opt_string:gmatch("(%w+): ?([^,]+)") do
|
||||
opts[opt_k] = opt_v;
|
||||
end
|
||||
end
|
||||
return create_list(list_backend, list_definition:match("^%S+"), opts);
|
||||
end
|
||||
|
||||
function definition_handlers.PATTERN(name, pattern)
|
||||
local ok, err = pcall(string.match, "", pattern);
|
||||
if not ok then
|
||||
error("Invalid pattern '"..name.."': "..err);
|
||||
end
|
||||
return pattern;
|
||||
end
|
||||
|
||||
function definition_handlers.SEARCH(name, pattern)
|
||||
return pattern;
|
||||
end
|
||||
|
||||
return definition_handlers;
|
35
roles/jitsi/files/prosody/modules/mod_firewall/marks.lib.lua
Normal file
35
roles/jitsi/files/prosody/modules/mod_firewall/marks.lib.lua
Normal file
@@ -0,0 +1,35 @@
|
||||
local mark_storage = module:open_store("firewall_marks");
|
||||
local mark_map_storage = module:open_store("firewall_marks", "map");
|
||||
|
||||
local user_sessions = prosody.hosts[module.host].sessions;
|
||||
|
||||
module:hook("firewall/marked/user", function (event)
|
||||
local user = user_sessions[event.username];
|
||||
local marks = user and user.firewall_marks;
|
||||
if user and not marks then
|
||||
-- Load marks from storage to cache on the user object
|
||||
marks = mark_storage:get(event.username) or {};
|
||||
user.firewall_marks = marks; --luacheck: ignore 122
|
||||
end
|
||||
if marks then
|
||||
marks[event.mark] = event.timestamp;
|
||||
end
|
||||
local ok, err = mark_map_storage:set(event.username, event.mark, event.timestamp);
|
||||
if not ok then
|
||||
module:log("error", "Failed to mark user %q with %q: %s", event.username, event.mark, err);
|
||||
end
|
||||
return true;
|
||||
end, -1);
|
||||
|
||||
module:hook("firewall/unmarked/user", function (event)
|
||||
local user = user_sessions[event.username];
|
||||
local marks = user and user.firewall_marks;
|
||||
if marks then
|
||||
marks[event.mark] = nil;
|
||||
end
|
||||
local ok, err = mark_map_storage:set(event.username, event.mark, nil);
|
||||
if not ok then
|
||||
module:log("error", "Failed to unmark user %q with %q: %s", event.username, event.mark, err);
|
||||
end
|
||||
return true;
|
||||
end, -1);
|
784
roles/jitsi/files/prosody/modules/mod_firewall/mod_firewall.lua
Normal file
784
roles/jitsi/files/prosody/modules/mod_firewall/mod_firewall.lua
Normal file
@@ -0,0 +1,784 @@
|
||||
|
||||
local lfs = require "lfs";
|
||||
local resolve_relative_path = require "core.configmanager".resolve_relative_path;
|
||||
local envload = require "util.envload".envload;
|
||||
local logger = require "util.logger".init;
|
||||
local it = require "util.iterators";
|
||||
local set = require "util.set";
|
||||
|
||||
local have_features, features = pcall(require, "core.features");
|
||||
features = have_features and features.available or set.new();
|
||||
|
||||
-- [definition_type] = definition_factory(param)
|
||||
local definitions = module:shared("definitions");
|
||||
|
||||
-- When a definition instance has been instantiated, it lives here
|
||||
-- [definition_type][definition_name] = definition_object
|
||||
local active_definitions = {
|
||||
ZONE = {
|
||||
-- Default zone that includes all local hosts
|
||||
["$local"] = setmetatable({}, { __index = prosody.hosts });
|
||||
};
|
||||
};
|
||||
|
||||
local default_chains = {
|
||||
preroute = {
|
||||
type = "event";
|
||||
priority = 0.1;
|
||||
"pre-message/bare", "pre-message/full", "pre-message/host";
|
||||
"pre-presence/bare", "pre-presence/full", "pre-presence/host";
|
||||
"pre-iq/bare", "pre-iq/full", "pre-iq/host";
|
||||
};
|
||||
deliver = {
|
||||
type = "event";
|
||||
priority = 0.1;
|
||||
"message/bare", "message/full", "message/host";
|
||||
"presence/bare", "presence/full", "presence/host";
|
||||
"iq/bare", "iq/full", "iq/host";
|
||||
};
|
||||
deliver_remote = {
|
||||
type = "event"; "route/remote";
|
||||
priority = 0.1;
|
||||
};
|
||||
};
|
||||
|
||||
local extra_chains = module:get_option("firewall_extra_chains", {});
|
||||
|
||||
local chains = {};
|
||||
for k,v in pairs(default_chains) do
|
||||
chains[k] = v;
|
||||
end
|
||||
for k,v in pairs(extra_chains) do
|
||||
chains[k] = v;
|
||||
end
|
||||
|
||||
-- Returns the input if it is safe to be used as a variable name, otherwise nil
|
||||
function idsafe(name)
|
||||
return name:match("^%a[%w_]*$");
|
||||
end
|
||||
|
||||
local meta_funcs = {
|
||||
bare = function (code)
|
||||
return "jid_bare("..code..")", {"jid_bare"};
|
||||
end;
|
||||
node = function (code)
|
||||
return "(jid_split("..code.."))", {"jid_split"};
|
||||
end;
|
||||
host = function (code)
|
||||
return "(select(2, jid_split("..code..")))", {"jid_split"};
|
||||
end;
|
||||
resource = function (code)
|
||||
return "(select(3, jid_split("..code..")))", {"jid_split"};
|
||||
end;
|
||||
};
|
||||
|
||||
-- Run quoted (%q) strings through this to allow them to contain code. e.g.: LOG=Received: $(stanza:top_tag())
|
||||
function meta(s, deps, extra)
|
||||
return (s:gsub("$(%b())", function (expr)
|
||||
expr = expr:gsub("\\(.)", "%1");
|
||||
return [["..tostring(]]..expr..[[).."]];
|
||||
end)
|
||||
:gsub("$(%b<>)", function (expr)
|
||||
expr = expr:sub(2,-2);
|
||||
local default = "<undefined>";
|
||||
expr = expr:gsub("||(%b\"\")$", function (default_string)
|
||||
default = stripslashes(default_string:sub(2,-2));
|
||||
return "";
|
||||
end);
|
||||
local func_chain = expr:match("|[%w|]+$");
|
||||
if func_chain then
|
||||
expr = expr:sub(1, -1-#func_chain);
|
||||
end
|
||||
local code;
|
||||
if expr:match("^@") then
|
||||
-- Skip stanza:find() for simple attribute lookup
|
||||
local attr_name = expr:sub(2);
|
||||
if deps and (attr_name == "to" or attr_name == "from" or attr_name == "type") then
|
||||
-- These attributes may be cached in locals
|
||||
code = attr_name;
|
||||
table.insert(deps, attr_name);
|
||||
else
|
||||
code = "stanza.attr["..("%q"):format(attr_name).."]";
|
||||
end
|
||||
elseif expr:match("^%w+#$") then
|
||||
code = ("stanza:get_child_text(%q)"):format(expr:sub(1, -2));
|
||||
else
|
||||
code = ("stanza:find(%q)"):format(expr);
|
||||
end
|
||||
if func_chain then
|
||||
for func_name in func_chain:gmatch("|(%w+)") do
|
||||
-- to/from are already available in local variables, use those if possible
|
||||
if (code == "to" or code == "from") and func_name == "bare" then
|
||||
code = "bare_"..code;
|
||||
table.insert(deps, code);
|
||||
elseif (code == "to" or code == "from") and (func_name == "node" or func_name == "host" or func_name == "resource") then
|
||||
table.insert(deps, "split_"..code);
|
||||
code = code.."_"..func_name;
|
||||
else
|
||||
assert(meta_funcs[func_name], "unknown function: "..func_name);
|
||||
local new_code, new_deps = meta_funcs[func_name](code);
|
||||
code = new_code;
|
||||
if new_deps and #new_deps > 0 then
|
||||
assert(deps, "function not supported here: "..func_name);
|
||||
for _, dep in ipairs(new_deps) do
|
||||
table.insert(deps, dep);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return "\"..tostring("..code.." or "..("%q"):format(default)..")..\"";
|
||||
end)
|
||||
:gsub("$$(%a+)", extra or {})
|
||||
:gsub([[^""%.%.]], "")
|
||||
:gsub([[%.%.""$]], ""));
|
||||
end
|
||||
|
||||
function metaq(s, ...)
|
||||
return meta(("%q"):format(s), ...);
|
||||
end
|
||||
|
||||
local escape_chars = {
|
||||
a = "\a", b = "\b", f = "\f", n = "\n", r = "\r", t = "\t",
|
||||
v = "\v", ["\\"] = "\\", ["\""] = "\"", ["\'"] = "\'"
|
||||
};
|
||||
function stripslashes(s)
|
||||
return (s:gsub("\\(.)", escape_chars));
|
||||
end
|
||||
|
||||
-- Dependency locations:
|
||||
-- <type lib>
|
||||
-- <type global>
|
||||
-- function handler()
|
||||
-- <local deps>
|
||||
-- if <conditions> then
|
||||
-- <actions>
|
||||
-- end
|
||||
-- end
|
||||
|
||||
local available_deps = {
|
||||
st = { global_code = [[local st = require "util.stanza";]]};
|
||||
it = { global_code = [[local it = require "util.iterators";]]};
|
||||
it_count = { global_code = [[local it_count = it.count;]], depends = { "it" } };
|
||||
current_host = { global_code = [[local current_host = module.host;]] };
|
||||
jid_split = {
|
||||
global_code = [[local jid_split = require "util.jid".split;]];
|
||||
};
|
||||
jid_bare = {
|
||||
global_code = [[local jid_bare = require "util.jid".bare;]];
|
||||
};
|
||||
to = { local_code = [[local to = stanza.attr.to or jid_bare(session.full_jid);]]; depends = { "jid_bare" } };
|
||||
from = { local_code = [[local from = stanza.attr.from;]] };
|
||||
type = { local_code = [[local type = stanza.attr.type;]] };
|
||||
name = { local_code = [[local name = stanza.name;]] };
|
||||
split_to = { -- The stanza's split to address
|
||||
depends = { "jid_split", "to" };
|
||||
local_code = [[local to_node, to_host, to_resource = jid_split(to);]];
|
||||
};
|
||||
split_from = { -- The stanza's split from address
|
||||
depends = { "jid_split", "from" };
|
||||
local_code = [[local from_node, from_host, from_resource = jid_split(from);]];
|
||||
};
|
||||
bare_to = { depends = { "jid_bare", "to" }, local_code = "local bare_to = jid_bare(to)"};
|
||||
bare_from = { depends = { "jid_bare", "from" }, local_code = "local bare_from = jid_bare(from)"};
|
||||
group_contains = {
|
||||
global_code = [[local group_contains = module:depends("groups").group_contains]];
|
||||
};
|
||||
is_admin = require"core.usermanager".is_admin and { global_code = [[local is_admin = require "core.usermanager".is_admin;]]} or nil;
|
||||
get_jid_role = require "core.usermanager".get_jid_role and { global_code = [[local get_jid_role = require "core.usermanager".get_jid_role;]] } or nil;
|
||||
core_post_stanza = { global_code = [[local core_post_stanza = prosody.core_post_stanza;]] };
|
||||
zone = { global_code = function (zone)
|
||||
local var = zone;
|
||||
if var == "$local" then
|
||||
var = "_local"; -- See #1090
|
||||
else
|
||||
assert(idsafe(var), "Invalid zone name: "..zone);
|
||||
end
|
||||
return ("local zone_%s = zones[%q] or {};"):format(var, zone);
|
||||
end };
|
||||
date_time = { global_code = [[local os_date = os.date]]; local_code = [[local current_date_time = os_date("*t");]] };
|
||||
time = { local_code = function (what)
|
||||
local defs = {};
|
||||
for field in what:gmatch("%a+") do
|
||||
table.insert(defs, ("local current_%s = current_date_time.%s;"):format(field, field));
|
||||
end
|
||||
return table.concat(defs, " ");
|
||||
end, depends = { "date_time" }; };
|
||||
timestamp = { global_code = [[local get_time = require "socket".gettime;]]; local_code = [[local current_timestamp = get_time();]]; };
|
||||
globalthrottle = {
|
||||
global_code = function (throttle)
|
||||
assert(idsafe(throttle), "Invalid rate limit name: "..throttle);
|
||||
assert(active_definitions.RATE[throttle], "Unknown rate limit: "..throttle);
|
||||
return ("local global_throttle_%s = rates.%s:single();"):format(throttle, throttle);
|
||||
end;
|
||||
};
|
||||
multithrottle = {
|
||||
global_code = function (throttle)
|
||||
assert(pcall(require, "util.cache"), "Using LIMIT with 'on' requires Prosody 0.10 or higher");
|
||||
assert(idsafe(throttle), "Invalid rate limit name: "..throttle);
|
||||
assert(active_definitions.RATE[throttle], "Unknown rate limit: "..throttle);
|
||||
return ("local multi_throttle_%s = rates.%s:multi();"):format(throttle, throttle);
|
||||
end;
|
||||
};
|
||||
full_sessions = {
|
||||
global_code = [[local full_sessions = prosody.full_sessions;]];
|
||||
};
|
||||
rostermanager = {
|
||||
global_code = [[local rostermanager = require "core.rostermanager";]];
|
||||
};
|
||||
roster_entry = {
|
||||
local_code = [[local roster_entry = (to_node and rostermanager.load_roster(to_node, to_host) or {})[bare_from];]];
|
||||
depends = { "rostermanager", "split_to", "bare_from" };
|
||||
};
|
||||
list = { global_code = function (list)
|
||||
assert(idsafe(list), "Invalid list name: "..list);
|
||||
assert(active_definitions.LIST[list], "Unknown list: "..list);
|
||||
return ("local list_%s = lists[%q];"):format(list, list);
|
||||
end
|
||||
};
|
||||
search = {
|
||||
local_code = function (search_name)
|
||||
local search_path = assert(active_definitions.SEARCH[search_name], "Undefined search path: "..search_name);
|
||||
return ("local search_%s = tostring(stanza:find(%q) or \"\")"):format(search_name, search_path);
|
||||
end;
|
||||
};
|
||||
pattern = {
|
||||
local_code = function (pattern_name)
|
||||
local pattern = assert(active_definitions.PATTERN[pattern_name], "Undefined pattern: "..pattern_name);
|
||||
return ("local pattern_%s = %q"):format(pattern_name, pattern);
|
||||
end;
|
||||
};
|
||||
tokens = {
|
||||
local_code = function (search_and_pattern)
|
||||
local search_name, pattern_name = search_and_pattern:match("^([^%-]+)-(.+)$");
|
||||
local code = ([[local tokens_%s_%s = {};
|
||||
if search_%s then
|
||||
for s in search_%s:gmatch(pattern_%s) do
|
||||
tokens_%s_%s[s] = true;
|
||||
end
|
||||
end
|
||||
]]):format(search_name, pattern_name, search_name, search_name, pattern_name, search_name, pattern_name);
|
||||
return code, { "search:"..search_name, "pattern:"..pattern_name };
|
||||
end;
|
||||
};
|
||||
scan_list = {
|
||||
global_code = [[local function scan_list(list, items) for item in pairs(items) do if list:contains(item) then return true; end end end]];
|
||||
}
|
||||
};
|
||||
|
||||
local function include_dep(dependency, code)
|
||||
local dep, dep_param = dependency:match("^([^:]+):?(.*)$");
|
||||
local dep_info = available_deps[dep];
|
||||
if not dep_info then
|
||||
module:log("error", "Dependency not found: %s", dep);
|
||||
return;
|
||||
end
|
||||
if code.included_deps[dependency] ~= nil then
|
||||
if code.included_deps[dependency] ~= true then
|
||||
module:log("error", "Circular dependency on %s", dep);
|
||||
end
|
||||
return;
|
||||
end
|
||||
code.included_deps[dependency] = false; -- Pending flag (used to detect circular references)
|
||||
for _, dep_dep in ipairs(dep_info.depends or {}) do
|
||||
include_dep(dep_dep, code);
|
||||
end
|
||||
if dep_info.global_code then
|
||||
if dep_param ~= "" then
|
||||
local global_code, deps = dep_info.global_code(dep_param);
|
||||
if deps then
|
||||
for _, dep_dep in ipairs(deps) do
|
||||
include_dep(dep_dep, code);
|
||||
end
|
||||
end
|
||||
table.insert(code.global_header, global_code);
|
||||
else
|
||||
table.insert(code.global_header, dep_info.global_code);
|
||||
end
|
||||
end
|
||||
if dep_info.local_code then
|
||||
if dep_param ~= "" then
|
||||
local local_code, deps = dep_info.local_code(dep_param);
|
||||
if deps then
|
||||
for _, dep_dep in ipairs(deps) do
|
||||
include_dep(dep_dep, code);
|
||||
end
|
||||
end
|
||||
table.insert(code, "\n\t\t-- "..dep.."\n\t\t"..local_code.."\n");
|
||||
else
|
||||
table.insert(code, "\n\t\t-- "..dep.."\n\t\t"..dep_info.local_code.."\n");
|
||||
end
|
||||
end
|
||||
code.included_deps[dependency] = true;
|
||||
end
|
||||
|
||||
local definition_handlers = module:require("definitions");
|
||||
local condition_handlers = module:require("conditions");
|
||||
local action_handlers = module:require("actions");
|
||||
|
||||
if module:get_option_boolean("firewall_experimental_user_marks", true) then
|
||||
module:require"marks";
|
||||
end
|
||||
|
||||
local function new_rule(ruleset, chain)
|
||||
assert(chain, "no chain specified");
|
||||
local rule = { conditions = {}, actions = {}, deps = {} };
|
||||
table.insert(ruleset[chain], rule);
|
||||
return rule;
|
||||
end
|
||||
|
||||
local function parse_firewall_rules(filename)
|
||||
local line_no = 0;
|
||||
|
||||
local function errmsg(err)
|
||||
return "Error compiling "..filename.." on line "..line_no..": "..err;
|
||||
end
|
||||
|
||||
local ruleset = {
|
||||
deliver = {};
|
||||
};
|
||||
|
||||
local chain = "deliver"; -- Default chain
|
||||
local rule;
|
||||
|
||||
local file, err = io.open(filename);
|
||||
if not file then return nil, err; end
|
||||
|
||||
local state; -- nil -> "rules" -> "actions" -> nil -> ...
|
||||
|
||||
local line_hold;
|
||||
for line in file:lines() do
|
||||
line = line:match("^%s*(.-)%s*$");
|
||||
if line_hold and line:sub(-1,-1) ~= "\\" then
|
||||
line = line_hold..line;
|
||||
line_hold = nil;
|
||||
elseif line:sub(-1,-1) == "\\" then
|
||||
line_hold = (line_hold or "")..line:sub(1,-2);
|
||||
end
|
||||
line_no = line_no + 1;
|
||||
|
||||
if line_hold or line:find("^[#;]") then -- luacheck: ignore 542
|
||||
-- No action; comment or partial line
|
||||
elseif line == "" then
|
||||
if state == "rules" then
|
||||
return nil, ("Expected an action on line %d for preceding criteria")
|
||||
:format(line_no);
|
||||
end
|
||||
state = nil;
|
||||
elseif not(state) and line:sub(1, 2) == "::" then
|
||||
chain = line:gsub("^::%s*", "");
|
||||
local chain_info = chains[chain];
|
||||
if not chain_info then
|
||||
if chain:match("^user/") then
|
||||
chains[chain] = { type = "event", priority = 1, pass_return = false };
|
||||
else
|
||||
return nil, errmsg("Unknown chain: "..chain);
|
||||
end
|
||||
elseif chain_info.type ~= "event" then
|
||||
return nil, errmsg("Only event chains supported at the moment");
|
||||
end
|
||||
ruleset[chain] = ruleset[chain] or {};
|
||||
elseif not(state) and line:sub(1,1) == "%" then -- Definition (zone, limit, etc.)
|
||||
local what, name = line:match("^%%%s*([%w_]+) +([^ :]+)");
|
||||
if not definition_handlers[what] then
|
||||
return nil, errmsg("Definition of unknown object: "..what);
|
||||
elseif not name or not idsafe(name) then
|
||||
return nil, errmsg("Invalid "..what.." name");
|
||||
end
|
||||
|
||||
local val = line:match(": ?(.*)$");
|
||||
if not val and line:find(":<") then -- Read from file
|
||||
local fn = line:match(":< ?(.-)%s*$");
|
||||
if not fn then
|
||||
return nil, errmsg("Unable to parse filename");
|
||||
end
|
||||
local f, err = io.open(fn);
|
||||
if not f then return nil, errmsg(err); end
|
||||
val = f:read("*a"):gsub("\r?\n", " "):gsub("%s+$", "");
|
||||
end
|
||||
if not val then
|
||||
return nil, errmsg("No value given for definition");
|
||||
end
|
||||
val = stripslashes(val);
|
||||
local ok, ret = pcall(definition_handlers[what], name, val);
|
||||
if not ok then
|
||||
return nil, errmsg(ret);
|
||||
end
|
||||
|
||||
if not active_definitions[what] then
|
||||
active_definitions[what] = {};
|
||||
end
|
||||
active_definitions[what][name] = ret;
|
||||
elseif line:find("^[%w_ ]+[%.=]") then
|
||||
-- Action
|
||||
if state == nil then
|
||||
-- This is a standalone action with no conditions
|
||||
rule = new_rule(ruleset, chain);
|
||||
end
|
||||
state = "actions";
|
||||
-- Action handlers?
|
||||
local action = line:match("^[%w_ ]+"):upper():gsub(" ", "_");
|
||||
if not action_handlers[action] then
|
||||
return nil, ("Unknown action on line %d: %s"):format(line_no, action or "<unknown>");
|
||||
end
|
||||
table.insert(rule.actions, "-- "..line)
|
||||
local ok, action_string, action_deps = pcall(action_handlers[action], line:match("=(.+)$"));
|
||||
if not ok then
|
||||
return nil, errmsg(action_string);
|
||||
end
|
||||
table.insert(rule.actions, action_string);
|
||||
for _, dep in ipairs(action_deps or {}) do
|
||||
table.insert(rule.deps, dep);
|
||||
end
|
||||
elseif state == "actions" then -- state is actions but action pattern did not match
|
||||
state = nil; -- Awaiting next rule, etc.
|
||||
table.insert(ruleset[chain], rule);
|
||||
rule = nil;
|
||||
else
|
||||
if not state then
|
||||
state = "rules";
|
||||
rule = new_rule(ruleset, chain);
|
||||
end
|
||||
-- Check standard modifiers for the condition (e.g. NOT)
|
||||
local negated;
|
||||
local condition = line:match("^[^:=%.?]*");
|
||||
if condition:find("%f[%w]NOT%f[^%w]") then
|
||||
local s, e = condition:match("%f[%w]()NOT()%f[^%w]");
|
||||
condition = (condition:sub(1,s-1)..condition:sub(e+1, -1)):match("^%s*(.-)%s*$");
|
||||
negated = true;
|
||||
end
|
||||
condition = condition:gsub(" ", "_");
|
||||
if not condition_handlers[condition] then
|
||||
return nil, ("Unknown condition on line %d: %s"):format(line_no, (condition:gsub("_", " ")));
|
||||
end
|
||||
-- Get the code for this condition
|
||||
local ok, condition_code, condition_deps = pcall(condition_handlers[condition], line:match(":%s?(.+)$"));
|
||||
if not ok then
|
||||
return nil, errmsg(condition_code);
|
||||
end
|
||||
if negated then condition_code = "not("..condition_code..")"; end
|
||||
table.insert(rule.conditions, condition_code);
|
||||
for _, dep in ipairs(condition_deps or {}) do
|
||||
table.insert(rule.deps, dep);
|
||||
end
|
||||
end
|
||||
end
|
||||
return ruleset;
|
||||
end
|
||||
|
||||
local function process_firewall_rules(ruleset)
|
||||
-- Compile ruleset and return complete code
|
||||
|
||||
local chain_handlers = {};
|
||||
|
||||
-- Loop through the chains in the parsed ruleset (e.g. incoming, outgoing)
|
||||
for chain_name, rules in pairs(ruleset) do
|
||||
local code = { included_deps = {}, global_header = {} };
|
||||
local condition_uses = {};
|
||||
-- This inner loop assumes chain is an event-based, not a filter-based
|
||||
-- chain (filter-based will be added later)
|
||||
for _, rule in ipairs(rules) do
|
||||
for _, condition in ipairs(rule.conditions) do
|
||||
if condition:find("^not%(.+%)$") then
|
||||
condition = condition:match("^not%((.+)%)$");
|
||||
end
|
||||
condition_uses[condition] = (condition_uses[condition] or 0) + 1;
|
||||
end
|
||||
end
|
||||
|
||||
local condition_cache, n_conditions = {}, 0;
|
||||
for _, rule in ipairs(rules) do
|
||||
for _, dep in ipairs(rule.deps) do
|
||||
include_dep(dep, code);
|
||||
end
|
||||
table.insert(code, "\n\t\t");
|
||||
local rule_code;
|
||||
if #rule.conditions > 0 then
|
||||
for i, condition in ipairs(rule.conditions) do
|
||||
local negated = condition:match("^not%(.+%)$");
|
||||
if negated then
|
||||
condition = condition:match("^not%((.+)%)$");
|
||||
end
|
||||
if condition_uses[condition] > 1 then
|
||||
local name = condition_cache[condition];
|
||||
if not name then
|
||||
n_conditions = n_conditions + 1;
|
||||
name = "condition"..n_conditions;
|
||||
condition_cache[condition] = name;
|
||||
table.insert(code, "local "..name.." = "..condition..";\n\t\t");
|
||||
end
|
||||
rule.conditions[i] = (negated and "not(" or "")..name..(negated and ")" or "");
|
||||
else
|
||||
rule.conditions[i] = (negated and "not(" or "(")..condition..")";
|
||||
end
|
||||
end
|
||||
|
||||
rule_code = "if "..table.concat(rule.conditions, " and ").." then\n\t\t\t"
|
||||
..table.concat(rule.actions, "\n\t\t\t")
|
||||
.."\n\t\tend\n";
|
||||
else
|
||||
rule_code = table.concat(rule.actions, "\n\t\t");
|
||||
end
|
||||
table.insert(code, rule_code);
|
||||
end
|
||||
|
||||
for name in pairs(definition_handlers) do
|
||||
table.insert(code.global_header, 1, "local "..name:lower().."s = definitions."..name..";");
|
||||
end
|
||||
|
||||
local code_string = "return function (definitions, fire_event, log, module, pass_return)\n\t"
|
||||
..table.concat(code.global_header, "\n\t")
|
||||
.."\n\tlocal db = require 'util.debug';\n\n\t"
|
||||
.."return function (event)\n\t\t"
|
||||
.."local stanza, session = event.stanza, event.origin;\n"
|
||||
..table.concat(code, "")
|
||||
.."\n\tend;\nend";
|
||||
|
||||
chain_handlers[chain_name] = code_string;
|
||||
end
|
||||
|
||||
return chain_handlers;
|
||||
end
|
||||
|
||||
local function compile_firewall_rules(filename)
|
||||
local ruleset, err = parse_firewall_rules(filename);
|
||||
if not ruleset then return nil, err; end
|
||||
local chain_handlers = process_firewall_rules(ruleset);
|
||||
return chain_handlers;
|
||||
end
|
||||
|
||||
-- Compile handler code into a factory that produces a valid event handler. Factory accepts
|
||||
-- a value to be returned on PASS
|
||||
local function compile_handler(code_string, filename)
|
||||
-- Prepare event handler function
|
||||
local chunk, err = envload(code_string, "="..filename, _G);
|
||||
if not chunk then
|
||||
return nil, "Error compiling (probably a compiler bug, please report): "..err;
|
||||
end
|
||||
local function fire_event(name, data)
|
||||
return module:fire_event(name, data);
|
||||
end
|
||||
local init_ok, initialized_chunk = pcall(chunk);
|
||||
if not init_ok then
|
||||
return nil, "Error initializing compiled rules: "..initialized_chunk;
|
||||
end
|
||||
return function (pass_return)
|
||||
return initialized_chunk(active_definitions, fire_event, logger(filename), module, pass_return); -- Returns event handler with upvalues
|
||||
end
|
||||
end
|
||||
|
||||
local function resolve_script_path(script_path)
|
||||
local relative_to = prosody.paths.config;
|
||||
if script_path:match("^module:") then
|
||||
relative_to = module.path:sub(1, -#("/mod_"..module.name..".lua"));
|
||||
script_path = script_path:match("^module:(.+)$");
|
||||
end
|
||||
return resolve_relative_path(relative_to, script_path);
|
||||
end
|
||||
|
||||
-- [filename] = { last_modified = ..., events_hooked = { [name] = handler } }
|
||||
local loaded_scripts = {};
|
||||
|
||||
function load_script(script)
|
||||
script = resolve_script_path(script);
|
||||
local last_modified = (lfs.attributes(script) or {}).modification or os.time();
|
||||
if loaded_scripts[script] then
|
||||
if loaded_scripts[script].last_modified == last_modified then
|
||||
return; -- Already loaded, and source file hasn't changed
|
||||
end
|
||||
module:log("debug", "Reloading %s", script);
|
||||
-- Already loaded, but the source file has changed
|
||||
-- unload it now, and we'll load the new version below
|
||||
unload_script(script, true);
|
||||
end
|
||||
local chain_functions, err = compile_firewall_rules(script);
|
||||
|
||||
if not chain_functions then
|
||||
module:log("error", "Error compiling %s: %s", script, err or "unknown error");
|
||||
return;
|
||||
end
|
||||
|
||||
-- Loop through the chains in the script, and for each chain attach the compiled code to the
|
||||
-- relevant events, keeping track in events_hooked so we can cleanly unload later
|
||||
local events_hooked = {};
|
||||
for chain, handler_code in pairs(chain_functions) do
|
||||
local new_handler, err = compile_handler(handler_code, "mod_firewall::"..chain);
|
||||
if not new_handler then
|
||||
module:log("error", "Compilation error for %s: %s", script, err);
|
||||
else
|
||||
local chain_definition = chains[chain];
|
||||
if chain_definition and chain_definition.type == "event" then
|
||||
local handler = new_handler(chain_definition.pass_return);
|
||||
for _, event_name in ipairs(chain_definition) do
|
||||
events_hooked[event_name] = handler;
|
||||
module:hook(event_name, handler, chain_definition.priority);
|
||||
end
|
||||
elseif not chain:sub(1, 5) == "user/" then
|
||||
module:log("warn", "Unknown chain %q", chain);
|
||||
end
|
||||
local event_name, handler = "firewall/chains/"..chain, new_handler(false);
|
||||
events_hooked[event_name] = handler;
|
||||
module:hook(event_name, handler);
|
||||
end
|
||||
end
|
||||
loaded_scripts[script] = { last_modified = last_modified, events_hooked = events_hooked };
|
||||
module:log("debug", "Loaded %s", script);
|
||||
end
|
||||
|
||||
--COMPAT w/0.9 (no module:unhook()!)
|
||||
local function module_unhook(event, handler)
|
||||
return module:unhook_object_event((hosts[module.host] or prosody).events, event, handler);
|
||||
end
|
||||
|
||||
function unload_script(script, is_reload)
|
||||
script = resolve_script_path(script);
|
||||
local script_info = loaded_scripts[script];
|
||||
if not script_info then
|
||||
return; -- Script not loaded
|
||||
end
|
||||
local events_hooked = script_info.events_hooked;
|
||||
for event_name, event_handler in pairs(events_hooked) do
|
||||
module_unhook(event_name, event_handler);
|
||||
events_hooked[event_name] = nil;
|
||||
end
|
||||
loaded_scripts[script] = nil;
|
||||
if not is_reload then
|
||||
module:log("debug", "Unloaded %s", script);
|
||||
end
|
||||
end
|
||||
|
||||
-- Given a set of scripts (e.g. from config) figure out which ones need to
|
||||
-- be loaded, which are already loaded but need unloading, and which to reload
|
||||
function load_unload_scripts(script_list)
|
||||
local wanted_scripts = script_list / resolve_script_path;
|
||||
local currently_loaded = set.new(it.to_array(it.keys(loaded_scripts)));
|
||||
local scripts_to_unload = currently_loaded - wanted_scripts;
|
||||
for script in wanted_scripts do
|
||||
-- If the script is already loaded, this is fine - it will
|
||||
-- reload the script for us if the file has changed
|
||||
load_script(script);
|
||||
end
|
||||
for script in scripts_to_unload do
|
||||
unload_script(script);
|
||||
end
|
||||
end
|
||||
|
||||
function module.load()
|
||||
if not prosody.arg then return end -- Don't run in prosodyctl
|
||||
local firewall_scripts = module:get_option_set("firewall_scripts", {});
|
||||
load_unload_scripts(firewall_scripts);
|
||||
-- Replace contents of definitions table (shared) with active definitions
|
||||
for k in it.keys(definitions) do definitions[k] = nil; end
|
||||
for k,v in pairs(active_definitions) do definitions[k] = v; end
|
||||
end
|
||||
|
||||
function module.save()
|
||||
return { active_definitions = active_definitions, loaded_scripts = loaded_scripts };
|
||||
end
|
||||
|
||||
function module.restore(state)
|
||||
active_definitions = state.active_definitions;
|
||||
loaded_scripts = state.loaded_scripts;
|
||||
end
|
||||
|
||||
module:hook_global("config-reloaded", function ()
|
||||
load_unload_scripts(module:get_option_set("firewall_scripts", {}));
|
||||
end);
|
||||
|
||||
function module.command(arg)
|
||||
if not arg[1] or arg[1] == "--help" then
|
||||
require"util.prosodyctl".show_usage([[mod_firewall <firewall.pfw>]], [[Compile files with firewall rules to Lua code]]);
|
||||
return 1;
|
||||
end
|
||||
local verbose = arg[1] == "-v";
|
||||
if verbose then table.remove(arg, 1); end
|
||||
|
||||
if arg[1] == "test" then
|
||||
table.remove(arg, 1);
|
||||
return module:require("test")(arg);
|
||||
end
|
||||
|
||||
local serialize = require "util.serialization".serialize;
|
||||
if verbose then
|
||||
print("local logger = require \"util.logger\".init;");
|
||||
print();
|
||||
print("local function fire_event(name, data)\n\tmodule:fire_event(name, data)\nend");
|
||||
print();
|
||||
end
|
||||
|
||||
for _, filename in ipairs(arg) do
|
||||
filename = resolve_script_path(filename);
|
||||
print("do -- File "..filename);
|
||||
local chain_functions = assert(compile_firewall_rules(filename));
|
||||
if verbose then
|
||||
print();
|
||||
print("local active_definitions = "..serialize(active_definitions)..";");
|
||||
print();
|
||||
end
|
||||
local c = 0;
|
||||
for chain, handler_code in pairs(chain_functions) do
|
||||
c = c + 1;
|
||||
print("---- Chain "..chain:gsub("_", " "));
|
||||
local chain_func_name = "chain_"..tostring(c).."_"..chain:gsub("%p", "_");
|
||||
if not verbose then
|
||||
print(("%s = %s;"):format(chain_func_name, handler_code:sub(8)));
|
||||
else
|
||||
|
||||
print(("local %s = (%s)(active_definitions, fire_event, logger(%q));"):format(chain_func_name, handler_code:sub(8), filename));
|
||||
print();
|
||||
|
||||
local chain_definition = chains[chain];
|
||||
if chain_definition and chain_definition.type == "event" then
|
||||
for _, event_name in ipairs(chain_definition) do
|
||||
print(("module:hook(%q, %s, %d);"):format(event_name, chain_func_name, chain_definition.priority or 0));
|
||||
end
|
||||
end
|
||||
print(("module:hook(%q, %s, %d);"):format("firewall/chains/"..chain, chain_func_name, chain_definition.priority or 0));
|
||||
end
|
||||
|
||||
print("---- End of chain "..chain);
|
||||
print();
|
||||
end
|
||||
print("end -- End of file "..filename);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Console
|
||||
|
||||
local console_env = module:shared("/*/admin_shell/env");
|
||||
|
||||
console_env.firewall = {};
|
||||
|
||||
function console_env.firewall:mark(user_jid, mark_name)
|
||||
local username, host = jid.split(user_jid);
|
||||
if not username or not hosts[host] then
|
||||
return nil, "Invalid JID supplied";
|
||||
elseif not idsafe(mark_name) then
|
||||
return nil, "Invalid characters in mark name";
|
||||
end
|
||||
if not module:context(host):fire_event("firewall/marked/user", {
|
||||
username = session.username;
|
||||
mark = mark_name;
|
||||
timestamp = os.time();
|
||||
}) then
|
||||
return nil, "Mark not set - is mod_firewall loaded on that host?";
|
||||
end
|
||||
return true, "User marked";
|
||||
end
|
||||
|
||||
function console_env.firewall:unmark(jid, mark_name)
|
||||
local username, host = jid.split(user_jid);
|
||||
if not username or not hosts[host] then
|
||||
return nil, "Invalid JID supplied";
|
||||
elseif not idsafe(mark_name) then
|
||||
return nil, "Invalid characters in mark name";
|
||||
end
|
||||
if not module:context(host):fire_event("firewall/unmarked/user", {
|
||||
username = session.username;
|
||||
mark = mark_name;
|
||||
}) then
|
||||
return nil, "Mark not removed - is mod_firewall loaded on that host?";
|
||||
end
|
||||
return true, "User unmarked";
|
||||
end
|
75
roles/jitsi/files/prosody/modules/mod_firewall/test.lib.lua
Normal file
75
roles/jitsi/files/prosody/modules/mod_firewall/test.lib.lua
Normal file
@@ -0,0 +1,75 @@
|
||||
-- luacheck: globals load_unload_scripts
|
||||
local set = require "util.set";
|
||||
local ltn12 = require "ltn12";
|
||||
|
||||
local xmppstream = require "util.xmppstream";
|
||||
|
||||
local function stderr(...)
|
||||
io.stderr:write("** ", table.concat({...}, "\t", 1, select("#", ...)), "\n");
|
||||
end
|
||||
|
||||
return function (arg)
|
||||
require "net.http".request = function (url, ex, cb)
|
||||
stderr("Making HTTP request to "..url);
|
||||
local body_table = {};
|
||||
local ok, response_status, response_headers = require "ssl.https".request({
|
||||
url = url;
|
||||
headers = ex.headers;
|
||||
method = ex.body and "POST" or "GET";
|
||||
sink = ltn12.sink.table(body_table);
|
||||
source = ex.body and ltn12.source.string(ex.body) or nil;
|
||||
});
|
||||
stderr("HTTP response "..response_status);
|
||||
cb(table.concat(body_table), response_status, { headers = response_headers });
|
||||
return true;
|
||||
end;
|
||||
|
||||
local stats_dropped, stats_passed = 0, 0;
|
||||
|
||||
load_unload_scripts(set.new(arg));
|
||||
local stream_callbacks = { default_ns = "jabber:client" };
|
||||
|
||||
function stream_callbacks.streamopened(session)
|
||||
session.notopen = nil;
|
||||
end
|
||||
function stream_callbacks.streamclosed()
|
||||
end
|
||||
function stream_callbacks.error(session, error_name, error_message) -- luacheck: ignore 212/session
|
||||
stderr("Fatal error parsing XML stream: "..error_name..": "..tostring(error_message))
|
||||
assert(false);
|
||||
end
|
||||
function stream_callbacks.handlestanza(session, stanza)
|
||||
if not module:fire_event("firewall/chains/deliver", { origin = session, stanza = stanza }) then
|
||||
stats_passed = stats_passed + 1;
|
||||
print(stanza);
|
||||
print("");
|
||||
else
|
||||
stats_dropped = stats_dropped + 1;
|
||||
end
|
||||
end
|
||||
|
||||
local session = { notopen = true };
|
||||
function session.send(stanza)
|
||||
stderr("Reply:", "\n"..tostring(stanza).."\n");
|
||||
end
|
||||
local stream = xmppstream.new(session, stream_callbacks);
|
||||
stream:feed("<stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client'>");
|
||||
local line_count = 0;
|
||||
for line in io.lines() do
|
||||
line_count = line_count + 1;
|
||||
local ok, err = stream:feed(line.."\n");
|
||||
if not ok then
|
||||
stderr("Fatal XML parse error on line "..line_count..": "..err);
|
||||
return 1;
|
||||
end
|
||||
end
|
||||
|
||||
stderr("Summary");
|
||||
stderr("-------");
|
||||
stderr("");
|
||||
stderr(stats_dropped + stats_passed, "processed");
|
||||
stderr(stats_passed, "passed");
|
||||
stderr(stats_dropped, "dropped");
|
||||
stderr(line_count, "input lines");
|
||||
stderr("");
|
||||
end
|
Reference in New Issue
Block a user