Mercurial > prosody-hg
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 |
| parents | |
| children | 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 |
