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