changeset 3794:4b258329e6e4

mod_rest: Initial commit of another RESTful API module
author Kim Alvefur <zash@zash.se>
date Mon, 30 Dec 2019 04:04:34 +0100
parents 0d3926e49b55
children f51308fcba83
files .luacheckrc mod_rest/README.markdown mod_rest/mod_rest.lua
diffstat 3 files changed, 135 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/.luacheckrc	Wed Jan 01 10:11:08 2020 +0100
+++ b/.luacheckrc	Mon Dec 30 04:04:34 2019 +0100
@@ -1,6 +1,6 @@
 cache = true
 allow_defined_top = true
-unused_secondaries = false
+--unused_secondaries = false
 max_line_length = 150
 codes = true
 ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV" };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_rest/README.markdown	Mon Dec 30 04:04:34 2019 +0100
@@ -0,0 +1,54 @@
+---
+labels:
+- 'Stage-Alpha'
+summary: RESTful XMPP API
+---
+
+# Introduction
+
+This is yet another RESTful API for sending stanzas via Prosody.
+
+# Usage
+
+Note that there is currently **no authentication**, so be careful with
+exposing the API endpoint to the Internet.
+
+## Enabling
+
+``` {.lua}
+Component "rest.example.net" "rest"
+```
+
+## Sending stanzas
+
+The API endpoint becomes available at the path `/rest`, so the full URL
+will be something like `https://your-prosody.example:5281/rest`.
+
+To try it, simply `curl` an XML stanza payload:
+
+``` {.sh}
+curl https://prosody.example:5281/rest \
+    -H 'Content-Type: application/xmpp+xml' \
+    --data-binary '<message type="chat" to="user@example.org">
+            <body>Hello!</body>
+        </body>'
+```
+
+The `Content-Type` **MUST** be `application/xmpp+xml`.
+
+### Replies
+
+A POST containing an `<iq>` stanza automatically wait for the reply,
+long-polling style.
+
+``` {.sh}
+curl https://prosody.example:5281/rest \
+    -H 'Content-Type: application/xmpp+xml' \
+    --data-binary '<iq type="get" to="example.net">
+            <ping xmlns="urn:xmpp:ping"/>
+        </iq>'
+```
+
+# Compatibility
+
+Requires Prosody trunk / 0.12
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_rest/mod_rest.lua	Mon Dec 30 04:04:34 2019 +0100
@@ -0,0 +1,80 @@
+-- RESTful API
+--
+-- Copyright (c) 2019 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+
+local errors = require "util.error";
+local id = require "util.id";
+local jid = require "util.jid";
+local xml = require "util.xml";
+
+local allow_any_source = module:get_host_type() == "component";
+local validate_from_addresses = module:get_option_boolean("validate_from_addresses", true);
+
+local function handle_post(event)
+	local request, response = event.request, event.response;
+	if request.headers.content_type ~= "application/xmpp+xml" then
+		return errors.new({ code = 415, text = "'application/xmpp+xml' expected"  });
+	end
+	local payload, err = xml.parse(request.body);
+	if not payload then
+		-- parse fail
+		return errors.new({ code = 400, text = err });
+	end
+	local to = jid.prep(payload.attr.to);
+	if not to then
+		return errors.new({ code = 400, text = "Invalid destination JID" });
+	end
+	local from = module.host;
+	if allow_any_source and payload.attr.from then
+		from = jid.prep(payload.attr.from);
+		if not from then
+			return errors.new({ code = 400, text = "Invalid source JID" });
+		end
+		if validate_from_addresses and not jid.compare(from, module.host) then
+			return errors.new({ code = 403, text = "Source JID must belong to current host" });
+		end
+	end
+	payload.attr = {
+		from = from,
+		to = to,
+		id = payload.attr.id or id.medium(),
+		type = payload.attr.type,
+		["xml:lang"] = payload.attr["xml:lang"],
+	};
+	if payload.name == "iq" then
+		if payload.attr.type ~= "get" and payload.attr.type ~= "set" then
+			return errors.new({ code = 400, text = "'iq' stanza must be of type 'get' or 'set'" });
+		end
+		return module:send_iq(payload):next(
+			function (result)
+				response.headers.content_type = "application/xmpp+xml";
+				return tostring(result.stanza);
+			end,
+			function (error)
+				if error.context.stanza then
+					response.headers.content_type = "application/xmpp+xml";
+					return tostring(error.context.stanza);
+				else
+					return error;
+				end
+			end);
+	elseif payload.name == "message" or payload.name == "presence" then
+		if module:send(payload) then
+			return 202;
+		else
+			return 500;
+		end
+	else
+		return errors.new({ code = 400, text = "Invalid stanza, must be 'message', 'presence' or 'iq'." });
+	end
+end
+
+-- Handle stanzas submitted via HTTP
+module:depends("http");
+module:provides("http", {
+		route = {
+			POST = handle_post;
+		};
+	});