File

mod_rest/jsonmap.lib.lua @ 3866:c0df50ce96f0

mod_rest: Handle internal http request errors early and then return Skips over attempted parsing of the payload which usually failed since the body is an error string like "connection refused", so this produced useless errors.
author Kim Alvefur <zash@zash.se>
date Sat, 25 Jan 2020 20:22:12 +0100
parent 3860:9752a6f1b9f3
child 3870:3261a82884bb
line wrap: on
line source

local array = require "util.array";
local jid = require "util.jid";
local json = require "util.json";
local st = require "util.stanza";
local xml = require "util.xml";

local simple_types = {
	-- basic message
	body = "text_tag",
	subject = "text_tag",
	thread = "text_tag",

	-- basic presence
	show = "text_tag",
	status = "text_tag",
	priority = "text_tag",

	state = {"name", "http://jabber.org/protocol/chatstates"},
	nick = {"text_tag", "http://jabber.org/protocol/nick", "nick"},
	delay = {"attr", "urn:xmpp:delay", "delay", "stamp"},
	replace = {"attr", "urn:xmpp:message-correct:0", "replace", "id"},

	-- XEP-0045 MUC
	-- TODO history, password, ???
	join = {"bool_tag", "http://jabber.org/protocol/muc", "x"},

	-- XEP-0071
	html = {
		"func", "http://jabber.org/protocol/xhtml-im", "html",
		function (s) --> json string
			return (tostring(s:get_child("body", "http://www.w3.org/1999/xhtml")):gsub(" xmlns='[^']*'","", 1));
		end;
		function (s) --> xml
			if type(s) == "string" then
				return assert(xml.parse([[<x:html xmlns:x='http://jabber.org/protocol/xhtml-im' xmlns='http://www.w3.org/1999/xhtml'>]]..s..[[</x:html>]]));
			end
		end;
	};

	-- XEP-0199: XMPP Ping
	ping = {"bool_tag", "urn:xmpp:ping", "ping"},

	-- XEP-0092: Software Version
	version = {"func", "jabber:iq:version", "query",
		function (s)
			return {
				name = s:get_child_text("name");
				version = s:get_child_text("version");
				os = s:get_child_text("os");
			}
		end,
		function (s)
			local v = st.stanza("query", { xmlns = "jabber:iq:version" });
			if type(s) == "table" then
				v:text_tag("name", s.name);
				v:text_tag("version", s.version);
				if s.os then
					v:text_tag("os", s.os);
				end
			end
			return v;
		end
	};

	-- XEP-0030
	disco = {
		"func", "http://jabber.org/protocol/disco#info", "query",
		function (s) --> array of features
			local identities, features = array(), array();
			for tag in s:childtags() do
				if tag.name == "identity" and tag.attr.category and tag.attr.type then
					identities:push({ category = tag.attr.category, type = tag.attr.type, name = tag.attr.name });
				elseif tag.name == "feature" and tag.attr.var then
					features:push(tag.attr.var);
				end
			end
			return { node = s.attr.node, identities = identities, features = features, };
		end;
		function  (s)
			if type(s) == "table" and s ~= json.null then
				local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", node = s.node });
				if s.identities then
					for _, identity in ipairs(s.identities) do
						disco:tag("identity", { category = identity.category, type = identity.type, name = identity.name }):up();
					end
				end
				if s.features then
					for _, feature in ipairs(s.features) do
						disco:tag("feature", { var = feature }):up();
					end
				end
				return disco;
			else
				st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", });
			end
		end;
	};

	items = {
		"func", "http://jabber.org/protocol/disco#items", "query",
		function (s) --> array of features
			local items = array();
			for item in s:childtags("item") do
				items:push({ jid = item.attr.jid, node = item.attr.node, name = item.attr.name });
			end
			return items;
		end;
		function  (s)
			local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#items" });
			if type(s) == "table" and s ~= json.null then
				for _, item in ipairs(s) do
					if type(item) == "string" then
						disco:tag("item", { jid = item });
					elseif type(item) == "table" then
						disco:tag("item", { jid = item.jid, node = item.node, name = item.name });
					end
				end
			end
			return disco;
		end;
	};

	-- XEP-0066: Out of Band Data
	oob_url = {"func", "jabber:iq:oob", "query",
		function (s)
			return s:get_child_text("url");
		end;
		function (s)
			if type(s) == "string" then
				return st.stanza("query", { xmlns = "jabber:iq:oob" }):text_tag("url", s);
			end
		end;
	};

	-- XEP-XXXX: User-defined Data Transfer
	payload = {"func", "urn:xmpp:udt:0", "payload",
		function (s)
			local rawjson = s:get_child_text("json", "urn:xmpp:json:0");
			if not rawjson then return nil, "missing-json-payload"; end
			local parsed, err = json.decode(rawjson);
			if not parsed then return nil, err; end
			return {
				datatype = s.attr.datatype;
				data = parsed;
			};
		end;
		function (s)
			if type(s) == "table" then
				return st.stanza("payload", { xmlns = "urn:xmpp:udt:0", datatype = s.datatype })
					:tag("json", { xmlns = "urn:xmpp:json:0" }):text(json.encode(s.data));
			end;
		end
	};

};

