1
2-- Requires a host 'localhost' with SASL ANONYMOUS
3
4local bosh_url = "http://localhost:5280/http-bind"
5
6local logger = require "util.logger";
7
8local debug = false;
9
10local print = print;
11if debug then
12	logger.add_simple_sink(print, {
13		--"debug";
14		"info";
15		"warn";
16		"error";
17	});
18else
19	print = function () end
20end
21
22describe("#mod_bosh", function ()
23	local server = require "net.server_select";
24	package.loaded["net.server"] = server;
25	local async = require "util.async";
26	local timer = require "util.timer";
27	local http = require "net.http".new({ suppress_errors = false });
28
29	local function sleep(n)
30		local wait, done = async.waiter();
31		timer.add_task(n, function () done() end);
32		wait();
33	end
34
35	local st = require "util.stanza";
36	local xml = require "util.xml";
37
38	local function request(url, opt, cb, auto_wait)
39		local wait, done = async.waiter();
40		local ok, err;
41		http:request(url, opt, function (...)
42			ok, err = pcall(cb, ...);
43			if not ok then print("CAUGHT", err) end
44			done();
45		end);
46		local function err_wait(throw)
47			wait();
48			if throw ~= false and not ok then
49				error(err);
50			end
51			return ok, err;
52		end
53		if auto_wait == false then
54			return err_wait;
55		else
56			err_wait();
57		end
58	end
59
60	local function run_async(f)
61		local err;
62		local r = async.runner();
63		r:onerror(function (_, err_)
64			print("EER", err_)
65			err = err_;
66			server.setquitting("once");
67		end)
68		:onwaiting(function ()
69			--server.loop();
70		end)
71		:run(function ()
72			f()
73			server.setquitting("once");
74		end);
75		server.loop();
76		if err then
77			error(err);
78		end
79		if r.state ~= "ready" then
80			error("Runner in unexpected state: "..r.state);
81		end
82	end
83
84	it("test endpoint should be reachable", function ()
85		-- This is partly just to ensure the other tests have a chance to succeed
86		-- (i.e. the BOSH endpoint is up and functioning)
87		local function test()
88			request(bosh_url, nil, function (resp, code)
89				if code ~= 200 then
90					error("Unable to reach BOSH endpoint "..bosh_url);
91				end
92				assert.is_string(resp);
93			end);
94		end
95		run_async(test);
96	end);
97
98	it("should respond to past rids with past responses", function ()
99		local resp_1000_1, resp_1000_2 = "1", "2";
100
101		local function test_bosh()
102			local sid;
103
104		-- Set up BOSH session
105			request(bosh_url, {
106				body = tostring(st.stanza("body", {
107					to = "localhost";
108					from = "test@localhost";
109					content = "text/xml; charset=utf-8";
110					hold = "1";
111					rid = "998";
112					wait = "10";
113					["xml:lang"] = "en";
114					["xmpp:version"] = "1.0";
115					xmlns = "http://jabber.org/protocol/httpbind";
116					["xmlns:xmpp"] = "urn:xmpp:xbosh";
117				})
118				:tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
119				:tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" })
120					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" })
121						:tag("resource"):text("bosh-test1"):up()
122					:up()
123				:up()
124				);
125			}, function (response_body)
126				local resp = xml.parse(response_body);
127				if not response_body:find("<jid>", 1, true) then
128					print("ERR", resp:pretty_print());
129					error("Failed to set up BOSH session");
130				end
131				sid = assert(resp.attr.sid);
132				print("SID", sid);
133			end);
134
135		-- Receive some additional post-login stuff
136			request(bosh_url, {
137				body = tostring(st.stanza("body", {
138					sid = sid;
139					rid = "999";
140					content = "text/xml; charset=utf-8";
141					["xml:lang"] = "en";
142					xmlns = "http://jabber.org/protocol/httpbind";
143					["xmlns:xmpp"] = "urn:xmpp:xbosh";
144				})
145				)
146			}, function (response_body)
147				local resp = xml.parse(response_body);
148				print("RESP 999", resp:pretty_print());
149			end);
150
151		-- Send first long poll
152			print "SEND 1000#1"
153			local wait1000 = request(bosh_url, {
154				body = tostring(st.stanza("body", {
155					sid = sid;
156					rid = "1000";
157					content = "text/xml; charset=utf-8";
158					["xml:lang"] = "en";
159					xmlns = "http://jabber.org/protocol/httpbind";
160					["xmlns:xmpp"] = "urn:xmpp:xbosh";
161				}))
162			}, function (response_body)
163				local resp = xml.parse(response_body);
164				resp_1000_1 = resp;
165				print("RESP 1000#1", resp:pretty_print());
166			end, false);
167
168		-- Wait a couple of seconds
169			sleep(2)
170
171		-- Send an early request, causing rid 1000 to return early
172			print "SEND 1001"
173			local wait1001 = request(bosh_url, {
174				body = tostring(st.stanza("body", {
175					sid = sid;
176					rid = "1001";
177					content = "text/xml; charset=utf-8";
178					["xml:lang"] = "en";
179					xmlns = "http://jabber.org/protocol/httpbind";
180					["xmlns:xmpp"] = "urn:xmpp:xbosh";
181				}))
182			}, function (response_body)
183				local resp = xml.parse(response_body);
184				print("RESP 1001", resp:pretty_print());
185			end, false);
186		-- Ensure we've received the response for rid 1000
187			wait1000();
188
189		-- Sleep a couple of seconds
190			print "...pause..."
191			sleep(2);
192
193		-- Re-send rid 1000, we should get the same response
194			print "SEND 1000#2"
195			request(bosh_url, {
196				body = tostring(st.stanza("body", {
197					sid = sid;
198					rid = "1000";
199					content = "text/xml; charset=utf-8";
200					["xml:lang"] = "en";
201					xmlns = "http://jabber.org/protocol/httpbind";
202					["xmlns:xmpp"] = "urn:xmpp:xbosh";
203				}))
204			}, function (response_body)
205				local resp = xml.parse(response_body);
206				resp_1000_2 = resp;
207				print("RESP 1000#2", resp:pretty_print());
208			end);
209
210			local wait_final = request(bosh_url, {
211				body = tostring(st.stanza("body", {
212					sid = sid;
213					rid = "1002";
214					type = "terminate";
215					content = "text/xml; charset=utf-8";
216					["xml:lang"] = "en";
217					xmlns = "http://jabber.org/protocol/httpbind";
218					["xmlns:xmpp"] = "urn:xmpp:xbosh";
219				}))
220			}, function ()
221			end, false);
222
223			print "WAIT 1001"
224			wait1001();
225			wait_final();
226			print "DONE ALL"
227		end
228		run_async(test_bosh);
229		assert.truthy(resp_1000_1);
230		assert.same(resp_1000_1, resp_1000_2);
231	end);
232
233	it("should handle out-of-order requests", function ()
234		local function test()
235			local sid;
236		-- Set up BOSH session
237			local wait, done = async.waiter();
238			http:request(bosh_url, {
239				body = tostring(st.stanza("body", {
240					to = "localhost";
241					from = "test@localhost";
242					content = "text/xml; charset=utf-8";
243					hold = "1";
244					rid = "1";
245					wait = "10";
246					["xml:lang"] = "en";
247					["xmpp:version"] = "1.0";
248					xmlns = "http://jabber.org/protocol/httpbind";
249					["xmlns:xmpp"] = "urn:xmpp:xbosh";
250				}));
251			}, function (response_body)
252				local resp = xml.parse(response_body);
253				sid = assert(resp.attr.sid, "Failed to set up BOSH session");
254				print("SID", sid);
255				done();
256			end);
257			print "WAIT 1"
258			wait();
259			print "DONE 1"
260
261			local rid2_response_received = false;
262
263		-- Temporarily skip rid 2, to simulate missed request
264			local wait3, done3 = async.waiter();
265			http:request(bosh_url, {
266				body = tostring(st.stanza("body", {
267					sid = sid;
268					rid = "3";
269					content = "text/xml; charset=utf-8";
270					["xml:lang"] = "en";
271					xmlns = "http://jabber.org/protocol/httpbind";
272					["xmlns:xmpp"] = "urn:xmpp:xbosh";
273				}):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
274					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
275				:up()
276				)
277			}, function (response_body)
278				local resp = xml.parse(response_body);
279				print("RESP 3", resp:pretty_print());
280				done3();
281				-- The server should not respond to this request until
282				-- it has responded to rid 2
283				assert.is_true(rid2_response_received);
284			end);
285
286			print "SLEEPING"
287			sleep(2);
288			print "SLEPT"
289
290		-- Send the "missed" rid 2
291			local wait2, done2 = async.waiter();
292			http:request(bosh_url, {
293				body = tostring(st.stanza("body", {
294					sid = sid;
295					rid = "2";
296					content = "text/xml; charset=utf-8";
297					["xml:lang"] = "en";
298					xmlns = "http://jabber.org/protocol/httpbind";
299					["xmlns:xmpp"] = "urn:xmpp:xbosh";
300				}):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
301				)
302			}, function (response_body)
303				local resp = xml.parse(response_body);
304				print("RESP 2", resp:pretty_print());
305				rid2_response_received = true;
306				done2();
307			end);
308			print "WAIT 2"
309			wait2();
310			print "WAIT 3"
311			wait3();
312			print "QUIT"
313		end
314		run_async(test);
315	end);
316
317	it("should work", function ()
318		local function test()
319			local sid;
320		-- Set up BOSH session
321			local wait, done = async.waiter();
322			http:request(bosh_url, {
323				body = tostring(st.stanza("body", {
324					to = "localhost";
325					from = "test@localhost";
326					content = "text/xml; charset=utf-8";
327					hold = "1";
328					rid = "1";
329					wait = "10";
330					["xml:lang"] = "en";
331					["xmpp:version"] = "1.0";
332					xmlns = "http://jabber.org/protocol/httpbind";
333					["xmlns:xmpp"] = "urn:xmpp:xbosh";
334				}));
335			}, function (response_body)
336				local resp = xml.parse(response_body);
337				sid = assert(resp.attr.sid, "Failed to set up BOSH session");
338				print("SID", sid);
339				done();
340			end);
341			print "WAIT 1"
342			wait();
343			print "DONE 1"
344
345			local rid2_response_received = false;
346
347		-- Send the "missed" rid 2
348			local wait2, done2 = async.waiter();
349			http:request(bosh_url, {
350				body = tostring(st.stanza("body", {
351					sid = sid;
352					rid = "2";
353					content = "text/xml; charset=utf-8";
354					["xml:lang"] = "en";
355					xmlns = "http://jabber.org/protocol/httpbind";
356					["xmlns:xmpp"] = "urn:xmpp:xbosh";
357				}):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
358				)
359			}, function (response_body)
360				local resp = xml.parse(response_body);
361				print("RESP 2", resp:pretty_print());
362				rid2_response_received = true;
363				done2();
364			end);
365
366			local wait3, done3 = async.waiter();
367			http:request(bosh_url, {
368				body = tostring(st.stanza("body", {
369					sid = sid;
370					rid = "3";
371					content = "text/xml; charset=utf-8";
372					["xml:lang"] = "en";
373					xmlns = "http://jabber.org/protocol/httpbind";
374					["xmlns:xmpp"] = "urn:xmpp:xbosh";
375				}):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
376					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
377				:up()
378				)
379			}, function (response_body)
380				local resp = xml.parse(response_body);
381				print("RESP 3", resp:pretty_print());
382				done3();
383				-- The server should not respond to this request until
384				-- it has responded to rid 2
385				assert.is_true(rid2_response_received);
386			end);
387
388			print "SLEEPING"
389			sleep(2);
390			print "SLEPT"
391
392			print "WAIT 2"
393			wait2();
394			print "WAIT 3"
395			wait3();
396			print "QUIT"
397		end
398		run_async(test);
399	end);
400
401	it("should handle aborted pending requests", function ()
402		local resp_1000_1, resp_1000_2 = "1", "2";
403
404		local function test_bosh()
405			local sid;
406
407		-- Set up BOSH session
408			request(bosh_url, {
409				body = tostring(st.stanza("body", {
410					to = "localhost";
411					from = "test@localhost";
412					content = "text/xml; charset=utf-8";
413					hold = "1";
414					rid = "998";
415					wait = "10";
416					["xml:lang"] = "en";
417					["xmpp:version"] = "1.0";
418					xmlns = "http://jabber.org/protocol/httpbind";
419					["xmlns:xmpp"] = "urn:xmpp:xbosh";
420				})
421				:tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
422				:tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" })
423					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" })
424						:tag("resource"):text("bosh-test1"):up()
425					:up()
426				:up()
427				);
428			}, function (response_body)
429				local resp = xml.parse(response_body);
430				if not response_body:find("<jid>", 1, true) then
431					print("ERR", resp:pretty_print());
432					error("Failed to set up BOSH session");
433				end
434				sid = assert(resp.attr.sid);
435				print("SID", sid);
436			end);
437
438		-- Receive some additional post-login stuff
439			request(bosh_url, {
440				body = tostring(st.stanza("body", {
441					sid = sid;
442					rid = "999";
443					content = "text/xml; charset=utf-8";
444					["xml:lang"] = "en";
445					xmlns = "http://jabber.org/protocol/httpbind";
446					["xmlns:xmpp"] = "urn:xmpp:xbosh";
447				})
448				)
449			}, function (response_body)
450				local resp = xml.parse(response_body);
451				print("RESP 999", resp:pretty_print());
452			end);
453
454		-- Send first long poll
455			print "SEND 1000#1"
456			local wait1000_1 = request(bosh_url, {
457				body = tostring(st.stanza("body", {
458					sid = sid;
459					rid = "1000";
460					content = "text/xml; charset=utf-8";
461					["xml:lang"] = "en";
462					xmlns = "http://jabber.org/protocol/httpbind";
463					["xmlns:xmpp"] = "urn:xmpp:xbosh";
464				}))
465			}, function (response_body)
466				local resp = xml.parse(response_body);
467				resp_1000_1 = resp;
468				assert.is_nil(resp.attr.type);
469				print("RESP 1000#1", resp:pretty_print());
470			end, false);
471
472		-- Wait a couple of seconds
473			sleep(2)
474
475		-- Re-send rid 1000, we should eventually get a normal response (with no stanzas)
476			print "SEND 1000#2"
477			request(bosh_url, {
478				body = tostring(st.stanza("body", {
479					sid = sid;
480					rid = "1000";
481					content = "text/xml; charset=utf-8";
482					["xml:lang"] = "en";
483					xmlns = "http://jabber.org/protocol/httpbind";
484					["xmlns:xmpp"] = "urn:xmpp:xbosh";
485				}))
486			}, function (response_body)
487				local resp = xml.parse(response_body);
488				resp_1000_2 = resp;
489				assert.is_nil(resp.attr.type);
490				print("RESP 1000#2", resp:pretty_print());
491			end);
492
493			wait1000_1();
494			print "DONE ALL"
495		end
496		run_async(test_bosh);
497		assert.truthy(resp_1000_1);
498		assert.same(resp_1000_1, resp_1000_2);
499	end);
500
501	it("should fail on requests beyond rid window", function ()
502		local function test_bosh()
503			local sid;
504
505		-- Set up BOSH session
506			request(bosh_url, {
507				body = tostring(st.stanza("body", {
508					to = "localhost";
509					from = "test@localhost";
510					content = "text/xml; charset=utf-8";
511					hold = "1";
512					rid = "998";
513					wait = "10";
514					["xml:lang"] = "en";
515					["xmpp:version"] = "1.0";
516					xmlns = "http://jabber.org/protocol/httpbind";
517					["xmlns:xmpp"] = "urn:xmpp:xbosh";
518				})
519				:tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
520				:tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" })
521					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" })
522						:tag("resource"):text("bosh-test1"):up()
523					:up()
524				:up()
525				);
526			}, function (response_body)
527				local resp = xml.parse(response_body);
528				if not response_body:find("<jid>", 1, true) then
529					print("ERR", resp:pretty_print());
530					error("Failed to set up BOSH session");
531				end
532				sid = assert(resp.attr.sid);
533				print("SID", sid);
534			end);
535
536		-- Receive some additional post-login stuff
537			request(bosh_url, {
538				body = tostring(st.stanza("body", {
539					sid = sid;
540					rid = "999";
541					content = "text/xml; charset=utf-8";
542					["xml:lang"] = "en";
543					xmlns = "http://jabber.org/protocol/httpbind";
544					["xmlns:xmpp"] = "urn:xmpp:xbosh";
545				})
546				)
547			}, function (response_body)
548				local resp = xml.parse(response_body);
549				print("RESP 999", resp:pretty_print());
550			end);
551
552		-- Send poll with a rid that's too high (current + 2, where only current + 1 is allowed)
553			print "SEND 1002(!)"
554			request(bosh_url, {
555				body = tostring(st.stanza("body", {
556					sid = sid;
557					rid = "1002";
558					content = "text/xml; charset=utf-8";
559					["xml:lang"] = "en";
560					xmlns = "http://jabber.org/protocol/httpbind";
561					["xmlns:xmpp"] = "urn:xmpp:xbosh";
562				}))
563			}, function (response_body)
564				local resp = xml.parse(response_body);
565				assert.equal("terminate", resp.attr.type);
566				print("RESP 1002(!)", resp:pretty_print());
567			end);
568
569			print "DONE ALL"
570		end
571		run_async(test_bosh);
572	end);
573
574	it("should always succeed for requests within the rid window", function ()
575		local function test()
576			local sid;
577		-- Set up BOSH session
578			request(bosh_url, {
579				body = tostring(st.stanza("body", {
580					to = "localhost";
581					from = "test@localhost";
582					content = "text/xml; charset=utf-8";
583					hold = "1";
584					rid = "1";
585					wait = "10";
586					["xml:lang"] = "en";
587					["xmpp:version"] = "1.0";
588					xmlns = "http://jabber.org/protocol/httpbind";
589					["xmlns:xmpp"] = "urn:xmpp:xbosh";
590				}));
591			}, function (response_body)
592				local resp = xml.parse(response_body);
593				sid = assert(resp.attr.sid, "Failed to set up BOSH session");
594				print("SID", sid);
595			end);
596			print "DONE 1"
597
598			request(bosh_url, {
599				body = tostring(st.stanza("body", {
600					sid = sid;
601					rid = "2";
602					content = "text/xml; charset=utf-8";
603					["xml:lang"] = "en";
604					xmlns = "http://jabber.org/protocol/httpbind";
605					["xmlns:xmpp"] = "urn:xmpp:xbosh";
606				}):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
607				)
608			}, function (response_body)
609				local resp = xml.parse(response_body);
610				print("RESP 2", resp:pretty_print());
611			end);
612
613			local resp3;
614			request(bosh_url, {
615				body = tostring(st.stanza("body", {
616					sid = sid;
617					rid = "3";
618					content = "text/xml; charset=utf-8";
619					["xml:lang"] = "en";
620					xmlns = "http://jabber.org/protocol/httpbind";
621					["xmlns:xmpp"] = "urn:xmpp:xbosh";
622				}):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
623					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
624				:up()
625				)
626			}, function (response_body)
627				local resp = xml.parse(response_body);
628				print("RESP 3#1", resp:pretty_print());
629				resp3 = resp;
630			end);
631
632
633			request(bosh_url, {
634				body = tostring(st.stanza("body", {
635					sid = sid;
636					rid = "4";
637					content = "text/xml; charset=utf-8";
638					["xml:lang"] = "en";
639					xmlns = "http://jabber.org/protocol/httpbind";
640					["xmlns:xmpp"] = "urn:xmpp:xbosh";
641				}):tag("iq", { xmlns = "jabber:client", type = "get", id = "ping1" })
642					:tag("ping", { xmlns = "urn:xmpp:ping" }):up()
643				:up()
644				)
645			}, function (response_body)
646				local resp = xml.parse(response_body);
647				print("RESP 4", resp:pretty_print());
648			end);
649
650			request(bosh_url, {
651				body = tostring(st.stanza("body", {
652					sid = sid;
653					rid = "3";
654					content = "text/xml; charset=utf-8";
655					["xml:lang"] = "en";
656					xmlns = "http://jabber.org/protocol/httpbind";
657					["xmlns:xmpp"] = "urn:xmpp:xbosh";
658				}):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
659					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
660				:up()
661				)
662			}, function (response_body)
663				local resp = xml.parse(response_body);
664				print("RESP 3#2", resp:pretty_print());
665				assert.not_equal("terminate", resp.attr.type);
666				assert.same(resp3, resp);
667			end);
668
669
670			print "QUIT"
671		end
672		run_async(test);
673	end);
674end);
675