Software /
code /
prosody-modules
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 |
child | 955: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 |