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