* new setting * new configuration screen for Peertube admins * include the mod_firewall module * load mod_firewall if enabled * sys admin can disable the firewall config editing by creating a special file on the disk * user documentation
		
			
				
	
	
		
			336 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			336 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
 | 
						|
-- Name arguments are unused here
 | 
						|
-- luacheck: ignore 212
 | 
						|
 | 
						|
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;
 | 
						|
local hashes = require "util.hashes";
 | 
						|
local jid = require "util.jid";
 | 
						|
local lfs = require "lfs";
 | 
						|
 | 
						|
local multirate_cache_size = module:get_option_number("firewall_multirate_cache_limit", 1000);
 | 
						|
 | 
						|
function definition_handlers.ZONE(zone_name, zone_members)
 | 
						|
			local zone_member_list = {};
 | 
						|
			for member in zone_members:gmatch("[^, ]+") do
 | 
						|
				zone_member_list[#zone_member_list+1] = member;
 | 
						|
			end
 | 
						|
			return set.new(zone_member_list)._items;
 | 
						|
end
 | 
						|
 | 
						|
-- Helper function used by RATE handler
 | 
						|
local function evict_only_unthrottled(name, throttle)
 | 
						|
	throttle:update();
 | 
						|
	-- Check whether the throttle is at max balance (i.e. totally safe to forget about it)
 | 
						|
	if throttle.balance < throttle.max then
 | 
						|
		-- Not safe to forget
 | 
						|
		return false;
 | 
						|
	end
 | 
						|
end
 | 
						|
 | 
						|
function definition_handlers.RATE(name, line)
 | 
						|
			local rate = assert(tonumber(line:match("([%d.]+)")), "Unable to parse rate");
 | 
						|
			local burst = tonumber(line:match("%(%s*burst%s+([%d.]+)%s*%)")) or 1;
 | 
						|
			local max_throttles = tonumber(line:match("%(%s*entries%s+([%d]+)%s*%)")) or multirate_cache_size;
 | 
						|
			local deny_when_full = not line:match("%(allow overflow%)");
 | 
						|
			return {
 | 
						|
				single = function ()
 | 
						|
					return new_throttle(rate*burst, burst);
 | 
						|
				end;
 | 
						|
 | 
						|
				multi = function ()
 | 
						|
					local cache = require "util.cache".new(max_throttles, deny_when_full and evict_only_unthrottled or nil);
 | 
						|
					return {
 | 
						|
						poll_on = function (_, key, amount)
 | 
						|
							assert(key, "no key");
 | 
						|
							local throttle = cache:get(key);
 | 
						|
							if not throttle then
 | 
						|
								throttle = new_throttle(rate*burst, burst);
 | 
						|
								if not cache:set(key, throttle) then
 | 
						|
									module:log("warn", "Multirate '%s' has hit its maximum number of active throttles (%d), denying new events", name, max_throttles);
 | 
						|
									return false;
 | 
						|
								end
 | 
						|
							end
 | 
						|
							return throttle:poll(amount);
 | 
						|
						end;
 | 
						|
					}
 | 
						|
				end;
 | 
						|
			};
 | 
						|
end
 | 
						|
 | 
						|
local list_backends = {
 | 
						|
	-- %LIST name: memory (limit: number)
 | 
						|
	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;
 | 
						|
	};
 | 
						|
 | 
						|
	-- %LIST name: http://example.com/ (ttl: number, pattern: pat, hash: sha1)
 | 
						|
	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 failure_count = 0;
 | 
						|
			local retry_intervals = { 60, 120, 300 };
 | 
						|
			-- By default only check the certificate if net.http supports SNI
 | 
						|
			local sni_supported = http.feature and http.features.sni;
 | 
						|
			local insecure = false;
 | 
						|
			if opts.checkcert == "never" then
 | 
						|
				insecure = true;
 | 
						|
			elseif (opts.checkcert == nil or opts.checkcert == "when-sni") and not sni_supported then
 | 
						|
				insecure = false;
 | 
						|
			end
 | 
						|
			local function update_list()
 | 
						|
				http.request(url, {
 | 
						|
					insecure = insecure;
 | 
						|
					headers = {
 | 
						|
						["If-None-Match"] = etag;
 | 
						|
					};
 | 
						|
				}, function (body, code, response)
 | 
						|
					local next_poll = poll_interval;
 | 
						|
					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);
 | 
						|
					elseif code == 0 or (code >= 400 and code <=599) then
 | 
						|
						module:log("warn", "Failed to fetch list from <%s>: %d %s", url, code, tostring(body));
 | 
						|
						failure_count = failure_count + 1;
 | 
						|
						next_poll = retry_intervals[failure_count] or retry_intervals[#retry_intervals];
 | 
						|
					end
 | 
						|
					if next_poll > 0 then
 | 
						|
						timer.add_task(next_poll+math.random(0, 60), update_list);
 | 
						|
					end
 | 
						|
				end);
 | 
						|
			end
 | 
						|
			update_list();
 | 
						|
		end;
 | 
						|
		add = function ()
 | 
						|
		end;
 | 
						|
		remove = function ()
 | 
						|
		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;
 | 
						|
	};
 | 
						|
 | 
						|
	-- %LIST: file:/path/to/file
 | 
						|
	file = {
 | 
						|
		init = function (self, file_spec, opts)
 | 
						|
			local n, items = 0, {};
 | 
						|
			self.items = items;
 | 
						|
			local filename = file_spec:gsub("^file:", "");
 | 
						|
			if opts.missing == "ignore" and not lfs.attributes(filename, "mode") then
 | 
						|
				module:log("debug", "Ignoring missing list file: %s", filename);
 | 
						|
				return;
 | 
						|
			end
 | 
						|
			local file, err = io.open(filename);
 | 
						|
			if not file then
 | 
						|
				module:log("warn", "Failed to open list from %s: %s", filename, err);
 | 
						|
				return;
 | 
						|
			else
 | 
						|
				for line in file:lines() do
 | 
						|
					if not items[line] then
 | 
						|
						n = n + 1;
 | 
						|
						items[line] = true;
 | 
						|
					end
 | 
						|
				end
 | 
						|
			end
 | 
						|
			module:log("debug", "Loaded %d items from %s", n, filename);
 | 
						|
		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 and self.items[item] == true;
 | 
						|
		end;
 | 
						|
	};
 | 
						|
 | 
						|
	-- %LIST: pubsub:pubsub.example.com/node
 | 
						|
	-- TODO or the actual URI scheme? Bit overkill maybe?
 | 
						|
	-- TODO Publish items back to the service?
 | 
						|
	-- Step 1: Receiving pubsub events and storing them in the list
 | 
						|
	-- We'll start by using only the item id.
 | 
						|
	-- TODO Invent some custom schema for this? Needed for just a set of strings?
 | 
						|
	pubsubitemid = {
 | 
						|
		init = function(self, pubsub_spec, opts)
 | 
						|
			local service_addr, node = pubsub_spec:match("^pubsubitemid:([^/]*)/(.*)");
 | 
						|
			if not service_addr then
 | 
						|
				module:log("warn", "Invalid list specification (expected 'pubsubitemid:<service>/<node>', got: '%s')", pubsub_spec);
 | 
						|
				return;
 | 
						|
			end
 | 
						|
			module:depends("pubsub_subscription");
 | 
						|
			module:add_item("pubsub-subscription", {
 | 
						|
					service = service_addr;
 | 
						|
					node = node;
 | 
						|
					on_subscribed = function ()
 | 
						|
						self.items = {};
 | 
						|
					end;
 | 
						|
					on_item = function (event)
 | 
						|
						self:add(event.item.attr.id);
 | 
						|
					end;
 | 
						|
					on_retract = function (event)
 | 
						|
						self:remove(event.item.attr.id);
 | 
						|
					end;
 | 
						|
					on_purge = function ()
 | 
						|
						self.items = {};
 | 
						|
					end;
 | 
						|
					on_unsubscribed = function ()
 | 
						|
						self.items = nil;
 | 
						|
					end;
 | 
						|
					on_delete= function ()
 | 
						|
						self.items = nil;
 | 
						|
					end;
 | 
						|
				});
 | 
						|
			-- TODO Initial fetch? Or should mod_pubsub_subscription do this?
 | 
						|
		end;
 | 
						|
		add = function (self, item)
 | 
						|
			if self.items then
 | 
						|
				self.items[item] = true;
 | 
						|
			end
 | 
						|
		end;
 | 
						|
		remove = function (self, item)
 | 
						|
			if self.items then
 | 
						|
				self.items[item] = nil;
 | 
						|
			end
 | 
						|
		end;
 | 
						|
		contains = function (self, item)
 | 
						|
			return self.items and self.items[item] == true;
 | 
						|
		end;
 | 
						|
	};
 | 
						|
};
 | 
						|
