diff options
Diffstat (limited to 'plugins/mod_bosh.lua')
-rw-r--r-- | plugins/mod_bosh.lua | 221 |
1 files changed, 102 insertions, 119 deletions
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index 9ef4a41e..60d3e4b4 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -6,9 +6,6 @@ -- COPYING file in the source package for more information. -- -module:set_global(); -- Global module - -local hosts = _G.hosts; local new_xmpp_stream = require "util.xmppstream".new; local sm = require "core.sessionmanager"; local sm_destroy_session = sm.destroy_session; @@ -16,12 +13,14 @@ local new_uuid = require "util.uuid".generate; local core_process_stanza = prosody.core_process_stanza; local st = require "util.stanza"; local logger = require "util.logger"; -local log = logger.init("mod_bosh"); +local log = module._log; local initialize_filters = require "util.filters".initialize; local math_min = math.min; -local xpcall, tostring, type = xpcall, tostring, type; +local tostring, type = tostring, type; local traceback = debug.traceback; +local runner = require"util.async".runner; local nameprep = require "util.encodings".stringprep.nameprep; +local cache = require "util.cache"; local xmlns_streams = "http://etherx.jabber.org/streams"; local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; @@ -48,33 +47,14 @@ local cross_domain = module:get_option("cross_domain_bosh", false); if cross_domain == true then cross_domain = "*"; end if type(cross_domain) == "table" then cross_domain = table.concat(cross_domain, ", "); end -local trusted_proxies = module:get_option_set("trusted_proxies", { "127.0.0.1", "::1" })._items; - -local function get_ip_from_request(request) - local ip = request.conn:ip(); - local forwarded_for = request.headers.x_forwarded_for; - if forwarded_for then - forwarded_for = forwarded_for..", "..ip; - for forwarded_ip in forwarded_for:gmatch("[^%s,]+") do - if not trusted_proxies[forwarded_ip] then - ip = forwarded_ip; - end - end - end - return ip; -end - local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; -local os_time = os.time; -- All sessions, and sessions that have no requests open -local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions"); +local sessions = module:shared("sessions"); -- Used to respond to idle sessions (those with waiting requests) -local waiting_requests = module:shared("waiting_requests"); function on_destroy_request(request) log("debug", "Request destroyed: %s", tostring(request)); - waiting_requests[request] = nil; local session = sessions[request.context.sid]; if session then local requests = session.requests; @@ -88,9 +68,24 @@ function on_destroy_request(request) -- If this session now has no requests open, mark it as inactive local max_inactive = session.bosh_max_inactive; if max_inactive and #requests == 0 then - inactive_sessions[session] = os_time() + max_inactive; + if session.inactive_timer then + session.inactive_timer:stop(); + end + session.inactive_timer = module:add_timer(max_inactive, check_inactive, session, request.context, + "BOSH client silent for over "..max_inactive.." seconds"); (session.log or log)("debug", "BOSH session marked as inactive (for %ds)", max_inactive); end + if session.bosh_wait_timer then + session.bosh_wait_timer:stop(); + session.bosh_wait_timer = nil; + end + end +end + +function check_inactive(now, session, context, reason) -- luacheck: ignore 212/now + if not session.destroyed then + sessions[context.sid] = nil; + sm_destroy_session(session, reason); end end @@ -124,7 +119,7 @@ function handle_POST(event) local headers = response.headers; headers.content_type = "text/xml; charset=utf-8"; - if cross_domain and event.request.headers.origin then + if cross_domain and request.headers.origin then set_cross_domain_headers(response); end @@ -148,8 +143,14 @@ function handle_POST(event) if session then -- Session was marked as inactive, since we have -- a request open now, unmark it - if inactive_sessions[session] and #session.requests > 0 then - inactive_sessions[session] = nil; + if session.inactive_timer and #session.requests > 0 then + session.inactive_timer:stop(); + session.inactive_timer = nil; + end + + if session.bosh_wait_timer then + session.bosh_wait_timer:stop(); + session.bosh_wait_timer = nil; end local r = session.requests; @@ -177,9 +178,6 @@ function handle_POST(event) if not response.finished then -- We're keeping this request open, to respond later log("debug", "Have nothing to say, so leaving request unanswered for now"); - if session.bosh_wait then - waiting_requests[response] = os_time() + session.bosh_wait; - end end if session.bosh_terminate then @@ -187,10 +185,16 @@ function handle_POST(event) session:close(); return nil; else + if session.bosh_wait and #session.requests > 0 then + session.bosh_wait_timer = module:add_timer(session.bosh_wait, after_bosh_wait, session.requests[1], session) + end + return true; -- Inform http server we shall reply later end - elseif response.finished then - return; -- A response has been sent already + elseif response.finished or context.ignore_request then + -- A response has been sent already, or we're ignoring this request + -- (e.g. so a different instance of the module can handle it) + return; end module:log("warn", "Unable to associate request with a session (incomplete request?)"); local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", @@ -198,13 +202,17 @@ function handle_POST(event) return tostring(close_reply) .. "\n"; end +function after_bosh_wait(now, request, session) -- luacheck: ignore 212 + if request.conn then + session.send(""); + end +end local function bosh_reset_stream(session) session.notopen = true; end local stream_xmlns_attr = { xmlns = "urn:ietf:params:xml:ns:xmpp-streams" }; - local function bosh_close_stream(session, reason) - (session.log or log)("info", "BOSH client disconnected"); + (session.log or log)("info", "BOSH client disconnected: %s", tostring((reason and reason.condition or reason) or "session close")); local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams }); @@ -237,21 +245,22 @@ local function bosh_close_stream(session, reason) held_request:send(response_body); end sessions[session.sid] = nil; - inactive_sessions[session] = nil; sm_destroy_session(session); end +local runner_callbacks = { }; + -- Handle the <body> tag in the request payload. function stream_callbacks.streamopened(context, attr) local request, response = context.request, context.response; - local sid = attr.sid; + local sid, rid = attr.sid, tonumber(attr.rid); log("debug", "BOSH body open (sid: %s)", sid or "<none>"); + context.rid = rid; if not sid then -- New session request context.notopen = nil; -- Signals that we accept this opening tag local to_host = nameprep(attr.to); - local rid = tonumber(attr.rid); local wait = tonumber(attr.wait); if not to_host then log("debug", "BOSH client tried to connect to invalid host: %s", tostring(attr.to)); @@ -259,12 +268,10 @@ function stream_callbacks.streamopened(context, attr) ["xmlns:stream"] = xmlns_streams, condition = "improper-addressing" }); response:send(tostring(close_reply)); return; - elseif not hosts[to_host] then - -- Unknown host - log("debug", "BOSH client tried to connect to unknown host: %s", tostring(attr.to)); - local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", - ["xmlns:stream"] = xmlns_streams, condition = "host-unknown" }); - response:send(tostring(close_reply)); + elseif to_host ~= module.host then + -- Could be meant for a different instance of the module + -- if multiple instances are loaded with the same URL then this can happen + context.ignore_request = true; return; end if not rid or (not wait and attr.wait or wait < 0 or wait % 1 ~= 0) then @@ -275,28 +282,32 @@ function stream_callbacks.streamopened(context, attr) return; end - rid = rid - 1; wait = math_min(wait, bosh_max_wait); -- New session sid = new_uuid(); local session = { - type = "c2s_unauthed", conn = request.conn, sid = sid, rid = rid, host = attr.to, + type = "c2s_unauthed", conn = request.conn, sid = sid, host = attr.to, + rid = rid - 1, -- Hack for initial session setup, "previous" rid was $current_request - 1 bosh_version = attr.ver, bosh_wait = wait, streamid = sid, - bosh_max_inactive = bosh_max_inactivity, + bosh_max_inactive = bosh_max_inactivity, bosh_responses = cache.new(BOSH_HOLD+1):table(); requests = { }, send_buffer = {}, reset_stream = bosh_reset_stream, close = bosh_close_stream, dispatch_stanza = core_process_stanza, notopen = true, log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure, - ip = get_ip_from_request(request); + ip = request.ip; }; sessions[sid] = session; + session.thread = runner(function (stanza) + session:dispatch_stanza(stanza); + end, runner_callbacks, session); + local filter = initialize_filters(session); session.log("debug", "BOSH session created for request from %s", session.ip); log("info", "New BOSH session, assigned it sid '%s'", sid); - hosts[session.host].events.fire_event("bosh-session", { session = session, request = request }); + module:fire_event("bosh-session", { session = session, request = request }); -- Send creation response local creating_session = true; @@ -335,8 +346,9 @@ function stream_callbacks.streamopened(context, attr) body_attr["xmlns:xmpp"] = "urn:xmpp:xbosh"; body_attr["xmpp:version"] = "1.0"; end - session.bosh_last_response = st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer).."</body>"; - oldest_request:send(session.bosh_last_response); + local response_xml = st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer).."</body>"; + session.bosh_responses[oldest_request.context.rid] = response_xml; + oldest_request:send(response_xml); session.send_buffer = {}; end return true; @@ -356,24 +368,31 @@ function stream_callbacks.streamopened(context, attr) session.conn = request.conn; if session.rid then - local rid = tonumber(attr.rid); local diff = rid - session.rid; -- Diff should be 1 for a healthy request + session.log("debug", "rid: %d, sess: %s, diff: %d", rid, session.rid, diff) if diff ~= 1 then context.sid = sid; context.notopen = nil; - if diff == 2 then + if diff == 2 then -- Missed a request -- Hold request, but don't process it (ouch!) session.log("debug", "rid skipped: %d, deferring this request", rid-1) context.defer = true; session.bosh_deferred = { context = context, sid = sid, rid = rid, terminate = attr.type == "terminate" }; return; end + -- Set a marker to indicate that stanzas in this request should NOT be processed + -- (these stanzas will already be in the XML parser's buffer) context.ignore = true; - if diff == 0 then - -- Re-send previous response, ignore stanzas in this request - session.log("debug", "rid repeated, ignoring: %s (diff %d)", session.rid, diff); - response:send(session.bosh_last_response); + if session.bosh_responses[rid] then + -- Re-send past response, ignore stanzas in this request + session.log("debug", "rid repeated within window, replaying old response"); + response:send(session.bosh_responses[rid]); + return; + elseif diff == 0 then + session.log("debug", "current rid repeated, ignoring stanzas"); + t_insert(session.requests, response); + context.sid = sid; return; end -- Session broken, destroy it @@ -397,13 +416,18 @@ function stream_callbacks.streamopened(context, attr) if session.notopen then local features = st.stanza("stream:features"); - hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); + module:fire_event("stream-features", { origin = session, features = features }); session.send(features); session.notopen = nil; end end local function handleerr(err) log("error", "Traceback[bosh]: %s", traceback(tostring(err), 2)); end + +function runner_callbacks:error(err) -- luacheck: ignore 212/self + return handleerr(err); +end + function stream_callbacks.handlestanza(context, stanza) if context.ignore then return; end log("debug", "BOSH stanza received: %s\n", stanza:top_tag()); @@ -417,9 +441,7 @@ function stream_callbacks.handlestanza(context, stanza) t_insert(session.bosh_deferred, stanza); else stanza = session.filter("stanzas/in", stanza); - if stanza then - return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); - end + session.thread:run(stanza); end else log("debug", "No session for this stanza! (sid: %s)", context.sid or "none!"); @@ -432,13 +454,13 @@ function stream_callbacks.streamclosed(context) if not context.defer and session.bosh_deferred then -- Handle deferred stanzas now local deferred_stanzas = session.bosh_deferred; - local context = deferred_stanzas.context; + local deferred_context = deferred_stanzas.context; session.bosh_deferred = nil; log("debug", "Handling deferred stanzas from rid %d", deferred_stanzas.rid); session.rid = deferred_stanzas.rid; - t_insert(session.requests, context.response); + t_insert(session.requests, deferred_context.response); for _, stanza in ipairs(deferred_stanzas) do - stream_callbacks.handlestanza(context, stanza); + stream_callbacks.handlestanza(deferred_context, stanza); end if deferred_stanzas.terminate then session.bosh_terminate = true; @@ -452,8 +474,8 @@ function stream_callbacks.streamclosed(context) end function stream_callbacks.error(context, error) - log("debug", "Error parsing BOSH request payload; %s", error); if not context.sid then + log("debug", "Error parsing BOSH request payload; %s", error); local response = context.response; local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams, condition = "bad-request" }); @@ -462,6 +484,7 @@ function stream_callbacks.error(context, error) end local session = sessions[context.sid]; + (session and session.log or log)("warn", "Error parsing BOSH request payload; %s", error); if error == "stream-error" then -- Remote stream error, we close normally session:close(); else @@ -469,65 +492,25 @@ function stream_callbacks.error(context, error) end end -local dead_sessions = module:shared("dead_sessions"); -function on_timer() - -- log("debug", "Checking for requests soon to timeout..."); - -- Identify requests timing out within the next few seconds - local now = os_time() + 3; - for request, reply_before in pairs(waiting_requests) do - if reply_before <= now then - log("debug", "%s was soon to timeout (at %d, now %d), sending empty response", tostring(request), reply_before, now); - -- Send empty response to let the - -- client know we're still here - if request.conn then - sessions[request.context.sid].send(""); - end - end - end - - now = now - 3; - local n_dead_sessions = 0; - for session, close_after in pairs(inactive_sessions) do - if close_after < now then - (session.log or log)("debug", "BOSH client inactive too long, destroying session at %d", now); - sessions[session.sid] = nil; - inactive_sessions[session] = nil; - n_dead_sessions = n_dead_sessions + 1; - dead_sessions[n_dead_sessions] = session; - end - end - - for i=1,n_dead_sessions do - local session = dead_sessions[i]; - dead_sessions[i] = nil; - sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds"); - end - return 1; -end -module:add_timer(1, on_timer); - - local GET_response = { headers = { content_type = "text/html"; }; body = [[<html><body> <p>It works! Now point your BOSH client to this URL to connect to Prosody.</p> - <p>For more information see <a href="http://prosody.im/doc/setting_up_bosh">Prosody: Setting up BOSH</a>.</p> + <p>For more information see <a href="https://prosody.im/doc/setting_up_bosh">Prosody: Setting up BOSH</a>.</p> </body></html>]]; }; -function module.add_host(module) - module:depends("http"); - module:provides("http", { - default_path = "/http-bind"; - route = { - ["GET"] = GET_response; - ["GET /"] = GET_response; - ["OPTIONS"] = handle_OPTIONS; - ["OPTIONS /"] = handle_OPTIONS; - ["POST"] = handle_POST; - ["POST /"] = handle_POST; - }; - }); -end +module:depends("http"); +module:provides("http", { + default_path = "/http-bind"; + route = { + ["GET"] = GET_response; + ["GET /"] = GET_response; + ["OPTIONS"] = handle_OPTIONS; + ["OPTIONS /"] = handle_OPTIONS; + ["POST"] = handle_POST; + ["POST /"] = handle_POST; + }; +}); |