comparison mod_rest/mod_rest.lua @ 4477:8df6cc648963

mod_rest: Add more REST-looking way to send stanzas Example: POST /rest/message/chat/juliet@example.net { body: "Hello" } Becomes equivalent to POST /rest { kind: "message", type: "chat", to: "juliet@example.net", body: "Hello" } Sending messages as plain/text also becomes more convenient. IQ stazas are still weird, but we'll do something special for those.
author Kim Alvefur <zash@zash.se>
date Sun, 28 Feb 2021 19:25:45 +0100
parents 8b489203e4d3
children 7ab0c423688a
comparison
equal deleted inserted replaced
4476:125279f4a5b8 4477:8df6cc648963
57 return token_info.session; 57 return token_info.session;
58 end 58 end
59 return nil; 59 return nil;
60 end 60 end
61 61
62 local function parse(mimetype, data) 62 -- (table, string) -> table
63 local function amend_from_path(data, path)
64 local st_kind, st_type, st_to = path:match("^([mpi]%w+)/(%w+)/(.*)$");
65 if not st_kind then return; end
66 data.kind = st_kind;
67 data.type = st_type;
68 if st_to and st_to ~= "" then
69 data.to = st_to;
70 end
71 return data;
72 end
73
74 local function parse(mimetype, data, path) --> Stanza, error enum
63 mimetype = mimetype and mimetype:match("^[^; ]*"); 75 mimetype = mimetype and mimetype:match("^[^; ]*");
64 if mimetype == "application/xmpp+xml" then 76 if mimetype == "application/xmpp+xml" then
65 return xml.parse(data); 77 return xml.parse(data);
66 elseif mimetype == "application/json" then 78 elseif mimetype == "application/json" then
67 local parsed, err = json.decode(data); 79 local parsed, err = json.decode(data);
68 if not parsed then 80 if not parsed then
69 return parsed, err; 81 return parsed, err;
70 end 82 end
83 if path and not amend_from_path(parsed, path) then return nil, "invalid-path"; end
71 return jsonmap.json2st(parsed); 84 return jsonmap.json2st(parsed);
72 elseif mimetype == "application/cbor" and have_cbor then 85 elseif mimetype == "application/cbor" and have_cbor then
73 local parsed, err = cbor.decode(data); 86 local parsed, err = cbor.decode(data);
74 if not parsed then 87 if not parsed then
75 return parsed, err; 88 return parsed, err;
81 return parse("text/plain", parsed); 94 return parse("text/plain", parsed);
82 end 95 end
83 for i = #parsed, 1, -1 do 96 for i = #parsed, 1, -1 do
84 parsed[i] = nil; 97 parsed[i] = nil;
85 end 98 end
99 if path and not amend_from_path(parsed, path) then return nil, "invalid-path"; end
86 return jsonmap.json2st(parsed); 100 return jsonmap.json2st(parsed);
87 elseif mimetype == "text/plain" then 101 elseif mimetype == "text/plain" then
88 return st.message({ type = "chat" }, data); 102 if not path then
103 return st.message({ type = "chat" }, data);
104 end
105 local parsed = {};
106 if not amend_from_path(parsed, path) then return nil, "invalid-path"; end
107 if parsed.kind == "message" then
108 parsed.body = data;
109 elseif parsed.kind == "presence" then
110 parsed.show = data;
111 else
112 return nil, "invalid-path";
113 end
114 return jsonmap.json2st(parsed);
89 end 115 end
90 return nil, "unknown-payload-type"; 116 return nil, "unknown-payload-type";
91 end 117 end
92 118
93 local function decide_type(accept, supported_types) 119 local function decide_type(accept, supported_types)
160 iq_type = { code = 422, condition = "invalid-xml", text = "'iq' stanza must be of type 'get' or 'set'", }, 186 iq_type = { code = 422, condition = "invalid-xml", text = "'iq' stanza must be of type 'get' or 'set'", },
161 iq_tags = { code = 422, condition = "bad-format", text = "'iq' stanza must have exactly one child tag", }, 187 iq_tags = { code = 422, condition = "bad-format", text = "'iq' stanza must have exactly one child tag", },
162 mediatype = { code = 415, condition = "bad-format", text = "Unsupported media type" }, 188 mediatype = { code = 415, condition = "bad-format", text = "Unsupported media type" },
163 }); 189 });
164 190
165 local function handle_post(event) 191 local function handle_post(event, path)
166 local request, response = event.request, event.response; 192 local request, response = event.request, event.response;
167 local from; 193 local from;
168 local origin; 194 local origin;
169 195
170 if not request.headers.authorization then 196 if not request.headers.authorization then
175 if not origin then 201 if not origin then
176 return post_errors.new("unauthz"); 202 return post_errors.new("unauthz");
177 end 203 end
178 from = jid.join(origin.username, origin.host, origin.resource); 204 from = jid.join(origin.username, origin.host, origin.resource);
179 end 205 end
180 local payload, err = parse(request.headers.content_type, request.body); 206 local payload, err = parse(request.headers.content_type, request.body, path);
181 if not payload then 207 if not payload then
182 -- parse fail 208 -- parse fail
183 local ctx = { error = err, type = request.headers.content_type, data = request.body, }; 209 local ctx = { error = err, type = request.headers.content_type, data = request.body, };
184 if err == "unknown-payload-type" then 210 if err == "unknown-payload-type" then
185 return post_errors.new("mediatype", ctx); 211 return post_errors.new("mediatype", ctx);
265 -- Handle stanzas submitted via HTTP 291 -- Handle stanzas submitted via HTTP
266 module:depends("http"); 292 module:depends("http");
267 module:provides("http", { 293 module:provides("http", {
268 route = { 294 route = {
269 POST = handle_post; 295 POST = handle_post;
296 ["POST /*"] = handle_post;
270 }; 297 };
271 }); 298 });
272 299
273 -- Forward stanzas from XMPP to HTTP and return any reply 300 -- Forward stanzas from XMPP to HTTP and return any reply
274 local rest_url = module:get_option_string("rest_callback_url", nil); 301 local rest_url = module:get_option_string("rest_callback_url", nil);