Software /
code /
prosody-modules
Comparison
mod_rest/mod_rest.lua @ 6211:750d64c47ec6 draft
Merge
author | Trần H. Trung <xmpp:trần.h.trung@trung.fun> |
---|---|
date | Tue, 18 Mar 2025 00:31:36 +0700 |
parent | 6206:ac7e2992fe6e |
child | 6244:c71d8bc77c95 |
comparison
equal
deleted
inserted
replaced
6210:24316a399978 | 6211:750d64c47ec6 |
---|---|
21 local tokens = module:depends("tokenauth"); | 21 local tokens = module:depends("tokenauth"); |
22 | 22 |
23 -- Lower than the default c2s size limit to account for possible JSON->XML size increase | 23 -- Lower than the default c2s size limit to account for possible JSON->XML size increase |
24 local stanza_size_limit = module:get_option_number("rest_stanza_size_limit", 1024 * 192); | 24 local stanza_size_limit = module:get_option_number("rest_stanza_size_limit", 1024 * 192); |
25 | 25 |
26 local auth_mechanisms = module:get_option_set("rest_auth_mechanisms", { "Basic", "Bearer" }); | 26 local auth_mechanisms = module:get_option_set("rest_auth_mechanisms", { "Basic", "Bearer" }) / string.lower; |
27 | 27 |
28 local www_authenticate_header; | 28 local www_authenticate_header; |
29 do | 29 do |
30 local header, realm = {}, module.host.."/"..module.name; | 30 local header, realm = {}, module.host.."/"..module.name; |
31 for mech in auth_mechanisms do | 31 for mech in auth_mechanisms do |
32 header[#header+1] = ("%s realm=%q"):format(mech, realm); | 32 header[#header+1] = ("%s realm=%q"):format(mech, realm); |
33 end | 33 end |
34 www_authenticate_header = table.concat(header, ", "); | 34 www_authenticate_header = table.concat(header, ", "); |
35 end | 35 end |
36 | 36 |
37 local function check_credentials(request) | 37 local post_errors = errors.init("mod_rest", { |
38 noauthz = { code = 401; type = "auth"; condition = "not-authorized"; text = "No credentials provided" }; | |
39 unauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials not accepted" }; | |
40 malformauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials malformed" }; | |
41 prepauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials failed stringprep" }; | |
42 parse = { code = 400; type = "modify"; condition = "not-well-formed"; text = "Failed to parse payload" }; | |
43 xmlns = { code = 422; type = "modify"; condition = "invalid-namespace"; text = "'xmlns' attribute must be empty" }; | |
44 name = { code = 422; type = "modify"; condition = "unsupported-stanza-type"; text = "Invalid stanza, must be 'message', 'presence' or 'iq'." }; | |
45 to = { code = 422; type = "modify"; condition = "improper-addressing"; text = "Invalid destination JID" }; | |
46 from = { code = 422; type = "modify"; condition = "invalid-from"; text = "Invalid source JID" }; | |
47 from_auth = { code = 403; type = "auth"; condition = "not-authorized"; text = "Not authorized to send stanza with requested 'from'" }; | |
48 iq_type = { code = 422; type = "modify"; condition = "invalid-xml"; text = "'iq' stanza must be of type 'get' or 'set'" }; | |
49 iq_tags = { code = 422; type = "modify"; condition = "bad-format"; text = "'iq' stanza must have exactly one child tag" }; | |
50 mediatype = { code = 415; type = "cancel"; condition = "bad-format"; text = "Unsupported media type" }; | |
51 size = { code = 413; type = "modify"; condition = "resource-constraint", text = "Payload too large" }; | |
52 }); | |
53 | |
54 local token_session_errors = errors.init("mod_tokenauth", { | |
55 ["internal-error"] = { code = 500; type = "wait"; condition = "internal-server-error" }; | |
56 ["invalid-token-format"] = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials malformed" }; | |
57 ["not-authorized"] = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials not accepted" }; | |
58 }); | |
59 | |
60 local function check_credentials(request) -- > session | boolean, error | |
38 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$"); | 61 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$"); |
62 auth_type = auth_type and auth_type:lower(); | |
39 if not (auth_type and auth_data) or not auth_mechanisms:contains(auth_type) then | 63 if not (auth_type and auth_data) or not auth_mechanisms:contains(auth_type) then |
40 return false; | 64 return nil, post_errors.new("noauthz", { request = request }); |
41 end | 65 end |
42 | 66 |
43 if auth_type == "Basic" then | 67 if auth_type == "basic" then |
44 local creds = base64.decode(auth_data); | 68 local creds = base64.decode(auth_data); |
45 if not creds then return false; end | 69 if not creds then |
70 return nil, post_errors.new("malformauthz", { request = request }); | |
71 end | |
46 local username, password = string.match(creds, "^([^:]+):(.*)$"); | 72 local username, password = string.match(creds, "^([^:]+):(.*)$"); |
47 if not username then return false; end | 73 if not username then |
74 return nil, post_errors.new("malformauthz", { request = request }); | |
75 end | |
48 username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password); | 76 username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password); |
49 if not username then return false; end | 77 if not username or not password then |
78 return false, post_errors.new("prepauthz", { request = request }); | |
79 end | |
50 if not um.test_password(username, module.host, password) then | 80 if not um.test_password(username, module.host, password) then |
51 return false; | 81 return false, post_errors.new("unauthz", { request = request }); |
52 end | 82 end |
53 return { username = username, host = module.host }; | 83 return { username = username; host = module.host }; |
54 elseif auth_type == "Bearer" then | 84 elseif auth_type == "bearer" then |
55 if tokens.get_token_session then | 85 if tokens.get_token_session then |
56 return tokens.get_token_session(auth_data); | 86 local token_session, err = tokens.get_token_session(auth_data); |
87 if not token_session then | |
88 return false, token_session_errors.new(err or "not-authorized", { request = request }); | |
89 end | |
90 return token_session; | |
57 else -- COMPAT w/0.12 | 91 else -- COMPAT w/0.12 |
58 local token_info = tokens.get_token_info(auth_data); | 92 local token_info = tokens.get_token_info(auth_data); |
59 if not token_info or not token_info.session then | 93 if not token_info or not token_info.session then |
60 return false; | 94 return false, post_errors.new("unauthz", { request = request }); |
61 end | 95 end |
62 return token_info.session; | 96 return token_info.session; |
63 end | 97 end |
64 end | 98 end |
65 return nil; | 99 return nil, post_errors.new("noauthz", { request = request }); |
66 end | 100 end |
67 | 101 |
68 if module:get_option_string("authentication") == "anonymous" and module:get_option_boolean("anonymous_rest") then | 102 if module:get_option_string("authentication") == "anonymous" and module:get_option_boolean("anonymous_rest") then |
69 www_authenticate_header = nil; | 103 www_authenticate_header = nil; |
70 function check_credentials(request) -- luacheck: ignore 212/request | 104 function check_credentials(request) -- luacheck: ignore 212/request |
123 return false | 157 return false |
124 end | 158 end |
125 | 159 |
126 -- (table, string) -> table | 160 -- (table, string) -> table |
127 local function amend_from_path(data, path) | 161 local function amend_from_path(data, path) |
128 local st_kind, st_type, st_to = path:match("^([mpi]%w+)/(%w+)/(.*)$"); | 162 local st_kind, st_type, st_to = path:match("^([mpi]%w+)/([%w_]+)/(.*)$"); |
129 if not st_kind then return; end | 163 if not st_kind then return; end |
130 if st_kind == "iq" and st_type ~= "get" and st_type ~= "set" then | 164 if st_kind == "iq" and st_type ~= "get" and st_type ~= "set" then |
131 -- GET /iq/disco/jid | 165 -- GET /iq/disco/jid |
132 data = { | 166 data = { |
133 kind = "iq"; | 167 kind = "iq"; |
266 return cbor.encode(mapped); | 300 return cbor.encode(mapped); |
267 end | 301 end |
268 error "unsupported encoding"; | 302 error "unsupported encoding"; |
269 end | 303 end |
270 | 304 |
271 local post_errors = errors.init("mod_rest", { | |
272 noauthz = { code = 401; type = "auth"; condition = "not-authorized"; text = "No credentials provided" }; | |
273 unauthz = { code = 403; type = "auth"; condition = "not-authorized"; text = "Credentials not accepted" }; | |
274 parse = { code = 400; type = "modify"; condition = "not-well-formed"; text = "Failed to parse payload" }; | |
275 xmlns = { code = 422; type = "modify"; condition = "invalid-namespace"; text = "'xmlns' attribute must be empty" }; | |
276 name = { code = 422; type = "modify"; condition = "unsupported-stanza-type"; text = "Invalid stanza, must be 'message', 'presence' or 'iq'." }; | |
277 to = { code = 422; type = "modify"; condition = "improper-addressing"; text = "Invalid destination JID" }; | |
278 from = { code = 422; type = "modify"; condition = "invalid-from"; text = "Invalid source JID" }; | |
279 from_auth = { code = 403; type = "auth"; condition = "not-authorized"; text = "Not authorized to send stanza with requested 'from'" }; | |
280 iq_type = { code = 422; type = "modify"; condition = "invalid-xml"; text = "'iq' stanza must be of type 'get' or 'set'" }; | |
281 iq_tags = { code = 422; type = "modify"; condition = "bad-format"; text = "'iq' stanza must have exactly one child tag" }; | |
282 mediatype = { code = 415; type = "cancel"; condition = "bad-format"; text = "Unsupported media type" }; | |
283 size = { code = 413; type = "modify"; condition = "resource-constraint", text = "Payload too large" }; | |
284 }); | |
285 | |
286 -- GET → iq-get | 305 -- GET → iq-get |
287 local function parse_request(request, path) | 306 local function parse_request(request, path) |
288 if path and request.method == "GET" then | 307 if path and request.method == "GET" then |
289 -- e.g. /version/{to} | 308 -- e.g. /version/{to} |
290 if request.url.query then | 309 if request.url.query then |
306 | 325 |
307 if not request.headers.authorization and www_authenticate_header then | 326 if not request.headers.authorization and www_authenticate_header then |
308 response.headers.www_authenticate = www_authenticate_header; | 327 response.headers.www_authenticate = www_authenticate_header; |
309 return post_errors.new("noauthz"); | 328 return post_errors.new("noauthz"); |
310 else | 329 else |
311 origin = check_credentials(request); | 330 local err; |
331 origin, err = check_credentials(request); | |
312 if not origin then | 332 if not origin then |
313 return post_errors.new("unauthz"); | 333 return err or post_errors.new("unauthz"); |
314 end | 334 end |
315 from = jid.join(origin.username, origin.host, origin.resource); | 335 from = jid.join(origin.username, origin.host, origin.resource); |
316 origin.full_jid = from; | 336 origin.full_jid = from; |
317 origin.type = "c2s"; | 337 origin.type = "c2s"; |
318 origin.log = log; | 338 origin.log = log; |
640 "text/html", | 660 "text/html", |
641 "application/xmpp+xml", | 661 "application/xmpp+xml", |
642 "application/json", | 662 "application/json", |
643 }; | 663 }; |
644 | 664 |
665 -- strip some stuff, notably the optional traceback table that casues stack overflow in util.json | |
666 local function simplify_error(e) | |
667 return { | |
668 type = e.type; | |
669 condition = e.condition; | |
670 text = e.text; | |
671 extra = e.extra; | |
672 source = e.source; | |
673 }; | |
674 end | |
675 | |
645 local http_server = require "net.http.server"; | 676 local http_server = require "net.http.server"; |
646 module:hook_object_event(http_server, "http-error", function (event) | 677 module:hook_object_event(http_server, "http-error", function (event) |
647 local request, response = event.request, event.response; | 678 local request, response = event.request, event.response; |
648 local response_as = decide_type(request and request.headers.accept or "", supported_errors); | 679 local response_as = decide_type(request and request.headers.accept or "", supported_errors); |
649 if response_as == "application/xmpp+xml" then | 680 if response_as == "application/xmpp+xml" then |
662 if response then | 693 if response then |
663 response.headers.content_type = "application/json"; | 694 response.headers.content_type = "application/json"; |
664 end | 695 end |
665 return json.encode({ | 696 return json.encode({ |
666 type = "error", | 697 type = "error", |
667 error = event.error, | 698 error = simplify_error(event.error), |
668 code = event.code, | 699 code = event.code, |
669 }); | 700 }); |
670 end | 701 end |
671 end, 1); | 702 end, 1); |