Software /
code /
prosody
Comparison
plugins/mod_blocklist.lua @ 6344:68b5c1ed18dd
mod_blocklist: XEP-0191 implementation written for speed and independence from mod_privacy
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Sun, 10 Aug 2014 10:27:00 +0200 |
child | 6350:bba5f4ffe75a |
comparison
equal
deleted
inserted
replaced
6341:ab9a1af80632 | 6344:68b5c1ed18dd |
---|---|
1 -- Prosody IM | |
2 -- Copyright (C) 2009-2010 Matthew Wild | |
3 -- Copyright (C) 2009-2010 Waqas Hussain | |
4 -- Copyright (C) 2014 Kim Alvefur | |
5 -- | |
6 -- This project is MIT/X11 licensed. Please see the | |
7 -- COPYING file in the source package for more information. | |
8 -- | |
9 -- This module implements XEP-0191: Blocking Command | |
10 -- | |
11 | |
12 local user_exists = require"core.usermanager".user_exists; | |
13 local is_contact_subscribed = require"core.rostermanager".is_contact_subscribed; | |
14 local st = require"util.stanza"; | |
15 local st_error_reply = st.error_reply; | |
16 local jid_prep, jid_split = import("jid", "prep", "split"); | |
17 | |
18 local host = module.host; | |
19 local storage = module:open_store(); | |
20 local sessions = prosody.hosts[host].sessions; | |
21 | |
22 -- Cache of blocklists used since module was loaded | |
23 local cache = {}; | |
24 if module:get_option_boolean("blocklist_weak_cache") then | |
25 -- Lower memory usage, more IO and latency | |
26 setmetatable(cache, { __mode = "v" }); | |
27 end | |
28 | |
29 local null_blocklist = {}; | |
30 | |
31 module:add_feature("urn:xmpp:blocking"); | |
32 | |
33 local function set_blocklist(username, blocklist) | |
34 local ok, err = storage:set(username, blocklist); | |
35 if not ok then | |
36 return ok, err; | |
37 end | |
38 -- Successful save, update the cache | |
39 cache[username] = blocklist; | |
40 return true; | |
41 end | |
42 | |
43 -- Migrates from the old mod_privacy storage | |
44 local function migrate_privacy_list(username) | |
45 local migrated_data = { [false] = "not empty" }; | |
46 module:log("info", "Migrating blocklist from mod_privacy storage for user '%s'", username); | |
47 local legacy_data = module:open_store("privacy"):get(username); | |
48 if legacy_data and legacy_data.lists and legacy_data.default then | |
49 legacy_data = legacy_data.lists[legacy_data.default]; | |
50 legacy_data = legacy_data and legacy_data.items; | |
51 else | |
52 return migrated_data; | |
53 end | |
54 if legacy_data then | |
55 local item, jid; | |
56 for i = 1, #legacy_data do | |
57 item = legacy_data[i]; | |
58 if item.type == "jid" and item.action == "deny" then | |
59 jid = jid_prep(item.value); | |
60 if not jid then | |
61 module:log("warn", "Invalid JID in privacy store for user '%s' not migrated: %s", username, tostring(item.value)); | |
62 else | |
63 migrated_data[jid] = true; | |
64 end | |
65 end | |
66 end | |
67 end | |
68 set_blocklist(username, migrated_data); | |
69 return migrated_data; | |
70 end | |
71 | |
72 local function get_blocklist(username) | |
73 local blocklist = cache[username]; | |
74 if not blocklist then | |
75 if not user_exists(username, host) then | |
76 return null_blocklist; | |
77 end | |
78 blocklist = storage:get(username); | |
79 if not blocklist then | |
80 blocklist = migrate_privacy_list(username); | |
81 end | |
82 cache[username] = blocklist; | |
83 end | |
84 return blocklist; | |
85 end | |
86 | |
87 module:hook("iq-get/self/urn:xmpp:blocking:blocklist", function (event) | |
88 local origin, stanza = event.origin, event.stanza; | |
89 local username = origin.username; | |
90 local reply = st.reply(stanza):tag("blocklist", { xmlns = "urn:xmpp:blocking" }); | |
91 local blocklist = get_blocklist(username); | |
92 for jid in pairs(blocklist) do | |
93 if jid then | |
94 reply:tag("item", { jid = jid }):up(); | |
95 end | |
96 end | |
97 origin.interested_blocklist = true; -- Gets notified about changes | |
98 return origin.send(reply); | |
99 end); | |
100 | |
101 -- Add or remove a bare jid from the blocklist | |
102 -- We want this to be atomic and not do a partial update | |
103 local function edit_blocklist(event) | |
104 local origin, stanza = event.origin, event.stanza; | |
105 local username = origin.username; | |
106 local act = stanza.tags[1]; | |
107 local new = {}; | |
108 | |
109 local jid; | |
110 for item in act:childtags("item") do | |
111 jid = jid_prep(item.attr.jid); | |
112 if not jid then | |
113 return origin.send(st_error_reply(stanza, "modify", "jid-malformed")); | |
114 end | |
115 item.attr.jid = jid; -- echo back prepped | |
116 new[jid] = is_contact_subscribed(username, host, jid) or false; | |
117 end | |
118 | |
119 local mode = act.name == "block" or nil; | |
120 | |
121 if mode and not next(new) then | |
122 -- <block/> element does not contain at least one <item/> child element | |
123 return origin.send(st_error_reply(stanza, "modify", "bad-request")); | |
124 end | |
125 | |
126 local blocklist = get_blocklist(username); | |
127 | |
128 local new_blocklist = {}; | |
129 | |
130 if mode and next(new) then | |
131 for jid in pairs(blocklist) do | |
132 new_blocklist[jid] = true; | |
133 end | |
134 for jid in pairs(new) do | |
135 new_blocklist[jid] = mode; | |
136 end | |
137 -- else empty the blocklist | |
138 end | |
139 new_blocklist[false] = "not empty"; -- In order to avoid doing the migration thing twice | |
140 | |
141 local ok, err = set_blocklist(username, new_blocklist); | |
142 if ok then | |
143 origin.send(st.reply(stanza)); | |
144 else | |
145 return origin.send(st_error_reply(stanza, "wait", "internal-server-error", err)); | |
146 end | |
147 | |
148 if mode then | |
149 for jid, in_roster in pairs(new) do | |
150 if not blocklist[jid] and in_roster and sessions[username] then | |
151 for _, session in pairs(sessions[username].sessions) do | |
152 module:send(st.presence({ type = "unavailable", to = jid, from = session.full_jid })); | |
153 end | |
154 end | |
155 end | |
156 end | |
157 if sessions[username] then | |
158 local blocklist_push = st.iq({ type = "set", id = "blocklist-push" }) | |
159 :add_child(act); -- I am lazy | |
160 | |
161 for _, session in pairs(sessions[username].sessions) do | |
162 if session.interested_blocklist then | |
163 blocklist_push.attr.to = session.full_jid; | |
164 session.send(blocklist_push); | |
165 end | |
166 end | |
167 end | |
168 | |
169 return true; | |
170 end | |
171 | |
172 module:hook("iq-set/self/urn:xmpp:blocking:block", edit_blocklist); | |
173 module:hook("iq-set/self/urn:xmpp:blocking:unblock", edit_blocklist); | |
174 | |
175 -- Cache invalidation, solved! | |
176 module:hook_global("user-deleted", function (event) | |
177 if event.host == host then | |
178 cache[event.username] = nil; | |
179 end | |
180 end); | |
181 | |
182 -- Buggy clients | |
183 module:hook("iq-error/self/blocklist-push", function (event) | |
184 local type, condition, text = event.stanza:get_error(); | |
185 (event.origin.log or module._log)("warn", "client returned an error in response to notification from mod_%s: %s%s%s", module.name, condition, text and ": " or "", text or ""); | |
186 return true; | |
187 end); | |
188 | |
189 local function is_blocked(user, jid) | |
190 local blocklist = cache[user] or get_blocklist(user); | |
191 if blocklist[jid] then return true; end | |
192 local node, host = jid_split(jid); | |
193 return blocklist[host] or node and blocklist[node..'@'..host]; | |
194 end | |
195 | |
196 -- Event handlers for bouncing or dropping stanzas | |
197 local function drop_stanza(event) | |
198 local stanza = event.stanza; | |
199 local attr = stanza.attr; | |
200 local to, from = attr.to, attr.from; | |
201 to = to and jid_split(to); | |
202 if to and from then | |
203 return is_blocked(to, from); | |
204 end | |
205 end | |
206 | |
207 local function bounce_stanza(event) | |
208 local origin, stanza = event.origin, event.stanza; | |
209 if drop_stanza(event) then | |
210 return origin.send(st_error_reply(stanza, "cancel", "service-unavailable")); | |
211 end | |
212 end | |
213 | |
214 local function bounce_iq(event) | |
215 local type = event.stanza.attr.type; | |
216 if type == "set" or type == "get" then | |
217 return bounce_stanza(event); | |
218 end | |
219 return drop_stanza(event); -- result or error | |
220 end | |
221 | |
222 local function bounce_message(event) | |
223 local type = event.stanza.attr.type; | |
224 if type == "chat" or not type or type == "normal" then | |
225 return bounce_stanza(event); | |
226 end | |
227 return drop_stanza(event); -- drop headlines, groupchats etc | |
228 end | |
229 | |
230 local function drop_outgoing(event) | |
231 local origin, stanza = event.origin, event.stanza; | |
232 local username = origin.username or jid_split(stanza.attr.from); | |
233 if not username then return end | |
234 local to = stanza.attr.to; | |
235 if to then return is_blocked(username, to); end | |
236 -- nil 'to' means a self event, don't bock those | |
237 end | |
238 | |
239 local function bounce_outgoing(event) | |
240 local origin, stanza = event.origin, event.stanza; | |
241 local type = stanza.attr.type; | |
242 if type == "error" or stanza.name == "iq" and type == "result" then | |
243 return drop_outgoing(event); | |
244 end | |
245 if drop_outgoing(event) then | |
246 return origin.send(st_error_reply(stanza, "cancel", "not-acceptable", "You have blocked this JID") | |
247 :tag("blocked", { xmlns = "urn:xmpp:blocking:errors" })); | |
248 end | |
249 end | |
250 | |
251 -- Hook all the events! | |
252 local prio_in, prio_out = 100, 100; | |
253 module:hook("presence/bare", drop_stanza, prio_in); | |
254 module:hook("presence/full", drop_stanza, prio_in); | |
255 | |
256 module:hook("message/bare", bounce_message, prio_in); | |
257 module:hook("message/full", bounce_message, prio_in); | |
258 | |
259 module:hook("iq/bare", bounce_iq, prio_in); | |
260 module:hook("iq/full", bounce_iq, prio_in); | |
261 | |
262 module:hook("pre-message/bare", bounce_outgoing, prio_out); | |
263 module:hook("pre-message/full", bounce_outgoing, prio_out); | |
264 module:hook("pre-message/host", bounce_outgoing, prio_out); | |
265 | |
266 module:hook("pre-presence/bare", drop_outgoing, prio_out); | |
267 module:hook("pre-presence/full", drop_outgoing, prio_out); | |
268 module:hook("pre-presence/host", drop_outgoing, prio_out); | |
269 | |
270 module:hook("pre-iq/bare", bounce_outgoing, prio_out); | |
271 module:hook("pre-iq/full", bounce_outgoing, prio_out); | |
272 module:hook("pre-iq/host", bounce_outgoing, prio_out); | |
273 |