Mercurial > 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 |
| 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 |
