mod_anti_spam/mod_anti_spam.lua
changeset 5863 259ffdbf8906
equal deleted inserted replaced
5862:761142ee0ff2 5863:259ffdbf8906
       
     1 local ip = require "util.ip";
       
     2 local jid_bare = require "util.jid".bare;
       
     3 local jid_split = require "util.jid".split;
       
     4 local set = require "util.set";
       
     5 local sha256 = require "util.hashes".sha256;
       
     6 local st = require"util.stanza";
       
     7 local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
       
     8 local full_sessions = prosody.full_sessions;
       
     9 
       
    10 local user_exists = require "core.usermanager".user_exists;
       
    11 
       
    12 local new_rtbl_subscription = module:require("rtbl").new_rtbl_subscription;
       
    13 local trie = module:require("trie");
       
    14 
       
    15 local spam_source_domains = set.new();
       
    16 local spam_source_ips = trie.new();
       
    17 local spam_source_jids = set.new();
       
    18 
       
    19 local count_spam_blocked = module:metric("counter", "anti_spam_blocked", "stanzas", "Stanzas blocked as spam", {"reason"});
       
    20 
       
    21 function block_spam(event, reason, action)
       
    22 	event.spam_reason = reason;
       
    23 	event.spam_action = action;
       
    24 	if module:fire_event("spam-blocked", event) == false then
       
    25 		module:log("debug", "Spam allowed by another module");
       
    26 		return;
       
    27 	end
       
    28 
       
    29 	count_spam_blocked:with_labels(reason):add(1);
       
    30 
       
    31 	if action == "bounce" then
       
    32 		module:log("debug", "Bouncing likely spam %s from %s (%s)", event.stanza.name, event.stanza.attr.from, reason);
       
    33 		event.origin.send(st.error_reply("cancel", "policy-violation", "Rejected as spam"));
       
    34 	else
       
    35 		module:log("debug", "Discarding likely spam %s from %s (%s)", event.stanza.name, event.stanza.attr.from, reason);
       
    36 	end
       
    37 
       
    38 	return true;
       
    39 end
       
    40 
       
    41 function is_from_stranger(from_jid, event)
       
    42 	local stanza = event.stanza;
       
    43 	local to_user, to_host, to_resource = jid_split(stanza.attr.to);
       
    44 
       
    45 	if not to_user then return false; end
       
    46 
       
    47 	local to_session = full_sessions[stanza.attr.to];
       
    48 	if to_session then return false; end
       
    49 
       
    50 	if not is_contact_subscribed(to_user, to_host, from_jid) then
       
    51 		-- Allow all messages from your own jid
       
    52 		if from_jid == to_user.."@"..to_host then
       
    53 			return false; -- Pass through
       
    54 		end
       
    55 		if to_resource and stanza.attr.type == "groupchat" then
       
    56 			return false; -- Pass through
       
    57 		end
       
    58 		return true; -- Stranger danger
       
    59 	end
       
    60 end
       
    61 
       
    62 function is_spammy_server(session)
       
    63 	if spam_source_domains:contains(session.from_host) then
       
    64 		return true;
       
    65 	end
       
    66 	local origin_ip = ip.new(session.ip);
       
    67 	if spam_source_ips:contains_ip(origin_ip) then
       
    68 		return true;
       
    69 	end
       
    70 end
       
    71 
       
    72 function is_spammy_sender(sender_jid)
       
    73 	return spam_source_jids:contains(sha256(sender_jid, true));
       
    74 end
       
    75 
       
    76 local spammy_strings = module:get_option_array("anti_spam_block_strings");
       
    77 local spammy_patterns = module:get_option_array("anti_spam_block_patterns");
       
    78 
       
    79 function is_spammy_content(stanza)
       
    80 	-- Only support message content
       
    81 	if stanza.name ~= "message" then return; end
       
    82 	if not (spammy_strings or spammy_patterns) then return; end
       
    83 
       
    84 	local body = stanza:get_child_text("body");
       
    85 	if spammy_strings then
       
    86 		for _, s in ipairs(spammy_strings) do
       
    87 			if body:find(s, 1, true) then
       
    88 				return true;
       
    89 			end
       
    90 		end
       
    91 	end
       
    92 	if spammy_patterns then
       
    93 		for _, s in ipairs(spammy_patterns) do
       
    94 			if body:find(s) then
       
    95 				return true;
       
    96 			end
       
    97 		end
       
    98 	end
       
    99 end
       
   100 
       
   101 -- Set up RTBLs
       
   102 
       
   103 local anti_spam_services = module:get_option_array("anti_spam_services");
       
   104 
       
   105 for _, rtbl_service_jid in ipairs(anti_spam_services) do
       
   106 	new_rtbl_subscription(rtbl_service_jid, "spam_source_domains", {
       
   107 		added = function (item)
       
   108 			spam_source_domains:add(item);
       
   109 		end;
       
   110 		removed = function (item)
       
   111 			spam_source_domains:remove(item);
       
   112 		end;
       
   113 	});
       
   114 	new_rtbl_subscription(rtbl_service_jid, "spam_source_ips", {
       
   115 		added = function (item)
       
   116 			spam_source_ips:add_subnet(ip.parse_cidr(item));
       
   117 		end;
       
   118 		removed = function (item)
       
   119 			spam_source_ips:remove_subnet(ip.parse_cidr(item));
       
   120 		end;
       
   121 	});
       
   122 	new_rtbl_subscription(rtbl_service_jid, "spam_source_jids_sha256", {
       
   123 		added = function (item)
       
   124 			spam_source_jids:add(item);
       
   125 		end;
       
   126 		removed = function (item)
       
   127 			spam_source_jids:remove(item);
       
   128 		end;
       
   129 	});
       
   130 end
       
   131 
       
   132 module:hook("message/bare", function (event)
       
   133 	local to_bare = jid_bare(event.stanza.attr.to);
       
   134 
       
   135 	if not user_exists(to_bare) then return; end
       
   136 
       
   137 	local from_bare = jid_bare(event.stanza.attr.from);
       
   138 	if not is_from_stranger(from_bare, event) then return; end
       
   139 
       
   140 	if is_spammy_server(event.origin) then
       
   141 		return block_spam(event, "known-spam-source", "drop");
       
   142 	end
       
   143 
       
   144 	if is_spammy_sender(from_bare) then
       
   145 		return block_spam(event, "known-spam-jid", "drop");
       
   146 	end
       
   147 
       
   148 	if is_spammy_content(event.stanza) then
       
   149 		return block_spam(event, "spam-content", "drop");
       
   150 	end
       
   151 end, 500);
       
   152 
       
   153 module:hook("presence/bare", function (event)
       
   154 	if event.stanza.type ~= "subscribe" then
       
   155 		return;
       
   156 	end
       
   157 
       
   158 	if is_spammy_server(event.origin) then
       
   159 		return block_spam(event, "known-spam-source", "drop");
       
   160 	end
       
   161 
       
   162 	if is_spammy_sender(event.stanza) then
       
   163 		return block_spam(event, "known-spam-jid", "drop");
       
   164 	end
       
   165 end, 500);