| 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 | |
| 10 | |
| 11 | local tonumber, tostring, setmetatable = tonumber, tostring, setmetatable; |
| 12 | local ipairs, pairs, print, next= ipairs, pairs, print, next; |
| 13 | local format = import("string", "format"); |
| 14 | |
| 15 | local hosts = hosts; |
| 16 | local full_sessions = full_sessions; |
| 17 | local bare_sessions = bare_sessions; |
| 18 | |
| 19 | local modulemanager = require "core.modulemanager"; |
| 20 | local logger = require "util.logger"; |
| 21 | local log = logger.init("sessionmanager"); |
| 22 | local error = error; |
| 23 | local uuid_generate = require "util.uuid".generate; |
| 24 | local rm_load_roster = require "core.rostermanager".load_roster; |
| 25 | local config_get = require "core.configmanager".get; |
| 26 | local nameprep = require "util.encodings".stringprep.nameprep; |
| 27 | local resourceprep = require "util.encodings".stringprep.resourceprep; |
| 28 | local nodeprep = require "util.encodings".stringprep.nodeprep; |
| 29 | |
| 30 | local initialize_filters = require "util.filters".initialize; |
| 31 | local fire_event = prosody.events.fire_event; |
| 32 | local add_task = require "util.timer".add_task; |
| 33 | local gettime = require "socket".gettime; |
| 34 | |
| 35 | local st = require "util.stanza"; |
| 36 | |
| 37 | local c2s_timeout = config_get("*", "core", "c2s_timeout"); |
| 38 | |
| 39 | local newproxy = newproxy; |
| 40 | local getmetatable = getmetatable; |
| 41 | |
| 42 | module "sessionmanager" |
| 43 | |
| 44 | local open_sessions = 0; |
| 45 | |
| 46 | function new_session(conn) |
| 47 | local session = { conn = conn, type = "c2s_unauthed", conntime = gettime() }; |
| 48 | if true then |
| 49 | session.trace = newproxy(true); |
| 50 | getmetatable(session.trace).__gc = function () open_sessions = open_sessions - 1; end; |
| 51 | end |
| 52 | open_sessions = open_sessions + 1; |
| 53 | log("debug", "open sessions now: ".. open_sessions); |
| 54 | |
| 55 | local filter = initialize_filters(session); |
| 56 | local w = conn.write; |
| 57 | session.send = function (t) |
| 58 | if t.name then |
| 59 | t = filter("stanzas/out", t); |
| 60 | end |
| 61 | if t then |
| 62 | t = filter("bytes/out", tostring(t)); |
| 63 | if t then |
| 64 | return w(conn, t); |
| 65 | end |
| 66 | end |
| 67 | end |
| 68 | session.ip = conn:ip(); |
| 69 | local conn_name = "c2s"..tostring(conn):match("[a-f0-9]+$"); |
| 70 | session.log = logger.init(conn_name); |
| 71 | |
| 72 | if c2s_timeout then |
| 73 | add_task(c2s_timeout, function () |
| 74 | if session.type == "c2s_unauthed" then |
| 75 | session:close("connection-timeout"); |
| 76 | end |
| 77 | end); |
| 78 | end |
| 79 | |
| 80 | return session; |
| 81 | end |
| 82 | |
| 83 | local resting_session = { -- Resting, not dead |
| 84 | destroyed = true; |
| 85 | type = "c2s_destroyed"; |
| 86 | close = function (session) |
| 87 | session.log("debug", "Attempt to close already-closed session"); |
| 88 | end; |
| 89 | filter = function (type, data) return data; end; |
| 90 | }; resting_session.__index = resting_session; |
| 91 | |
| 92 | function retire_session(session) |
| 93 | local log = session.log or log; |
| 94 | for k in pairs(session) do |
| 95 | if k ~= "trace" and k ~= "log" and k ~= "id" then |
| 96 | session[k] = nil; |
| 97 | end |
| 98 | end |
| 99 | |
| 100 | function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); end |
| 101 | function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end |
| 102 | return setmetatable(session, resting_session); |
| 103 | end |
| 104 | |
| 105 | function destroy_session(session, err) |
| 106 | (session.log or log)("info", "Destroying session for %s (%s@%s)", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)"); |
| 107 | if session.destroyed then return; end |
| 108 | |
| 109 | -- Remove session/resource from user's session list |
| 110 | if session.full_jid then |
| 111 | hosts[session.host].sessions[session.username].sessions[session.resource] = nil; |
| 112 | full_sessions[session.full_jid] = nil; |
| 113 | |
| 114 | if not next(hosts[session.host].sessions[session.username].sessions) then |
| 115 | log("debug", "All resources of %s are now offline", session.username); |
| 116 | hosts[session.host].sessions[session.username] = nil; |
| 117 | bare_sessions[session.username..'@'..session.host] = nil; |
| 118 | end |
| 119 | |
| 120 | hosts[session.host].events.fire_event("resource-unbind", {session=session, error=err}); |
| 121 | end |
| 122 | |
| 123 | retire_session(session); |
| 124 | end |
| 125 | |
| 126 | function make_authenticated(session, username) |
| 127 | username = nodeprep(username); |
| 128 | if not username or #username == 0 then return nil, "Invalid username"; end |
| 129 | session.username = username; |
| 130 | if session.type == "c2s_unauthed" then |
| 131 | session.type = "c2s"; |
| 132 | end |
| 133 | session.log("info", "Authenticated as %s@%s", username or "(unknown)", session.host or "(unknown)"); |
| 134 | return true; |
| 135 | end |
| 136 | |
| 137 | -- returns true, nil on success |
| 138 | -- returns nil, err_type, err, err_message on failure |
| 139 | function bind_resource(session, resource) |
| 140 | if not session.username then return nil, "auth", "not-authorized", "Cannot bind resource before authentication"; end |
| 141 | if session.resource then return nil, "cancel", "already-bound", "Cannot bind multiple resources on a single connection"; end |
| 142 | -- We don't support binding multiple resources |
| 143 | |
| 144 | resource = resourceprep(resource); |
| 145 | resource = resource ~= "" and resource or uuid_generate(); |
| 146 | --FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing |
| 147 | |
| 148 | if not hosts[session.host].sessions[session.username] then |
| 149 | local sessions = { sessions = {} }; |
| 150 | hosts[session.host].sessions[session.username] = sessions; |
| 151 | bare_sessions[session.username..'@'..session.host] = sessions; |
| 152 | else |
| 153 | local sessions = hosts[session.host].sessions[session.username].sessions; |
| 154 | local limit = config_get(session.host, "core", "max_resources") or 10; |
| 155 | if #sessions >= limit then |
| 156 | return nil, "cancel", "resource-constraint", "Resource limit reached; only "..limit.." resources allowed"; |
| 157 | end |
| 158 | if sessions[resource] then |
| 159 | -- Resource conflict |
| 160 | local policy = config_get(session.host, "core", "conflict_resolve"); |
| 161 | local increment; |
| 162 | if policy == "random" then |
| 163 | resource = uuid_generate(); |
| 164 | increment = true; |
| 165 | elseif policy == "increment" then |
| 166 | increment = true; -- TODO ping old resource |
| 167 | elseif policy == "kick_new" then |
| 168 | return nil, "cancel", "conflict", "Resource already exists"; |
| 169 | else -- if policy == "kick_old" then |
| 170 | sessions[resource]:close { |
| 171 | condition = "conflict"; |
| 172 | text = "Replaced by new connection"; |
| 173 | }; |
| 174 | if not next(sessions) then |
| 175 | hosts[session.host].sessions[session.username] = { sessions = sessions }; |
| 176 | bare_sessions[session.username.."@"..session.host] = hosts[session.host].sessions[session.username]; |
| 177 | end |
| 178 | end |
| 179 | if increment and sessions[resource] then |
| 180 | local count = 1; |
| 181 | while sessions[resource.."#"..count] do |
| 182 | count = count + 1; |
| 183 | end |
| 184 | resource = resource.."#"..count; |
| 185 | end |
| 186 | end |
| 187 | end |
| 188 | |
| 189 | session.resource = resource; |
| 190 | session.full_jid = session.username .. '@' .. session.host .. '/' .. resource; |
| 191 | hosts[session.host].sessions[session.username].sessions[resource] = session; |
| 192 | full_sessions[session.full_jid] = session; |
| 193 | |
| 194 | local err; |
| 195 | session.roster, err = rm_load_roster(session.username, session.host); |
| 196 | if err then |
| 197 | full_sessions[session.full_jid] = nil; |
| 198 | hosts[session.host].sessions[session.username].sessions[resource] = nil; |
| 199 | session.full_jid = nil; |
| 200 | session.resource = nil; |
| 201 | if next(bare_sessions[session.username..'@'..session.host].sessions) == nil then |
| 202 | bare_sessions[session.username..'@'..session.host] = nil; |
| 203 | hosts[session.host].sessions[session.username] = nil; |
| 204 | end |
| 205 | return nil, "cancel", "internal-server-error", "Error loading roster"; |
| 206 | end |
| 207 | |
| 208 | hosts[session.host].events.fire_event("resource-bind", {session=session}); |
| 209 | |
| 210 | return true; |
| 211 | end |
| 212 | |
| 213 | function streamopened(session, attr) |
| 214 | local send = session.send; |
| 215 | session.host = attr.to; |
| 216 | if not session.host then |
| 217 | session:close{ condition = "improper-addressing", |
| 218 | text = "A 'to' attribute is required on stream headers" }; |
| 219 | return; |
| 220 | end |
| 221 | session.host = nameprep(session.host); |
| 222 | session.version = tonumber(attr.version) or 0; |
| 223 | session.streamid = uuid_generate(); |
| 224 | (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host); |
| 225 | |
| 226 | if not hosts[session.host] then |
| 227 | -- We don't serve this host... |
| 228 | session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)}; |
| 229 | return; |
| 230 | end |
| 231 | |
| 232 | send("<?xml version='1.0'?>"); |
| 233 | send(format("<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s' version='1.0' xml:lang='en'>", session.streamid, session.host)); |
| 234 | |
| 235 | (session.log or log)("debug", "Sent reply <stream:stream> to client"); |
| 236 | session.notopen = nil; |
| 237 | |
| 238 | -- If session.secure is *false* (not nil) then it means we /were/ encrypting |
| 239 | -- since we now have a new stream header, session is secured |
| 240 | if session.secure == false then |
| 241 | session.secure = true; |
| 242 | end |
| 243 | |
| 244 | local features = st.stanza("stream:features"); |
| 245 | hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); |
| 246 | fire_event("stream-features", session, features); |
| 247 | |
| 248 | send(features); |
| 249 | |
| 250 | end |
| 251 | |
| 252 | function streamclosed(session) |
| 253 | session.log("debug", "Received </stream:stream>"); |
| 254 | session:close(); |
| 255 | end |
| 256 | |
| 257 | function send_to_available_resources(user, host, stanza) |
| 258 | local jid = user.."@"..host; |
| 259 | local count = 0; |
| 260 | local user = bare_sessions[jid]; |
| 261 | if user then |
| 262 | for k, session in pairs(user.sessions) do |
| 263 | if session.presence then |
| 264 | session.send(stanza); |
| 265 | count = count + 1; |
| 266 | end |
| 267 | end |
| 268 | end |
| 269 | return count; |
| 270 | end |
| 271 | |
| 272 | function send_to_interested_resources(user, host, stanza) |
| 273 | local jid = user.."@"..host; |
| 274 | local count = 0; |
| 275 | local user = bare_sessions[jid]; |
| 276 | if user then |
| 277 | for k, session in pairs(user.sessions) do |
| 278 | if session.interested then |
| 279 | session.send(stanza); |
| 280 | count = count + 1; |
| 281 | end |
| 282 | end |
| 283 | end |
| 284 | return count; |
| 285 | end |
| 286 | |
| 287 | return _M; |