prosodyctl: Bump util.pposix version for API change
[prosody.git] / core / sessionmanager.lua
1 -- Prosody IM
2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
4 -- 
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
7 --
8
9 local tostring, setmetatable = tostring, setmetatable;
10 local pairs, next= pairs, next;
11
12 local hosts = hosts;
13 local full_sessions = full_sessions;
14 local bare_sessions = bare_sessions;
15
16 local logger = require "util.logger";
17 local log = logger.init("sessionmanager");
18 local rm_load_roster = require "core.rostermanager".load_roster;
19 local config_get = require "core.configmanager".get;
20 local resourceprep = require "util.encodings".stringprep.resourceprep;
21 local nodeprep = require "util.encodings".stringprep.nodeprep;
22 local uuid_generate = require "util.uuid".generate;
23
24 local initialize_filters = require "util.filters".initialize;
25 local gettime = require "socket".gettime;
26
27 local newproxy = newproxy;
28 local getmetatable = getmetatable;
29
30 module "sessionmanager"
31
32 function new_session(conn)
33         local session = { conn = conn, type = "c2s_unauthed", conntime = gettime() };
34         local filter = initialize_filters(session);
35         local w = conn.write;
36         session.send = function (t)
37                 if t.name then
38                         t = filter("stanzas/out", t);
39                 end
40                 if t then
41                         t = filter("bytes/out", tostring(t));
42                         if t then
43                                 return w(conn, t);
44                         end
45                 end
46         end
47         session.ip = conn:ip();
48         local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$");
49         session.log = logger.init(conn_name);
50                 
51         return session;
52 end
53
54 local resting_session = { -- Resting, not dead
55                 destroyed = true;
56                 type = "c2s_destroyed";
57                 close = function (session)
58                         session.log("debug", "Attempt to close already-closed session");
59                 end;
60                 filter = function (type, data) return data; end;
61         }; resting_session.__index = resting_session;
62
63 function retire_session(session)
64         local log = session.log or log;
65         for k in pairs(session) do
66                 if k ~= "log" and k ~= "id" then
67                         session[k] = nil;
68                 end
69         end
70
71         function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); return false; end
72         function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end
73         return setmetatable(session, resting_session);
74 end
75
76 function destroy_session(session, err)
77         (session.log or log)("debug", "Destroying session for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)", err and (": "..err) or "");
78         if session.destroyed then return; end
79         
80         -- Remove session/resource from user's session list
81         if session.full_jid then
82                 local host_session = hosts[session.host];
83                 
84                 -- Allow plugins to prevent session destruction
85                 if host_session.events.fire_event("pre-resource-unbind", {session=session, error=err}) then
86                         return;
87                 end
88                 
89                 host_session.sessions[session.username].sessions[session.resource] = nil;
90                 full_sessions[session.full_jid] = nil;
91                 
92                 if not next(host_session.sessions[session.username].sessions) then
93                         log("debug", "All resources of %s are now offline", session.username);
94                         host_session.sessions[session.username] = nil;
95                         bare_sessions[session.username..'@'..session.host] = nil;
96                 end
97
98                 host_session.events.fire_event("resource-unbind", {session=session, error=err});
99         end
100         
101         retire_session(session);
102 end
103
104 function make_authenticated(session, username)
105         username = nodeprep(username);
106         if not username or #username == 0 then return nil, "Invalid username"; end
107         session.username = username;
108         if session.type == "c2s_unauthed" then
109                 session.type = "c2s";
110         end
111         session.log("info", "Authenticated as %s@%s", username or "(unknown)", session.host or "(unknown)");
112         return true;
113 end
114
115 -- returns true, nil on success
116 -- returns nil, err_type, err, err_message on failure
117 function bind_resource(session, resource)
118         if not session.username then return nil, "auth", "not-authorized", "Cannot bind resource before authentication"; end
119         if session.resource then return nil, "cancel", "already-bound", "Cannot bind multiple resources on a single connection"; end
120         -- We don't support binding multiple resources
121
122         resource = resourceprep(resource);
123         resource = resource ~= "" and resource or uuid_generate();
124         --FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing
125         
126         if not hosts[session.host].sessions[session.username] then
127                 local sessions = { sessions = {} };
128                 hosts[session.host].sessions[session.username] = sessions;
129                 bare_sessions[session.username..'@'..session.host] = sessions;
130         else
131                 local sessions = hosts[session.host].sessions[session.username].sessions;
132                 if sessions[resource] then
133                         -- Resource conflict
134                         local policy = config_get(session.host, "conflict_resolve");
135                         local increment;
136                         if policy == "random" then
137                                 resource = uuid_generate();
138                                 increment = true;
139                         elseif policy == "increment" then
140                                 increment = true; -- TODO ping old resource
141                         elseif policy == "kick_new" then
142                                 return nil, "cancel", "conflict", "Resource already exists";
143                         else -- if policy == "kick_old" then
144                                 sessions[resource]:close {
145                                         condition = "conflict";
146                                         text = "Replaced by new connection";
147                                 };
148                                 if not next(sessions) then
149                                         hosts[session.host].sessions[session.username] = { sessions = sessions };
150                                         bare_sessions[session.username.."@"..session.host] = hosts[session.host].sessions[session.username];
151                                 end
152                         end
153                         if increment and sessions[resource] then
154                                 local count = 1;
155                                 while sessions[resource.."#"..count] do
156                                         count = count + 1;
157                                 end
158                                 resource = resource.."#"..count;
159                         end
160                 end
161         end
162         
163         session.resource = resource;
164         session.full_jid = session.username .. '@' .. session.host .. '/' .. resource;
165         hosts[session.host].sessions[session.username].sessions[resource] = session;
166         full_sessions[session.full_jid] = session;
167         
168         local err;
169         session.roster, err = rm_load_roster(session.username, session.host);
170         if err then
171                 full_sessions[session.full_jid] = nil;
172                 hosts[session.host].sessions[session.username].sessions[resource] = nil;
173                 session.full_jid = nil;
174                 session.resource = nil;
175                 if next(bare_sessions[session.username..'@'..session.host].sessions) == nil then
176                         bare_sessions[session.username..'@'..session.host] = nil;
177                         hosts[session.host].sessions[session.username] = nil;
178                 end
179                 session.log("error", "Roster loading failed: %s", err);
180                 return nil, "cancel", "internal-server-error", "Error loading roster";
181         end
182         
183         hosts[session.host].events.fire_event("resource-bind", {session=session});
184         
185         return true;
186 end
187
188 function send_to_available_resources(user, host, stanza)
189         local jid = user.."@"..host;
190         local count = 0;
191         local user = bare_sessions[jid];
192         if user then
193                 for k, session in pairs(user.sessions) do
194                         if session.presence then
195                                 session.send(stanza);
196                                 count = count + 1;
197                         end
198                 end
199         end
200         return count;
201 end
202
203 function send_to_interested_resources(user, host, stanza)
204         local jid = user.."@"..host;
205         local count = 0;
206         local user = bare_sessions[jid];
207         if user then
208                 for k, session in pairs(user.sessions) do
209                         if session.interested then
210                                 session.send(stanza);
211                                 count = count + 1;
212                         end
213                 end
214         end
215         return count;
216 end
217
218 return _M;