1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
|
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-- luacheck: globals prosody.full_sessions prosody.bare_sessions
local tostring, setmetatable = tostring, setmetatable;
local pairs, next= pairs, next;
local prosody, hosts = prosody, prosody.hosts;
local full_sessions = prosody.full_sessions;
local bare_sessions = prosody.bare_sessions;
local logger = require "prosody.util.logger";
local log = logger.init("sessionmanager");
local rm_load_roster = require "prosody.core.rostermanager".load_roster;
local config_get = require "prosody.core.configmanager".get;
local resourceprep = require "prosody.util.encodings".stringprep.resourceprep;
local nodeprep = require "prosody.util.encodings".stringprep.nodeprep;
local generate_identifier = require "prosody.util.id".short;
local sessionlib = require "prosody.util.session";
local initialize_filters = require "prosody.util.filters".initialize;
local gettime = require "socket".gettime;
local _ENV = nil;
-- luacheck: std none
local function new_session(conn)
local session = sessionlib.new("c2s");
sessionlib.set_id(session);
sessionlib.set_logger(session);
sessionlib.set_conn(session, conn);
session.conntime = gettime();
local filter = initialize_filters(session);
local w = conn.write;
function session.rawsend(t)
t = filter("bytes/out", tostring(t));
if t then
local ret, err = w(conn, t);
if not ret then
session.log("debug", "Error writing to connection: %s", err);
return false, err;
end
end
return true;
end
session.send = function (t)
session.log("debug", "Sending[%s]: %s", session.type, t.top_tag and t:top_tag() or t:match("^[^>]*>?"));
if t.name then
t = filter("stanzas/out", t);
end
if t then
return session.rawsend(t);
end
return true;
end
session.ip = conn:ip();
local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$");
session.log = logger.init(conn_name);
return session;
end
local resting_session = { -- Resting, not dead
destroyed = true;
type = "c2s_destroyed";
close = function (session)
session.log("debug", "Attempt to close already-closed session");
end;
filter = function (type, data) return data; end; --luacheck: ignore 212/type
}; resting_session.__index = resting_session;
local function retire_session(session)
local log = session.log or log; --luacheck: ignore 431/log
for k in pairs(session) do
if k ~= "log" and k ~= "id" then
session[k] = nil;
end
end
function session.send(data) log("debug", "Discarding data sent to resting session: %s", data); return false; end
function session.rawsend(data) log("debug", "Discarding data sent to resting session: %s", data); return false; end
function session.data(data) log("debug", "Discarding data received from resting session: %s", data); end
session.thread = { run = function (_, data) return session.data(data) end };
return setmetatable(session, resting_session);
end
-- Update a session with a new one (transplanting connection, filters, etc.)
-- new_session should be discarded after this call returns
local function update_session(to_session, from_session)
to_session.log("debug", "Updating with parameters from session %s", from_session.id);
from_session.log("debug", "Session absorbed into %s", to_session.id);
local replaced_conn = to_session.conn;
if replaced_conn then
to_session.conn = nil;
end
to_session.since = from_session.since;
to_session.ip = from_session.ip;
to_session.conn = from_session.conn;
to_session.rawsend = from_session.rawsend;
to_session.rawsend.session = to_session;
to_session.rawsend.conn = to_session.conn;
to_session.send = from_session.send;
to_session.send.session = to_session;
to_session.close = from_session.close;
to_session.filter = from_session.filter;
to_session.filter.session = to_session;
to_session.filters = from_session.filters;
to_session.send.filter = to_session.filter;
to_session.sasl_handler = from_session.sasl_handler;
to_session.stream = from_session.stream;
to_session.secure = from_session.secure;
to_session.hibernating = nil;
to_session.resumption_counter = (to_session.resumption_counter or 0) + 1;
from_session.log = to_session.log;
from_session.type = to_session.type;
-- Inform xmppstream of the new session (passed to its callbacks)
to_session.stream:set_session(to_session);
-- Notify modules, allowing them to copy further fields or update state
prosody.events.fire_event("c2s-session-updated", {
session = to_session;
from_session = from_session;
replaced_conn = replaced_conn;
});
-- Retire the session we've pulled from, to avoid two sessions on the same connection
retire_session(from_session);
end
local function destroy_session(session, err)
if session.destroyed then return; end
-- Remove session/resource from user's session list
if session.full_jid then
local host_session = hosts[session.host];
-- Allow plugins to prevent session destruction
if host_session.events.fire_event("pre-resource-unbind", {session=session, error=err}) then
(session.log or log)("debug", "Resource unbind prevented by module");
return;
end
(session.log or log)("debug", "Unbinding resource for %s (%s@%s)%s",
session.full_jid or "(unknown)", session.username or "(unknown)",
session.host or "(unknown)", err and (": "..err) or "");
session.destroyed = true; -- Past this point the session is DOOMED!
host_session.sessions[session.username].sessions[session.resource] = nil;
full_sessions[session.full_jid] = nil;
if not next(host_session.sessions[session.username].sessions) then
log("debug", "All resources of %s are now offline", session.username);
host_session.sessions[session.username] = nil;
bare_sessions[session.username..'@'..session.host] = nil;
end
host_session.events.fire_event("resource-unbind", {session=session, error=err});
else
(session.log or log)("debug", "Destroying unbound session for <%s@%s>%s",
session.username or "(unknown)", session.host or "(unknown)",
err and (": "..err) or "");
end
retire_session(session);
end
local function make_authenticated(session, username, role_name)
username = nodeprep(username);
if not username or #username == 0 then return nil, "Invalid username"; end
session.username = username;
if session.type == "c2s_unauthed" then
session.type = "c2s_unbound";
end
local role;
if role_name then
role = hosts[session.host].authz.get_role_by_name(role_name);
else
role = hosts[session.host].authz.get_user_role(username);
end
if role then
sessionlib.set_role(session, role);
end
session.log("info", "Authenticated as %s@%s [%s]", username, session.host or "(unknown)", role and role.name or "no role");
return true;
end
-- returns true, nil on success
-- returns nil, err_type, err, err_message on failure
local function bind_resource(session, resource)
if not session.username then return nil, "auth", "not-authorized", "Cannot bind resource before authentication"; end
if session.resource then return nil, "cancel", "not-allowed", "Cannot bind multiple resources on a single connection"; end
-- We don't support binding multiple resources
local event_payload = { session = session, resource = resource };
if hosts[session.host].events.fire_event("pre-resource-bind", event_payload) == false then
local err = event_payload.error;
if err then return nil, err.type, err.condition, err.text; end
return nil, "cancel", "not-allowed";
else
-- In case a plugin wants to poke at it
resource = event_payload.resource;
end
resource = resourceprep(resource or "", true);
resource = resource ~= "" and resource or generate_identifier();
--FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing
if not hosts[session.host].sessions[session.username] then
local sessions = { sessions = {} };
hosts[session.host].sessions[session.username] = sessions;
bare_sessions[session.username..'@'..session.host] = sessions;
else
local sessions = hosts[session.host].sessions[session.username].sessions;
if sessions[resource] then
-- Resource conflict
local policy = config_get(session.host, "conflict_resolve");
local increment;
if policy == "random" then
resource = generate_identifier();
increment = true;
elseif policy == "increment" then
increment = true; -- TODO ping old resource
elseif policy == "kick_new" then
return nil, "cancel", "conflict", "Resource already exists";
else -- if policy == "kick_old" then
sessions[resource]:close {
condition = "conflict";
text = "Replaced by new connection";
};
if not next(sessions) then
hosts[session.host].sessions[session.username] = { sessions = sessions };
bare_sessions[session.username.."@"..session.host] = hosts[session.host].sessions[session.username];
end
end
if increment and sessions[resource] then
local count = 1;
while sessions[resource.."#"..count] do
count = count + 1;
end
resource = resource.."#"..count;
end
end
end
session.resource = resource;
session.full_jid = session.username .. '@' .. session.host .. '/' .. resource;
hosts[session.host].sessions[session.username].sessions[resource] = session;
full_sessions[session.full_jid] = session;
if session.type == "c2s_unbound" then
session.type = "c2s";
end
local err;
session.roster, err = rm_load_roster(session.username, session.host);
if err then
-- FIXME: Why is all this rollback down here, instead of just doing the roster test up above?
full_sessions[session.full_jid] = nil;
hosts[session.host].sessions[session.username].sessions[resource] = nil;
session.full_jid = nil;
session.resource = nil;
if session.type == "c2s" then
session.type = "c2s_unbound";
end
if next(bare_sessions[session.username..'@'..session.host].sessions) == nil then
bare_sessions[session.username..'@'..session.host] = nil;
hosts[session.host].sessions[session.username] = nil;
end
session.log("error", "Roster loading failed: %s", err);
return nil, "cancel", "internal-server-error", "Error loading roster";
end
hosts[session.host].events.fire_event("resource-bind", {session=session});
return true;
end
local function send_to_available_resources(username, host, stanza)
local jid = username.."@"..host;
local count = 0;
local user = bare_sessions[jid];
if user then
for _, session in pairs(user.sessions) do
if session.presence then
session.send(stanza);
count = count + 1;
end
end
end
return count;
end
local function send_to_interested_resources(username, host, stanza)
local jid = username.."@"..host;
local count = 0;
local user = bare_sessions[jid];
if user then
for _, session in pairs(user.sessions) do
if session.interested then
session.send(stanza);
count = count + 1;
end
end
end
return count;
end
return {
new_session = new_session;
retire_session = retire_session;
update_session = update_session;
destroy_session = destroy_session;
make_authenticated = make_authenticated;
bind_resource = bind_resource;
send_to_available_resources = send_to_available_resources;
send_to_interested_resources = send_to_interested_resources;
};
|