local implied_kinds = {
	disco = "iq",
	items = "iq",
	ping = "iq",
	version = "iq",

	body = "message",
	html = "message",
	replace = "message",
	state = "message",
	subject = "message",
	thread = "message",

	join = "presence",
	priority = "presence",
	show = "presence",
	status = "presence",
}

local kind_by_type = {
	get = "iq", set = "iq", result = "iq",
	normal = "message", chat = "message", headline = "message", groupchat = "message",
	available = "presence", unavailable = "presence",
	subscribe = "presence", unsubscribe = "presence",
	subscribed = "presence", unsubscribed = "presence",
}

local function st2json(s)
	local t = {
		kind = s.name,
		type = s.attr.type,
		to = s.attr.to,
		from = s.attr.from,
		id = s.attr.id,
	};
	if s.name == "presence" and not s.attr.type then
		t.type = "available";
	end

	if t.to then
		t.to = jid.prep(t.to);
		if not t.to then return nil, "invalid-jid-to"; end
	end
	if t.from then
		t.from = jid.prep(t.from);
		if not t.from then return nil, "invalid-jid-from"; end
	end

	if t.type == "error" then
		local err_typ, err_condition, err_text = s:get_error();
		t.error = {
			type = err_typ,
			condition = err_condition,
			text = err_text
		};
		return t;
	end

	for k, typ in pairs(simple_types) do
		if typ == "text_tag" then
			t[k] = s:get_child_text(k);
		elseif typ[1] == "text_tag" then
			t[k] = s:get_child_text(typ[3], typ[2]);
		elseif typ[1] == "name" then
			local child = s:get_child(nil, typ[2]);
			if child then
				t[k] = child.name;
			end
		elseif typ[1] == "attr" then
			local child = s:get_child(typ[3], typ[2])
			if child then
				t[k] = child.attr[typ[4]];
			end
		elseif typ[1] == "bool_tag" then
			if s:get_child(typ[3], typ[2]) then
				t[k] = true;
			end
		elseif typ[1] == "func" then
			local child = s:get_child(typ[3], typ[2] or k);
			-- TODO handle err
			if child then
				t[k] = typ[4](child);
			end
		end
	end

	return t;
end

local function str(s)
	if type(s) == "string" then
		return s;
	end
end

local function json2st(t)
	if type(t) ~= "table" or not str(next(t)) then
		return nil, "invalid-json";
	end
	local kind = str(t.kind) or kind_by_type[str(t.type)];
	if not kind then
		for k, implied in pairs(implied_kinds) do
			if t[k] then
				kind = implied;
				break
			end
		end
	end

	local s = st.stanza(kind or "message", {
		type = t.type ~= "available" and str(t.type) or nil,
		to = str(t.to) and jid.prep(t.to);
		from = str(t.to) and jid.prep(t.from);
		id = str(t.id),
	});

	if t.to and not s.attr.to then
		return nil, "invalid-jid-to";
	end
	if t.from and not s.attr.from then
		return nil, "invalid-jid-from";
	end
	if kind == "iq" and not s.attr.type then
		s.attr.type = "get";
	end

	if type(t.error) == "table" then
		return st.error_reply(st.reply(s), str(t.error.type), str(t.error.condition), str(t.error.text));
	elseif t.type == "error" then
		s:text_tag("error", t.body, { code = t.error_code and tostring(t.error_code) });
		return s;
	end

	for k, v in pairs(t) do
		local typ = simple_types[k];
		if typ then
			if typ == "text_tag" then
				s:text_tag(k, v);
			elseif typ[1] == "text_tag" then
				s:text_tag(typ[3] or k, v, typ[2] and { xmlns = typ[2] });
			elseif typ[1] == "name" then
				s:tag(v, { xmlns = typ[2] }):up();
			elseif typ[1] == "attr" then
				s:tag(typ[3] or k, { xmlns = typ[2], [ typ[4] or k ] = v }):up();
			elseif typ[1] == "bool_tag" then
				s:tag(typ[3] or k, { xmlns = typ[2] }):up();
			elseif typ[1] == "func" then
				s:add_child(typ[5](v)):up();
			end
		end
	end

	s:reset();

	return s;
end

return {
	st2json = st2json;
	json2st = json2st;
};