comparison mod_firewall/mod_firewall.lua @ 947:c91cac3b823f

mod_firewall: General stanza filtering plugin with a declarative rule-based syntax
author Matthew Wild <mwild1@gmail.com>
date Wed, 03 Apr 2013 16:11:20 +0100
parents
children 97454c088b6c
comparison
equal deleted inserted replaced
946:2c5430ff1c11 947:c91cac3b823f
1
2 local resolve_relative_path = require "core.configmanager".resolve_relative_path;
3 local logger = require "util.logger".init;
4 local set = require "util.set";
5 local add_filter = require "util.filters".add_filter;
6
7
8 zones = {};
9 local zones = zones;
10 setmetatable(zones, {
11 __index = function (zones, zone)
12 local t = { [zone] = true };
13 rawset(zones, zone, t);
14 return t;
15 end;
16 });
17
18 local chains = {
19 preroute = {
20 type = "event";
21 priority = 0.1;
22 "pre-message/bare", "pre-message/full", "pre-message/host";
23 "pre-presence/bare", "pre-presence/full", "pre-presence/host";
24 "pre-iq/bare", "pre-iq/full", "pre-iq/host";
25 };
26 deliver = {
27 type = "event";
28 priority = 0.1;
29 "message/bare", "message/full", "message/host";
30 "presence/bare", "presence/full", "presence/host";
31 "iq/bare", "iq/full", "iq/host";
32 };
33 deliver_remote = {
34 type = "event"; "route/remote";
35 priority = 0.1;
36 };
37 };
38
39 -- Dependency locations:
40 -- <type lib>
41 -- <type global>
42 -- function handler()
43 -- <local deps>
44 -- if <conditions> then
45 -- <actions>
46 -- end
47 -- end
48
49 local available_deps = {
50 st = { global_code = [[local st = require "util.stanza"]]};
51 jid_split = {
52 global_code = [[local jid_split = require "util.jid".split;]];
53 };
54 jid_bare = {
55 global_code = [[local jid_bare = require "util.jid".bare;]];
56 };
57 to = { local_code = [[local to = stanza.attr.to;]] };
58 from = { local_code = [[local from = stanza.attr.from;]] };
59 type = { local_code = [[local type = stanza.attr.type;]] };
60 name = { local_code = [[local name = stanza.name]] };
61 split_to = { -- The stanza's split to address
62 depends = { "jid_split", "to" };
63 local_code = [[local to_node, to_host, to_resource = jid_split(to);]];
64 };
65 split_from = { -- The stanza's split from address
66 depends = { "jid_split", "from" };
67 local_code = [[local from_node, from_host, from_resource = jid_split(from);]];
68 };
69 bare_to = { depends = { "jid_bare", "to" }, local_code = "local bare_to = jid_bare(to)"};
70 bare_from = { depends = { "jid_bare", "from" }, local_code = "local bare_from = jid_bare(from)"};
71 group_contains = {
72 global_code = [[local group_contains = module:depends("groups").group_contains]];
73 };
74 is_admin = { global_code = [[local is_admin = require "core.usermanager".is_admin]]};
75 core_post_stanza = { global_code = [[local core_post_stanza = prosody.core_post_stanza]] };
76 };
77
78 local function include_dep(dep, code)
79 local dep_info = available_deps[dep];
80 if not dep_info then
81 module:log("error", "Dependency not found: %s", dep);
82 return;
83 end
84 if code.included_deps[dep] then
85 if code.included_deps[dep] ~= true then
86 module:log("error", "Circular dependency on %s", dep);
87 end
88 return;
89 end
90 code.included_deps[dep] = false; -- Pending flag (used to detect circular references)
91 for _, dep_dep in ipairs(dep_info.depends or {}) do
92 include_dep(dep_dep, code);
93 end
94 if dep_info.global_code then
95 table.insert(code.global_header, dep_info.global_code);
96 end
97 if dep_info.local_code then
98 table.insert(code, "\n\t-- "..dep.."\n\t"..dep_info.local_code.."\n\n\t");
99 end
100 code.included_deps[dep] = true;
101 end
102
103 local condition_handlers = module:require("conditions");
104 local action_handlers = module:require("actions");
105
106 local function new_rule(ruleset, chain)
107 assert(chain, "no chain specified");
108 local rule = { conditions = {}, actions = {}, deps = {} };
109 table.insert(ruleset[chain], rule);
110 return rule;
111 end
112
113 local function compile_firewall_rules(filename)
114 local line_no = 0;
115
116 local ruleset = {
117 deliver = {};
118 };
119
120 local chain = "deliver"; -- Default chain
121 local rule;
122
123 local file, err = io.open(filename);
124 if not file then return nil, err; end
125
126 local state; -- nil -> "rules" -> "actions" -> nil -> ...
127
128 local line_hold;
129 for line in file:lines() do
130 line = line:match("^%s*(.-)%s*$");
131 if line_hold and line:sub(-1,-1) ~= "\\" then
132 line = line_hold..line;
133 line_hold = nil;
134 elseif line:sub(-1,-1) == "\\" then
135 line_hold = (line_hold or "")..line:sub(1,-2);
136 end
137 line_no = line_no + 1;
138
139 if line_hold or line:match("^[#;]") then
140 -- No action; comment or partial line
141 elseif line == "" then
142 if state == "rules" then
143 return nil, ("Expected an action on line %d for preceding criteria")
144 :format(line_no);
145 end
146 state = nil;
147 elseif not(state) and line:match("^::") then
148 chain = line:gsub("^::%s*", "");
149 ruleset[chain] = ruleset[chain] or {};
150 elseif not(state) and line:match("^ZONE ") then
151 local zone_name = line:match("^ZONE ([^:]+)");
152 local zone_members = line:match("^ZONE .-: ?(.*)");
153 local zone_member_list = {};
154 for member in zone_members:gmatch("[^, ]+") do
155 zone_member_list[#zone_member_list+1] = member;
156 end
157 zones[zone_name] = set.new(zone_member_list)._items;
158 elseif line:match("^[^%s:]+[%.=]") then
159 -- Action
160 if state == nil then
161 -- This is a standalone action with no conditions
162 rule = new_rule(ruleset, chain);
163 end
164 state = "actions";
165 -- Action handlers?
166 local action = line:match("^%P+");
167 if not action_handlers[action] then
168 return nil, ("Unknown action on line %d: %s"):format(line_no, action or "<unknown>");
169 end
170 table.insert(rule.actions, "-- "..line)
171 local action_string, action_deps = action_handlers[action](line:match("=(.+)$"));
172 table.insert(rule.actions, action_string);
173 for _, dep in ipairs(action_deps or {}) do
174 table.insert(rule.deps, dep);
175 end
176 elseif state == "actions" then -- state is actions but action pattern did not match
177 state = nil; -- Awaiting next rule, etc.
178 table.insert(ruleset[chain], rule);
179 rule = nil;
180 else
181 if not state then
182 state = "rules";
183 rule = new_rule(ruleset, chain);
184 end
185 -- Check standard modifiers for the condition (e.g. NOT)
186 local negated;
187 local condition = line:match("^[^:=%.]*");
188 if condition:match("%f[%w]NOT%f[^%w]") then
189 local s, e = condition:match("%f[%w]()NOT()%f[^%w]");
190 condition = (condition:sub(1,s-1)..condition:sub(e+1, -1)):match("^%s*(.-)%s*$");
191 negated = true;
192 end
193 condition = condition:gsub(" ", "");
194 if not condition_handlers[condition] then
195 return nil, ("Unknown condition on line %d: %s"):format(line_no, condition);
196 end
197 -- Get the code for this condition
198 local condition_code, condition_deps = condition_handlers[condition](line:match(":%s?(.+)$"));
199 if negated then condition_code = "not("..condition_code..")"; end
200 table.insert(rule.conditions, condition_code);
201 for _, dep in ipairs(condition_deps or {}) do
202 table.insert(rule.deps, dep);
203 end
204 end
205 end
206
207 -- Compile ruleset and return complete code
208
209 local chain_handlers = {};
210
211 -- Loop through the chains in the parsed ruleset (e.g. incoming, outgoing)
212 for chain_name, rules in pairs(ruleset) do
213 local code = { included_deps = {}, global_header = {} };
214 -- This inner loop assumes chain is an event-based, not a filter-based
215 -- chain (filter-based will be added later)
216 for _, rule in ipairs(rules) do
217 for _, dep in ipairs(rule.deps) do
218 include_dep(dep, code);
219 end
220 local rule_code = "if ("..table.concat(rule.conditions, ") and (")..") then\n\t"
221 ..table.concat(rule.actions, "\n\t")
222 .."\n end\n";
223 table.insert(code, rule_code);
224 end
225
226 assert(chains[chain_name].type == "event", "Only event chains supported at the moment")
227
228 local code_string = [[return function (zones, log)
229 ]]..table.concat(code.global_header, "\n")..[[
230 local db = require 'util.debug'
231 return function (event)
232 local stanza, session = event.stanza, event.origin;
233
234 ]]..table.concat(code, " ")..[[
235 end;
236 end]];
237
238 print(code_string)
239
240 -- Prepare event handler function
241 local chunk, err = loadstring(code_string, "="..filename);
242 if not chunk then
243 return nil, "Error compiling (probably a compiler bug, please report): "..err;
244 end
245 chunk = chunk()(zones, logger(filename)); -- Returns event handler with 'zones' upvalue.
246 chain_handlers[chain_name] = chunk;
247 end
248
249 return chain_handlers;
250 end
251
252 function module.load()
253 local firewall_scripts = module:get_option_set("firewall_scripts", {});
254 for script in firewall_scripts do
255 script = resolve_relative_path(script) or script;
256 local chain_functions, err = compile_firewall_rules(script)
257
258 if not chain_functions then
259 module:log("error", "Error compiling %s: %s", script, err or "unknown error");
260 else
261 for chain, handler in pairs(chain_functions) do
262 local chain_definition = chains[chain];
263 if chain_definition.type == "event" then
264 for _, event_name in ipairs(chain_definition) do
265 module:hook(event_name, handler, chain_definition.priority);
266 end
267 end
268 end
269 end
270 end
271 end