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