Software /
code /
prosody-modules
Comparison
mod_anti_spam/mod_anti_spam.lua @ 5883:259ffdbf8906
mod_anti_spam: New module for spam filtering (pre-alpha)
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Tue, 05 Mar 2024 18:26:29 +0000 |
comparison
equal
deleted
inserted
replaced
5882:761142ee0ff2 | 5883: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); |