mod_muc_webchat_url: Fix default url
[prosody-modules.git] / mod_firewall / conditions.lib.lua
blob78bed58b37fbddc531060c4bbed41c4f6c2708fe
1 --luacheck: globals meta idsafe
2 local condition_handlers = {};
4 local jid = require "util.jid";
6 -- Helper to convert user-input strings (yes/true//no/false) to a bool
7 local function string_to_boolean(s)
8 s = s:lower();
9 return s == "yes" or s == "true";
10 end
12 -- Return a code string for a condition that checks whether the contents
13 -- of variable with the name 'name' matches any of the values in the
14 -- comma/space/pipe delimited list 'values'.
15 local function compile_comparison_list(name, values)
16 local conditions = {};
17 for value in values:gmatch("[^%s,|]+") do
18 table.insert(conditions, ("%s == %q"):format(name, value));
19 end
20 return table.concat(conditions, " or ");
21 end
23 function condition_handlers.KIND(kind)
24 assert(kind, "Expected stanza kind to match against");
25 return compile_comparison_list("name", kind), { "name" };
26 end
28 local wildcard_equivs = { ["*"] = ".*", ["?"] = "." };
30 local function compile_jid_match_part(part, match)
31 if not match then
32 return part.." == nil";
33 end
34 local pattern = match:match("^<(.*)>$");
35 if pattern then
36 if pattern == "*" then
37 return part;
38 end
39 if pattern:find("^<.*>$") then
40 pattern = pattern:match("^<(.*)>$");
41 else
42 pattern = pattern:gsub("%p", "%%%0"):gsub("%%(%p)", wildcard_equivs);
43 end
44 return ("(%s and %s:find(%q))"):format(part, part, "^"..pattern.."$");
45 else
46 return ("%s == %q"):format(part, match);
47 end
48 end
50 local function compile_jid_match(which, match_jid)
51 local match_node, match_host, match_resource = jid.split(match_jid);
52 local conditions = {};
53 conditions[#conditions+1] = compile_jid_match_part(which.."_node", match_node);
54 conditions[#conditions+1] = compile_jid_match_part(which.."_host", match_host);
55 if match_resource then
56 conditions[#conditions+1] = compile_jid_match_part(which.."_resource", match_resource);
57 end
58 return table.concat(conditions, " and ");
59 end
61 function condition_handlers.TO(to)
62 return compile_jid_match("to", to), { "split_to" };
63 end
65 function condition_handlers.FROM(from)
66 return compile_jid_match("from", from), { "split_from" };
67 end
69 function condition_handlers.FROM_EXACTLY(from)
70 local metadeps = {};
71 return ("from == %s"):format(metaq(from, metadeps)), { "from", unpack(metadeps) };
72 end
74 function condition_handlers.TO_EXACTLY(to)
75 local metadeps = {};
76 return ("to == %s"):format(metaq(to, metadeps)), { "to", unpack(metadeps) };
77 end
79 function condition_handlers.TO_SELF()
80 -- Intentionally not using 'to' here, as that defaults to bare JID when nil
81 return ("stanza.attr.to == nil");
82 end
84 function condition_handlers.TYPE(type)
85 assert(type, "Expected 'type' value to match against");
86 return compile_comparison_list("(type or (name == 'message' and 'normal') or (name == 'presence' and 'available'))", type), { "type", "name" };
87 end
89 local function zone_check(zone, which)
90 local zone_var = zone;
91 if zone == "$local" then zone_var = "_local" end
92 local which_not = which == "from" and "to" or "from";
93 return ("(zone_%s[%s_host] or zone_%s[%s] or zone_%s[bare_%s]) "
94 .."and not(zone_%s[%s_host] or zone_%s[%s] or zone_%s[bare_%s])"
96 :format(zone_var, which, zone_var, which, zone_var, which,
97 zone_var, which_not, zone_var, which_not, zone_var, which_not), {
98 "split_to", "split_from", "bare_to", "bare_from", "zone:"..zone
102 function condition_handlers.ENTERING(zone)
103 return zone_check(zone, "to");
106 function condition_handlers.LEAVING(zone)
107 return zone_check(zone, "from");
110 -- IN ROSTER? (parameter is deprecated)
111 function condition_handlers.IN_ROSTER(yes_no)
112 local in_roster_requirement = string_to_boolean(yes_no or "yes"); -- COMPAT w/ older scripts
113 return "not "..(in_roster_requirement and "not" or "").." roster_entry", { "roster_entry" };
116 function condition_handlers.IN_ROSTER_GROUP(group)
117 return ("not not (roster_entry and roster_entry.groups[%q])"):format(group), { "roster_entry" };
120 function condition_handlers.SUBSCRIBED()
121 return "(bare_to == bare_from or to_node and rostermanager.is_contact_subscribed(to_node, to_host, bare_from))",
122 { "rostermanager", "split_to", "bare_to", "bare_from" };
125 function condition_handlers.PENDING_SUBSCRIPTION_FROM_SENDER()
126 return "(bare_to == bare_from or to_node and rostermanager.is_contact_pending_in(to_node, to_host, bare_from))",
127 { "rostermanager", "split_to", "bare_to", "bare_from" };
130 function condition_handlers.PAYLOAD(payload_ns)
131 return ("stanza:get_child(nil, %q)"):format(payload_ns);
134 function condition_handlers.INSPECT(path)
135 if path:find("=") then
136 local query, match_type, value = path:match("(.-)([~/$]*)=(.*)");
137 if not(query:match("#$") or query:match("@[^/]+")) then
138 error("Stanza path does not return a string (append # for text content or @name for value of named attribute)", 0);
140 local meta_deps = {};
141 local quoted_value = ("%q"):format(value);
142 if match_type:find("$", 1, true) then
143 match_type = match_type:gsub("%$", "");
144 quoted_value = meta(quoted_value, meta_deps);
146 if match_type == "~" then -- Lua pattern match
147 return ("(stanza:find(%q) or ''):match(%s)"):format(query, quoted_value), meta_deps;
148 elseif match_type == "/" then -- find literal substring
149 return ("(stanza:find(%q) or ''):find(%s, 1, true)"):format(query, quoted_value), meta_deps;
150 elseif match_type == "" then -- exact match
151 return ("stanza:find(%q) == %s"):format(query, quoted_value), meta_deps;
152 else
153 error("Unrecognised comparison '"..match_type.."='", 0);
156 return ("stanza:find(%q)"):format(path);
159 function condition_handlers.FROM_GROUP(group_name)
160 return ("group_contains(%q, bare_from)"):format(group_name), { "group_contains", "bare_from" };
163 function condition_handlers.TO_GROUP(group_name)
164 return ("group_contains(%q, bare_to)"):format(group_name), { "group_contains", "bare_to" };
167 function condition_handlers.CROSSING_GROUPS(group_names)
168 local code = {};
169 for group_name in group_names:gmatch("([^, ][^,]+)") do
170 group_name = group_name:match("^%s*(.-)%s*$"); -- Trim leading/trailing whitespace
171 -- Just check that's it is crossing from outside group to inside group
172 table.insert(code, ("(group_contains(%q, bare_to) and group_contains(%q, bare_from))"):format(group_name, group_name))
174 return "not "..table.concat(code, " or "), { "group_contains", "bare_to", "bare_from" };
177 function condition_handlers.FROM_ADMIN_OF(host)
178 return ("is_admin(bare_from, %s)"):format(host ~= "*" and metaq(host) or nil), { "is_admin", "bare_from" };
181 function condition_handlers.TO_ADMIN_OF(host)
182 return ("is_admin(bare_to, %s)"):format(host ~= "*" and metaq(host) or nil), { "is_admin", "bare_to" };
185 function condition_handlers.FROM_ADMIN()
186 return ("is_admin(bare_from, current_host)"), { "is_admin", "bare_from", "current_host" };
189 function condition_handlers.TO_ADMIN()
190 return ("is_admin(bare_to, current_host)"), { "is_admin", "bare_to", "current_host" };
193 local day_numbers = { sun = 0, mon = 2, tue = 3, wed = 4, thu = 5, fri = 6, sat = 7 };
195 local function current_time_check(op, hour, minute)
196 hour, minute = tonumber(hour), tonumber(minute);
197 local adj_op = op == "<" and "<" or ">="; -- Start time inclusive, end time exclusive
198 if minute == 0 then
199 return "(current_hour"..adj_op..hour..")";
200 else
201 return "((current_hour"..op..hour..") or (current_hour == "..hour.." and current_minute"..adj_op..minute.."))";
205 local function resolve_day_number(day_name)
206 return assert(day_numbers[day_name:sub(1,3):lower()], "Unknown day name: "..day_name);
209 function condition_handlers.DAY(days)
210 local conditions = {};
211 for day_range in days:gmatch("[^,]+") do
212 local day_start, day_end = day_range:match("(%a+)%s*%-%s*(%a+)");
213 if day_start and day_end then
214 local day_start_num, day_end_num = resolve_day_number(day_start), resolve_day_number(day_end);
215 local op = "and";
216 if day_end_num < day_start_num then
217 op = "or";
219 table.insert(conditions, ("current_day >= %d %s current_day <= %d"):format(day_start_num, op, day_end_num));
220 elseif day_range:find("%a") then
221 local day = resolve_day_number(day_range:match("%a+"));
222 table.insert(conditions, "current_day == "..day);
223 else
224 error("Unable to parse day/day range: "..day_range);
227 assert(#conditions>0, "Expected a list of days or day ranges");
228 return "("..table.concat(conditions, ") or (")..")", { "time:day" };
231 function condition_handlers.TIME(ranges)
232 local conditions = {};
233 for range in ranges:gmatch("([^,]+)") do
234 local clause = {};
235 range = range:lower()
236 :gsub("(%d+):?(%d*) *am", function (h, m) return tostring(tonumber(h)%12)..":"..(tonumber(m) or "00"); end)
237 :gsub("(%d+):?(%d*) *pm", function (h, m) return tostring(tonumber(h)%12+12)..":"..(tonumber(m) or "00"); end);
238 local start_hour, start_minute = range:match("(%d+):(%d+) *%-");
239 local end_hour, end_minute = range:match("%- *(%d+):(%d+)");
240 local op = tonumber(start_hour) > tonumber(end_hour) and " or " or " and ";
241 if start_hour and end_hour then
242 table.insert(clause, current_time_check(">", start_hour, start_minute));
243 table.insert(clause, current_time_check("<", end_hour, end_minute));
245 if #clause == 0 then
246 error("Unable to parse time range: "..range);
248 table.insert(conditions, "("..table.concat(clause, " "..op.." ")..")");
250 return table.concat(conditions, " or "), { "time:hour,min" };
253 function condition_handlers.LIMIT(spec)
254 local name, param = spec:match("^(%w+) on (.+)$");
255 local meta_deps = {};
257 if not name then
258 name = spec:match("^%w+$");
259 if not name then
260 error("Unable to parse LIMIT specification");
262 else
263 param = meta(("%q"):format(param), meta_deps);
266 if not param then
267 return ("not global_throttle_%s:poll(1)"):format(name), { "globalthrottle:"..name, unpack(meta_deps) };
269 return ("not multi_throttle_%s:poll_on(%s, 1)"):format(name, param), { "multithrottle:"..name, unpack(meta_deps) };
272 function condition_handlers.ORIGIN_MARKED(name_and_time)
273 local name, time = name_and_time:match("^%s*([%w_]+)%s+%(([^)]+)s%)%s*$");
274 if not name then
275 name = name_and_time:match("^%s*([%w_]+)%s*$");
277 if not name then
278 error("Error parsing mark name, see documentation for usage examples");
280 if time then
281 return ("(current_timestamp - (session.firewall_marked_%s or 0)) < %d"):format(idsafe(name), tonumber(time)), { "timestamp" };
283 return ("not not session.firewall_marked_"..idsafe(name));
286 function condition_handlers.USER_MARKED(name_and_time)
287 local name, time = name_and_time:match("^%s*([%w_]+)%s+%(([^)]+)s%)%s*$");
288 if not name then
289 name = name_and_time:match("^%s*([%w_]+)%s*$");
291 if not name then
292 error("Error parsing mark name, see documentation for usage examples");
294 if time then
295 return ("(current_timestamp - (session.firewall_marks and session.firewall_marks.%s or 0)) < %d"):format(idsafe(name), tonumber(time)), { "timestamp" };
297 return ("not not (session.firewall_marks and session.firewall_marks."..idsafe(name)..")");
300 function condition_handlers.SENT_DIRECTED_PRESENCE_TO_SENDER()
301 return "not not (session.directed and session.directed[from])", { "from" };
304 -- TO FULL JID?
305 function condition_handlers.TO_FULL_JID()
306 return "not not full_sessions[to]", { "to" };
309 -- CHECK LIST: spammers contains $<@from>
310 function condition_handlers.CHECK_LIST(list_condition)
311 local list_name, expr = list_condition:match("(%S+) contains (.+)$");
312 if not (list_name and expr) then
313 error("Error parsing list check, syntax: LISTNAME contains EXPRESSION");
315 local meta_deps = {};
316 expr = meta(("%q"):format(expr), meta_deps);
317 return ("list_%s:contains(%s) == true"):format(list_name, expr), { "list:"..list_name, unpack(meta_deps) };
320 -- SCAN: body for word in badwords
321 function condition_handlers.SCAN(scan_expression)
322 local search_name, pattern_name, list_name = scan_expression:match("(%S+) for (%S+) in (%S+)$");
323 if not (search_name) then
324 error("Error parsing SCAN expression, syntax: SEARCH for PATTERN in LIST");
326 return ("scan_list(list_%s, %s)"):format(list_name, "tokens_"..search_name.."_"..pattern_name), { "scan_list", "tokens:"..search_name.."-"..pattern_name, "list:"..list_name };
329 -- COUNT: lines in body < 10
330 local valid_comp_ops = { [">"] = ">", ["<"] = "<", ["="] = "==", ["=="] = "==", ["<="] = "<=", [">="] = ">=" };
331 function condition_handlers.COUNT(count_expression)
332 local pattern_name, search_name, comparator_expression = count_expression:match("(%S+) in (%S+) (.+)$");
333 if not (pattern_name) then
334 error("Error parsing COUNT expression, syntax: PATTERN in SEARCH COMPARATOR");
336 local value;
337 comparator_expression = comparator_expression:gsub("%d+", function (value_string)
338 value = tonumber(value_string);
339 return "";
340 end);
341 if not value then
342 error("Error parsing COUNT expression, expected value");
344 local comp_op = comparator_expression:gsub("%s+", "");
345 assert(valid_comp_ops[comp_op], "Error parsing COUNT expression, unknown comparison operator: "..comp_op);
346 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 };
349 return condition_handlers;