Software /
code /
prosody
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( |