Changeset

2520:c6fd8975704b

mod_firewall: Initial support for lists, in-memory and HTTP
author Matthew Wild <mwild1@gmail.com>
date Sun, 19 Feb 2017 21:10:26 +0000
parents 2519:d4bc434a60a4
children 2521:66b81e144ded
files mod_firewall/conditions.lib.lua mod_firewall/definitions.lib.lua mod_firewall/mod_firewall.lua
diffstat 3 files changed, 124 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/mod_firewall/conditions.lib.lua	Sun Feb 19 21:08:30 2017 +0000
+++ b/mod_firewall/conditions.lib.lua	Sun Feb 19 21:10:26 2017 +0000
@@ -252,4 +252,15 @@
 	return ("not not session.firewall_marked_"..idsafe(name));
 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
+
 return condition_handlers;
--- a/mod_firewall/definitions.lib.lua	Sun Feb 19 21:08:30 2017 +0000
+++ b/mod_firewall/definitions.lib.lua	Sun Feb 19 21:10:26 2017 +0000
@@ -4,6 +4,8 @@
 
 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;
 
@@ -57,5 +59,109 @@
 			};
 end
 
+local list_backends = {
+	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;
+	};
+	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 function update_list()
+				http.request(url, {
+					headers = {
+						["If-None-Match"] = etag;
+					};
+				}, function (body, code, response)
+					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);
+					else
+						module:log("warn", "Failed to fetch list from <%s>: %d %s", url, code, tostring(body));
+					end
+					if poll_interval > 0 then
+						timer.add_task(poll_interval, update_list);
+					end
+				end);
+			end
+			update_list();
+			timer.add_task(0, update_list);
+		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;
+	};
+};
+
+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
+	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
+
 return definition_handlers;
-
--- a/mod_firewall/mod_firewall.lua	Sun Feb 19 21:08:30 2017 +0000
+++ b/mod_firewall/mod_firewall.lua	Sun Feb 19 21:10:26 2017 +0000
@@ -191,6 +191,12 @@
 		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
+	};
 };
 
 local function include_dep(dependency, code)