Comparison

plugins/mod_admin_shell.lua @ 11885:197642f9972f

mod_admin_shell: New table based implementation of c2s and s2s:show() Nicer and more readable. Thanks jonas’ and prosody@ for JID length stats to inform column widths.
author Kim Alvefur <zash@zash.se>
date Wed, 10 Nov 2021 17:59:35 +0100
parent 11850:bfa85965106e
child 11886:b0b258e092da
comparison
equal deleted inserted replaced
11884:248477e45c64 11885:197642f9972f
39 local t_insert = table.insert; 39 local t_insert = table.insert;
40 local t_concat = table.concat; 40 local t_concat = table.concat;
41 41
42 local format_number = require "util.human.units".format; 42 local format_number = require "util.human.units".format;
43 local format_table = require "util.human.io".table; 43 local format_table = require "util.human.io".table;
44
45 local function capitalize(s)
46 return (s:gsub("^%a", string.upper):gsub("_", " "));
47 end
44 48
45 local commands = module:shared("commands") 49 local commands = module:shared("commands")
46 local def_env = module:shared("env"); 50 local def_env = module:shared("env");
47 local default_env_mt = { __index = def_env }; 51 local default_env_mt = { __index = def_env };
48 52
203 print [[xmpp - Commands for sending XMPP stanzas]] 207 print [[xmpp - Commands for sending XMPP stanzas]]
204 print [[debug - Commands for debugging the server]] 208 print [[debug - Commands for debugging the server]]
205 print [[config - Reloading the configuration, etc.]] 209 print [[config - Reloading the configuration, etc.]]
206 print [[console - Help regarding the console itself]] 210 print [[console - Help regarding the console itself]]
207 elseif section == "c2s" then 211 elseif section == "c2s" then
208 print [[c2s:show(jid) - Show all client sessions with the specified JID (or all if no JID given)]] 212 print [[c2s:show(jid, columns) - Show all client sessions with the specified JID (or all if no JID given)]]
209 print [[c2s:show_insecure() - Show all unencrypted client connections]] 213 print [[c2s:show_tls(jid) - Show TLS cipher info for encrypted sessions]]
210 print [[c2s:show_secure() - Show all encrypted client connections]]
211 print [[c2s:show_tls() - Show TLS cipher info for encrypted sessions]]
212 print [[c2s:count() - Count sessions without listing them]] 214 print [[c2s:count() - Count sessions without listing them]]
213 print [[c2s:close(jid) - Close all sessions for the specified JID]] 215 print [[c2s:close(jid) - Close all sessions for the specified JID]]
214 print [[c2s:closeall() - Close all active c2s connections ]] 216 print [[c2s:closeall() - Close all active c2s connections ]]
215 elseif section == "s2s" then 217 elseif section == "s2s" then
216 print [[s2s:show(domain) - Show all s2s connections for the given domain (or all if no domain given)]] 218 print [[s2s:show(domain, columns) - Show all s2s connections for the given domain (or all if no domain given)]]
217 print [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]] 219 print [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]]
218 print [[s2s:close(from, to) - Close a connection from one domain to another]] 220 print [[s2s:close(from, to) - Close a connection from one domain to another]]
219 print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]] 221 print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]]
220 elseif section == "http" then 222 elseif section == "http" then
221 print [[http:list(hosts) - Show HTTP endpoints]] 223 print [[http:list(hosts) - Show HTTP endpoints]]
580 function def_env.config:reload() 582 function def_env.config:reload()
581 local ok, err = prosody.reload_config(); 583 local ok, err = prosody.reload_config();
582 return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err); 584 return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err);
583 end 585 end
584 586
585 local function common_info(session, line)
586 if session.id then
587 line[#line+1] = "["..session.id.."]"
588 else
589 line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]"
590 end
591 end
592
593 local function session_flags(session, line)
594 line = line or {};
595 common_info(session, line);
596 if session.type == "c2s" then
597 local status, priority = "unavailable", tostring(session.priority or "-");
598 if session.presence then
599 status = session.presence:get_child_text("show") or "available";
600 end
601 line[#line+1] = status.."("..priority..")";
602 end
603 if session.cert_identity_status == "valid" then
604 line[#line+1] = "(authenticated)";
605 end
606 if session.dialback_key then
607 line[#line+1] = "(dialback)";
608 end
609 if session.external_auth then
610 line[#line+1] = "(SASL)";
611 end
612 if session.secure then
613 line[#line+1] = "(encrypted)";
614 end
615 if session.compressed then
616 line[#line+1] = "(compressed)";
617 end
618 if session.smacks then
619 line[#line+1] = "(sm)";
620 end
621 if session.state then
622 if type(session.csi_counter) == "number" then
623 line[#line+1] = string.format("(csi:%s queue #%d)", session.state, session.csi_counter);
624 else
625 line[#line+1] = string.format("(csi:%s)", session.state);
626 end
627 end
628 if session.ip and session.ip:match(":") then
629 line[#line+1] = "(IPv6)";
630 end
631 if session.remote then
632 line[#line+1] = "(remote)";
633 end
634 if session.incoming and session.outgoing then
635 line[#line+1] = "(bidi)";
636 elseif session.is_bidi or session.bidi_session then
637 line[#line+1] = "(bidi)";
638 end
639 if session.bosh_version then
640 line[#line+1] = "(bosh)";
641 end
642 if session.websocket_request then
643 line[#line+1] = "(websocket)";
644 end
645 return table.concat(line, " ");
646 end
647
648 local function tls_info(session, line)
649 line = line or {};
650 common_info(session, line);
651 if session.secure then
652 local sock = session.conn and session.conn.socket and session.conn:socket();
653 if sock then
654 local info = sock.info and sock:info();
655 if info then
656 line[#line+1] = ("(%s with %s)"):format(info.protocol, info.cipher);
657 else
658 -- TLS session might not be ready yet
659 line[#line+1] = "(cipher info unavailable)";
660 end
661 if sock.getsniname then
662 local name = sock:getsniname();
663 if name then
664 line[#line+1] = ("(SNI:%q)"):format(name);
665 end
666 end
667 if sock.getalpn then
668 local proto = sock:getalpn();
669 if proto then
670 line[#line+1] = ("(ALPN:%q)"):format(proto);
671 end
672 end
673 end
674 else
675 line[#line+1] = "(insecure)";
676 end
677 return table.concat(line, " ");
678 end
679
680 def_env.c2s = {}; 587 def_env.c2s = {};
681 588
682 local function get_jid(session) 589 local function get_jid(session)
683 if session.username then 590 if session.username then
684 return session.full_jid or jid_join(session.username, session.host, session.resource); 591 return session.full_jid or jid_join(session.username, session.host, session.resource);
698 c2s:append(array.collect(values(module:shared"/*/bosh/sessions"))); 605 c2s:append(array.collect(values(module:shared"/*/bosh/sessions")));
699 c2s:unique(); 606 c2s:unique();
700 return c2s; 607 return c2s;
701 end 608 end
702 609
610 local function _sort_by_jid(a, b)
611 if a.host == b.host then
612 if a.username == b.username then return (a.resource or "") > (b.resource or ""); end
613 return (a.username or "") > (b.username or "");
614 end
615 return _sort_hosts(a.host or "", b.host or "");
616 end
617
703 local function show_c2s(callback) 618 local function show_c2s(callback)
704 get_c2s():sort(function(a, b) 619 get_c2s():sort(_sort_by_jid):map(function (session)
705 if a.host == b.host then
706 if a.username == b.username then
707 return (a.resource or "") > (b.resource or "");
708 end
709 return (a.username or "") > (b.username or "");
710 end
711 return _sort_hosts(a.host or "", b.host or "");
712 end):map(function (session)
713 callback(get_jid(session), session) 620 callback(get_jid(session), session)
714 end); 621 end);
715 end 622 end
716 623
717 function def_env.c2s:count() 624 function def_env.c2s:count()
718 local c2s = get_c2s(); 625 local c2s = get_c2s();
719 return true, "Total: ".. #c2s .." clients"; 626 return true, "Total: ".. #c2s .." clients";
720 end 627 end
721 628
722 function def_env.c2s:show(match_jid, annotate) 629 local function get_s2s_hosts(session) --> local,remote
723 local print, count = self.session.print, 0; 630 if session.direction == "outgoing" then
724 annotate = annotate or session_flags; 631 return session.host or session.from_host, session.to_host;
725 local curr_host = false; 632 elseif session.direction == "incoming" then
726 show_c2s(function (jid, session) 633 return session.host or session.to_host, session.from_host;
727 if curr_host ~= session.host then 634 end
728 curr_host = session.host; 635 end
729 print(curr_host or "(not connected to any host yet)"); 636
730 end 637 local available_columns = {
731 if (not match_jid) or jid:match(match_jid) then 638 jid = {
732 count = count + 1; 639 title = "JID";
733 print(annotate(session, { " ", jid })); 640 width = 32;
734 end 641 key = "full_jid";
735 end); 642 mapper = function(full_jid, session) return full_jid or get_jid(session) end;
736 return true, "Total: "..count.." clients"; 643 };
737 end 644 host = {
738 645 title = "Host";
739 function def_env.c2s:show_insecure(match_jid) 646 key = "host";
740 local print, count = self.session.print, 0; 647 width = 22;
741 show_c2s(function (jid, session) 648 mapper = function(host, session)
742 if ((not match_jid) or jid:match(match_jid)) and not session.secure then 649 if host ~= "" then return host; end
743 count = count + 1; 650 return get_s2s_hosts(session) or "?";
744 print(jid); 651 end;
745 end 652 };
746 end); 653 remote = {
747 return true, "Total: "..count.." insecure client connections"; 654 title = "Remote";
748 end 655 width = 22;
749 656 mapper = function(_, session)
750 function def_env.c2s:show_secure(match_jid) 657 return select(2, get_s2s_hosts(session));
751 local print, count = self.session.print, 0; 658 end;
752 show_c2s(function (jid, session) 659 };
753 if ((not match_jid) or jid:match(match_jid)) and session.secure then 660 dir = {
754 count = count + 1; 661 title = "Dir";
755 print(jid); 662 width = 3;
756 end 663 key = "direction";
757 end); 664 mapper = function (dir)
758 return true, "Total: "..count.." secure client connections"; 665 if dir == "outgoing" then return "-->"; end
666 if dir == "incoming" then return "<--"; end
667 return ""
668 end;
669 };
670 id = { title = "Session ID"; width = 20; key = "id" };
671 type = { title = "Type"; width = #"c2s_unauthed"; key = "type" };
672 method = {
673 title = "Method";
674 width = 10;
675 mapper = function(_, session)
676 if session.bosh_version then
677 return "BOSH";
678 elseif session.websocket_request then
679 return "WebSocket";
680 else
681 return "TCP";
682 end
683 end;
684 };
685 ipv = {
686 title = "IPv";
687 width = 4;
688 key = "ip";
689 mapper = function(ip) return ip:find(":") and "IPv6" or "IPv4"; end;
690 };
691 ip = { title = "IP address"; width = 40; key = "ip" };
692 status = {
693 title = "Status";
694 width = 11;
695 key = "presence";
696 mapper = function(p)
697 if not p or p == "" then return "unavailable"; end
698 return p:get_child_text("show") or "available";
699 end;
700 };
701 secure = {
702 title = "Security";
703 key = "conn";
704 width = 11;
705 mapper = function(conn, session)
706 if not session.secure then return "insecure"; end
707 if conn == "" or not conn:ssl() then return "secure" end
708 local sock = conn ~= "" and conn:socket();
709 if not sock then return "unknown TLS"; end
710 local tls_info = sock.info and sock:info();
711 return tls_info and tls_info.protocol or "unknown TLS";
712 end;
713 };
714 encryption = {
715 title = "Encryption";
716 width = 30;
717 key = "conn";
718 mapper = function(conn)
719 local sock = conn ~= "" and conn:socket();
720 local info = sock and sock.info and sock:info();
721 if info then return info.cipher end
722 return ""
723 end;
724 };
725 cert = {
726 title = "Certificate";
727 key = "cert_identity_status";
728 mapper = function(cert_status, session)
729 if cert_status ~= "" then return capitalize(cert_status); end
730 if session.cert_chain_status == "Invalid" then
731 local cert_errors = set.new(session.cert_chain_errors[1]);
732 if cert_errors:contains("certificate has expired") then
733 return "Expired";
734 elseif cert_errors:contains("self signed certificate") then
735 return "Self-signed";
736 end
737 return "Untrusted";
738 elseif session.cert_identity_status == "invalid" then
739 return "Mismatched";
740 end
741 return "Not validated";
742 end;
743 };
744 sni = {
745 title = "SNI";
746 width = 22;
747 mapper = function(_, session)
748 if not session.conn then return "" end
749 local sock = session.conn:socket();
750 return sock and sock.getsniname and sock:getsniname() or "";
751 end;
752 };
753 alpn = {
754 title = "ALPN";
755 width = 11;
756 mapper = function(_, session)
757 if not session.conn then return "" end
758 local sock = session.conn:socket();
759 return sock and sock.getalpn and sock:getalpn() or "";
760 end;
761 };
762 smacks = {
763 title = "SM";
764 key = "smacks";
765 width = 11;
766 mapper = function(smacks_xmlns, session)
767 if smacks_xmlns == "" then return "no"; end
768 if session.hibernating then return "hibernating"; end
769 return "yes";
770 end;
771 };
772 smacks_queue = {
773 title = "SM Queue";
774 key = "outgoing_stanza_queue";
775 width = 8;
776 align = "right";
777 mapper = function (queue)
778 return tostring(#queue);
779 end
780 };
781 csi = {
782 title = "CSI State";
783 key = "state";
784 -- TODO include counter
785 };
786 s2s_sasl = {
787 title = "SASL";
788 key = "external_auth";
789 width = 10;
790 mapper = capitalize
791 };
792 dialback = {
793 title = "Dialback";
794 key = "dialback_key";
795 width = 13;
796 mapper = function (dialback_key, session)
797 if dialback_key == "" then
798 if session.type == "s2sin" or session.type == "s2sout" then
799 return "Not used";
800 end
801 return "Not initiated";
802 elseif session.type == "s2sin_unauthed" or session.type == "s2sout_unauthed" then
803 return "Initiated";
804 else
805 return "Completed";
806 end
807 end
808 };
809 };
810
811 local function get_colspec(colspec, default)
812 local columns = {};
813 for i, col in pairs(colspec or default) do
814 if type(col) == "string" then
815 columns[i] = available_columns[col] or { title = capitalize(col); width = 20; key = col };
816 elseif type(col) ~= "table" then
817 return false, ("argument %d: expected string|table but got %s"):format(i, type(col));
818 else
819 columns[i] = col;
820 end
821 end
822
823 return columns;
824 end
825
826 function def_env.c2s:show(match_jid, colspec)
827 local print = self.session.print;
828 local columns = get_colspec(colspec, { "id"; "jid"; "ipv"; "status"; "secure"; "smacks"; "csi" });
829 local row = format_table(columns, 120);
830
831 local function match(session)
832 local jid = get_jid(session)
833 return (not match_jid) or jid:match(match_jid)
834 end
835
836 print(row());
837
838 for _, session in ipairs(get_c2s():filter(match):sort(_sort_by_jid)) do
839 print(row(session));
840 end
841 return true;
759 end 842 end
760 843
761 function def_env.c2s:show_tls(match_jid) 844 function def_env.c2s:show_tls(match_jid)
762 return self:show(match_jid, tls_info); 845 return self:show(match_jid, { "jid"; "id"; "secure"; "encryption" });
763 end 846 end
764 847
765 local function build_reason(text, condition) 848 local function build_reason(text, condition)
766 if text or condition then 849 if text or condition then
767 return { 850 return {
792 return true, "Total: "..count.." sessions closed"; 875 return true, "Total: "..count.." sessions closed";
793 end 876 end
794 877
795 878
796 def_env.s2s = {}; 879 def_env.s2s = {};
797 function def_env.s2s:show(match_jid, annotate) 880 local function _sort_s2s(a, b)
881 local a_local, a_remote = get_s2s_hosts(a);
882 local b_local, b_remote = get_s2s_hosts(b);
883 if (a_local or "") == (b_local or "") then return _sort_hosts(a_remote or "", b_remote or ""); end
884 return _sort_hosts(a_local or "", b_local or "");
885 end
886
887 function def_env.s2s:show(match_jid, colspec)
798 local print = self.session.print; 888 local print = self.session.print;
799 annotate = annotate or session_flags; 889 local columns = get_colspec(colspec, { "id"; "host"; "dir"; "remote"; "ipv"; "secure"; "s2s_sasl"; "dialback" });
800 890 local row = format_table(columns, 132);
801 local count_in, count_out = 0,0; 891
802 local s2s_list = { }; 892 local function match(session)
803 893 local host, remote = get_s2s_hosts(session);
804 local s2s_sessions = module:shared"/*/s2s/sessions"; 894 return not match_jid or (host or ""):match(match_jid) or (remote or ""):match(match_jid);
805 for _, session in pairs(s2s_sessions) do 895 end
806 local remotehost, localhost, direction; 896
807 if session.direction == "outgoing" then 897 local s2s_sessions = array(iterators.values(module:shared"/*/s2s/sessions")):filter(match):sort(_sort_s2s);
808 direction = "->"; 898
809 count_out = count_out + 1; 899 print(row());
810 remotehost, localhost = session.to_host or "?", session.from_host or "?"; 900
811 else 901 for _, session in ipairs(s2s_sessions) do
812 direction = "<-"; 902 print(row(session));
813 count_in = count_in + 1; 903 end
814 remotehost, localhost = session.from_host or "?", session.to_host or "?"; 904 return true; -- TODO counts
815 end
816 local sess_lines = { l = localhost, r = remotehost,
817 annotate(session, { "", direction, remotehost or "?" })};
818
819 if (not match_jid) or remotehost:match(match_jid) or localhost:match(match_jid) then
820 table.insert(s2s_list, sess_lines);
821 -- luacheck: ignore 421/print
822 local print = function (s) table.insert(sess_lines, " "..s); end
823 if session.sendq then
824 print("There are "..#session.sendq.." queued outgoing stanzas for this connection");
825 end
826 if session.type == "s2sout_unauthed" then
827 if session.notopen then
828 print("The <stream> has not yet been opened");
829 elseif not session.dialback_key then
830 print("Dialback has not been initiated yet");
831 elseif session.dialback_key then
832 print("Dialback has been requested, but no result received");
833 end
834 end
835 if session.type == "s2sin_unauthed" then
836 print("Connection not yet authenticated");
837 elseif session.type == "s2sin" then
838 for name in pairs(session.hosts) do
839 if name ~= session.from_host then
840 print("also hosts "..tostring(name));
841 end
842 end
843 end
844 end
845 end
846
847 -- Sort by local host, then remote host
848 table.sort(s2s_list, function(a,b)
849 if a.l == b.l then return _sort_hosts(a.r, b.r); end
850 return _sort_hosts(a.l, b.l);
851 end);
852 local lasthost;
853 for _, sess_lines in ipairs(s2s_list) do
854 if sess_lines.l ~= lasthost then print(sess_lines.l); lasthost=sess_lines.l end
855 for _, line in ipairs(sess_lines) do print(line); end
856 end
857 return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections";
858 end 905 end
859 906
860 function def_env.s2s:show_tls(match_jid) 907 function def_env.s2s:show_tls(match_jid)
861 return self:show(match_jid, tls_info); 908 return self:show(match_jid, { "id"; "host"; "dir"; "remote"; "secure"; "encryption"; "cert" });
862 end 909 end
863 910
864 local function print_subject(print, subject) 911 local function print_subject(print, subject)
865 for _, entry in ipairs(subject) do 912 for _, entry in ipairs(subject) do
866 print( 913 print(