Mercurial > prosody-modules
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); |
