changeset 4229:3943032533a7

mod_http_prebind: New module
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Sun, 25 Oct 2020 17:58:02 +0100
parents 3eb595cf847f
children 495a23d61418
files mod_http_prebind/README.markdown mod_http_prebind/mod_http_prebind.lua
diffstat 2 files changed, 155 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_http_prebind/README.markdown	Sun Oct 25 17:58:02 2020 +0100
@@ -0,0 +1,14 @@
+---
+labels:
+- 'Stage-Alpha'
+summary: Implements BOSH pre-bind
+...
+
+For why this can be useful, see
+https://metajack.im/2009/12/14/fastest-xmpp-sessions-with-http-prebinding/
+
+# To enable this module
+
+Add `"http_prebind"` to `modules_enabled` on an anonymous virtual host.
+
+This only works on anonymous ones for now.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_http_prebind/mod_http_prebind.lua	Sun Oct 25 17:58:02 2020 +0100
@@ -0,0 +1,141 @@
+module:depends("http");
+
+local http = require "net.http";
+local format = require "util.format".format;
+local json_encode = require "util.json".encode;
+local promise = require "util.promise";
+local xml = require "util.xml";
+local t_insert = table.insert;
+
+local function new_options(host)
+	return {
+		headers = {
+			["Content-Type"] = "text/xml; charset=utf-8",
+			["Host"] = host,
+		},
+		method = "POST",
+	};
+end
+
+local function connect_to_bosh(url, hostname)
+	local rid = math.random(100000, 100000000)
+	local options = new_options(hostname);
+	options.body = format([[<body content='text/xml; charset=utf-8'
+	      hold='1'
+	      rid='%d'
+	      to='%s'
+	      wait='60'
+	      xml:lang='en'
+	      xmpp:version='1.0'
+	      xmlns='http://jabber.org/protocol/httpbind'
+	      xmlns:xmpp='urn:xmpp:xbosh'/>]], rid, hostname);
+	local rid = rid + 1;
+	return promise.new(function (on_fulfilled, on_error)
+		assert(http.request(url, options, function (body, code)
+			if code ~= 200 then
+				on_error("Failed to fetch, HTTP error code "..code);
+				return;
+			end
+			local body = xml.parse(body);
+			local sid = body.attr.sid;
+			local mechanisms = {};
+			for mechanism in body:get_child("features", "http://etherx.jabber.org/streams")
+				:get_child("mechanisms", "urn:ietf:params:xml:ns:xmpp-sasl")
+					:childtags("mechanism", "urn:ietf:params:xml:ns:xmpp-sasl") do
+				mechanisms[mechanism:get_text()] = true;
+			end
+			on_fulfilled({ url = url, sid = sid, rid = rid, mechanisms = mechanisms });
+		end));
+	end);
+end
+
+local function authenticate(data)
+	local options = new_options();
+	options.body = format([[<body sid='%s'
+	      rid='%d'
+	      xmlns='http://jabber.org/protocol/httpbind'>
+		<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'
+		      mechanism='ANONYMOUS'/>
+	</body>]], data.sid, data.rid);
+	data.rid = data.rid + 1;
+	return promise.new(function (on_fulfilled, on_error)
+		if data.mechanisms["ANONYMOUS"] == nil then
+			on_error("No SASL ANONYMOUS mechanism supported on this host.");
+			return;
+		end
+		assert(http.request(data.url, options, function (body, code)
+			if code ~= 200 then
+				on_error("Failed to fetch, HTTP error code "..code);
+				return;
+			end
+			local body = xml.parse(body);
+			local success = body:get_child("success", "urn:ietf:params:xml:ns:xmpp-sasl");
+			if success then
+				data.mechanisms = nil;
+				on_fulfilled(data);
+			else
+				on_error("Authentication failed.");
+			end
+		end));
+	end);
+end;
+
+local function restart_stream(data)
+	local options = new_options();
+	options.body = format([[
+	<body sid='%s'
+	      rid='%d'
+	      xml:lang='en'
+	      xmlns='http://jabber.org/protocol/httpbind'
+	      xmlns:xmpp='urn:xmpp:xbosh'
+	      xmpp:restart='true'/>]], data.sid, data.rid);
+	data.rid = data.rid + 1;
+	return promise.new(function (on_fulfilled, on_error)
+		assert(http.request(data.url, options, function (body, code)
+			if code ~= 200 then
+				on_error("Failed to fetch, HTTP error code "..code);
+				return;
+			end
+			local body = xml.parse(body);
+			on_fulfilled(data);
+		end));
+	end);
+end;
+
+local function bind(data)
+	local options = new_options();
+	options.body = format([[
+	<body sid='%s'
+	      rid='%d'
+	      xmlns='http://jabber.org/protocol/httpbind'>
+		<iq xmlns='jabber:client'
+		    type='set'>
+			<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
+		</iq>
+	</body>]], data.sid, data.rid);
+	data.rid = data.rid + 1;
+	return promise.new(function (on_fulfilled, on_error)
+		assert(http.request(data.url, options, function (body, code)
+			if code ~= 200 then
+				on_error("Failed to fetch, HTTP error code "..code);
+				return;
+			end
+			local body = xml.parse(body);
+			local jid = body:get_child("iq", "jabber:client")
+				:get_child("bind", "urn:ietf:params:xml:ns:xmpp-bind")
+					:get_child_text("jid", "urn:ietf:params:xml:ns:xmpp-bind");
+			on_fulfilled(json_encode({rid = data.rid, sid = data.sid, jid = jid}));
+		end));
+	end);
+end;
+
+module:provides("http", {
+	route = {
+		["GET"] = function (event)
+			return connect_to_bosh("http://[::1]:5280/http-bind", "anon.localhost")
+				:next(authenticate)
+				:next(restart_stream)
+				:next(bind);
+		end;
+	};
+});