X-Git-Url: https://git.enpas.org/?a=blobdiff_plain;f=plugins%2Fmod_bosh.lua;h=19f191c8bf6335a924d2de2876885d62453b184e;hb=99b003b1917a8ab0b561f7fb98d28d1fad8f8dc8;hp=8e87e140042dc4dea64e5ca83386e24eba63778b;hpb=808f755350d2e26697ba522185a9f5e34bda9176;p=prosody.git diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index 8e87e140..19f191c8 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -6,20 +6,20 @@ -- COPYING file in the source package for more information. -- -module.host = "*" -- Global module +module:set_global(); -- Global module local hosts = _G.hosts; local new_xmpp_stream = require "util.xmppstream".new; -local httpserver = require "net.httpserver"; local sm = require "core.sessionmanager"; local sm_destroy_session = sm.destroy_session; local new_uuid = require "util.uuid".generate; local fire_event = prosody.events.fire_event; -local core_process_stanza = core_process_stanza; +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 timer = require "util.timer"; +local initialize_filters = require "util.filters".initialize; +local math_min = math.min; local xmlns_streams = "http://etherx.jabber.org/streams"; local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; @@ -32,11 +32,11 @@ local BOSH_DEFAULT_HOLD = module:get_option_number("bosh_default_hold", 1); local BOSH_DEFAULT_INACTIVITY = module:get_option_number("bosh_max_inactivity", 60); local BOSH_DEFAULT_POLLING = module:get_option_number("bosh_max_polling", 5); local BOSH_DEFAULT_REQUESTS = module:get_option_number("bosh_max_requests", 2); +local bosh_max_wait = module:get_option_number("bosh_max_wait", 120); local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure"); -local auto_cork = module:get_option_boolean("bosh_auto_cork", false); -local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" }; +local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8", ["Connection"] = "keep-alive" }; local cross_domain = module:get_option("cross_domain_bosh", false); if cross_domain then @@ -58,7 +58,7 @@ local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"}). local function get_ip_from_request(request) local ip = request.conn:ip(); - local forwarded_for = request.headers["x-forwarded-for"]; + 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 @@ -73,17 +73,18 @@ end local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; local os_time = os.time; -local sessions = {}; -local inactive_sessions = {}; -- Sessions which have no open requests +-- All sessions, and sessions that have no requests open +local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions"); -- Used to respond to idle sessions (those with waiting requests) local waiting_requests = {}; function on_destroy_request(request) + log("debug", "Request destroyed: %s", tostring(request)); waiting_requests[request] = nil; - local session = sessions[request.sid]; + local session = sessions[request.context.sid]; if session then local requests = session.requests; - for i,r in ipairs(requests) do + for i, r in ipairs(requests) do if r == request then t_remove(requests, i); break; @@ -99,39 +100,35 @@ function on_destroy_request(request) end end -function handle_request(method, body, request) - if (not body) or request.method ~= "POST" then - if request.method == "OPTIONS" then - local headers = {}; - for k,v in pairs(default_headers) do headers[k] = v; end - headers["Content-Type"] = nil; - return { headers = headers, body = "" }; - else - return "You really don't look like a BOSH client to me... what do you want?"; - end - end - if not method then - log("debug", "Request %s suffered error %s", tostring(request.id), body); - return; - end - --log("debug", "Handling new request %s: %s\n----------", request.id, tostring(body)); - request.notopen = true; - request.log = log; - request.on_destroy = on_destroy_request; - - local stream = new_xmpp_stream(request, stream_callbacks); +function handle_OPTIONS(request) + local headers = {}; + for k,v in pairs(default_headers) do headers[k] = v; end + headers["Content-Type"] = nil; + return { headers = headers, body = "" }; +end + +function handle_POST(event) + log("debug", "Handling new request %s: %s\n----------", tostring(event.request), tostring(event.request.body)); + + local request, response = event.request, event.response; + response.on_destroy = on_destroy_request; + local body = request.body; + + local context = { request = request, response = response, notopen = true }; + local stream = new_xmpp_stream(context, stream_callbacks); + response.context = context; -- stream:feed() calls the stream_callbacks, so all stanzas in -- the body are processed in this next line before it returns. - local ok, err = stream:feed(body); - if not ok then - log("error", "Failed to parse BOSH payload: %s", err); - end + -- In particular, the streamopened() stream callback is where + -- much of the session logic happens, because it's where we first + -- get to see the 'sid' of this request. + stream:feed(body); -- Stanzas (if any) in the request have now been processed, and -- we take care of the high-level BOSH logic here, including -- giving a response or putting the request "on hold". - local session = sessions[request.sid]; + local session = sessions[context.sid]; if session then -- Session was marked as inactive, since we have -- a request open now, unmark it @@ -140,8 +137,11 @@ function handle_request(method, body, request) end local r = session.requests; - log("debug", "Session %s has %d out of %d requests open", request.sid, #r, session.bosh_hold); - log("debug", "and there are %d things in the send_buffer", #session.send_buffer); + log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.bosh_hold); + log("debug", "and there are %d things in the send_buffer:", #session.send_buffer); + for i, thing in ipairs(session.send_buffer) do + log("debug", " %s", tostring(thing)); + end if #r > session.bosh_hold then -- We are holding too many requests, send what's in the buffer, log("debug", "We are holding too many requests, so..."); @@ -161,12 +161,11 @@ function handle_request(method, body, request) session.send(resp); end - if not request.destroyed then + 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 - request.reply_before = os_time() + session.bosh_wait; - waiting_requests[request] = true; + waiting_requests[response] = os_time() + session.bosh_wait; end end @@ -175,7 +174,7 @@ function handle_request(method, body, request) session:close(); return nil; else - return true; -- Inform httpserver we shall reply later + return true; -- Inform http server we shall reply later end end end @@ -214,11 +213,10 @@ local function bosh_close_stream(session, reason) log("info", "Disconnecting client, is: %s", tostring(close_reply)); end - local session_close_response = { headers = default_headers, body = tostring(close_reply) }; - + local response_body = tostring(close_reply); for _, held_request in ipairs(session.requests) do - held_request:send(session_close_response); - held_request:destroy(); + held_request.headers = default_headers; + held_request:send(response_body); end sessions[session.sid] = nil; inactive_sessions[session] = nil; @@ -226,12 +224,13 @@ local function bosh_close_stream(session, reason) end -- Handle the tag in the request payload. -function stream_callbacks.streamopened(request, attr) +function stream_callbacks.streamopened(context, attr) + local request, response = context.request, context.response; local sid = attr.sid; log("debug", "BOSH body open (sid: %s)", sid or ""); if not sid then -- New session request - request.notopen = nil; -- Signals that we accept this opening tag + context.notopen = nil; -- Signals that we accept this opening tag -- TODO: Sanity checks here (rid, to, known host, etc.) if not hosts[attr.to] then @@ -239,95 +238,78 @@ function stream_callbacks.streamopened(request, attr) 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" }); - request:send(tostring(close_reply)); + response:send(tostring(close_reply)); return; end -- New session sid = new_uuid(); local session = { - type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid), host = attr.to, - bosh_version = attr.ver, bosh_wait = attr.wait, streamid = sid, + type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to, + bosh_version = attr.ver, bosh_wait = math_min(attr.wait, bosh_max_wait), streamid = sid, bosh_hold = BOSH_DEFAULT_HOLD, bosh_max_inactive = BOSH_DEFAULT_INACTIVITY, requests = { }, send_buffer = {}, reset_stream = bosh_reset_stream, - close = bosh_close_stream, dispatch_stanza = core_process_stanza, + 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); }; sessions[sid] = 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); - local r, send_buffer = session.requests, session.send_buffer; - local response = { headers = default_headers } + + -- Send creation response + local creating_session = true; + + local r = session.requests; function session.send(s) -- We need to ensure that outgoing stanzas have the jabber:client xmlns if s.attr and not s.attr.xmlns then s = st.clone(s); s.attr.xmlns = "jabber:client"; end + s = filter("stanzas/out", s); --log("debug", "Sending BOSH data: %s", tostring(s)); + t_insert(session.send_buffer, tostring(s)); + local oldest_request = r[1]; - if oldest_request and (not(auto_cork) or waiting_requests[oldest_request]) then + if oldest_request and not session.bosh_processing then log("debug", "We have an open request, so sending on that"); - response.body = t_concat({ - "", - tostring(s), - "" - }); - oldest_request:send(response); - --log("debug", "Sent"); - if oldest_request.stayopen then - if #r>1 then - -- Move front request to back - t_insert(r, oldest_request); - t_remove(r, 1); - end - else - log("debug", "Destroying the request now..."); - oldest_request:destroy(); + oldest_request.headers = default_headers; + local body_attr = { xmlns = "http://jabber.org/protocol/httpbind", + ["xmlns:stream"] = "http://etherx.jabber.org/streams"; + type = session.bosh_terminate and "terminate" or nil; + sid = sid; + }; + if creating_session then + body_attr.inactivity = tostring(BOSH_DEFAULT_INACTIVITY); + body_attr.polling = tostring(BOSH_DEFAULT_POLLING); + body_attr.requests = tostring(BOSH_DEFAULT_REQUESTS); + body_attr.wait = tostring(session.bosh_wait); + body_attr.hold = tostring(session.bosh_hold); + body_attr.authid = sid; + body_attr.secure = "true"; + body_attr.ver = '1.6'; from = session.host; + body_attr["xmlns:xmpp"] = "urn:xmpp:xbosh"; + body_attr["xmpp:version"] = "1.0"; end - elseif s ~= "" then - log("debug", "Saved to send buffer because there are %d open requests", #r); - -- Hmm, no requests are open :( - t_insert(session.send_buffer, tostring(s)); - log("debug", "There are now %d things in the send_buffer", #session.send_buffer); + oldest_request:send(st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer)..""); + session.send_buffer = {}; end return true; end - - -- Send creation response - - local features = st.stanza("stream:features"); - hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); - fire_event("stream-features", session, features); - --xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh' - local response = st.stanza("body", { xmlns = xmlns_bosh, - wait = attr.wait, - inactivity = tostring(BOSH_DEFAULT_INACTIVITY), - polling = tostring(BOSH_DEFAULT_POLLING), - requests = tostring(BOSH_DEFAULT_REQUESTS), - hold = tostring(session.bosh_hold), - sid = sid, authid = sid, - ver = '1.6', from = session.host, - secure = 'true', ["xmpp:version"] = "1.0", - ["xmlns:xmpp"] = "urn:xmpp:xbosh", - ["xmlns:stream"] = "http://etherx.jabber.org/streams" - }):add_child(features); - request:send{ headers = default_headers, body = tostring(response) }; - request.sid = sid; - return; end local session = sessions[sid]; if not session then -- Unknown sid log("info", "Client tried to use sid '%s' which we don't know about", sid); - request:send{ headers = default_headers, body = tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" })) }; - request.notopen = nil; + response.headers = default_headers; + response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" }))); + context.notopen = nil; return; end @@ -339,10 +321,10 @@ function stream_callbacks.streamopened(request, attr) elseif diff <= 0 then -- Repeated, ignore session.log("debug", "rid repeated (on request %s), ignoring: %s (diff %d)", request.id, session.rid, diff); - request.notopen = nil; - request.ignore = true; - request.sid = sid; - t_insert(session.requests, request); + context.notopen = nil; + context.ignore = true; + context.sid = sid; + t_insert(session.requests, response); return; end session.rid = rid; @@ -354,39 +336,54 @@ function stream_callbacks.streamopened(request, attr) session.bosh_terminate = true; end - request.notopen = nil; -- Signals that we accept this opening tag - t_insert(session.requests, request); - request.sid = sid; + context.notopen = nil; -- Signals that we accept this opening tag + t_insert(session.requests, response); + context.sid = sid; + session.bosh_processing = true; -- Used to suppress replies until processing of this request is done if session.notopen then local features = st.stanza("stream:features"); hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); fire_event("stream-features", session, features); - session.send(features); + table.insert(session.send_buffer, tostring(features)); session.notopen = nil; end end -function stream_callbacks.handlestanza(request, stanza) - if request.ignore then return; end +function stream_callbacks.handlestanza(context, stanza) + if context.ignore then return; end log("debug", "BOSH stanza received: %s\n", stanza:top_tag()); - local session = sessions[request.sid]; + local session = sessions[context.sid]; if session then if stanza.attr.xmlns == xmlns_bosh then stanza.attr.xmlns = nil; end + stanza = session.filter("stanzas/in", stanza); core_process_stanza(session, stanza); end end -function stream_callbacks.error(request, error) +function stream_callbacks.streamclosed(request) + local session = sessions[request.sid]; + if session then + session.bosh_processing = false; + if #session.send_buffer > 0 then + session.send(""); + end + end +end + +function stream_callbacks.error(context, error) log("debug", "Error parsing BOSH request payload; %s", error); - if not request.sid then - request:send({ headers = default_headers, status = "400 Bad Request" }); + if not context.sid then + local response = context.response; + response.headers = default_headers; + response.status_code = 400; + response:send(); return; end - local session = sessions[request.sid]; + local session = sessions[context.sid]; if error == "stream-error" then -- Remote stream error, we close normally session:close(); else @@ -399,13 +396,13 @@ 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 in pairs(waiting_requests) do - if request.reply_before <= now then - log("debug", "%s was soon to timeout, sending empty response", request.id); + 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.sid].send(""); + sessions[request.context.sid].send(""); end end end @@ -429,15 +426,30 @@ function on_timer() end return 1; end - - -local function setup() - local ports = module:get_option_array("bosh_ports") or { 5280 }; - httpserver.new_from_config(ports, handle_request, { base = "http-bind" }); - timer.add_task(1, on_timer); -end -if prosody.start_time then -- already started - setup(); -else - prosody.events.add_handler("server-started", setup); +module:add_timer(1, on_timer); + + +local GET_response = { + headers = { + content_type = "text/html"; + }; + body = [[ +

It works! Now point your BOSH client to this URL to connect to Prosody.

+

For more information see Prosody: Setting up BOSH.

+ ]]; +}; + +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