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);