Mercurial > prosody-hg
comparison plugins/mod_bosh.lua @ 6054:7a5ddbaf758d
Merge 0.9->0.10
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Wed, 02 Apr 2014 17:41:38 +0100 |
| parents | bd0ff8ae98a8 |
| children | f0687c313cf1 |
comparison
equal
deleted
inserted
replaced
| 6053:2f93a04564b2 | 6054:7a5ddbaf758d |
|---|---|
| 1 -- Prosody IM | 1 -- Prosody IM |
| 2 -- Copyright (C) 2008-2010 Matthew Wild | 2 -- Copyright (C) 2008-2010 Matthew Wild |
| 3 -- Copyright (C) 2008-2010 Waqas Hussain | 3 -- Copyright (C) 2008-2010 Waqas Hussain |
| 4 -- | 4 -- |
| 5 -- This project is MIT/X11 licensed. Please see the | 5 -- This project is MIT/X11 licensed. Please see the |
| 6 -- COPYING file in the source package for more information. | 6 -- COPYING file in the source package for more information. |
| 7 -- | 7 -- |
| 8 | 8 |
| 9 module:set_global(); -- Global module | 9 module:set_global(); -- Global module |
| 35 local BOSH_DEFAULT_POLLING = module:get_option_number("bosh_max_polling", 5); | 35 local BOSH_DEFAULT_POLLING = module:get_option_number("bosh_max_polling", 5); |
| 36 local BOSH_DEFAULT_REQUESTS = module:get_option_number("bosh_max_requests", 2); | 36 local BOSH_DEFAULT_REQUESTS = module:get_option_number("bosh_max_requests", 2); |
| 37 local bosh_max_wait = module:get_option_number("bosh_max_wait", 120); | 37 local bosh_max_wait = module:get_option_number("bosh_max_wait", 120); |
| 38 | 38 |
| 39 local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure"); | 39 local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure"); |
| 40 | |
| 41 local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" }; | |
| 42 | |
| 43 local cross_domain = module:get_option("cross_domain_bosh", false); | 40 local cross_domain = module:get_option("cross_domain_bosh", false); |
| 44 if cross_domain then | 41 |
| 45 default_headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"; | 42 if cross_domain == true then cross_domain = "*"; end |
| 46 default_headers["Access-Control-Allow-Headers"] = "Content-Type"; | 43 if type(cross_domain) == "table" then cross_domain = table.concat(cross_domain, ", "); end |
| 47 default_headers["Access-Control-Max-Age"] = "7200"; | |
| 48 | |
| 49 if cross_domain == true then | |
| 50 default_headers["Access-Control-Allow-Origin"] = "*"; | |
| 51 elseif type(cross_domain) == "table" then | |
| 52 cross_domain = table.concat(cross_domain, ", "); | |
| 53 end | |
| 54 if type(cross_domain) == "string" then | |
| 55 default_headers["Access-Control-Allow-Origin"] = cross_domain; | |
| 56 end | |
| 57 end | |
| 58 | 44 |
| 59 local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items; | 45 local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items; |
| 60 | 46 |
| 61 local function get_ip_from_request(request) | 47 local function get_ip_from_request(request) |
| 62 local ip = request.conn:ip(); | 48 local ip = request.conn:ip(); |
| 77 | 63 |
| 78 -- All sessions, and sessions that have no requests open | 64 -- All sessions, and sessions that have no requests open |
| 79 local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions"); | 65 local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions"); |
| 80 | 66 |
| 81 -- Used to respond to idle sessions (those with waiting requests) | 67 -- Used to respond to idle sessions (those with waiting requests) |
| 82 local waiting_requests = {}; | 68 local waiting_requests = module:shared("waiting_requests"); |
| 83 function on_destroy_request(request) | 69 function on_destroy_request(request) |
| 84 log("debug", "Request destroyed: %s", tostring(request)); | 70 log("debug", "Request destroyed: %s", tostring(request)); |
| 85 waiting_requests[request] = nil; | 71 waiting_requests[request] = nil; |
| 86 local session = sessions[request.context.sid]; | 72 local session = sessions[request.context.sid]; |
| 87 if session then | 73 if session then |
| 90 if r == request then | 76 if r == request then |
| 91 t_remove(requests, i); | 77 t_remove(requests, i); |
| 92 break; | 78 break; |
| 93 end | 79 end |
| 94 end | 80 end |
| 95 | 81 |
| 96 -- If this session now has no requests open, mark it as inactive | 82 -- If this session now has no requests open, mark it as inactive |
| 97 local max_inactive = session.bosh_max_inactive; | 83 local max_inactive = session.bosh_max_inactive; |
| 98 if max_inactive and #requests == 0 then | 84 if max_inactive and #requests == 0 then |
| 99 inactive_sessions[session] = os_time() + max_inactive; | 85 inactive_sessions[session] = os_time() + max_inactive; |
| 100 (session.log or log)("debug", "BOSH session marked as inactive (for %ds)", max_inactive); | 86 (session.log or log)("debug", "BOSH session marked as inactive (for %ds)", max_inactive); |
| 101 end | 87 end |
| 102 end | 88 end |
| 103 end | 89 end |
| 104 | 90 |
| 105 function handle_OPTIONS(request) | 91 local function set_cross_domain_headers(response) |
| 106 local headers = {}; | 92 local headers = response.headers; |
| 107 for k,v in pairs(default_headers) do headers[k] = v; end | 93 headers.access_control_allow_methods = "GET, POST, OPTIONS"; |
| 108 headers["Content-Type"] = nil; | 94 headers.access_control_allow_headers = "Content-Type"; |
| 109 return { headers = headers, body = "" }; | 95 headers.access_control_max_age = "7200"; |
| 96 headers.access_control_allow_origin = cross_domain; | |
| 97 return response; | |
| 98 end | |
| 99 | |
| 100 function handle_OPTIONS(event) | |
| 101 if cross_domain and event.request.headers.origin then | |
| 102 set_cross_domain_headers(event.response); | |
| 103 end | |
| 104 return ""; | |
| 110 end | 105 end |
| 111 | 106 |
| 112 function handle_POST(event) | 107 function handle_POST(event) |
| 113 log("debug", "Handling new request %s: %s\n----------", tostring(event.request), tostring(event.request.body)); | 108 log("debug", "Handling new request %s: %s\n----------", tostring(event.request), tostring(event.request.body)); |
| 114 | 109 |
| 117 local body = request.body; | 112 local body = request.body; |
| 118 | 113 |
| 119 local context = { request = request, response = response, notopen = true }; | 114 local context = { request = request, response = response, notopen = true }; |
| 120 local stream = new_xmpp_stream(context, stream_callbacks); | 115 local stream = new_xmpp_stream(context, stream_callbacks); |
| 121 response.context = context; | 116 response.context = context; |
| 122 | 117 |
| 118 local headers = response.headers; | |
| 119 headers.content_type = "text/xml; charset=utf-8"; | |
| 120 | |
| 121 if cross_domain and event.request.headers.origin then | |
| 122 set_cross_domain_headers(response); | |
| 123 end | |
| 124 | |
| 123 -- stream:feed() calls the stream_callbacks, so all stanzas in | 125 -- stream:feed() calls the stream_callbacks, so all stanzas in |
| 124 -- the body are processed in this next line before it returns. | 126 -- the body are processed in this next line before it returns. |
| 125 -- In particular, the streamopened() stream callback is where | 127 -- In particular, the streamopened() stream callback is where |
| 126 -- much of the session logic happens, because it's where we first | 128 -- much of the session logic happens, because it's where we first |
| 127 -- get to see the 'sid' of this request. | 129 -- get to see the 'sid' of this request. |
| 128 stream:feed(body); | 130 if not stream:feed(body) then |
| 129 | 131 module:log("warn", "Error parsing BOSH payload") |
| 132 return 400; | |
| 133 end | |
| 134 | |
| 130 -- Stanzas (if any) in the request have now been processed, and | 135 -- Stanzas (if any) in the request have now been processed, and |
| 131 -- we take care of the high-level BOSH logic here, including | 136 -- we take care of the high-level BOSH logic here, including |
| 132 -- giving a response or putting the request "on hold". | 137 -- giving a response or putting the request "on hold". |
| 133 local session = sessions[context.sid]; | 138 local session = sessions[context.sid]; |
| 134 if session then | 139 if session then |
| 139 end | 144 end |
| 140 | 145 |
| 141 local r = session.requests; | 146 local r = session.requests; |
| 142 log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.bosh_hold); | 147 log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.bosh_hold); |
| 143 log("debug", "and there are %d things in the send_buffer:", #session.send_buffer); | 148 log("debug", "and there are %d things in the send_buffer:", #session.send_buffer); |
| 144 for i, thing in ipairs(session.send_buffer) do | |
| 145 log("debug", " %s", tostring(thing)); | |
| 146 end | |
| 147 if #r > session.bosh_hold then | 149 if #r > session.bosh_hold then |
| 148 -- We are holding too many requests, send what's in the buffer, | 150 -- We are holding too many requests, send what's in the buffer, |
| 149 log("debug", "We are holding too many requests, so..."); | 151 log("debug", "We are holding too many requests, so..."); |
| 150 if #session.send_buffer > 0 then | 152 if #session.send_buffer > 0 then |
| 151 log("debug", "...sending what is in the buffer") | 153 log("debug", "...sending what is in the buffer") |
| 160 log("debug", "Session has data in the send buffer, will send now.."); | 162 log("debug", "Session has data in the send buffer, will send now.."); |
| 161 local resp = t_concat(session.send_buffer); | 163 local resp = t_concat(session.send_buffer); |
| 162 session.send_buffer = {}; | 164 session.send_buffer = {}; |
| 163 session.send(resp); | 165 session.send(resp); |
| 164 end | 166 end |
| 165 | 167 |
| 166 if not response.finished then | 168 if not response.finished then |
| 167 -- We're keeping this request open, to respond later | 169 -- We're keeping this request open, to respond later |
| 168 log("debug", "Have nothing to say, so leaving request unanswered for now"); | 170 log("debug", "Have nothing to say, so leaving request unanswered for now"); |
| 169 if session.bosh_wait then | 171 if session.bosh_wait then |
| 170 waiting_requests[response] = os_time() + session.bosh_wait; | 172 waiting_requests[response] = os_time() + session.bosh_wait; |
| 171 end | 173 end |
| 172 end | 174 end |
| 173 | 175 |
| 174 if session.bosh_terminate then | 176 if session.bosh_terminate then |
| 175 session.log("debug", "Closing session with %d requests open", #session.requests); | 177 session.log("debug", "Closing session with %d requests open", #session.requests); |
| 176 session:close(); | 178 session:close(); |
| 177 return nil; | 179 return nil; |
| 178 else | 180 else |
| 179 return true; -- Inform http server we shall reply later | 181 return true; -- Inform http server we shall reply later |
| 180 end | 182 end |
| 181 end | 183 end |
| 184 module:log("warn", "Unable to associate request with a session (incomplete request?)"); | |
| 185 return 400; | |
| 182 end | 186 end |
| 183 | 187 |
| 184 | 188 |
| 185 local function bosh_reset_stream(session) session.notopen = true; end | 189 local function bosh_reset_stream(session) session.notopen = true; end |
| 186 | 190 |
| 187 local stream_xmlns_attr = { xmlns = "urn:ietf:params:xml:ns:xmpp-streams" }; | 191 local stream_xmlns_attr = { xmlns = "urn:ietf:params:xml:ns:xmpp-streams" }; |
| 188 | 192 |
| 189 local function bosh_close_stream(session, reason) | 193 local function bosh_close_stream(session, reason) |
| 190 (session.log or log)("info", "BOSH client disconnected"); | 194 (session.log or log)("info", "BOSH client disconnected"); |
| 191 | 195 |
| 192 local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", | 196 local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", |
| 193 ["xmlns:stream"] = xmlns_streams }); | 197 ["xmlns:stream"] = xmlns_streams }); |
| 194 | 198 |
| 195 | 199 |
| 196 if reason then | 200 if reason then |
| 197 close_reply.attr.condition = "remote-stream-error"; | 201 close_reply.attr.condition = "remote-stream-error"; |
| 198 if type(reason) == "string" then -- assume stream error | 202 if type(reason) == "string" then -- assume stream error |
| 199 close_reply:tag("stream:error") | 203 close_reply:tag("stream:error") |
| 215 log("info", "Disconnecting client, <stream:error> is: %s", tostring(close_reply)); | 219 log("info", "Disconnecting client, <stream:error> is: %s", tostring(close_reply)); |
| 216 end | 220 end |
| 217 | 221 |
| 218 local response_body = tostring(close_reply); | 222 local response_body = tostring(close_reply); |
| 219 for _, held_request in ipairs(session.requests) do | 223 for _, held_request in ipairs(session.requests) do |
| 220 held_request.headers = default_headers; | |
| 221 held_request:send(response_body); | 224 held_request:send(response_body); |
| 222 end | 225 end |
| 223 sessions[session.sid] = nil; | 226 sessions[session.sid] = nil; |
| 224 inactive_sessions[session] = nil; | 227 inactive_sessions[session] = nil; |
| 225 sm_destroy_session(session); | 228 sm_destroy_session(session); |
| 226 end | 229 end |
| 227 | 230 |
| 228 -- Handle the <body> tag in the request payload. | 231 -- Handle the <body> tag in the request payload. |
| 231 local sid = attr.sid; | 234 local sid = attr.sid; |
| 232 log("debug", "BOSH body open (sid: %s)", sid or "<none>"); | 235 log("debug", "BOSH body open (sid: %s)", sid or "<none>"); |
| 233 if not sid then | 236 if not sid then |
| 234 -- New session request | 237 -- New session request |
| 235 context.notopen = nil; -- Signals that we accept this opening tag | 238 context.notopen = nil; -- Signals that we accept this opening tag |
| 236 | 239 |
| 237 -- TODO: Sanity checks here (rid, to, known host, etc.) | 240 -- TODO: Sanity checks here (rid, to, known host, etc.) |
| 238 if not hosts[attr.to] then | 241 if not hosts[attr.to] then |
| 239 -- Unknown host | 242 -- Unknown host |
| 240 log("debug", "BOSH client tried to connect to unknown host: %s", tostring(attr.to)); | 243 log("debug", "BOSH client tried to connect to unknown host: %s", tostring(attr.to)); |
| 241 local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", | 244 local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", |
| 242 ["xmlns:stream"] = xmlns_streams, condition = "host-unknown" }); | 245 ["xmlns:stream"] = xmlns_streams, condition = "host-unknown" }); |
| 243 response:send(tostring(close_reply)); | 246 response:send(tostring(close_reply)); |
| 244 return; | 247 return; |
| 245 end | 248 end |
| 246 | 249 |
| 247 -- New session | 250 -- New session |
| 248 sid = new_uuid(); | 251 sid = new_uuid(); |
| 249 local session = { | 252 local session = { |
| 250 type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to, | 253 type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to, |
| 251 bosh_version = attr.ver, bosh_wait = math_min(attr.wait, bosh_max_wait), streamid = sid, | 254 bosh_version = attr.ver, bosh_wait = math_min(attr.wait, bosh_max_wait), streamid = sid, |
| 254 close = bosh_close_stream, dispatch_stanza = core_process_stanza, notopen = true, | 257 close = bosh_close_stream, dispatch_stanza = core_process_stanza, notopen = true, |
| 255 log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure, | 258 log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure, |
| 256 ip = get_ip_from_request(request); | 259 ip = get_ip_from_request(request); |
| 257 }; | 260 }; |
| 258 sessions[sid] = session; | 261 sessions[sid] = session; |
| 259 | 262 |
| 260 local filter = initialize_filters(session); | 263 local filter = initialize_filters(session); |
| 261 | 264 |
| 262 session.log("debug", "BOSH session created for request from %s", session.ip); | 265 session.log("debug", "BOSH session created for request from %s", session.ip); |
| 263 log("info", "New BOSH session, assigned it sid '%s'", sid); | 266 log("info", "New BOSH session, assigned it sid '%s'", sid); |
| 264 | 267 |
| 265 -- Send creation response | 268 -- Send creation response |
| 266 local creating_session = true; | 269 local creating_session = true; |
| 277 t_insert(session.send_buffer, tostring(s)); | 280 t_insert(session.send_buffer, tostring(s)); |
| 278 | 281 |
| 279 local oldest_request = r[1]; | 282 local oldest_request = r[1]; |
| 280 if oldest_request and not session.bosh_processing then | 283 if oldest_request and not session.bosh_processing then |
| 281 log("debug", "We have an open request, so sending on that"); | 284 log("debug", "We have an open request, so sending on that"); |
| 282 oldest_request.headers = default_headers; | |
| 283 local body_attr = { xmlns = "http://jabber.org/protocol/httpbind", | 285 local body_attr = { xmlns = "http://jabber.org/protocol/httpbind", |
| 284 ["xmlns:stream"] = "http://etherx.jabber.org/streams"; | 286 ["xmlns:stream"] = "http://etherx.jabber.org/streams"; |
| 285 type = session.bosh_terminate and "terminate" or nil; | 287 type = session.bosh_terminate and "terminate" or nil; |
| 286 sid = sid; | 288 sid = sid; |
| 287 }; | 289 }; |
| 304 end | 306 end |
| 305 return true; | 307 return true; |
| 306 end | 308 end |
| 307 request.sid = sid; | 309 request.sid = sid; |
| 308 end | 310 end |
| 309 | 311 |
| 310 local session = sessions[sid]; | 312 local session = sessions[sid]; |
| 311 if not session then | 313 if not session then |
| 312 -- Unknown sid | 314 -- Unknown sid |
| 313 log("info", "Client tried to use sid '%s' which we don't know about", sid); | 315 log("info", "Client tried to use sid '%s' which we don't know about", sid); |
| 314 response.headers = default_headers; | |
| 315 response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" }))); | 316 response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" }))); |
| 316 context.notopen = nil; | 317 context.notopen = nil; |
| 317 return; | 318 return; |
| 318 end | 319 end |
| 319 | 320 |
| 320 if session.rid then | 321 if session.rid then |
| 321 local rid = tonumber(attr.rid); | 322 local rid = tonumber(attr.rid); |
| 322 local diff = rid - session.rid; | 323 local diff = rid - session.rid; |
| 323 if diff > 1 then | 324 if diff > 1 then |
| 324 session.log("warn", "rid too large (means a request was lost). Last rid: %d New rid: %s", session.rid, attr.rid); | 325 session.log("warn", "rid too large (means a request was lost). Last rid: %d New rid: %s", session.rid, attr.rid); |
| 331 t_insert(session.requests, response); | 332 t_insert(session.requests, response); |
| 332 return; | 333 return; |
| 333 end | 334 end |
| 334 session.rid = rid; | 335 session.rid = rid; |
| 335 end | 336 end |
| 336 | 337 |
| 337 if attr.type == "terminate" then | 338 if attr.type == "terminate" then |
| 338 -- Client wants to end this session, which we'll do | 339 -- Client wants to end this session, which we'll do |
| 339 -- after processing any stanzas in this request | 340 -- after processing any stanzas in this request |
| 340 session.bosh_terminate = true; | 341 session.bosh_terminate = true; |
| 341 end | 342 end |
| 346 session.bosh_processing = true; -- Used to suppress replies until processing of this request is done | 347 session.bosh_processing = true; -- Used to suppress replies until processing of this request is done |
| 347 | 348 |
| 348 if session.notopen then | 349 if session.notopen then |
| 349 local features = st.stanza("stream:features"); | 350 local features = st.stanza("stream:features"); |
| 350 hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); | 351 hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); |
| 351 fire_event("stream-features", session, features); | 352 session.send(features); |
| 352 session.send(tostring(features)); | |
| 353 session.notopen = nil; | 353 session.notopen = nil; |
| 354 end | 354 end |
| 355 end | 355 end |
| 356 | 356 |
| 357 local function handleerr(err) log("error", "Traceback[bosh]: %s", traceback(tostring(err), 2)); end | 357 local function handleerr(err) log("error", "Traceback[bosh]: %s", traceback(tostring(err), 2)); end |
| 368 return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); | 368 return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); |
| 369 end | 369 end |
| 370 end | 370 end |
| 371 end | 371 end |
| 372 | 372 |
| 373 function stream_callbacks.streamclosed(request) | 373 function stream_callbacks.streamclosed(context) |
| 374 local session = sessions[request.sid]; | 374 local session = sessions[context.sid]; |
| 375 if session then | 375 if session then |
| 376 session.bosh_processing = false; | 376 session.bosh_processing = false; |
| 377 if #session.send_buffer > 0 then | 377 if #session.send_buffer > 0 then |
| 378 session.send(""); | 378 session.send(""); |
| 379 end | 379 end |
| 382 | 382 |
| 383 function stream_callbacks.error(context, error) | 383 function stream_callbacks.error(context, error) |
| 384 log("debug", "Error parsing BOSH request payload; %s", error); | 384 log("debug", "Error parsing BOSH request payload; %s", error); |
| 385 if not context.sid then | 385 if not context.sid then |
| 386 local response = context.response; | 386 local response = context.response; |
| 387 response.headers = default_headers; | |
| 388 response.status_code = 400; | 387 response.status_code = 400; |
| 389 response:send(); | 388 response:send(); |
| 390 return; | 389 return; |
| 391 end | 390 end |
| 392 | 391 |
| 393 local session = sessions[context.sid]; | 392 local session = sessions[context.sid]; |
| 394 if error == "stream-error" then -- Remote stream error, we close normally | 393 if error == "stream-error" then -- Remote stream error, we close normally |
| 395 session:close(); | 394 session:close(); |
| 396 else | 395 else |
| 397 session:close({ condition = "bad-format", text = "Error processing stream" }); | 396 session:close({ condition = "bad-format", text = "Error processing stream" }); |
| 398 end | 397 end |
| 399 end | 398 end |
| 400 | 399 |
| 401 local dead_sessions = {}; | 400 local dead_sessions = module:shared("dead_sessions"); |
| 402 function on_timer() | 401 function on_timer() |
| 403 -- log("debug", "Checking for requests soon to timeout..."); | 402 -- log("debug", "Checking for requests soon to timeout..."); |
| 404 -- Identify requests timing out within the next few seconds | 403 -- Identify requests timing out within the next few seconds |
| 405 local now = os_time() + 3; | 404 local now = os_time() + 3; |
| 406 for request, reply_before in pairs(waiting_requests) do | 405 for request, reply_before in pairs(waiting_requests) do |
| 411 if request.conn then | 410 if request.conn then |
| 412 sessions[request.context.sid].send(""); | 411 sessions[request.context.sid].send(""); |
| 413 end | 412 end |
| 414 end | 413 end |
| 415 end | 414 end |
| 416 | 415 |
| 417 now = now - 3; | 416 now = now - 3; |
| 418 local n_dead_sessions = 0; | 417 local n_dead_sessions = 0; |
| 419 for session, close_after in pairs(inactive_sessions) do | 418 for session, close_after in pairs(inactive_sessions) do |
| 420 if close_after < now then | 419 if close_after < now then |
| 421 (session.log or log)("debug", "BOSH client inactive too long, destroying session at %d", now); | 420 (session.log or log)("debug", "BOSH client inactive too long, destroying session at %d", now); |
