Comparison

plugins/mod_storage_xep0227.lua @ 11789:f3085620b6ff

mod_storage_xep0227: Update for XEP-0227 r1.1: Support for SCRAM, MAM, PEP
author Matthew Wild <mwild1@gmail.com>
date Sun, 12 Sep 2021 11:38:47 +0100
parent 8352:6ff50541d2a6
child 11840:5e9e75c277a2
comparison
equal deleted inserted replaced
11788:1ceee8becb1a 11789:f3085620b6ff
1 1
2 local ipairs, pairs = ipairs, pairs; 2 local ipairs, pairs = ipairs, pairs;
3 local setmetatable = setmetatable; 3 local setmetatable = setmetatable;
4 local tostring = tostring; 4 local tostring = tostring;
5 local next = next; 5 local next, unpack = next, table.unpack or unpack; --luacheck: ignore 113/unpack
6 local t_remove = table.remove; 6 local t_remove = table.remove;
7 local os_remove = os.remove; 7 local os_remove = os.remove;
8 local io_open = io.open; 8 local io_open = io.open;
9 9 local jid_bare = require "util.jid".bare;
10 local jid_prep = require "util.jid".prep;
11
12 local array = require "util.array";
13 local base64 = require "util.encodings".base64;
14 local dt = require "util.datetime";
15 local hex = require "util.hex";
16 local it = require "util.iterators";
10 local paths = require"util.paths"; 17 local paths = require"util.paths";
18 local set = require "util.set";
11 local st = require "util.stanza"; 19 local st = require "util.stanza";
12 local parse_xml_real = require "util.xml".parse; 20 local parse_xml_real = require "util.xml".parse;
21
22 local lfs = require "lfs";
13 23
14 local function getXml(user, host) 24 local function getXml(user, host)
15 local jid = user.."@"..host; 25 local jid = user.."@"..host;
16 local path = paths.join(prosody.paths.data, jid..".xml"); 26 local path = paths.join(prosody.paths.data, jid..".xml");
17 local f = io_open(path); 27 local f, err = io_open(path);
18 if not f then return; end 28 if not f then
29 module:log("debug", "Unable to load XML file for <%s>: %s", jid, err);
30 return;
31 end
32 module:log("debug", "Loaded %s", path);
19 local s = f:read("*a"); 33 local s = f:read("*a");
20 f:close(); 34 f:close();
21 return parse_xml_real(s); 35 return parse_xml_real(s);
22 end 36 end
23 local function setXml(user, host, xml) 37 local function setXml(user, host, xml)
43 if user and user.name == "user" then 57 if user and user.name == "user" then
44 return user; 58 return user;
45 end 59 end
46 end 60 end
47 end 61 end
62 module:log("warn", "Unable to find user element");
48 end 63 end
49 local function createOuterXml(user, host) 64 local function createOuterXml(user, host)
50 return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'}) 65 return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'})
51 :tag("host", {jid=host}) 66 :tag("host", {jid=host})
52 :tag("user", {name = user}); 67 :tag("user", {name = user});
53 end 68 end
54 local function removeFromArray(array, value) 69 local function removeFromArray(arr, value)
55 for i,item in ipairs(array) do 70 for i,item in ipairs(arr) do
56 if item == value then 71 if item == value then
57 t_remove(array, i); 72 t_remove(arr, i);
58 return; 73 return;
59 end 74 end
60 end 75 end
61 end 76 end
62 local function removeStanzaChild(s, child) 77 local function removeStanzaChild(s, child)
63 removeFromArray(s.tags, child); 78 removeFromArray(s.tags, child);
64 removeFromArray(s, child); 79 removeFromArray(s, child);
65 end 80 end
66 81
82 local function hex_to_base64(s)
83 return base64.encode(hex.from(s));
84 end
85
86 local function base64_to_hex(s)
87 return base64.encode(hex.from(s));
88 end
89
67 local handlers = {}; 90 local handlers = {};
68 91
69 -- In order to support mod_auth_internal_hashed 92 -- In order to support custom account properties
70 local extended = "http://prosody.im/protocol/extended-xep0227\1"; 93 local extended = "http://prosody.im/protocol/extended-xep0227\1";
94
95 local scram_hash_name = module:get_option_string("password_hash", "SHA-1");
96 local scram_properties = set.new({ "server_key", "stored_key", "iteration_count", "salt" });
71 97
72 handlers.accounts = { 98 handlers.accounts = {
73 get = function(self, user) 99 get = function(self, user)
74 user = getUserElement(getXml(user, self.host)); 100 user = getUserElement(getXml(user, self.host));
75 if user and user.attr.password then 101 local scram_credentials = user and user:get_child_with_attr(
102 "scram-credentials", "urn:xmpp:pie:0#scram",
103 "mechanism", "SCRAM-"..scram_hash_name
104 );
105 if scram_credentials then
106 return {
107 iteration_count = tonumber(scram_credentials:get_child_text("iter-count"));
108 server_key = base64_to_hex(scram_credentials:get_child_text("server-key"));
109 stored_key = base64_to_hex(scram_credentials:get_child_text("stored-key"));
110 salt = base64.decode(scram_credentials:get_child_text("salt"));
111 };
112 elseif user and user.attr.password then
76 return { password = user.attr.password }; 113 return { password = user.attr.password };
77 elseif user then 114 elseif user then
78 local data = {}; 115 local data = {};
79 for k, v in pairs(user.attr) do 116 for k, v in pairs(user.attr) do
80 if k:sub(1, #extended) == extended then 117 if k:sub(1, #extended) == extended then
83 end 120 end
84 return data; 121 return data;
85 end 122 end
86 end; 123 end;
87 set = function(self, user, data) 124 set = function(self, user, data)
88 if data then 125 if not data then
89 local xml = getXml(user, self.host);
90 if not xml then xml = createOuterXml(user, self.host); end
91 local usere = getUserElement(xml);
92 for k, v in pairs(data) do
93 if k == "password" then
94 usere.attr.password = v;
95 else
96 usere.attr[extended..k] = v;
97 end
98 end
99 return setXml(user, self.host, xml);
100 else
101 return setXml(user, self.host, nil); 126 return setXml(user, self.host, nil);
102 end 127 end
128
129 local xml = getXml(user, self.host);
130 if not xml then xml = createOuterXml(user, self.host); end
131 local usere = getUserElement(xml);
132
133 local account_properties = set.new(it.to_array(it.keys(data)));
134
135 -- Include SCRAM credentials if known
136 if account_properties:contains_set(scram_properties) then
137 local scram_el = st.stanza("scram-credentials", { xmlns = "urn:xmpp:pie:0#scram", mechanism = "SCRAM-"..scram_hash_name })
138 :text_tag("server-key", hex_to_base64(data.server_key))
139 :text_tag("stored-key", hex_to_base64(data.stored_key))
140 :text_tag("iter-count", ("%d"):format(data.iteration_count))
141 :text_tag("salt", base64.encode(data.salt));
142 xml:add_child(scram_el);
143 account_properties:exclude(scram_properties);
144 end
145
146 -- Include the password if present
147 if account_properties:contains("password") then
148 usere.attr.password = data.password;
149 account_properties:remove("password");
150 end
151
152 -- Preserve remaining properties as namespaced attributes
153 for property in account_properties do
154 usere.attr[extended..property] = data[property];
155 end
156
157 return setXml(user, self.host, xml);
103 end; 158 end;
104 }; 159 };
105 handlers.vcard = { 160 handlers.vcard = {
106 get = function(self, user) 161 get = function(self, user)
107 user = getUserElement(getXml(user, self.host)); 162 user = getUserElement(getXml(user, self.host));
234 end 289 end
235 return true; 290 return true;
236 end; 291 end;
237 }; 292 };
238 293
294 -- PEP node configuration/etc. (not items)
295 local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
296 local lib_pubsub = module:require "pubsub";
297 handlers.pep = {
298 get = function (self, user)
299 local xml = getXml(user, self.host);
300 local user_el = xml and getUserElement(xml);
301 if not user_el then
302 return nil;
303 end
304 local nodes = {
305 --[[
306 [node_name] = {
307 name = node_name;
308 config = {};
309 affiliations = {};
310 subscribers = {};
311 };
312 ]]
313 };
314 local owner_el = user_el:get_child("pubsub", xmlns_pubsub_owner);
315 for node_el in owner_el:childtags() do
316 local node_name = node_el.attr.node;
317 local node = nodes[node_name];
318 if not node then
319 node = {
320 name = node_name;
321 config = {};
322 affiliations = {};
323 subscribers = {};
324 };
325 nodes[node_name] = node;
326 end
327 if node_el.name == "configure" then
328 local form = node_el:get_child("x", "jabber:x:data");
329 if form then
330 node.config = lib_pubsub.node_config_form:data(form);
331 end
332 elseif node_el.name == "affiliations" then
333 for affiliation_el in node_el:childtags("affiliation") do
334 local aff_jid = jid_prep(affiliation_el.attr.jid);
335 local aff_value = affiliation_el.attr.affiliation;
336 if aff_jid and aff_value then
337 node.affiliations[aff_jid] = aff_value;
338 end
339 end
340 elseif node_el.name == "subscriptions" then
341 for subscription_el in node_el:childtags("subscription") do
342 local sub_jid = jid_prep(subscription_el.attr.jid);
343 local sub_state = subscription_el.attr.subscription;
344 if sub_jid and sub_state == "subscribed" then
345 local options;
346 local subscription_options_el = subscription_el:get_child("options");
347 if subscription_options_el then
348 local options_form = subscription_options_el:get_child("x", "jabber:x:data");
349 if options_form then
350 options = lib_pubsub.subscription_options_form:data(options_form);
351 end
352 end
353 node.subscribers[sub_jid] = options or true;
354 end
355 end
356 else
357 module:log("warn", "Ignoring unknown pubsub element: %s", node_el.name);
358 end
359 end
360 return nodes;
361 end;
362 set = function(self, user, data)
363 local xml = getXml(user, self.host);
364 local user_el = xml and getUserElement(xml);
365 if not user_el then
366 return true;
367 end
368 -- Remove existing data, if any
369 user_el:remove_children("pubsub", xmlns_pubsub_owner);
370
371 -- Generate new data
372 local owner_el = st.stanza("pubsub", { xmlns = xmlns_pubsub_owner });
373
374 for node_name, node_data in pairs(data) do
375 local configure_el = st.stanza("configure", { node = node_name })
376 :add_child(lib_pubsub.node_config_form:form(node_data.config, "submit"));
377 owner_el:add_child(configure_el);
378 if node_data.affiliations and next(node_data.affiliations) ~= nil then
379 local affiliations_el = st.stanza("affiliations", { node = node_name });
380 for aff_jid, aff_value in pairs(node_data.affiliations) do
381 affiliations_el:tag("affiliation", { jid = aff_jid, affiliation = aff_value }):up();
382 end
383 owner_el:add_child(affiliations_el);
384 end
385 if node_data.subscribers and next(node_data.subscribers) ~= nil then
386 local subscriptions_el = st.stanza("subscriptions", { node = node_name });
387 for sub_jid, sub_data in pairs(node_data.subscribers) do
388 local sub_el = st.stanza("subscription", { jid = sub_jid, subscribed = "subscribed" });
389 if sub_data ~= true then
390 local options_form = lib_pubsub.subscription_options_form:form(sub_data, "submit");
391 sub_el:tag("options"):add_child(options_form):up();
392 end
393 subscriptions_el:add_child(sub_el);
394 end
395 owner_el:add_child(subscriptions_el);
396 end
397 end
398
399 user_el:add_child(owner_el);
400
401 return setXml(user, self.host, xml);
402 end;
403 };
404
405 -- PEP items
406 local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
407 handlers.pep_ = {
408 _stores = function (self, xml) --luacheck: ignore 212/self
409 local store_names = set.new();
410
411 local user_el = xml and getUserElement(xml);
412 if not user_el then
413 return store_names;
414 end
415
416 -- Locate existing pubsub element, if any
417 local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub);
418 if not pubsub_el then
419 return store_names;
420 end
421
422 -- Find node items element, if any
423 for items_el in pubsub_el:childtags("items") do
424 store_names:add("pep_"..items_el.attr.node);
425 end
426 return store_names;
427 end;
428 find = function (self, user, query)
429 -- query keys: limit, reverse, key (id)
430
431 local xml = getXml(user, self.host);
432 local user_el = xml and getUserElement(xml);
433 if not user_el then
434 return nil, "no 227 user element found";
435 end
436
437 local node_name = self.datastore:match("^pep_(.+)$");
438
439 -- Locate existing pubsub element, if any
440 local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub);
441 if not pubsub_el then
442 return nil;
443 end
444
445 -- Find node items element, if any
446 local node_items_el;
447 for items_el in pubsub_el:childtags("items") do
448 if items_el.attr.node == node_name then
449 node_items_el = items_el;
450 break;
451 end
452 end
453
454 if not node_items_el then
455 return nil;
456 end
457
458 local user_jid = user.."@"..self.host;
459
460 local results = {};
461 for item_el in node_items_el:childtags("item") do
462 if query and query.key then
463 if item_el.attr.id == query.key then
464 table.insert(results, { item_el.attr.id, item_el.tags[1], 0, user_jid });
465 break;
466 end
467 else
468 table.insert(results, { item_el.attr.id, item_el.tags[1], 0, user_jid });
469 end
470 if query and query.limit and #results >= query.limit then
471 break;
472 end
473 end
474 if query and query.reverse then
475 return array.reverse(results);
476 end
477 local i = 0;
478 return function ()
479 i = i + 1;
480 local v = results[i];
481 if v == nil then return nil; end
482 return unpack(v, 1, 4);
483 end;
484 end;
485 append = function (self, user, key, payload, when, with) --luacheck: ignore 212/when 212/with 212/key
486 local xml = getXml(user, self.host);
487 local user_el = xml and getUserElement(xml);
488 if not user_el then
489 return true;
490 end
491
492 local node_name = self.datastore:match("^pep_(.+)$");
493
494 -- Locate existing pubsub element, if any
495 local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub);
496 if not pubsub_el then
497 pubsub_el = st.stanza("pubsub", { xmlns = xmlns_pubsub });
498 user_el:add_child(pubsub_el);
499 end
500
501 -- Find node items element, if any
502 local node_items_el;
503 for items_el in pubsub_el:childtags("items") do
504 if items_el.attr.node == node_name then
505 node_items_el = items_el;
506 break;
507 end
508 end
509
510 if not node_items_el then
511 -- Doesn't exist yet, create one
512 node_items_el = st.stanza("items", { node = node_name });
513 pubsub_el:add_child(node_items_el);
514 end
515
516 -- Append item to pubsub_el
517 local item_el = st.stanza("item", { id = key })
518 :add_child(payload);
519 node_items_el:add_child(item_el);
520
521 return setXml(user, self.host, xml);
522 end;
523 delete = function (self, user, query)
524 -- query keys: limit, reverse, key (id)
525
526 local xml = getXml(user, self.host);
527 local user_el = xml and getUserElement(xml);
528 if not user_el then
529 return nil, "no 227 user element found";
530 end
531
532 local node_name = self.datastore:match("^pep_(.+)$");
533
534 -- Locate existing pubsub element, if any
535 local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub);
536 if not pubsub_el then
537 return nil;
538 end
539
540 -- Find node items element, if any
541 local node_items_el;
542 for items_el in pubsub_el:childtags("items") do
543 if items_el.attr.node == node_name then
544 node_items_el = items_el;
545 break;
546 end
547 end
548
549 if not node_items_el then
550 return nil;
551 end
552
553 local results = array();
554 for item_el in pubsub_el:childtags("item") do
555 if query and query.key then
556 if item_el.attr.id == query.key then
557 table.insert(results, item_el);
558 break;
559 end
560 else
561 table.insert(results, item_el);
562 end
563 if query and query.limit and #results >= query.limit then
564 break;
565 end
566 end
567 if query and query.truncate then
568 results:sub(-query.truncate);
569 end
570
571 -- Actually remove the matching items
572 local delete_keys = set.new(results:map(function (item) return item.attr.id; end));
573 pubsub_el:maptags(function (item_el)
574 if delete_keys:contains(item_el.attr.id) then
575 return nil;
576 end
577 return item_el;
578 end);
579 return setXml(user, self.host, xml);
580 end;
581 };
582
583 -- MAM archives
584 local xmlns_pie_mam = "urn:xmpp:pie:0#mam";
585 handlers.archive = {
586 find = function (self, user, query)
587 assert(query == nil, "XEP-0313 queries are not supported on XEP-0227 files");
588
589 local xml = getXml(user, self.host);
590 local user_el = xml and getUserElement(xml);
591 if not user_el then
592 return nil, "no 227 user element found";
593 end
594
595 -- Locate existing archive element, if any
596 local archive_el = user_el:get_child("archive", xmlns_pie_mam);
597 if not archive_el then
598 return nil;
599 end
600
601 local user_jid = user.."@"..self.host;
602
603
604 local f, s, result_el = archive_el:childtags("result", "urn:xmpp:mam:2");
605 return function ()
606 result_el = f(s, result_el);
607 if not result_el then return nil; end
608
609 local id = result_el.attr.id;
610 local item = result_el:find("{urn:xmpp:forward:0}forwarded/{jabber:client}message");
611 assert(item, "Invalid stanza in XEP-0227 archive");
612 local when = dt.parse(result_el:find("{urn:xmpp:forward:0}forwarded/{urn:xmpp:delay}delay@stamp"));
613 local to_bare, from_bare = jid_bare(item.attr.to), jid_bare(item.attr.from);
614 local with = to_bare == user_jid and from_bare or to_bare;
615 -- id, item, when, with
616 return id, item, when, with;
617 end;
618 end;
619 append = function (self, user, key, payload, when, with) --luacheck: ignore 212/when 212/with 212/key
620 local xml = getXml(user, self.host);
621 local user_el = xml and getUserElement(xml);
622 if not user_el then
623 return true;
624 end
625
626 -- Locate existing archive element, if any
627 local archive_el = user_el:get_child("archive", xmlns_pie_mam);
628 if not archive_el then
629 archive_el = st.stanza("archive", { xmlns = xmlns_pie_mam });
630 user_el:add_child(archive_el);
631 end
632
633 local item = st.clone(payload);
634 item.attr.xmlns = "jabber:client";
635
636 local result_el = st.stanza("result", { xmlns = "urn:xmpp:mam:2", id = key })
637 :tag("forwarded", { xmlns = "urn:xmpp:forward:0" })
638 :tag("delay", { xmlns = "urn:xmpp:delay", stamp = dt.datetime(when) }):up()
639 :add_child(item)
640 :up();
641
642 -- Append item to archive_el
643 archive_el:add_child(result_el);
644
645 return setXml(user, self.host, xml);
646 end;
647 };
239 648
240 ----------------------------- 649 -----------------------------
241 local driver = {}; 650 local driver = {};
242 651
652 local function users(self)
653 local file_patt = "^.*@"..(self.host:gsub("%p", "%%%1")).."%.xml$";
654
655 local f, s, filename = lfs.dir(prosody.paths.data);
656
657 return function ()
658 filename = f(s, filename);
659 while filename and not filename:match(file_patt) do
660 filename = f(s, filename);
661 end
662 if not filename then return nil; end
663 return filename:match("^[^@]+");
664 end;
665 end
666
243 function driver:open(datastore, typ) -- luacheck: ignore 212/self 667 function driver:open(datastore, typ) -- luacheck: ignore 212/self
244 if typ and typ ~= "keyval" then return nil, "unsupported-store"; end 668 if typ and typ ~= "keyval" and typ ~= "archive" then return nil, "unsupported-store"; end
245 local handler = handlers[datastore]; 669 local handler = handlers[datastore];
670 if not handler and datastore:match("^pep_") then
671 handler = handlers.pep_;
672 end
246 if not handler then return nil, "unsupported-datastore"; end 673 if not handler then return nil, "unsupported-datastore"; end
247 local instance = setmetatable({ host = module.host; datastore = datastore; }, { __index = handler }); 674 local instance = setmetatable({ host = module.host; datastore = datastore; users = users; }, { __index = handler });
248 if instance.init then instance:init(); end 675 if instance.init then instance:init(); end
249 return instance; 676 return instance;
250 end 677 end
251 678
679 local function get_store_names(self, path)
680 local stores = set.new();
681 local f, err = io_open(paths.join(prosody.paths.data, path));
682 if not f then
683 module:log("warn", "Unable to load XML file for <%s>: %s", "store listing", err);
684 return stores;
685 end
686 module:log("info", "Loaded %s", path);
687 local s = f:read("*a");
688 f:close();
689 local xml = parse_xml_real(s);
690 for _, handler_funcs in pairs(handlers) do
691 if handler_funcs._stores then
692 stores:include(handler_funcs._stores(self, xml));
693 end
694 end
695 return stores;
696 end
697
698 function driver:stores(username)
699 local store_dir = prosody.paths.data;
700
701 local mode, err = lfs.attributes(store_dir, "mode");
702 if not mode then
703 return function() module:log("debug", "Could not iterate over stores in %s: %s", store_dir, err); end
704 end
705
706 local file_patt = "^.*@"..(module.host:gsub("%p", "%%%1")).."%.xml$";
707
708 local all_users = username == true;
709
710 local store_names = set.new();
711
712 for filename in lfs.dir(prosody.paths.data) do
713 if filename:match(file_patt) then
714 if all_users or filename == username.."@"..module.host..".xml" then
715 store_names:include(get_store_names(self, filename));
716 if not all_users then break; end
717 end
718 end
719 end
720
721 return store_names:items();
722 end
723
252 module:provides("storage", driver); 724 module:provides("storage", driver);