mod_privacy: Reduced a function.
[prosody.git] / plugins / mod_privacy.lua
1 -- Prosody IM
2 -- Copyright (C) 2008-2009 Matthew Wild
3 -- Copyright (C) 2008-2009 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 local prosody = prosody;
11 local st = require "util.stanza";
12 local datamanager = require "util.datamanager";
13 local bare_sessions, full_sessions = bare_sessions, full_sessions;
14 local util_Jid = require "util.jid";
15 local jid_bare = util_Jid.bare;
16 local jid_split = util_Jid.split;
17 local load_roster = require "core.rostermanager".load_roster;
18 local to_number = tonumber;
19
20 function findNamedList (privacy_lists, name)
21         local ret = nil
22         if privacy_lists.lists == nil then
23                 return nil;
24         end
25
26         for i=1, #privacy_lists.lists do
27                 if privacy_lists.lists[i].name == name then
28                         ret = i;
29                         break;
30                 end
31         end
32         return ret;
33 end
34
35 function isListUsed(origin, name, privacy_lists)        
36         if bare_sessions[origin.username.."@"..origin.host].sessions ~= nil then
37                 for resource, session in pairs(bare_sessions[origin.username.."@"..origin.host].sessions) do
38                         if resource ~= origin.resource then
39                                 if session.activePrivacyList == name then
40                                         return true;
41                                 elseif session.activePrivacyList == nil and privacy_lists.default == name then
42                                         return true;
43                                 end
44                         end
45                 end
46         end
47         return false;
48 end
49
50 function isAnotherSessionUsingDefaultList(origin)
51         local ret = false
52         if bare_sessions[origin.username.."@"..origin.host].sessions ~= nil then
53                 for resource, session in pairs(bare_sessions[origin.username.."@"..origin.host].sessions) do
54                         if resource ~= origin.resource and session.activePrivacyList == nil then
55                                 ret = true;
56                                 break;
57                         end
58                 end
59         end
60         return ret;
61 end
62
63 function sendUnavailable(origin, to, from)
64 --[[ example unavailable presence stanza
65 <presence from="node@host/resource" type="unavailable" to="node@host" >
66         <status>Logged out</status>
67 </presence>
68 ]]--
69         local presence = st.presence({from=from, type="unavailable"})
70         presence:tag("status"):text("Logged out");
71
72         local node, host = jid_bare(to);
73         local bare = node .. "@" .. host;
74         
75         if bare_sessions[bare].sessions ~= nil then
76                 for resource, session in pairs(bare_sessions[bare].sessions) do
77                         presence.attr.to = session.full_jid;
78                         module:log("debug", "send unavailable to: %s; from: %s", tostring(presence.attr.to), tostring(presence.attr.from));
79                         origin.send(presence);
80                 end
81         end
82 end
83
84 function sendNeededUnavailablePersences(origin, listnameOrItem) -- TODO implement it correctly!
85         if type(listnameOrItem) == "string" then
86                 local listname = listnameOrItem;
87                 for _,list in ipairs(privacy_lists.lists) do
88                         if list.name == listname then
89                                 for _,item in ipairs(list.items) do
90                                         sendNeededUnavailablePersences(origin, item);
91                                 end
92                         end
93                 end
94         elseif type(listnameOrItem) == "table" then
95                 module:log("debug", "got an item, check wether to send unavailable presence stanza or not");
96                 local item = listnameOrItem;
97                 local serialize = require "util.serialization".serialize;
98
99
100                 if item["presence-out"] == true then
101                         if item.type == "jid" then
102                                 sendUnavailable(origin, item.value, origin.full_jid);
103                         elseif item.type == "group" then
104                         elseif item.type == "subscription" then
105                         elseif item.type == nil then
106                         end
107                 elseif item["presence-in"] == true then
108                         if item.type == "jid" then
109                                 sendUnavailable(origin, origin.full_jid, item.value);
110                         elseif item.type == "group" then
111                         elseif item.type == "subscription" then
112                         elseif item.type == nil then
113                         end             
114                 end
115         else
116                 module:log("debug", "got unknown type: %s", type(listnameOrItem));
117         end
118 end
119
120 function declineList (privacy_lists, origin, stanza, which)
121         if which == "default" then
122                 if isAnotherSessionUsingDefaultList(origin) then
123                         return { "cancel", "conflict", "Another session is online and using the default list."};
124                 end
125                 privacy_lists.default = nil;
126                 origin.send(st.reply(stanza));
127         elseif which == "active" then
128                 origin.activePrivacyList = nil;
129                 origin.send(st.reply(stanza));
130         else
131                 return {"modify", "bad-request", "Neither default nor active list specifed to decline."};
132         end
133         return true;
134 end
135
136 function activateList (privacy_lists, origin, stanza, which, name)
137         local idx = findNamedList(privacy_lists, name);
138
139         if privacy_lists.default == nil then
140                 privacy_lists.default = "";
141         end
142         if origin.activePrivacyList == nil then
143                 origin.activePrivacyList = "";
144         end
145         
146         if which == "default" and idx ~= nil then
147                 if isAnotherSessionUsingDefaultList(origin) then
148                         return {"cancel", "conflict", "Another session is online and using the default list."};
149                 end
150                 privacy_lists.default = name;
151                 origin.send(st.reply(stanza));
152 --[[
153                 if origin.activePrivacyList == nil then
154                         sendNeededUnavailablePersences(origin, name);
155                 end
156 ]]--
157         elseif which == "active" and idx ~= nil then
158                 origin.activePrivacyList = name;
159                 origin.send(st.reply(stanza));
160                 -- sendNeededUnavailablePersences(origin, name);
161         else
162                 return {"modify", "bad-request", "Either not active or default given or unknown list name specified."};
163         end
164         return true;
165 end
166
167 function deleteList (privacy_lists, origin, stanza, name)
168         local idx = findNamedList(privacy_lists, name);
169
170         if idx ~= nil then
171                 if isListUsed(origin, name, privacy_lists) then
172                         return {"cancel", "conflict", "Another session is online and using the list which should be deleted."};
173                 end
174                 if privacy_lists.default == name then
175                         privacy_lists.default = "";
176                 end
177                 if origin.activePrivacyList == name then
178                         origin.activePrivacyList = "";
179                 end
180                 table.remove(privacy_lists.lists, idx);
181                 origin.send(st.reply(stanza));
182                 return true;
183         end
184         return {"modify", "bad-request", "Not existing list specifed to be deleted."};
185 end
186
187 function createOrReplaceList (privacy_lists, origin, stanza, name, entries, roster)
188         local idx = findNamedList(privacy_lists, name);
189         local bare_jid = origin.username.."@"..origin.host;
190         
191         if privacy_lists.lists == nil then
192                 privacy_lists.lists = {};
193         end
194
195         if idx == nil then
196                 idx = #privacy_lists.lists + 1;
197         end
198
199         local orderCheck = {};
200         local list = {};
201         list.name = name;
202         list.items = {};
203
204         for _,item in ipairs(entries) do
205                 if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then
206                         return {"modify", "bad-request", "Order attribute not valid."};
207                 end
208                 
209                 if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then
210                         return {"modify", "bad-request", "Type attribute not valid."};
211                 end 
212                 
213                 local tmp = {};
214                 orderCheck[item.attr.order] = true;
215                 
216                 tmp["type"] = item.attr.type;
217                 tmp["value"] = item.attr.value;
218                 tmp["action"] = item.attr.action;
219                 tmp["order"] = to_number(item.attr.order);
220                 tmp["presence-in"] = false;
221                 tmp["presence-out"] = false;
222                 tmp["message"] = false;
223                 tmp["iq"] = false;
224                 
225                 if #item.tags > 0 then
226                         for _,tag in ipairs(item.tags) do
227                                 tmp[tag.name] = true;
228                         end
229                 end
230                 
231                 if tmp.type == "group" then
232                         local found = false;
233                         local roster = load_roster(origin.username, origin.host);
234                         for jid,item in pairs(roster) do
235                                 if item.groups ~= nil then
236                                         for group in pairs(item.groups) do
237                                                 if group == tmp.value then
238                                                         found = true;
239                                                         break;
240                                                 end
241                                         end
242                                         if found == true then
243                                                 break;
244                                         end
245                                 end
246                         end
247                         if found == false then
248                                 return {"cancel", "item-not-found", "Specifed roster group not existing."};
249                         end
250                 elseif tmp.type == "subscription" then
251                         if      tmp.value ~= "both" and
252                                 tmp.value ~= "to" and
253                                 tmp.value ~= "from" and
254                                 tmp.value ~= "none" then
255                                 return {"cancel", "bad-request", "Subscription value must be both, to, from or none."};
256                         end
257                 end
258                 
259                 if tmp.action ~= "deny" and tmp.action ~= "allow" then
260                         return {"cancel", "bad-request", "Action must be either deny or allow."};
261                 end
262                 
263 --[[            
264                 if (privacy_lists.default == name and origin.activePrivacyList == nil) or origin.activePrivacyList == name then
265                         module:log("debug", "calling sendNeededUnavailablePresences!");
266                         -- item is valid and list is active, so send needed unavailable stanzas
267                         sendNeededUnavailablePersences(origin, tmp);
268                 end
269 ]]--
270                 list.items[#list.items + 1] = tmp;
271         end
272         
273         table.sort(list, function(a, b) return a.order < b.order; end);
274
275         privacy_lists.lists[idx] = list;
276         origin.send(st.reply(stanza));
277         if bare_sessions[bare_jid] ~= nil then
278                 local iq = st.iq ( { type = "set", id="push1" } );
279                 iq:tag ("query", { xmlns = "jabber:iq:privacy" } );
280                 iq:tag ("list", { name = list.name } ):up();
281                 iq:up();
282                 for resource, session in pairs(bare_sessions[bare_jid].sessions) do
283                         iq.attr.to = bare_jid.."/"..resource
284                         session.send(iq);
285                 end
286         else
287                 return {"cancel", "bad-request", "internal error."};
288         end
289         return true;
290 end
291
292 function getList(privacy_lists, origin, stanza, name)
293         local reply = st.reply(stanza);
294         reply:tag("query", {xmlns="jabber:iq:privacy"});
295
296         if name == nil then
297                 reply:tag("active", {name=origin.activePrivacyList or ""}):up();
298                 reply:tag("default", {name=privacy_lists.default or ""}):up();
299                 if privacy_lists.lists then
300                         for _,list in ipairs(privacy_lists.lists) do
301                                 reply:tag("list", {name=list.name}):up();
302                         end
303                 end
304         else
305                 local idx = findNamedList(privacy_lists, name);
306                 if idx ~= nil then
307                         local list = privacy_lists.lists[idx];
308                         reply = reply:tag("list", {name=list.name});
309                         for _,item in ipairs(list.items) do
310                                 reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order});
311                                 if item["message"] then reply:tag("message"):up(); end
312                                 if item["iq"] then reply:tag("iq"):up(); end
313                                 if item["presence-in"] then reply:tag("presence-in"):up(); end
314                                 if item["presence-out"] then reply:tag("presence-out"):up(); end
315                                 reply:up();
316                         end
317                 else
318                         return {"cancel", "item-not-found", "Unknown list specified."};
319                 end
320         end
321         
322         origin.send(reply);
323         return true;
324 end
325
326 module:hook("iq/bare/jabber:iq:privacy:query", function(data)
327         local origin, stanza = data.origin, data.stanza;
328         
329         if stanza.attr.to == nil then -- only service requests to own bare JID
330                 local query = stanza.tags[1]; -- the query element
331                 local valid = false;
332                 local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or {};
333
334                 if stanza.attr.type == "set" then
335                         if #query.tags == 1 then --  the <query/> element MUST NOT include more than one child element 
336                                 for _,tag in ipairs(query.tags) do
337                                         if tag.name == "active" or tag.name == "default" then
338                                                 if tag.attr.name == nil then -- Client declines the use of active / default list
339                                                         valid = declineList(privacy_lists, origin, stanza, tag.name);
340                                                 else -- Client requests change of active / default list
341                                                         valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name);
342                                                 end
343                                         elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list
344                                                 if #tag.tags == 0 then -- Client removes a privacy list
345                                                         valid = deleteList(privacy_lists, origin, stanza, tag.attr.name);
346                                                 else -- Client edits a privacy list
347                                                         valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags);
348                                                 end
349                                         end
350                                 end
351                         end
352                 elseif stanza.attr.type == "get" then
353                         local name = nil;
354                         local listsToRetrieve = 0;
355                         if #query.tags >= 1 then
356                                 for _,tag in ipairs(query.tags) do
357                                         if tag.name == "list" then -- Client requests a privacy list from server
358                                                 name = tag.attr.name;
359                                                 listsToRetrieve = listsToRetrieve + 1;
360                                         end
361                                 end
362                         end
363                         if listsToRetrieve == 0 or listsToRetrieve == 1 then
364                                 valid = getList(privacy_lists, origin, stanza, name);
365                         end
366                 end
367
368                 if valid ~= true then
369                         if valid[0] == nil then
370                                 valid[0] = "cancel";
371                         end
372                         if valid[1] == nil then
373                                 valid[1] = "bad-request";
374                         end
375                         origin.send(st.error_reply(stanza, valid[0], valid[1], valid[2]));
376                 else
377                         datamanager.store(origin.username, origin.host, "privacy", privacy_lists);
378                 end
379                 return true;
380         end
381         return false;
382 end, 500);
383
384 function checkIfNeedToBeBlocked(e, session)
385         local origin, stanza = e.origin, e.stanza;
386         local privacy_lists = datamanager.load(session.username, session.host, "privacy") or {};
387         local bare_jid = session.username.."@"..session.host;
388
389         module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(stanza.attr.to), tostring(stanza.attr.from));
390         
391         if stanza.attr.to ~= nil and stanza.attr.from ~= nil then
392                 if privacy_lists.lists == nil or
393                    (session.activePrivacyList == nil or session.activePrivacyList == "") and
394                    (privacy_lists.default == nil     or privacy_lists.default == "")
395                 then 
396                         return; -- Nothing to block, default is Allow all
397                 end
398             if jid_bare(stanza.attr.from) == bare_jid and jid_bare(stanza.attr.to) == bare_jid then
399             module:log("debug", "Never block communications from one of a user's resources to another.");
400             return; -- from one of a user's resource to another => HANDS OFF!
401         end 
402
403                 local idx;
404                 local list;
405                 local item;
406                 local listname = session.activePrivacyList;
407                 if listname == nil or listname == "" then
408                         listname = privacy_lists.default; -- no active list selected, use default list
409                 end
410                 idx = findNamedList(privacy_lists, listname);
411                 if idx == nil then
412                         module:log("error", "given privacy listname not found. name: %s", listname);
413                         return;
414                 end
415                 list = privacy_lists.lists[idx];
416                 if list == nil then
417                         module:log("info", "privacy list index wrong. index: %d", idx);
418                         return;
419                 end
420                 for _,item in ipairs(list.items) do
421                         local apply = false;
422                         local block = false;
423                         if (
424                                 (stanza.name == "message" and item.message) or
425                                 (stanza.name == "iq" and item.iq) or
426                                 (stanza.name == "presence" and jid_bare(stanza.attr.to) == bare_jid and item["presence-in"]) or
427                                 (stanza.name == "presence" and jid_bare(stanza.attr.from) == bare_jid and item["presence-out"]) or
428                                 (item.message == false and item.iq == false and item["presence-in"] == false and item["presence-in"] == false)
429                         ) then
430                                 apply = true;
431                         end
432                         if apply then
433                                 local evilJid = {};
434                                 apply = false;
435                                 if jid_bare(stanza.attr.to) == bare_jid then
436                                         module:log("debug", "evil jid is (from): %s", stanza.attr.from);
437                                         evilJid.node, evilJid.host, evilJid.resource = jid_split(stanza.attr.from);
438                                 else
439                                         module:log("debug", "evil jid is (to): %s", stanza.attr.to);
440                                         evilJid.node, evilJid.host, evilJid.resource = jid_split(stanza.attr.to);
441                                 end
442                                 if      item.type == "jid" and 
443                                         (evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or
444                                         (evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or
445                                         (evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or
446                                         (evilJid.host and item.value == evilJid.host) then
447                                         apply = true;
448                                         block = (item.action == "deny");
449                                 elseif item.type == "group" then
450                                         local roster = load_roster(session.username, session.host);
451                                         local groups = roster[evilJid.node .. "@" .. evilJid.host].groups;
452                                         for group in pairs(groups) do
453                                                 if group == item.value then
454                                                         apply = true;
455                                                         block = (item.action == "deny");
456                                                         break;
457                                                 end
458                                         end
459                                 elseif item.type == "subscription" and evilJid.node ~= nil and evilJid.host ~= nil then -- we need a valid bare evil jid
460                                         local roster = load_roster(session.username, session.host);
461                                         if roster[evilJid.node .. "@" .. evilJid.host].subscription == item.value then
462                                                 apply = true;
463                                                 block = (item.action == "deny");
464                                         end
465                                 elseif item.type == nil then
466                                         apply = true;
467                                         block = (item.action == "deny");
468                                 end
469                         end
470                         if apply then
471                                 if block then
472                                         module:log("info", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(stanza.attr.to), tostring(stanza.attr.from));
473                                         if stanza.name == "message" then
474                                                 origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
475                                         elseif stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then
476                                                 origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
477                                         end
478                                         return true; -- stanza blocked !
479                                 else
480                                         module:log("info", "stanza explicit allowed!")
481                                         return;
482                                 end
483                         end
484                 end
485         end
486         return;
487 end
488
489 function preCheckIncoming(e)
490         local session;
491         if e.stanza.attr.to ~= nil then
492                 local node, host, resource = jid_split(e.stanza.attr.to);
493                 if node == nil or host == nil then
494                         return;
495                 end
496                 if resource == nil then
497                         local prio = 0;
498                         local session_;
499                         if bare_sessions[node.."@"..host] ~= nil then
500                                 for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do
501                                         if session_.priority ~= nil and session_.priority > prio then
502                                                 session = session_;
503                                                 prio = session_.priority;
504                                         end
505                                 end
506                         end
507                 else
508                         session = full_sessions[node.."@"..host.."/"..resource];
509                 end
510                 if session ~= nil then
511                         return checkIfNeedToBeBlocked(e, session);
512                 else
513                         module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource))
514                 end
515         end
516         return;
517 end
518
519 function preCheckOutgoing(e)
520         local session = e.origin;
521         if e.stanza.attr.from == nil then
522                 e.stanza.attr.from = session.username .. "@" .. session.host;
523                 if session.resource ~= nil then
524                         e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource;
525                 end
526         end
527         return checkIfNeedToBeBlocked(e, session);
528 end
529
530
531 module:hook("pre-message/full", preCheckOutgoing, 500);
532 module:hook("pre-message/bare", preCheckOutgoing, 500);
533 module:hook("pre-message/host", preCheckOutgoing, 500);
534 module:hook("pre-iq/full", preCheckOutgoing, 500);
535 module:hook("pre-iq/bare", preCheckOutgoing, 500);
536 module:hook("pre-iq/host", preCheckOutgoing, 500);
537 module:hook("pre-presence/full", preCheckOutgoing, 500);
538 module:hook("pre-presence/bare", preCheckOutgoing, 500);
539 module:hook("pre-presence/host", preCheckOutgoing, 500);
540
541 module:hook("message/full", preCheckIncoming, 500);
542 module:hook("message/bare", preCheckIncoming, 500);
543 module:hook("message/host", preCheckIncoming, 500);
544 module:hook("iq/full", preCheckIncoming, 500);
545 module:hook("iq/bare", preCheckIncoming, 500);
546 module:hook("iq/host", preCheckIncoming, 500);
547 module:hook("presence/full", preCheckIncoming, 500);
548 module:hook("presence/bare", preCheckIncoming, 500);
549 module:hook("presence/host", preCheckIncoming, 500);
550
551 module:log("info", "mod_privacy loaded ...");