Software /
code /
prosody-modules
Comparison
mod_privacy_lists/mod_privacy_lists.lua @ 1474:d4233dce479f
mod_privacy_lists: Import mod_privacy from 0.9
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Sun, 27 Jul 2014 14:38:04 +0100 |
child | 1475:58d48afca54d |
comparison
equal
deleted
inserted
replaced
1473:31c4d92a81e5 | 1474:d4233dce479f |
---|---|
1 -- Prosody IM | |
2 -- Copyright (C) 2009-2010 Matthew Wild | |
3 -- Copyright (C) 2009-2010 Waqas Hussain | |
4 -- Copyright (C) 2009 Thilo Cestonaro | |
5 -- | |
6 -- This project is MIT/X11 licensed. Please see the | |
7 -- COPYING file in the source package for more information. | |
8 -- | |
9 | |
10 module:add_feature("jabber:iq:privacy"); | |
11 | |
12 local st = require "util.stanza"; | |
13 local bare_sessions, full_sessions = prosody.bare_sessions, prosody.full_sessions; | |
14 local util_Jid = require "util.jid"; | |
15 local jid_bare = util_Jid.bare; | |
16 local jid_split, jid_join = util_Jid.split, util_Jid.join; | |
17 local load_roster = require "core.rostermanager".load_roster; | |
18 local to_number = tonumber; | |
19 | |
20 local privacy_storage = module:open_store(); | |
21 | |
22 function isListUsed(origin, name, privacy_lists) | |
23 local user = bare_sessions[origin.username.."@"..origin.host]; | |
24 if user then | |
25 for resource, session in pairs(user.sessions) do | |
26 if resource ~= origin.resource then | |
27 if session.activePrivacyList == name then | |
28 return true; | |
29 elseif session.activePrivacyList == nil and privacy_lists.default == name then | |
30 return true; | |
31 end | |
32 end | |
33 end | |
34 end | |
35 end | |
36 | |
37 function isAnotherSessionUsingDefaultList(origin) | |
38 local user = bare_sessions[origin.username.."@"..origin.host]; | |
39 if user then | |
40 for resource, session in pairs(user.sessions) do | |
41 if resource ~= origin.resource and session.activePrivacyList == nil then | |
42 return true; | |
43 end | |
44 end | |
45 end | |
46 end | |
47 | |
48 function declineList(privacy_lists, origin, stanza, which) | |
49 if which == "default" then | |
50 if isAnotherSessionUsingDefaultList(origin) then | |
51 return { "cancel", "conflict", "Another session is online and using the default list."}; | |
52 end | |
53 privacy_lists.default = nil; | |
54 origin.send(st.reply(stanza)); | |
55 elseif which == "active" then | |
56 origin.activePrivacyList = nil; | |
57 origin.send(st.reply(stanza)); | |
58 else | |
59 return {"modify", "bad-request", "Neither default nor active list specifed to decline."}; | |
60 end | |
61 return true; | |
62 end | |
63 | |
64 function activateList(privacy_lists, origin, stanza, which, name) | |
65 local list = privacy_lists.lists[name]; | |
66 | |
67 if which == "default" and list then | |
68 if isAnotherSessionUsingDefaultList(origin) then | |
69 return {"cancel", "conflict", "Another session is online and using the default list."}; | |
70 end | |
71 privacy_lists.default = name; | |
72 origin.send(st.reply(stanza)); | |
73 elseif which == "active" and list then | |
74 origin.activePrivacyList = name; | |
75 origin.send(st.reply(stanza)); | |
76 elseif not list then | |
77 return {"cancel", "item-not-found", "No such list: "..name}; | |
78 else | |
79 return {"modify", "bad-request", "No list chosen to be active or default."}; | |
80 end | |
81 return true; | |
82 end | |
83 | |
84 function deleteList(privacy_lists, origin, stanza, name) | |
85 local list = privacy_lists.lists[name]; | |
86 | |
87 if list then | |
88 if isListUsed(origin, name, privacy_lists) then | |
89 return {"cancel", "conflict", "Another session is online and using the list which should be deleted."}; | |
90 end | |
91 if privacy_lists.default == name then | |
92 privacy_lists.default = nil; | |
93 end | |
94 if origin.activePrivacyList == name then | |
95 origin.activePrivacyList = nil; | |
96 end | |
97 privacy_lists.lists[name] = nil; | |
98 origin.send(st.reply(stanza)); | |
99 return true; | |
100 end | |
101 return {"modify", "bad-request", "Not existing list specifed to be deleted."}; | |
102 end | |
103 | |
104 function createOrReplaceList (privacy_lists, origin, stanza, name, entries) | |
105 local bare_jid = origin.username.."@"..origin.host; | |
106 | |
107 if privacy_lists.lists == nil then | |
108 privacy_lists.lists = {}; | |
109 end | |
110 | |
111 local list = {}; | |
112 privacy_lists.lists[name] = list; | |
113 | |
114 local orderCheck = {}; | |
115 list.name = name; | |
116 list.items = {}; | |
117 | |
118 for _,item in ipairs(entries) do | |
119 if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then | |
120 return {"modify", "bad-request", "Order attribute not valid."}; | |
121 end | |
122 | |
123 if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then | |
124 return {"modify", "bad-request", "Type attribute not valid."}; | |
125 end | |
126 | |
127 local tmp = {}; | |
128 orderCheck[item.attr.order] = true; | |
129 | |
130 tmp["type"] = item.attr.type; | |
131 tmp["value"] = item.attr.value; | |
132 tmp["action"] = item.attr.action; | |
133 tmp["order"] = to_number(item.attr.order); | |
134 tmp["presence-in"] = false; | |
135 tmp["presence-out"] = false; | |
136 tmp["message"] = false; | |
137 tmp["iq"] = false; | |
138 | |
139 if #item.tags > 0 then | |
140 for _,tag in ipairs(item.tags) do | |
141 tmp[tag.name] = true; | |
142 end | |
143 end | |
144 | |
145 if tmp.type == "subscription" then | |
146 if tmp.value ~= "both" and | |
147 tmp.value ~= "to" and | |
148 tmp.value ~= "from" and | |
149 tmp.value ~= "none" then | |
150 return {"cancel", "bad-request", "Subscription value must be both, to, from or none."}; | |
151 end | |
152 end | |
153 | |
154 if tmp.action ~= "deny" and tmp.action ~= "allow" then | |
155 return {"cancel", "bad-request", "Action must be either deny or allow."}; | |
156 end | |
157 list.items[#list.items + 1] = tmp; | |
158 end | |
159 | |
160 table.sort(list, function(a, b) return a.order < b.order; end); | |
161 | |
162 origin.send(st.reply(stanza)); | |
163 if bare_sessions[bare_jid] ~= nil then | |
164 local iq = st.iq ( { type = "set", id="push1" } ); | |
165 iq:tag ("query", { xmlns = "jabber:iq:privacy" } ); | |
166 iq:tag ("list", { name = list.name } ):up(); | |
167 iq:up(); | |
168 for resource, session in pairs(bare_sessions[bare_jid].sessions) do | |
169 iq.attr.to = bare_jid.."/"..resource | |
170 session.send(iq); | |
171 end | |
172 else | |
173 return {"cancel", "bad-request", "internal error."}; | |
174 end | |
175 return true; | |
176 end | |
177 | |
178 function getList(privacy_lists, origin, stanza, name) | |
179 local reply = st.reply(stanza); | |
180 reply:tag("query", {xmlns="jabber:iq:privacy"}); | |
181 | |
182 if name == nil then | |
183 if privacy_lists.lists then | |
184 if origin.activePrivacyList then | |
185 reply:tag("active", {name=origin.activePrivacyList}):up(); | |
186 end | |
187 if privacy_lists.default then | |
188 reply:tag("default", {name=privacy_lists.default}):up(); | |
189 end | |
190 for name,list in pairs(privacy_lists.lists) do | |
191 reply:tag("list", {name=name}):up(); | |
192 end | |
193 end | |
194 else | |
195 local list = privacy_lists.lists[name]; | |
196 if list then | |
197 reply = reply:tag("list", {name=list.name}); | |
198 for _,item in ipairs(list.items) do | |
199 reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order}); | |
200 if item["message"] then reply:tag("message"):up(); end | |
201 if item["iq"] then reply:tag("iq"):up(); end | |
202 if item["presence-in"] then reply:tag("presence-in"):up(); end | |
203 if item["presence-out"] then reply:tag("presence-out"):up(); end | |
204 reply:up(); | |
205 end | |
206 else | |
207 return {"cancel", "item-not-found", "Unknown list specified."}; | |
208 end | |
209 end | |
210 | |
211 origin.send(reply); | |
212 return true; | |
213 end | |
214 | |
215 module:hook("iq/bare/jabber:iq:privacy:query", function(data) | |
216 local origin, stanza = data.origin, data.stanza; | |
217 | |
218 if stanza.attr.to == nil then -- only service requests to own bare JID | |
219 local query = stanza.tags[1]; -- the query element | |
220 local valid = false; | |
221 local privacy_lists = privacy_storage:get(origin.username) or { lists = {} }; | |
222 | |
223 if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8 | |
224 module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host); | |
225 local lists = privacy_lists.lists; | |
226 for idx, list in ipairs(lists) do | |
227 lists[list.name] = list; | |
228 lists[idx] = nil; | |
229 end | |
230 end | |
231 | |
232 if stanza.attr.type == "set" then | |
233 if #query.tags == 1 then -- the <query/> element MUST NOT include more than one child element | |
234 for _,tag in ipairs(query.tags) do | |
235 if tag.name == "active" or tag.name == "default" then | |
236 if tag.attr.name == nil then -- Client declines the use of active / default list | |
237 valid = declineList(privacy_lists, origin, stanza, tag.name); | |
238 else -- Client requests change of active / default list | |
239 valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name); | |
240 end | |
241 elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list | |
242 if #tag.tags == 0 then -- Client removes a privacy list | |
243 valid = deleteList(privacy_lists, origin, stanza, tag.attr.name); | |
244 else -- Client edits a privacy list | |
245 valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags); | |
246 end | |
247 end | |
248 end | |
249 end | |
250 elseif stanza.attr.type == "get" then | |
251 local name = nil; | |
252 local listsToRetrieve = 0; | |
253 if #query.tags >= 1 then | |
254 for _,tag in ipairs(query.tags) do | |
255 if tag.name == "list" then -- Client requests a privacy list from server | |
256 name = tag.attr.name; | |
257 listsToRetrieve = listsToRetrieve + 1; | |
258 end | |
259 end | |
260 end | |
261 if listsToRetrieve == 0 or listsToRetrieve == 1 then | |
262 valid = getList(privacy_lists, origin, stanza, name); | |
263 end | |
264 end | |
265 | |
266 if valid ~= true then | |
267 valid = valid or { "cancel", "bad-request", "Couldn't understand request" }; | |
268 if valid[1] == nil then | |
269 valid[1] = "cancel"; | |
270 end | |
271 if valid[2] == nil then | |
272 valid[2] = "bad-request"; | |
273 end | |
274 origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3])); | |
275 else | |
276 privacy_storage:set(origin.username, privacy_lists); | |
277 end | |
278 return true; | |
279 end | |
280 end); | |
281 | |
282 function checkIfNeedToBeBlocked(e, session) | |
283 local origin, stanza = e.origin, e.stanza; | |
284 local privacy_lists = privacy_storage:get(session.username) or {}; | |
285 local bare_jid = session.username.."@"..session.host; | |
286 local to = stanza.attr.to or bare_jid; | |
287 local from = stanza.attr.from; | |
288 | |
289 local is_to_user = bare_jid == jid_bare(to); | |
290 local is_from_user = bare_jid == jid_bare(from); | |
291 | |
292 --module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from)); | |
293 | |
294 if privacy_lists.lists == nil or | |
295 not (session.activePrivacyList or privacy_lists.default) | |
296 then | |
297 return; -- Nothing to block, default is Allow all | |
298 end | |
299 if is_from_user and is_to_user then | |
300 --module:log("debug", "Not blocking communications between user's resources"); | |
301 return; -- from one of a user's resource to another => HANDS OFF! | |
302 end | |
303 | |
304 local listname = session.activePrivacyList; | |
305 if listname == nil then | |
306 listname = privacy_lists.default; -- no active list selected, use default list | |
307 end | |
308 local list = privacy_lists.lists[listname]; | |
309 if not list then -- should never happen | |
310 module:log("warn", "given privacy list not found. name: %s for user %s", listname, bare_jid); | |
311 return; | |
312 end | |
313 for _,item in ipairs(list.items) do | |
314 local apply = false; | |
315 local block = false; | |
316 if ( | |
317 (stanza.name == "message" and item.message) or | |
318 (stanza.name == "iq" and item.iq) or | |
319 (stanza.name == "presence" and is_to_user and item["presence-in"]) or | |
320 (stanza.name == "presence" and is_from_user and item["presence-out"]) or | |
321 (item.message == false and item.iq == false and item["presence-in"] == false and item["presence-out"] == false) | |
322 ) then | |
323 apply = true; | |
324 end | |
325 if apply then | |
326 local evilJid = {}; | |
327 apply = false; | |
328 if is_to_user then | |
329 --module:log("debug", "evil jid is (from): %s", from); | |
330 evilJid.node, evilJid.host, evilJid.resource = jid_split(from); | |
331 else | |
332 --module:log("debug", "evil jid is (to): %s", to); | |
333 evilJid.node, evilJid.host, evilJid.resource = jid_split(to); | |
334 end | |
335 if item.type == "jid" and | |
336 (evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or | |
337 (evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or | |
338 (evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or | |
339 (evilJid.host and item.value == evilJid.host) then | |
340 apply = true; | |
341 block = (item.action == "deny"); | |
342 elseif item.type == "group" then | |
343 local roster = load_roster(session.username, session.host); | |
344 local roster_entry = roster[jid_join(evilJid.node, evilJid.host)]; | |
345 if roster_entry then | |
346 local groups = roster_entry.groups; | |
347 for group in pairs(groups) do | |
348 if group == item.value then | |
349 apply = true; | |
350 block = (item.action == "deny"); | |
351 break; | |
352 end | |
353 end | |
354 end | |
355 elseif item.type == "subscription" then -- we need a valid bare evil jid | |
356 local roster = load_roster(session.username, session.host); | |
357 local roster_entry = roster[jid_join(evilJid.node, evilJid.host)]; | |
358 if (not(roster_entry) and item.value == "none") | |
359 or (roster_entry and roster_entry.subscription == item.value) then | |
360 apply = true; | |
361 block = (item.action == "deny"); | |
362 end | |
363 elseif item.type == nil then | |
364 apply = true; | |
365 block = (item.action == "deny"); | |
366 end | |
367 end | |
368 if apply then | |
369 if block then | |
370 -- drop and not bounce groupchat messages, otherwise users will get kicked | |
371 if stanza.attr.type == "groupchat" then | |
372 return true; | |
373 end | |
374 module:log("debug", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from)); | |
375 if stanza.name == "message" then | |
376 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
377 elseif stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then | |
378 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
379 end | |
380 return true; -- stanza blocked ! | |
381 else | |
382 --module:log("debug", "stanza explicitly allowed!") | |
383 return; | |
384 end | |
385 end | |
386 end | |
387 end | |
388 | |
389 function preCheckIncoming(e) | |
390 local session; | |
391 if e.stanza.attr.to ~= nil then | |
392 local node, host, resource = jid_split(e.stanza.attr.to); | |
393 if node == nil or host == nil then | |
394 return; | |
395 end | |
396 if resource == nil then | |
397 local prio = 0; | |
398 if bare_sessions[node.."@"..host] ~= nil then | |
399 for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do | |
400 if session_.priority ~= nil and session_.priority > prio then | |
401 session = session_; | |
402 prio = session_.priority; | |
403 end | |
404 end | |
405 end | |
406 else | |
407 session = full_sessions[node.."@"..host.."/"..resource]; | |
408 end | |
409 if session ~= nil then | |
410 return checkIfNeedToBeBlocked(e, session); | |
411 else | |
412 --module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource)); | |
413 end | |
414 end | |
415 end | |
416 | |
417 function preCheckOutgoing(e) | |
418 local session = e.origin; | |
419 if e.stanza.attr.from == nil then | |
420 e.stanza.attr.from = session.username .. "@" .. session.host; | |
421 if session.resource ~= nil then | |
422 e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource; | |
423 end | |
424 end | |
425 if session.username then -- FIXME do properly | |
426 return checkIfNeedToBeBlocked(e, session); | |
427 end | |
428 end | |
429 | |
430 module:hook("pre-message/full", preCheckOutgoing, 500); | |
431 module:hook("pre-message/bare", preCheckOutgoing, 500); | |
432 module:hook("pre-message/host", preCheckOutgoing, 500); | |
433 module:hook("pre-iq/full", preCheckOutgoing, 500); | |
434 module:hook("pre-iq/bare", preCheckOutgoing, 500); | |
435 module:hook("pre-iq/host", preCheckOutgoing, 500); | |
436 module:hook("pre-presence/full", preCheckOutgoing, 500); | |
437 module:hook("pre-presence/bare", preCheckOutgoing, 500); | |
438 module:hook("pre-presence/host", preCheckOutgoing, 500); | |
439 | |
440 module:hook("message/full", preCheckIncoming, 500); | |
441 module:hook("message/bare", preCheckIncoming, 500); | |
442 module:hook("message/host", preCheckIncoming, 500); | |
443 module:hook("iq/full", preCheckIncoming, 500); | |
444 module:hook("iq/bare", preCheckIncoming, 500); | |
445 module:hook("iq/host", preCheckIncoming, 500); | |
446 module:hook("presence/full", preCheckIncoming, 500); | |
447 module:hook("presence/bare", preCheckIncoming, 500); | |
448 module:hook("presence/host", preCheckIncoming, 500); |