aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.busted10
-rw-r--r--spec/mod_bosh_spec.lua674
2 files changed, 684 insertions, 0 deletions
diff --git a/.busted b/.busted
new file mode 100644
index 00000000..8e444f9c
--- /dev/null
+++ b/.busted
@@ -0,0 +1,10 @@
+return {
+ _all = {
+ },
+ default = {
+ ["exclude-tags"] = "mod_bosh";
+ },
+ bosh = {
+ tags = "mod_bosh";
+ }
+}
diff --git a/spec/mod_bosh_spec.lua b/spec/mod_bosh_spec.lua
new file mode 100644
index 00000000..053e4b2a
--- /dev/null
+++ b/spec/mod_bosh_spec.lua
@@ -0,0 +1,674 @@
+
+-- Requires a host 'localhost' with SASL ANONYMOUS
+
+local bosh_url = "http://localhost:5280/http-bind"
+
+local logger = require "util.logger";
+
+local debug = false;
+
+local print = print;
+if debug then
+ logger.add_simple_sink(print, {
+ --"debug";
+ "info";
+ "warn";
+ "error";
+ });
+else
+ print = function () end
+end
+
+describe("#mod_bosh", function ()
+ local server = require "net.server_select";
+ package.loaded["net.server"] = server;
+ local async = require "util.async";
+ local timer = require "util.timer";
+ local http = require "net.http".new({ suppress_errors = false });
+
+ local function sleep(n)
+ local wait, done = async.waiter();
+ timer.add_task(n, function () done() end);
+ wait();
+ end
+
+ local st = require "util.stanza";
+ local xml = require "util.xml";
+
+ local function request(url, opt, cb, auto_wait)
+ local wait, done = async.waiter();
+ local ok, err;
+ http:request(url, opt, function (...)
+ ok, err = pcall(cb, ...);
+ if not ok then print("CAUGHT", err) end
+ done();
+ end);
+ local function err_wait(throw)
+ wait();
+ if throw ~= false and not ok then
+ error(err);
+ end
+ return ok, err;
+ end
+ if auto_wait == false then
+ return err_wait;
+ else
+ err_wait();
+ end
+ end
+
+ local function run_async(f)
+ local err;
+ local r = async.runner();
+ r:onerror(function (_, err_)
+ print("EER", err_)
+ err = err_;
+ server.setquitting("once");
+ end)
+ :onwaiting(function ()
+ --server.loop();
+ end)
+ :run(function ()
+ f()
+ server.setquitting("once");
+ end);
+ server.loop();
+ if err then
+ error(err);
+ end
+ if r.state ~= "ready" then
+ error("Runner in unexpected state: "..r.state);
+ end
+ end
+
+ it("test endpoint should be reachable", function ()
+ -- This is partly just to ensure the other tests have a chance to succeed
+ -- (i.e. the BOSH endpoint is up and functioning)
+ local function test()
+ request(bosh_url, nil, function (resp, code)
+ if code ~= 200 then
+ error("Unable to reach BOSH endpoint "..bosh_url);
+ end
+ assert.is_string(resp);
+ end);
+ end
+ run_async(test);
+ end);
+
+ it("should respond to past rids with past responses", function ()
+ local resp_1000_1, resp_1000_2 = "1", "2";
+
+ local function test_bosh()
+ local sid;
+
+ -- Set up BOSH session
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ to = "localhost";
+ from = "test@localhost";
+ content = "text/xml; charset=utf-8";
+ hold = "1";
+ rid = "998";
+ wait = "10";
+ ["xml:lang"] = "en";
+ ["xmpp:version"] = "1.0";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ })
+ :tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+ :tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" })
+ :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" })
+ :tag("resource"):text("bosh-test1"):up()
+ :up()
+ :up()
+ );
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ if not response_body:find("<jid>", 1, true) then
+ print("ERR", resp:pretty_print());
+ error("Failed to set up BOSH session");
+ end
+ sid = assert(resp.attr.sid);
+ print("SID", sid);
+ end);
+
+ -- Receive some additional post-login stuff
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "999";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ })
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 999", resp:pretty_print());
+ end);
+
+ -- Send first long poll
+ print "SEND 1000#1"
+ local wait1000 = request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "1000";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }))
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ resp_1000_1 = resp;
+ print("RESP 1000#1", resp:pretty_print());
+ end, false);
+
+ -- Wait a couple of seconds
+ sleep(2)
+
+ -- Send an early request, causing rid 1000 to return early
+ print "SEND 1001"
+ local wait1001 = request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "1001";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }))
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 1001", resp:pretty_print());
+ end, false);
+ -- Ensure we've received the response for rid 1000
+ wait1000();
+
+ -- Sleep a couple of seconds
+ print "...pause..."
+ sleep(2);
+
+ -- Re-send rid 1000, we should get the same response
+ print "SEND 1000#2"
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "1000";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }))
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ resp_1000_2 = resp;
+ print("RESP 1000#2", resp:pretty_print());
+ end);
+
+ local wait_final = request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "1002";
+ type = "terminate";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }))
+ }, function ()
+ end, false);
+
+ print "WAIT 1001"
+ wait1001();
+ wait_final();
+ print "DONE ALL"
+ end
+ run_async(test_bosh);
+ assert.truthy(resp_1000_1);
+ assert.same(resp_1000_1, resp_1000_2);
+ end);
+
+ it("should handle out-of-order requests", function ()
+ local function test()
+ local sid;
+ -- Set up BOSH session
+ local wait, done = async.waiter();
+ http:request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ to = "localhost";
+ from = "test@localhost";
+ content = "text/xml; charset=utf-8";
+ hold = "1";
+ rid = "1";
+ wait = "10";
+ ["xml:lang"] = "en";
+ ["xmpp:version"] = "1.0";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }));
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ sid = assert(resp.attr.sid, "Failed to set up BOSH session");
+ print("SID", sid);
+ done();
+ end);
+ print "WAIT 1"
+ wait();
+ print "DONE 1"
+
+ local rid2_response_received = false;
+
+ -- Temporarily skip rid 2, to simulate missed request
+ local wait3, done3 = async.waiter();
+ http:request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "3";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
+ :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
+ :up()
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 3", resp:pretty_print());
+ done3();
+ -- The server should not respond to this request until
+ -- it has responded to rid 2
+ assert.is_true(rid2_response_received);
+ end);
+
+ print "SLEEPING"
+ sleep(2);
+ print "SLEPT"
+
+ -- Send the "missed" rid 2
+ local wait2, done2 = async.waiter();
+ http:request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "2";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 2", resp:pretty_print());
+ rid2_response_received = true;
+ done2();
+ end);
+ print "WAIT 2"
+ wait2();
+ print "WAIT 3"
+ wait3();
+ print "QUIT"
+ end
+ run_async(test);
+ end);
+
+ it("should work", function ()
+ local function test()
+ local sid;
+ -- Set up BOSH session
+ local wait, done = async.waiter();
+ http:request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ to = "localhost";
+ from = "test@localhost";
+ content = "text/xml; charset=utf-8";
+ hold = "1";
+ rid = "1";
+ wait = "10";
+ ["xml:lang"] = "en";
+ ["xmpp:version"] = "1.0";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }));
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ sid = assert(resp.attr.sid, "Failed to set up BOSH session");
+ print("SID", sid);
+ done();
+ end);
+ print "WAIT 1"
+ wait();
+ print "DONE 1"
+
+ local rid2_response_received = false;
+
+ -- Send the "missed" rid 2
+ local wait2, done2 = async.waiter();
+ http:request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "2";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 2", resp:pretty_print());
+ rid2_response_received = true;
+ done2();
+ end);
+
+ local wait3, done3 = async.waiter();
+ http:request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "3";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
+ :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
+ :up()
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 3", resp:pretty_print());
+ done3();
+ -- The server should not respond to this request until
+ -- it has responded to rid 2
+ assert.is_true(rid2_response_received);
+ end);
+
+ print "SLEEPING"
+ sleep(2);
+ print "SLEPT"
+
+ print "WAIT 2"
+ wait2();
+ print "WAIT 3"
+ wait3();
+ print "QUIT"
+ end
+ run_async(test);
+ end);
+
+ it("should handle aborted pending requests", function ()
+ local resp_1000_1, resp_1000_2 = "1", "2";
+
+ local function test_bosh()
+ local sid;
+
+ -- Set up BOSH session
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ to = "localhost";
+ from = "test@localhost";
+ content = "text/xml; charset=utf-8";
+ hold = "1";
+ rid = "998";
+ wait = "10";
+ ["xml:lang"] = "en";
+ ["xmpp:version"] = "1.0";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ })
+ :tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+ :tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" })
+ :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" })
+ :tag("resource"):text("bosh-test1"):up()
+ :up()
+ :up()
+ );
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ if not response_body:find("<jid>", 1, true) then
+ print("ERR", resp:pretty_print());
+ error("Failed to set up BOSH session");
+ end
+ sid = assert(resp.attr.sid);
+ print("SID", sid);
+ end);
+
+ -- Receive some additional post-login stuff
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "999";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ })
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 999", resp:pretty_print());
+ end);
+
+ -- Send first long poll
+ print "SEND 1000#1"
+ local wait1000_1 = request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "1000";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }))
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ resp_1000_1 = resp;
+ assert.is_nil(resp.attr.type);
+ print("RESP 1000#1", resp:pretty_print());
+ end, false);
+
+ -- Wait a couple of seconds
+ sleep(2)
+
+ -- Re-send rid 1000, we should eventually get a normal response (with no stanzas)
+ print "SEND 1000#2"
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "1000";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }))
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ resp_1000_2 = resp;
+ assert.is_nil(resp.attr.type);
+ print("RESP 1000#2", resp:pretty_print());
+ end);
+
+ wait1000_1();
+ print "DONE ALL"
+ end
+ run_async(test_bosh);
+ assert.truthy(resp_1000_1);
+ assert.same(resp_1000_1, resp_1000_2);
+ end);
+
+ it("should fail on requests beyond rid window", function ()
+ local function test_bosh()
+ local sid;
+
+ -- Set up BOSH session
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ to = "localhost";
+ from = "test@localhost";
+ content = "text/xml; charset=utf-8";
+ hold = "1";
+ rid = "998";
+ wait = "10";
+ ["xml:lang"] = "en";
+ ["xmpp:version"] = "1.0";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ })
+ :tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+ :tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" })
+ :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" })
+ :tag("resource"):text("bosh-test1"):up()
+ :up()
+ :up()
+ );
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ if not response_body:find("<jid>", 1, true) then
+ print("ERR", resp:pretty_print());
+ error("Failed to set up BOSH session");
+ end
+ sid = assert(resp.attr.sid);
+ print("SID", sid);
+ end);
+
+ -- Receive some additional post-login stuff
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "999";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ })
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 999", resp:pretty_print());
+ end);
+
+ -- Send poll with a rid that's too high (current + 2, where only current + 1 is allowed)
+ print "SEND 1002(!)"
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "1002";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }))
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ assert.equal("terminate", resp.attr.type);
+ print("RESP 1002(!)", resp:pretty_print());
+ end);
+
+ print "DONE ALL"
+ end
+ run_async(test_bosh);
+ end);
+
+ it("should always succeed for requests within the rid window", function ()
+ local function test()
+ local sid;
+ -- Set up BOSH session
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ to = "localhost";
+ from = "test@localhost";
+ content = "text/xml; charset=utf-8";
+ hold = "1";
+ rid = "1";
+ wait = "10";
+ ["xml:lang"] = "en";
+ ["xmpp:version"] = "1.0";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }));
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ sid = assert(resp.attr.sid, "Failed to set up BOSH session");
+ print("SID", sid);
+ end);
+ print "DONE 1"
+
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "2";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 2", resp:pretty_print());
+ end);
+
+ local resp3;
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "3";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
+ :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
+ :up()
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 3#1", resp:pretty_print());
+ resp3 = resp;
+ end);
+
+
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "4";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }):tag("iq", { xmlns = "jabber:client", type = "get", id = "ping1" })
+ :tag("ping", { xmlns = "urn:xmpp:ping" }):up()
+ :up()
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 4", resp:pretty_print());
+ end);
+
+ request(bosh_url, {
+ body = tostring(st.stanza("body", {
+ sid = sid;
+ rid = "3";
+ content = "text/xml; charset=utf-8";
+ ["xml:lang"] = "en";
+ xmlns = "http://jabber.org/protocol/httpbind";
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ }):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
+ :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
+ :up()
+ )
+ }, function (response_body)
+ local resp = xml.parse(response_body);
+ print("RESP 3#2", resp:pretty_print());
+ assert.not_equal("terminate", resp.attr.type);
+ assert.same(resp3, resp);
+ end);
+
+
+ print "QUIT"
+ end
+ run_async(test);
+ end);
+end);