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