changeset 1240:e0d97eb52ab8

mod_pubsub_mqtt: MQTT (a lightweight binary pubsub protocol) interface for mod_pubsub
author Matthew Wild <mwild1@gmail.com>
date Sun, 01 Dec 2013 19:12:08 +0000
parents cc5cbeeb9fc7
children 2380a5d71448
files mod_pubsub_mqtt/mod_pubsub_mqtt.lua mod_pubsub_mqtt/mqtt.lib.lua
diffstat 2 files changed, 322 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_pubsub_mqtt/mod_pubsub_mqtt.lua	Sun Dec 01 19:12:08 2013 +0000
@@ -0,0 +1,161 @@
+module:set_global();
+
+local mqtt = module:require "mqtt";
+local st = require "util.stanza";
+
+local pubsub_services = {};
+local pubsub_subscribers = {};
+local packet_handlers = {};
+
+function handle_packet(session, packet)
+	module:log("warn", "MQTT packet received! Length: %d", packet.length);
+	for k,v in pairs(packet) do
+		module:log("debug", "MQTT %s: %s", tostring(k), tostring(v));
+	end
+	local handler = packet_handlers[packet.type];
+	if not handler then
+		module:log("warn", "Unhandled command: %s", tostring(packet.type));
+		return;
+	end
+	handler(session, packet);
+end
+
+function packet_handlers.connect(session, packet)
+	session.conn:write(mqtt.serialize_packet{
+		type = "connack";
+		data = string.char(0x00, 0x00);
+	});
+end
+
+function packet_handlers.disconnect(session, packet)
+	session.conn:close();
+end
+
+function packet_handlers.publish(session, packet)
+	module:log("warn", "PUBLISH to %s", packet.topic);
+	local host, node = packet.topic:match("^([^/]+)/(.+)$");
+	local pubsub = pubsub_services[host];
+	if not pubsub then
+		module:log("warn", "Unable to locate host/node: %s", packet.topic);
+		return;
+	end
+	local id = "mqtt";
+	local ok, err = pubsub:publish(node, true, id,
+		st.stanza("data", { xmlns = "https://prosody.im/protocol/mqtt" })
+			:text(packet.data)
+	);
+	if not ok then
+		module:log("warn", "Error publishing MQTT data: %s", tostring(err));
+	end
+end
+
+function packet_handlers.subscribe(session, packet)
+	for _, topic in ipairs(packet.topics) do
+		module:log("warn", "SUBSCRIBE to %s", topic);
+		local host, node = topic:match("^([^/]+)/(.+)$");
+		local pubsub = pubsub_subscribers[host];
+		if not pubsub then
+			module:log("warn", "Unable to locate host/node: %s", topic);
+			return;
+		end
+		local node_subs = pubsub[node];
+		if not node_subs then
+			node_subs = {};
+			pubsub[node] = node_subs;
+		end
+		session.subscriptions[topic] = true;
+		node_subs[session] = true;
+	end
+	
+end
+
+function packet_handlers.pingreq(session, packet)
+	session.conn:write(mqtt.serialize_packet{type = "pingresp"});
+end
+
+local sessions = {};
+
+local mqtt_listener = {};
+
+function mqtt_listener.onconnect(conn)
+	sessions[conn] = {
+		conn = conn;
+		stream = mqtt.new_stream();
+		subscriptions = {};
+	};
+end
+
+function mqtt_listener.onincoming(conn, data)
+	local session = sessions[conn];
+	if session then
+		local packets = session.stream:feed(data);
+		for i = 1, #packets do
+			handle_packet(session, packets[i]);
+		end
+	end
+end
+
+function mqtt_listener.ondisconnect(conn)
+	local session = sessions[conn];
+	for topic in pairs(session.subscriptions) do
+		local host, node = topic:match("^([^/]+)/(.+)$");
+		local subs = pubsub_subscribers[host];
+		if subs then
+			local node_subs = subs[node];
+			if node_subs then
+				node_subs[session] = nil;
+			end
+		end
+	end
+	sessions[conn] = nil;
+	module:log("debug", "MQTT client disconnected");
+end
+
+module:provides("net", {
+	default_port = 1883;
+	listener = mqtt_listener;
+});
+
+local function tostring_content(item)
+	return tostring(item[1]);
+end
+
+local data_translators = setmetatable({
+	["data https://prosody.im/protocol/mqtt"] = tostring_content;
+	["json urn:xmpp:json:0"] = tostring_content;
+}, {
+	__index = function () return tostring; end;
+});
+
+function module.add_host(module)
+	local pubsub_module = hosts[module.host].modules.pubsub
+	if pubsub_module then
+		module:log("debug", "MQTT enabled for %s", module.host);
+		module:depends("pubsub");
+		pubsub_services[module.host] = assert(pubsub_module.service);
+		local subscribers = {};
+		pubsub_subscribers[module.host] = subscribers;
+		local function handle_publish(event)
+			-- Build MQTT packet
+			local packet = mqtt.serialize_packet{
+				type = "publish";
+				id = "\000\000";
+				topic = module.host.."/"..event.node;
+				data = data_translators[event.item.name.." "..event.item.attr.xmlns](event.item);
+			};
+			-- Broadcast to subscribers
+			module:log("debug", "Broadcasting PUBLISH to subscribers of %s/%s", module.host, event.node);
+			for session in pairs(subscribers[event.node] or {}) do
+				session.conn:write(packet);
+				module:log("debug", "Sent to %s", tostring(session));
+			end
+		end
+		pubsub_services[module.host].events.add_handler("item-published", handle_publish);
+		function module.unload()
+			module:log("debug", "MQTT disabled for %s", module.host);
+			pubsub_module.service.remove_handler("item-published", handle_publish);
+			pubsub_services[module.host] = nil;
+			pubsub_subscribers[module.host] = nil;
+		end
+	end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_pubsub_mqtt/mqtt.lib.lua	Sun Dec 01 19:12:08 2013 +0000
@@ -0,0 +1,161 @@
+local bit = require "bit";
+
+local stream_mt = {};
+stream_mt.__index = stream_mt;
+
+function stream_mt:read_bytes(n_bytes)
+	module:log("debug", "Reading %d bytes... (buffer: %d)", n_bytes, #self.buffer);
+	local data = self.buffer;
+	if not data then
+		module:log("debug", "No data, pausing.");
+		data = coroutine.yield();
+		module:log("debug", "Have %d bytes of data now (want %d)", #data, n_bytes);
+	end
+	if #data >= n_bytes then
+		data, self.buffer = data:sub(1, n_bytes), data:sub(n_bytes+1);
+	elseif #data < n_bytes then
+		module:log("debug", "Not enough data (only %d bytes out of %d), pausing.", #data, n_bytes);
+		self.buffer = data..coroutine.yield();
+		module:log("debug", "Now we have %d bytes, reading...", #data);
+		return self:read_bytes(n_bytes);
+	end
+	module:log("debug", "Returning %d bytes (buffer: %d)", #data, #self.buffer);
+	return data;
+end
+
+function stream_mt:read_string()
+	local len1, len2 = self:read_bytes(2):byte(1,2);
+	local len = bit.lshift(len1, 8) + len2;
+	return self:read_bytes(len), len+2;
+end
+
+local packet_type_codes = {
+	"connect", "connack",
+	"publish", "puback", "pubrec", "pubrel", "pubcomp",
+	"subscribe", "subak", "unsubscribe", "unsuback",
+	"pingreq", "pingresp",
+	"disconnect"
+};
+
+function stream_mt:read_packet()
+	local packet = {};
+	local header = self:read_bytes(1):byte();
+	packet.type = packet_type_codes[bit.rshift(bit.band(header, 0xf0), 4)];
+	packet.dup = bit.band(header, 0x08) == 0x08;
+	packet.qos = bit.rshift(bit.band(header, 0x06), 1);
+	packet.retain = bit.band(header, 0x01) == 0x01;
+	
+	-- Get length
+	local length, multiplier = 0, 1;
+	repeat
+		local digit = self:read_bytes(1):byte();
+		length = length + bit.band(digit, 0x7f)*multiplier;
+		multiplier = multiplier*128;
+	until bit.band(digit, 0x80) == 0;
+	packet.length = length;
+	if packet.type == "connect" then
+		if self:read_string() ~= "MQIsdp" then
+			module:log("warn", "Unexpected packet signature!");
+			packet.type = nil; -- Invalid packet
+		else
+			packet.version = self:read_bytes(1):byte();
+			packet.connect_flags = self:read_bytes(1):byte();
+			packet.keepalive_timer = self:read_bytes(1):byte();
+			length = length - 11;
+		end
+	elseif packet.type == "publish" then
+		packet.topic = self:read_string();
+		length = length - (#packet.topic+2);
+		if packet.qos == 1 or packet.qos == 2 then
+			packet.id = self:read_bytes(2);
+			length = length - 2;
+		end
+	elseif packet.type == "subscribe" then
+		if packet.qos == 1 or packet.qos == 2 then
+			packet.id = self:read_bytes(2);
+			length = length - 2;
+		end
+		local topics = {};
+		while length > 0 do
+			local topic, len = self:read_string();
+			table.insert(topics, topic);
+			self:read_bytes(1); -- QoS not used
+			length = length - (len+1);
+		end
+		packet.topics = topics;
+	end
+	if length > 0 then
+		packet.data = self:read_bytes(length);
+	end
+	return packet;
+end
+
+local function new_parser(self)
+	return coroutine.wrap(function (data)
+		self.buffer = data;
+		while true do
+			data = coroutine.yield(self:read_packet());
+			module:log("debug", "Parser: %d new bytes", #data);
+			self.buffer = (self.buffer or "")..data;
+		end
+	end);
+end
+
+function stream_mt:feed(data)
+	module:log("debug", "Feeding %d bytes", #data);
+	local packets = {};
+	local packet = self.parser(data);
+	while packet do
+		module:log("debug", "Received packet");
+		table.insert(packets, packet);
+		packet = self.parser("");
+	end
+	module:log("debug", "Returning %d packets", #packets);
+	return packets;
+end
+
+local function new_stream()
+	local stream = setmetatable({}, stream_mt);
+	stream.parser = new_parser(stream);
+	return stream;
+end
+
+local function serialize_packet(packet)
+	local type_num = 0;
+	for i, v in ipairs(packet_type_codes) do -- FIXME: I'm so tired right now.
+		if v == packet.type then
+			type_num = i;
+			break;
+		end
+	end
+	local header = string.char(bit.lshift(type_num, 4));
+	
+	if packet.type == "publish" then
+		local topic = packet.topic or "";
+		packet.data = string.char(bit.band(#topic, 0xff00), bit.band(#topic, 0x00ff))..topic..packet.data;
+	elseif packet.type == "suback" then
+		local t = {};
+		for _, topic in ipairs(packet.topics) do
+			table.insert(t, string.char(bit.band(#topic, 0xff00), bit.band(#topic, 0x00ff))..topic.."\000");
+		end
+		packet.data = table.concat(t);
+	end
+	
+	-- Get length
+	local length = #(packet.data or "");
+	repeat
+		local digit = length%128;
+		length = math.floor(length/128);
+		if length > 0 then
+			digit = bit.bor(digit, 0x80);
+		end
+		header = header..string.char(digit); -- FIXME: ...
+	until length <= 0;
+	
+	return header..(packet.data or "");
+end
+
+return {
+	new_stream = new_stream;
+	serialize_packet = serialize_packet;
+};