comparison mod_rest/mod_rest.lua @ 3939:a6b3b41a116c

Merge commit
author tmolitor <thilo@eightysoft.de>
date Sun, 08 Mar 2020 19:59:49 +0100
parents 93147b89ea67
children ae5ac41c391d
comparison
equal deleted inserted replaced
3938:3f4df08dce14 3939:a6b3b41a116c
81 return st.message({ type = "chat" }, data); 81 return st.message({ type = "chat" }, data);
82 end 82 end
83 return nil, "unknown-payload-type"; 83 return nil, "unknown-payload-type";
84 end 84 end
85 85
86 local supported_types = { 86 local function decide_type(accept, supported_types)
87 -- assumes the accept header is sorted
88 local ret = supported_types[1];
89 for i = 2, #supported_types do
90 if (accept:find(supported_types[i], 1, true) or 1000) < (accept:find(ret, 1, true) or 1000) then
91 ret = supported_types[i];
92 end
93 end
94 return ret;
95 end
96
97 local supported_inputs = {
87 "application/xmpp+xml", 98 "application/xmpp+xml",
88 "application/json", 99 "application/json",
89 "application/x-www-form-urlencoded", 100 "application/x-www-form-urlencoded",
90 "text/plain", 101 "text/plain",
91 }; 102 };
92 103
93 local function decide_type(accept) 104 local supported_outputs = {
94 -- assumes the accept header is sorted 105 "application/xmpp+xml",
95 local ret = supported_types[1]; 106 "application/json",
96 for i = 2, #supported_types do 107 };
97 if (accept:find(supported_types[i], 1, true) or 1000) < (accept:find(ret, 1, true) or 1000) then
98 ret = supported_types[i];
99 end
100 end
101 return ret;
102 end
103 108
104 local function encode(type, s) 109 local function encode(type, s)
105 if type == "application/json" then 110 if type == "application/json" then
106 return json.encode(jsonmap.st2json(s)); 111 return json.encode(jsonmap.st2json(s));
107 elseif type == "text/plain" then 112 elseif type == "text/plain" then
108 return s:get_child_text("body") or ""; 113 return s:get_child_text("body") or "";
109 end 114 end
110 return tostring(s); 115 return tostring(s);
111 end 116 end
117
118 local post_errors = {
119 parse = { code = 400, condition = "not-well-formed", text = "Failed to parse payload", },
120 xmlns = { code = 422, condition = "invalid-namespace", text = "'xmlns' attribute must be empty", },
121 name = { code = 422, condition = "unsupported-stanza-type", text = "Invalid stanza, must be 'message', 'presence' or 'iq'.", },
122 to = { code = 422, condition = "improper-addressing", text = "Invalid destination JID", },
123 from = { code = 422, condition = "invalid-from", text = "Invalid source JID", },
124 post_auth = { code = 403, condition = "not-authorized", text = "Not authorized to send stanza with requested 'from'", },
125 iq_type = { code = 422, condition = "invalid-xml", text = "'iq' stanza must be of type 'get' or 'set'", },
126 iq_tags = { code = 422, condition = "bad-format", text = "'iq' stanza must have exactly one child tag", },
127 };
112 128
113 local function handle_post(event) 129 local function handle_post(event)
114 local request, response = event.request, event.response; 130 local request, response = event.request, event.response;
115 local from; 131 local from;
116 local origin; 132 local origin;
126 from = jid.join(origin.username, origin.host, origin.resource); 142 from = jid.join(origin.username, origin.host, origin.resource);
127 end 143 end
128 local payload, err = parse(request.headers.content_type, request.body); 144 local payload, err = parse(request.headers.content_type, request.body);
129 if not payload then 145 if not payload then
130 -- parse fail 146 -- parse fail
131 return errors.new({ code = 400, text = "Failed to parse payload" }, { error = err, type = request.headers.content_type, data = request.body }); 147 return errors.new("parse", { error = err, type = request.headers.content_type, data = request.body, }, post_errors);
132 end 148 end
133 if payload.attr.xmlns then 149 if payload.attr.xmlns then
134 return errors.new({ code = 422, text = "'xmlns' attribute must be empty" }); 150 return errors.new("xmlns", nil, post_errors);
135 elseif payload.name ~= "message" and payload.name ~= "presence" and payload.name ~= "iq" then 151 elseif payload.name ~= "message" and payload.name ~= "presence" and payload.name ~= "iq" then
136 return errors.new({ code = 422, text = "Invalid stanza, must be 'message', 'presence' or 'iq'." }); 152 return errors.new("name", nil, post_errors);
137 end 153 end
138 local to = jid.prep(payload.attr.to); 154 local to = jid.prep(payload.attr.to);
139 if not to then 155 if not to then
140 return errors.new({ code = 422, text = "Invalid destination JID" }); 156 return errors.new("to", nil, post_errors);
141 end 157 end
142 if payload.attr.from then 158 if payload.attr.from then
143 local requested_from = jid.prep(payload.attr.from); 159 local requested_from = jid.prep(payload.attr.from);
144 if not requested_from then 160 if not requested_from then
145 return errors.new({ code = 422, text = "Invalid source JID" }); 161 return errors.new("from", nil, post_errors);
146 end 162 end
147 if jid.compare(requested_from, from) then 163 if jid.compare(requested_from, from) then
148 from = requested_from; 164 from = requested_from;
149 else 165 else
150 return errors.new({ code = 403, text = "Not authorized to send from "..requested_from }); 166 return errors.new("from_auth", nil, post_errors);
151 end 167 end
152 end 168 end
153 payload.attr = { 169 payload.attr = {
154 from = from, 170 from = from,
155 to = to, 171 to = to,
156 id = payload.attr.id or id.medium(), 172 id = payload.attr.id or id.medium(),
157 type = payload.attr.type, 173 type = payload.attr.type,
158 ["xml:lang"] = payload.attr["xml:lang"], 174 ["xml:lang"] = payload.attr["xml:lang"],
159 }; 175 };
160 module:log("debug", "Received[rest]: %s", payload:top_tag()); 176 module:log("debug", "Received[rest]: %s", payload:top_tag());
161 local send_type = decide_type((request.headers.accept or "") ..",".. request.headers.content_type) 177 local send_type = decide_type((request.headers.accept or "") ..",".. request.headers.content_type, supported_outputs)
162 if payload.name == "iq" then 178 if payload.name == "iq" then
163 function origin.send(stanza) 179 function origin.send(stanza)
164 module:send(stanza); 180 module:send(stanza);
165 end 181 end
166 if payload.attr.type ~= "get" and payload.attr.type ~= "set" then 182 if payload.attr.type ~= "get" and payload.attr.type ~= "set" then
167 return errors.new({ code = 422, text = "'iq' stanza must be of type 'get' or 'set'" }); 183 return errors.new("iq_type", nil, post_errors);
168 elseif #payload.tags ~= 1 then 184 elseif #payload.tags ~= 1 then
169 return errors.new({ code = 422, text = "'iq' stanza must have exactly one child tag" }); 185 return errors.new("iq_tags", nil, post_errors);
170 end 186 end
171 return module:send_iq(payload, origin):next( 187 return module:send_iq(payload, origin):next(
172 function (result) 188 function (result)
173 module:log("debug", "Sending[rest]: %s", result.stanza:top_tag()); 189 module:log("debug", "Sending[rest]: %s", result.stanza:top_tag());
174 response.headers.content_type = send_type; 190 response.headers.content_type = send_type;
222 return module:log_status("error", "Could not connect to callback URL %q: %s", rest_url, body); 238 return module:log_status("error", "Could not connect to callback URL %q: %s", rest_url, body);
223 else 239 else
224 module:set_status("info", "Connected"); 240 module:set_status("info", "Connected");
225 end 241 end
226 if code == 200 and response.headers.accept then 242 if code == 200 and response.headers.accept then
227 send_type = decide_type(response.headers.accept); 243 send_type = decide_type(response.headers.accept, supported_outputs);
228 module:log("debug", "Set 'rest_callback_content_type' = %q based on Accept header", send_type); 244 module:log("debug", "Set 'rest_callback_content_type' = %q based on Accept header", send_type);
229 end 245 end
230 end); 246 end);
231 247
232 local code2err = { 248 local code2err = {
278 http.request(rest_url, { 294 http.request(rest_url, {
279 body = request_body, 295 body = request_body,
280 headers = { 296 headers = {
281 ["Content-Type"] = send_type, 297 ["Content-Type"] = send_type,
282 ["Content-Language"] = stanza.attr["xml:lang"], 298 ["Content-Language"] = stanza.attr["xml:lang"],
283 Accept = table.concat(supported_types, ", "); 299 Accept = table.concat(supported_inputs, ", ");
284 }, 300 },
285 }, function (body, code, response) 301 }, function (body, code, response)
286 if code == 0 then 302 if code == 0 then
287 module:log_status("error", "Could not connect to callback URL %q: %s", rest_url, body); 303 module:log_status("error", "Could not connect to callback URL %q: %s", rest_url, body);
288 origin.send(st.error_reply(stanza, "wait", "recipient-unavailable", body)); 304 origin.send(st.error_reply(stanza, "wait", "recipient-unavailable", body));
374 module:hook("message/host", handle_stanza, -1); 390 module:hook("message/host", handle_stanza, -1);
375 module:hook("presence/host", handle_stanza, -1); 391 module:hook("presence/host", handle_stanza, -1);
376 end 392 end
377 end 393 end
378 394
395 local supported_errors = {
396 "text/html",
397 "application/xmpp+xml",
398 "application/json",
399 };
400
379 local http_server = require "net.http.server"; 401 local http_server = require "net.http.server";
380 module:hook_object_event(http_server, "http-error", function (event) 402 module:hook_object_event(http_server, "http-error", function (event)
381 local request, response = event.request, event.response; 403 local request, response = event.request, event.response;
382 if decide_type(request and request.headers.accept or "") == "application/json" then 404 local response_as = decide_type(request and request.headers.accept or "", supported_errors);
405 if response_as == "application/xmpp+xml" then
406 if response then
407 response.headers.content_type = "application/xmpp+xml";
408 end
409 local stream_error = st.stanza("error", { xmlns = "http://etherx.jabber.org/streams" });
410 if event.error then
411 stream_error:tag(event.error.condition, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' }):up();
412 if event.error.text then
413 stream_error:text_tag("text", event.error.text, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' });
414 end
415 end
416 return tostring(stream_error);
417 elseif response_as == "application/json" then
383 if response then 418 if response then
384 response.headers.content_type = "application/json"; 419 response.headers.content_type = "application/json";
385 end 420 end
386 return json.encode({ 421 return json.encode({
387 type = "error", 422 type = "error",
388 error = event.error, 423 error = event.error,
389 code = event.code, 424 code = event.code,
390 }); 425 });
391 end 426 end
392 end, 10); 427 end, 1);