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);