comparison mod_rest/mod_rest.lua @ 4938:bc8832c6696b

upstream merge
author Goffi <goffi@goffi.org>
date Wed, 11 May 2022 12:44:32 +0200
parents c83b009b5bc5
children e7b9bc629ecc
comparison
equal deleted inserted replaced
4913:3ddab718f717 4938:bc8832c6696b
1 -- RESTful API 1 -- RESTful API
2 -- 2 --
3 -- Copyright (c) 2019-2020 Kim Alvefur 3 -- Copyright (c) 2019-2022 Kim Alvefur
4 -- 4 --
5 -- This file is MIT/X11 licensed. 5 -- This file is MIT/X11 licensed.
6 6
7 local encodings = require "util.encodings"; 7 local encodings = require "util.encodings";
8 local base64 = encodings.base64; 8 local base64 = encodings.base64;
228 end 228 end
229 return form; 229 return form;
230 end 230 end
231 231
232 local function encode(type, s) 232 local function encode(type, s)
233 if type == "text/plain" then
234 return s:get_child_text("body") or "";
235 elseif type == "application/xmpp+xml" then
236 return tostring(s);
237 end
238 local mapped, err = jsonmap.st2json(s);
239 if not mapped then return mapped, err; end
233 if type == "application/json" then 240 if type == "application/json" then
234 return json.encode(jsonmap.st2json(s)); 241 return json.encode(mapped);
235 elseif type == "application/x-www-form-urlencoded" then 242 elseif type == "application/x-www-form-urlencoded" then
236 return http.formencode(flatten(jsonmap.st2json(s))); 243 return http.formencode(flatten(mapped));
237 elseif type == "application/cbor" then 244 elseif type == "application/cbor" then
238 return cbor.encode(jsonmap.st2json(s)); 245 return cbor.encode(mapped);
239 elseif type == "text/plain" then 246 end
240 return s:get_child_text("body") or ""; 247 error "unsupported encoding";
241 end
242 return tostring(s);
243 end 248 end
244 249
245 local post_errors = errors.init("mod_rest", { 250 local post_errors = errors.init("mod_rest", {
246 noauthz = { code = 401, type = "auth", condition = "not-authorized", text = "No credentials provided" }, 251 noauthz = { code = 401, type = "auth", condition = "not-authorized", text = "No credentials provided" },
247 unauthz = { code = 403, type = "auth", condition = "not-authorized", text = "Credentials not accepted" }, 252 unauthz = { code = 403, type = "auth", condition = "not-authorized", text = "Credentials not accepted" },
330 335
331 module:log("debug", "Received[rest]: %s", payload:top_tag()); 336 module:log("debug", "Received[rest]: %s", payload:top_tag());
332 local send_type = decide_type((request.headers.accept or "") ..",".. (request.headers.content_type or ""), supported_outputs) 337 local send_type = decide_type((request.headers.accept or "") ..",".. (request.headers.content_type or ""), supported_outputs)
333 338
334 if echo then 339 if echo then
340 local ret, err = errors.coerce(encode(send_type, payload));
341 if not ret then return err; end
335 response.headers.content_type = send_type; 342 response.headers.content_type = send_type;
336 return encode(send_type, payload); 343 return ret;
337 end 344 end
338 345
339 if payload.name == "iq" then 346 if payload.name == "iq" then
340 local responses = st.stanza("xmpp"); 347 local responses = st.stanza("xmpp");
341 function origin.send(stanza) 348 function origin.send(stanza)
407 }); 414 });
408 415
409 -- Forward stanzas from XMPP to HTTP and return any reply 416 -- Forward stanzas from XMPP to HTTP and return any reply
410 local rest_url = module:get_option_string("rest_callback_url", nil); 417 local rest_url = module:get_option_string("rest_callback_url", nil);
411 if rest_url then 418 if rest_url then
419 local function get_url() return rest_url; end
420 if rest_url:find("%b{}") then
421 local httputil = require "util.http";
422 local render_url = require"util.interpolation".new("%b{}", httputil.urlencode);
423 function get_url(stanza)
424 local at = stanza.attr;
425 return render_url(rest_url, { kind = stanza.name, type = at.type, to = at.to, from = at.from });
426 end
427 end
412 local send_type = module:get_option_string("rest_callback_content_type", "application/xmpp+xml"); 428 local send_type = module:get_option_string("rest_callback_content_type", "application/xmpp+xml");
413 if send_type == "json" then 429 if send_type == "json" then
414 send_type = "application/json"; 430 send_type = "application/json";
415 end 431 end
416 432
417 module:set_status("info", "Not yet connected"); 433 module:set_status("info", "Not yet connected");
418 http.request(rest_url, { 434 http.request(get_url(st.stanza("meta", { type = "info", to = module.host, from = module.host })), {
419 method = "OPTIONS", 435 method = "OPTIONS",
420 }, function (body, code, response) 436 }, function (body, code, response)
421 if code == 0 then 437 if code == 0 then
422 return module:log_status("error", "Could not connect to callback URL %q: %s", rest_url, body); 438 module:log_status("error", "Could not connect to callback URL %q: %s", rest_url, body);
439 elseif code == 200 then
440 module:set_status("info", "Connected");
441 if response.headers.accept then
442 send_type = decide_type(response.headers.accept, supported_outputs);
443 module:log("debug", "Set 'rest_callback_content_type' = %q based on Accept header", send_type);
444 end
423 else 445 else
424 module:set_status("info", "Connected"); 446 module:log_status("warn", "Unexpected response code %d from OPTIONS probe", code);
425 end 447 module:log("warn", "Endpoint said: %s", body);
426 if code == 200 and response.headers.accept then
427 send_type = decide_type(response.headers.accept, supported_outputs);
428 module:log("debug", "Set 'rest_callback_content_type' = %q based on Accept header", send_type);
429 end 448 end
430 end); 449 end);
431 450
432 local code2err = require "net.http.errors".registry; 451 local code2err = require "net.http.errors".registry;
433 452
446 465
447 -- Keep only the top level element and let the rest be GC'd 466 -- Keep only the top level element and let the rest be GC'd
448 stanza = st.clone(stanza, true); 467 stanza = st.clone(stanza, true);
449 468
450 module:log("debug", "Sending[rest]: %s", stanza:top_tag()); 469 module:log("debug", "Sending[rest]: %s", stanza:top_tag());
451 http.request(rest_url, { 470 http.request(get_url(stanza), {
452 body = request_body, 471 body = request_body,
453 headers = { 472 headers = {
454 ["Content-Type"] = send_type, 473 ["Content-Type"] = send_type,
455 ["Content-Language"] = stanza.attr["xml:lang"], 474 ["Content-Language"] = stanza.attr["xml:lang"],
456 Accept = table.concat(supported_inputs, ", "); 475 Accept = table.concat(supported_inputs, ", ");
532 end); 551 end);
533 552
534 return true; 553 return true;
535 end 554 end
536 555
537 if module:get_host_type() == "component" then 556 local send_kinds = module:get_option_set("rest_callback_stanzas", { "message", "presence", "iq" });
538 module:hook("iq/bare", handle_stanza, -1); 557
539 module:hook("message/bare", handle_stanza, -1); 558 local event_presets = {
540 module:hook("presence/bare", handle_stanza, -1); 559 -- Don't override everything on normal VirtualHosts by default
541 module:hook("iq/full", handle_stanza, -1); 560 ["local"] = { "host" },
542 module:hook("message/full", handle_stanza, -1); 561 -- Comonents get to handle all kinds of stanzas
543 module:hook("presence/full", handle_stanza, -1); 562 ["component"] = { "bare", "full", "host" },
544 module:hook("iq/host", handle_stanza, -1); 563 };
545 module:hook("message/host", handle_stanza, -1); 564 local hook_events = module:get_option_set("rest_callback_events", event_presets[module:get_host_type()]);
546 module:hook("presence/host", handle_stanza, -1); 565 for kind in send_kinds do
547 else 566 for event in hook_events do
548 -- Don't override everything on normal VirtualHosts 567 module:hook(kind.."/"..event, handle_stanza, -1);
549 module:hook("iq/host", handle_stanza, -1); 568 end
550 module:hook("message/host", handle_stanza, -1);
551 module:hook("presence/host", handle_stanza, -1);
552 end 569 end
553 end 570 end
554 571
555 local supported_errors = { 572 local supported_errors = {
556 "text/html", 573 "text/html",