list_backends.https = list_backends.http;
 | 
						|
 | 
						|
local normalize_functions = {
 | 
						|
	upper = string.upper, lower = string.lower;
 | 
						|
	md5 = hashes.md5, sha1 = hashes.sha1, sha256 = hashes.sha256;
 | 
						|
	prep = jid.prep, bare = jid.bare;
 | 
						|
};
 | 
						|
 | 
						|
local function wrap_list_method(list_method, filter)
 | 
						|
	return function (self, item)
 | 
						|
		return list_method(self, filter(item));
 | 
						|
	end
 | 
						|
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
 | 
						|
	if opts.filter then
 | 
						|
		local filters = {};
 | 
						|
		for func_name in opts.filter:gmatch("[%w_]+") do
 | 
						|
			if func_name == "log" then
 | 
						|
				table.insert(filters, function (s)
 | 
						|
					--print("&&&&&", s);
 | 
						|
					module:log("debug", "Checking list <%s> for: %s", list_def, s);
 | 
						|
					return s;
 | 
						|
				end);
 | 
						|
			else
 | 
						|
				assert(normalize_functions[func_name], "Unknown list filter: "..func_name);
 | 
						|
				table.insert(filters, normalize_functions[func_name]);
 | 
						|
			end
 | 
						|
		end
 | 
						|
 | 
						|
		local filter;
 | 
						|
		local n = #filters;
 | 
						|
		if n == 1 then
 | 
						|
			filter = filters[1];
 | 
						|
		else
 | 
						|
			function filter(s)
 | 
						|
				for i = 1, n do
 | 
						|
					s = filters[i](s or "");
 | 
						|
				end
 | 
						|
				return s;
 | 
						|
			end
 | 
						|
		end
 | 
						|
 | 
						|
		list.add = wrap_list_method(list.add, filter);
 | 
						|
		list.remove = wrap_list_method(list.remove, filter);
 | 
						|
		list.contains = wrap_list_method(list.contains, filter);
 | 
						|
	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
 | 
						|
 | 
						|
function definition_handlers.PATTERN(name, pattern)
 | 
						|
	local ok, err = pcall(string.match, "", pattern);
 | 
						|
	if not ok then
 | 
						|
		error("Invalid pattern '"..name.."': "..err);
 | 
						|
	end
 | 
						|
	return pattern;
 | 
						|
end
 | 
						|
 | 
						|
function definition_handlers.SEARCH(name, pattern)
 | 
						|
	return pattern;
 | 
						|
end
 | 
						|
 | 
						|
return definition_handlers;
 |