Comparison

plugins/mod_admin_shell.lua @ 11120:b2331f3dfeea

Merge 0.11->trunk
author Matthew Wild <mwild1@gmail.com>
date Wed, 30 Sep 2020 09:50:33 +0100
parent 11042:8a243ab49cb5
child 11361:dab1a6e46087
comparison
equal deleted inserted replaced
11119:68df52bf08d5 11120:b2331f3dfeea
1 -- Prosody IM
2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
4 --
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
7 --
8 -- luacheck: ignore 212/self
9
10 module:set_global();
11 module:depends("admin_socket");
12
13 local hostmanager = require "core.hostmanager";
14 local modulemanager = require "core.modulemanager";
15 local s2smanager = require "core.s2smanager";
16 local portmanager = require "core.portmanager";
17 local helpers = require "util.helpers";
18 local server = require "net.server";
19 local st = require "util.stanza";
20
21 local _G = _G;
22
23 local prosody = _G.prosody;
24
25 local unpack = table.unpack or unpack; -- luacheck: ignore 113
26 local iterators = require "util.iterators";
27 local keys, values = iterators.keys, iterators.values;
28 local jid_bare, jid_split, jid_join = import("util.jid", "bare", "prepped_split", "join");
29 local set, array = require "util.set", require "util.array";
30 local cert_verify_identity = require "util.x509".verify_identity;
31 local envload = require "util.envload".envload;
32 local envloadfile = require "util.envload".envloadfile;
33 local has_pposix, pposix = pcall(require, "util.pposix");
34 local async = require "util.async";
35 local serialization = require "util.serialization";
36 local serialize_config = serialization.new ({ fatal = false, unquoted = true});
37 local time = require "util.time";
38
39 local format_number = require "util.human.units".format;
40
41 local commands = module:shared("commands")
42 local def_env = module:shared("env");
43 local default_env_mt = { __index = def_env };
44
45 local function redirect_output(target, session)
46 local env = setmetatable({ print = session.print }, { __index = function (_, k) return rawget(target, k); end });
47 env.dofile = function(name)
48 local f, err = envloadfile(name, env);
49 if not f then return f, err; end
50 return f();
51 end;
52 return env;
53 end
54
55 console = {};
56
57 local runner_callbacks = {};
58
59 function runner_callbacks:error(err)
60 module:log("error", "Traceback[shell]: %s", err);
61
62 self.data.print("Fatal error while running command, it did not complete");
63 self.data.print("Error: "..tostring(err));
64 end
65
66 local function send_repl_output(session, line)
67 return session.send(st.stanza("repl-output"):text(tostring(line)));
68 end
69
70 function console:new_session(admin_session)
71 local session = {
72 send = function (t)
73 return send_repl_output(admin_session, t);
74 end;
75 print = function (...)
76 local t = {};
77 for i=1,select("#", ...) do
78 t[i] = tostring(select(i, ...));
79 end
80 return send_repl_output(admin_session, table.concat(t, "\t"));
81 end;
82 serialize = tostring;
83 disconnect = function () admin_session:close(); end;
84 };
85 session.env = setmetatable({}, default_env_mt);
86
87 session.thread = async.runner(function (line)
88 console:process_line(session, line);
89 end, runner_callbacks, session);
90
91 -- Load up environment with helper objects
92 for name, t in pairs(def_env) do
93 if type(t) == "table" then
94 session.env[name] = setmetatable({ session = session }, { __index = t });
95 end
96 end
97
98 session.env.output:configure();
99
100 return session;
101 end
102
103 local function handle_line(event)
104 local session = event.origin.shell_session;
105 if not session then
106 session = console:new_session(event.origin);
107 event.origin.shell_session = session;
108 end
109 local line = event.stanza:get_text();
110 local useglobalenv;
111
112 local result = st.stanza("repl-result");
113
114 if line:match("^>") then
115 line = line:gsub("^>", "");
116 useglobalenv = true;
117 else
118 local command = line:match("^%w+") or line:match("%p");
119 if commands[command] then
120 commands[command](session, line);
121 event.origin.send(result);
122 return;
123 end
124 end
125
126 session.env._ = line;
127
128 if not useglobalenv and commands[line:lower()] then
129 commands[line:lower()](session, line);
130 event.origin.send(result);
131 return;
132 end
133
134 local chunkname = "=console";
135 local env = (useglobalenv and redirect_output(_G, session)) or session.env or nil
136 -- luacheck: ignore 311/err
137 local chunk, err = envload("return "..line, chunkname, env);
138 if not chunk then
139 chunk, err = envload(line, chunkname, env);
140 if not chunk then
141 err = err:gsub("^%[string .-%]:%d+: ", "");
142 err = err:gsub("^:%d+: ", "");
143 err = err:gsub("'<eof>'", "the end of the line");
144 result.attr.type = "error";
145 result:text("Sorry, I couldn't understand that... "..err);
146 event.origin.send(result);
147 return;
148 end
149 end
150
151 local taskok, message = chunk();
152
153 if not message then
154 if type(taskok) ~= "string" and useglobalenv then
155 taskok = session.serialize(taskok);
156 end
157 result:text("Result: "..tostring(taskok));
158 elseif (not taskok) and message then
159 result.attr.type = "error";
160 result:text("Error: "..tostring(message));
161 else
162 result:text("OK: "..tostring(message));
163 end
164
165 event.origin.send(result);
166 end
167
168 module:hook("admin/repl-input", function (event)
169 local ok, err = pcall(handle_line, event);
170 if not ok then
171 event.origin.send(st.stanza("repl-result", { type = "error" }):text(err));
172 end
173 end);
174
175 -- Console commands --
176 -- These are simple commands, not valid standalone in Lua
177
178 function commands.help(session, data)
179 local print = session.print;
180 local section = data:match("^help (%w+)");
181 if not section then
182 print [[Commands are divided into multiple sections. For help on a particular section, ]]
183 print [[type: help SECTION (for example, 'help c2s'). Sections are: ]]
184 print [[]]
185 print [[c2s - Commands to manage local client-to-server sessions]]
186 print [[s2s - Commands to manage sessions between this server and others]]
187 print [[http - Commands to inspect HTTP services]] -- XXX plural but there is only one so far
188 print [[module - Commands to load/reload/unload modules/plugins]]
189 print [[host - Commands to activate, deactivate and list virtual hosts]]
190 print [[user - Commands to create and delete users, and change their passwords]]
191 print [[server - Uptime, version, shutting down, etc.]]
192 print [[port - Commands to manage ports the server is listening on]]
193 print [[dns - Commands to manage and inspect the internal DNS resolver]]
194 print [[xmpp - Commands for sending XMPP stanzas]]
195 print [[debug - Commands for debugging the server]]
196 print [[config - Reloading the configuration, etc.]]
197 print [[console - Help regarding the console itself]]
198 elseif section == "c2s" then
199 print [[c2s:show(jid) - Show all client sessions with the specified JID (or all if no JID given)]]
200 print [[c2s:show_insecure() - Show all unencrypted client connections]]
201 print [[c2s:show_secure() - Show all encrypted client connections]]
202 print [[c2s:show_tls() - Show TLS cipher info for encrypted sessions]]
203 print [[c2s:count() - Count sessions without listing them]]
204 print [[c2s:close(jid) - Close all sessions for the specified JID]]
205 print [[c2s:closeall() - Close all active c2s connections ]]
206 elseif section == "s2s" then
207 print [[s2s:show(domain) - Show all s2s connections for the given domain (or all if no domain given)]]
208 print [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]]
209 print [[s2s:close(from, to) - Close a connection from one domain to another]]
210 print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]]
211 elseif section == "http" then
212 print [[http:list(hosts) - Show HTTP endpoints]]
213 elseif section == "module" then
214 print [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]]
215 print [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]]
216 print [[module:unload(module, host) - The same, but just unloads the module from memory]]
217 print [[module:list(host) - List the modules loaded on the specified host]]
218 elseif section == "host" then
219 print [[host:activate(hostname) - Activates the specified host]]
220 print [[host:deactivate(hostname) - Disconnects all clients on this host and deactivates]]
221 print [[host:list() - List the currently-activated hosts]]
222 elseif section == "user" then
223 print [[user:create(jid, password) - Create the specified user account]]
224 print [[user:password(jid, password) - Set the password for the specified user account]]
225 print [[user:delete(jid) - Permanently remove the specified user account]]
226 print [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]]
227 elseif section == "server" then
228 print [[server:version() - Show the server's version number]]
229 print [[server:uptime() - Show how long the server has been running]]
230 print [[server:memory() - Show details about the server's memory usage]]
231 print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]]
232 elseif section == "port" then
233 print [[port:list() - Lists all network ports prosody currently listens on]]
234 print [[port:close(port, interface) - Close a port]]
235 elseif section == "dns" then
236 print [[dns:lookup(name, type, class) - Do a DNS lookup]]
237 print [[dns:addnameserver(nameserver) - Add a nameserver to the list]]
238 print [[dns:setnameserver(nameserver) - Replace the list of name servers with the supplied one]]
239 print [[dns:purge() - Clear the DNS cache]]
240 print [[dns:cache() - Show cached records]]
241 elseif section == "xmpp" then
242 print [[xmpp:ping(localhost, remotehost) -- Sends a ping to a remote XMPP server and reports the response]]
243 elseif section == "config" then
244 print [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]]
245 print [[config:get([host,] option) - Show the value of a config option.]]
246 elseif section == "stats" then -- luacheck: ignore 542
247 -- TODO describe how stats:show() works
248 elseif section == "debug" then
249 print [[debug:logevents(host) - Enable logging of fired events on host]]
250 print [[debug:events(host, event) - Show registered event handlers]]
251 print [[debug:timers() - Show information about scheduled timers]]
252 elseif section == "console" then
253 print [[Hey! Welcome to Prosody's admin console.]]
254 print [[First thing, if you're ever wondering how to get out, simply type 'quit'.]]
255 print [[Secondly, note that we don't support the full telnet protocol yet (it's coming)]]
256 print [[so you may have trouble using the arrow keys, etc. depending on your system.]]
257 print [[]]
258 print [[For now we offer a couple of handy shortcuts:]]
259 print [[!! - Repeat the last command]]
260 print [[!old!new! - repeat the last command, but with 'old' replaced by 'new']]
261 print [[]]
262 print [[For those well-versed in Prosody's internals, or taking instruction from those who are,]]
263 print [[you can prefix a command with > to escape the console sandbox, and access everything in]]
264 print [[the running server. Great fun, but be careful not to break anything :)]]
265 end
266 end
267
268 -- Session environment --
269 -- Anything in def_env will be accessible within the session as a global variable
270
271 --luacheck: ignore 212/self
272 local serialize_defaults = module:get_option("console_prettyprint_settings", { fatal = false, unquoted = true, maxdepth = 2})
273
274 def_env.output = {};
275 function def_env.output:configure(opts)
276 if type(opts) ~= "table" then
277 opts = { preset = opts };
278 end
279 if not opts.fallback then
280 -- XXX Error message passed to fallback is lost, does it matter?
281 opts.fallback = tostring;
282 end
283 for k,v in pairs(serialize_defaults) do
284 if opts[k] == nil then
285 opts[k] = v;
286 end
287 end
288 self.session.serialize = serialization.new(opts);
289 end
290
291 def_env.server = {};
292
293 function def_env.server:insane_reload()
294 prosody.unlock_globals();
295 dofile "prosody"
296 prosody = _G.prosody;
297 return true, "Server reloaded";
298 end
299
300 function def_env.server:version()
301 return true, tostring(prosody.version or "unknown");
302 end
303
304 function def_env.server:uptime()
305 local t = os.time()-prosody.start_time;
306 local seconds = t%60;
307 t = (t - seconds)/60;
308 local minutes = t%60;
309 t = (t - minutes)/60;
310 local hours = t%24;
311 t = (t - hours)/24;
312 local days = t;
313 return true, string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)",
314 days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "",
315 minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time));
316 end
317
318 function def_env.server:shutdown(reason)
319 prosody.shutdown(reason);
320 return true, "Shutdown initiated";
321 end
322
323 local function human(kb)
324 return format_number(kb*1024, "B", "b");
325 end
326
327 function def_env.server:memory()
328 if not has_pposix or not pposix.meminfo then
329 return true, "Lua is using "..human(collectgarbage("count"));
330 end
331 local mem, lua_mem = pposix.meminfo(), collectgarbage("count");
332 local print = self.session.print;
333 print("Process: "..human((mem.allocated+mem.allocated_mmap)/1024));
334 print(" Used: "..human(mem.used/1024).." ("..human(lua_mem).." by Lua)");
335 print(" Free: "..human(mem.unused/1024).." ("..human(mem.returnable/1024).." returnable)");
336 return true, "OK";
337 end
338
339 def_env.module = {};
340
341 local function get_hosts_set(hosts)
342 if type(hosts) == "table" then
343 if hosts[1] then
344 return set.new(hosts);
345 elseif hosts._items then
346 return hosts;
347 end
348 elseif type(hosts) == "string" then
349 return set.new { hosts };
350 elseif hosts == nil then
351 return set.new(array.collect(keys(prosody.hosts)));
352 end
353 end
354
355 -- Hosts with a module or all virtualhosts if no module given
356 -- matching modules_enabled in the global section
357 local function get_hosts_with_module(hosts, module)
358 local hosts_set = get_hosts_set(hosts)
359 / function (host)
360 if module then
361 -- Module given, filter in hosts with this module loaded
362 if modulemanager.is_loaded(host, module) then
363 return host;
364 else
365 return nil;
366 end
367 end
368 if not hosts then
369 -- No hosts given, filter in VirtualHosts
370 if prosody.hosts[host].type == "local" then
371 return host;
372 else
373 return nil
374 end
375 end;
376 -- No module given, but hosts are, don't filter at all
377 return host;
378 end;
379 if module and modulemanager.get_module("*", module) then
380 hosts_set:add("*");
381 end
382 return hosts_set;
383 end
384
385 function def_env.module:load(name, hosts, config)
386 hosts = get_hosts_with_module(hosts);
387
388 -- Load the module for each host
389 local ok, err, count, mod = true, nil, 0;
390 for host in hosts do
391 if (not modulemanager.is_loaded(host, name)) then
392 mod, err = modulemanager.load(host, name, config);
393 if not mod then
394 ok = false;
395 if err == "global-module-already-loaded" then
396 if count > 0 then
397 ok, err, count = true, nil, 1;
398 end
399 break;
400 end
401 self.session.print(err or "Unknown error loading module");
402 else
403 count = count + 1;
404 self.session.print("Loaded for "..mod.module.host);
405 end
406 end
407 end
408
409 return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
410 end
411
412 function def_env.module:unload(name, hosts)
413 hosts = get_hosts_with_module(hosts, name);
414
415 -- Unload the module for each host
416 local ok, err, count = true, nil, 0;
417 for host in hosts do
418 if modulemanager.is_loaded(host, name) then
419 ok, err = modulemanager.unload(host, name);
420 if not ok then
421 ok = false;
422 self.session.print(err or "Unknown error unloading module");
423 else
424 count = count + 1;
425 self.session.print("Unloaded from "..host);
426 end
427 end
428 end
429 return ok, (ok and "Module unloaded from "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
430 end
431
432 local function _sort_hosts(a, b)
433 if a == "*" then return true
434 elseif b == "*" then return false
435 else return a:gsub("[^.]+", string.reverse):reverse() < b:gsub("[^.]+", string.reverse):reverse(); end
436 end
437
438 function def_env.module:reload(name, hosts)
439 hosts = array.collect(get_hosts_with_module(hosts, name)):sort(_sort_hosts)
440
441 -- Reload the module for each host
442 local ok, err, count = true, nil, 0;
443 for _, host in ipairs(hosts) do
444 if modulemanager.is_loaded(host, name) then
445 ok, err = modulemanager.reload(host, name);
446 if not ok then
447 ok = false;
448 self.session.print(err or "Unknown error reloading module");
449 else
450 count = count + 1;
451 if ok == nil then
452 ok = true;
453 end
454 self.session.print("Reloaded on "..host);
455 end
456 end
457 end
458 return ok, (ok and "Module reloaded on "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
459 end
460
461 function def_env.module:list(hosts)
462 hosts = array.collect(set.new({ not hosts and "*" or nil }) + get_hosts_set(hosts)):sort(_sort_hosts);
463
464 local print = self.session.print;
465 for _, host in ipairs(hosts) do
466 print((host == "*" and "Global" or host)..":");
467 local modules = array.collect(keys(modulemanager.get_modules(host) or {})):sort();
468 if #modules == 0 then
469 if prosody.hosts[host] then
470 print(" No modules loaded");
471 else
472 print(" Host not found");
473 end
474 else
475 for _, name in ipairs(modules) do
476 local status, status_text = modulemanager.get_module(host, name).module:get_status();
477 local status_summary = "";
478 if status == "warn" or status == "error" then
479 status_summary = (" (%s: %s)"):format(status, status_text);
480 end
481 print((" %s%s"):format(name, status_summary));
482 end
483 end
484 end
485 end
486
487 def_env.config = {};
488 function def_env.config:load(filename, format)
489 local config_load = require "core.configmanager".load;
490 local ok, err = config_load(filename, format);
491 if not ok then
492 return false, err or "Unknown error loading config";
493 end
494 return true, "Config loaded";
495 end
496
497 function def_env.config:get(host, key)
498 if key == nil then
499 host, key = "*", host;
500 end
501 local config_get = require "core.configmanager".get
502 return true, serialize_config(config_get(host, key));
503 end
504
505 function def_env.config:reload()
506 local ok, err = prosody.reload_config();
507 return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err);
508 end
509
510 local function common_info(session, line)
511 if session.id then
512 line[#line+1] = "["..session.id.."]"
513 else
514 line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]"
515 end
516 end
517
518 local function session_flags(session, line)
519 line = line or {};
520 common_info(session, line);
521 if session.type == "c2s" then
522 local status, priority = "unavailable", tostring(session.priority or "-");
523 if session.presence then
524 status = session.presence:get_child_text("show") or "available";
525 end
526 line[#line+1] = status.."("..priority..")";
527 end
528 if session.cert_identity_status == "valid" then
529 line[#line+1] = "(authenticated)";
530 end
531 if session.dialback_key then
532 line[#line+1] = "(dialback)";
533 end
534 if session.external_auth then
535 line[#line+1] = "(SASL)";
536 end
537 if session.secure then
538 line[#line+1] = "(encrypted)";
539 end
540 if session.compressed then
541 line[#line+1] = "(compressed)";
542 end
543 if session.smacks then
544 line[#line+1] = "(sm)";
545 end
546 if session.state then
547 if type(session.csi_counter) == "number" then
548 line[#line+1] = string.format("(csi:%s queue #%d)", session.state, session.csi_counter);
549 else
550 line[#line+1] = string.format("(csi:%s)", session.state);
551 end
552 end
553 if session.ip and session.ip:match(":") then
554 line[#line+1] = "(IPv6)";
555 end
556 if session.remote then
557 line[#line+1] = "(remote)";
558 end
559 if session.incoming and session.outgoing then
560 line[#line+1] = "(bidi)";
561 elseif session.is_bidi or session.bidi_session then
562 line[#line+1] = "(bidi)";
563 end
564 if session.bosh_version then
565 line[#line+1] = "(bosh)";
566 end
567 if session.websocket_request then
568 line[#line+1] = "(websocket)";
569 end
570 return table.concat(line, " ");
571 end
572
573 local function tls_info(session, line)
574 line = line or {};
575 common_info(session, line);
576 if session.secure then
577 local sock = session.conn and session.conn.socket and session.conn:socket();
578 if sock then
579 local info = sock.info and sock:info();
580 if info then
581 line[#line+1] = ("(%s with %s)"):format(info.protocol, info.cipher);
582 else
583 -- TLS session might not be ready yet
584 line[#line+1] = "(cipher info unavailable)";
585 end
586 if sock.getsniname then
587 local name = sock:getsniname();
588 if name then
589 line[#line+1] = ("(SNI:%q)"):format(name);
590 end
591 end
592 if sock.getalpn then
593 local proto = sock:getalpn();
594 if proto then
595 line[#line+1] = ("(ALPN:%q)"):format(proto);
596 end
597 end
598 end
599 else
600 line[#line+1] = "(insecure)";
601 end
602 return table.concat(line, " ");
603 end
604
605 def_env.c2s = {};
606
607 local function get_jid(session)
608 if session.username then
609 return session.full_jid or jid_join(session.username, session.host, session.resource);
610 end
611
612 local conn = session.conn;
613 local ip = session.ip or "?";
614 local clientport = conn and conn:clientport() or "?";
615 local serverip = conn and conn.server and conn:server():ip() or "?";
616 local serverport = conn and conn:serverport() or "?"
617 return jid_join("["..ip.."]:"..clientport, session.host or "["..serverip.."]:"..serverport);
618 end
619
620 local function get_c2s()
621 local c2s = array.collect(values(prosody.full_sessions));
622 c2s:append(array.collect(values(module:shared"/*/c2s/sessions")));
623 c2s:append(array.collect(values(module:shared"/*/bosh/sessions")));
624 c2s:unique();
625 return c2s;
626 end
627
628 local function show_c2s(callback)
629 get_c2s():sort(function(a, b)
630 if a.host == b.host then
631 if a.username == b.username then
632 return (a.resource or "") > (b.resource or "");
633 end
634 return (a.username or "") > (b.username or "");
635 end
636 return _sort_hosts(a.host or "", b.host or "");
637 end):map(function (session)
638 callback(get_jid(session), session)
639 end);
640 end
641
642 function def_env.c2s:count()
643 local c2s = get_c2s();
644 return true, "Total: ".. #c2s .." clients";
645 end
646
647 function def_env.c2s:show(match_jid, annotate)
648 local print, count = self.session.print, 0;
649 annotate = annotate or session_flags;
650 local curr_host = false;
651 show_c2s(function (jid, session)
652 if curr_host ~= session.host then
653 curr_host = session.host;
654 print(curr_host or "(not connected to any host yet)");
655 end
656 if (not match_jid) or jid:match(match_jid) then
657 count = count + 1;
658 print(annotate(session, { " ", jid }));
659 end
660 end);
661 return true, "Total: "..count.." clients";
662 end
663
664 function def_env.c2s:show_insecure(match_jid)
665 local print, count = self.session.print, 0;
666 show_c2s(function (jid, session)
667 if ((not match_jid) or jid:match(match_jid)) and not session.secure then
668 count = count + 1;
669 print(jid);
670 end
671 end);
672 return true, "Total: "..count.." insecure client connections";
673 end
674
675 function def_env.c2s:show_secure(match_jid)
676 local print, count = self.session.print, 0;
677 show_c2s(function (jid, session)
678 if ((not match_jid) or jid:match(match_jid)) and session.secure then
679 count = count + 1;
680 print(jid);
681 end
682 end);
683 return true, "Total: "..count.." secure client connections";
684 end
685
686 function def_env.c2s:show_tls(match_jid)
687 return self:show(match_jid, tls_info);
688 end
689
690 local function build_reason(text, condition)
691 if text or condition then
692 return {
693 text = text,
694 condition = condition or "undefined-condition",
695 };
696 end
697 end
698
699 function def_env.c2s:close(match_jid, text, condition)
700 local count = 0;
701 show_c2s(function (jid, session)
702 if jid == match_jid or jid_bare(jid) == match_jid then
703 count = count + 1;
704 session:close(build_reason(text, condition));
705 end
706 end);
707 return true, "Total: "..count.." sessions closed";
708 end
709
710 function def_env.c2s:closeall(text, condition)
711 local count = 0;
712 --luacheck: ignore 212/jid
713 show_c2s(function (jid, session)
714 count = count + 1;
715 session:close(build_reason(text, condition));
716 end);
717 return true, "Total: "..count.." sessions closed";
718 end
719
720
721 def_env.s2s = {};
722 function def_env.s2s:show(match_jid, annotate)
723 local print = self.session.print;
724 annotate = annotate or session_flags;
725
726 local count_in, count_out = 0,0;
727 local s2s_list = { };
728
729 local s2s_sessions = module:shared"/*/s2s/sessions";
730 for _, session in pairs(s2s_sessions) do
731 local remotehost, localhost, direction;
732 if session.direction == "outgoing" then
733 direction = "->";
734 count_out = count_out + 1;
735 remotehost, localhost = session.to_host or "?", session.from_host or "?";
736 else
737 direction = "<-";
738 count_in = count_in + 1;
739 remotehost, localhost = session.from_host or "?", session.to_host or "?";
740 end
741 local sess_lines = { l = localhost, r = remotehost,
742 annotate(session, { "", direction, remotehost or "?" })};
743
744 if (not match_jid) or remotehost:match(match_jid) or localhost:match(match_jid) then
745 table.insert(s2s_list, sess_lines);
746 -- luacheck: ignore 421/print
747 local print = function (s) table.insert(sess_lines, " "..s); end
748 if session.sendq then
749 print("There are "..#session.sendq.." queued outgoing stanzas for this connection");
750 end
751 if session.type == "s2sout_unauthed" then
752 if session.connecting then
753 print("Connection not yet established");
754 if not session.srv_hosts then
755 if not session.conn then
756 print("We do not yet have a DNS answer for this host's SRV records");
757 else
758 print("This host has no SRV records, using A record instead");
759 end
760 elseif session.srv_choice then
761 print("We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts);
762 local srv_choice = session.srv_hosts[session.srv_choice];
763 print("Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269));
764 end
765 elseif session.notopen then
766 print("The <stream> has not yet been opened");
767 elseif not session.dialback_key then
768 print("Dialback has not been initiated yet");
769 elseif session.dialback_key then
770 print("Dialback has been requested, but no result received");
771 end
772 end
773 if session.type == "s2sin_unauthed" then
774 print("Connection not yet authenticated");
775 elseif session.type == "s2sin" then
776 for name in pairs(session.hosts) do
777 if name ~= session.from_host then
778 print("also hosts "..tostring(name));
779 end
780 end
781 end
782 end
783 end
784
785 -- Sort by local host, then remote host
786 table.sort(s2s_list, function(a,b)
787 if a.l == b.l then return _sort_hosts(a.r, b.r); end
788 return _sort_hosts(a.l, b.l);
789 end);
790 local lasthost;
791 for _, sess_lines in ipairs(s2s_list) do
792 if sess_lines.l ~= lasthost then print(sess_lines.l); lasthost=sess_lines.l end
793 for _, line in ipairs(sess_lines) do print(line); end
794 end
795 return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections";
796 end
797
798 function def_env.s2s:show_tls(match_jid)
799 return self:show(match_jid, tls_info);
800 end
801
802 local function print_subject(print, subject)
803 for _, entry in ipairs(subject) do
804 print(
805 (" %s: %q"):format(
806 entry.name or entry.oid,
807 entry.value:gsub("[\r\n%z%c]", " ")
808 )
809 );
810 end
811 end
812
813 -- As much as it pains me to use the 0-based depths that OpenSSL does,
814 -- I think there's going to be more confusion among operators if we
815 -- break from that.
816 local function print_errors(print, errors)
817 for depth, t in pairs(errors) do
818 print(
819 (" %d: %s"):format(
820 depth-1,
821 table.concat(t, "\n| ")
822 )
823 );
824 end
825 end
826
827 function def_env.s2s:showcert(domain)
828 local print = self.session.print;
829 local s2s_sessions = module:shared"/*/s2s/sessions";
830 local domain_sessions = set.new(array.collect(values(s2s_sessions)))
831 /function(session) return (session.to_host == domain or session.from_host == domain) and session or nil; end;
832 local cert_set = {};
833 for session in domain_sessions do
834 local conn = session.conn;
835 conn = conn and conn:socket();
836 if not conn.getpeerchain then
837 if conn.dohandshake then
838 error("This version of LuaSec does not support certificate viewing");
839 end
840 else
841 local cert = conn:getpeercertificate();
842 if cert then
843 local certs = conn:getpeerchain();
844 local digest = cert:digest("sha1");
845 if not cert_set[digest] then
846 local chain_valid, chain_errors = conn:getpeerverification();
847 cert_set[digest] = {
848 {
849 from = session.from_host,
850 to = session.to_host,
851 direction = session.direction
852 };
853 chain_valid = chain_valid;
854 chain_errors = chain_errors;
855 certs = certs;
856 };
857 else
858 table.insert(cert_set[digest], {
859 from = session.from_host,
860 to = session.to_host,
861 direction = session.direction
862 });
863 end
864 end
865 end
866 end
867 local domain_certs = array.collect(values(cert_set));
868 -- Phew. We now have a array of unique certificates presented by domain.
869 local n_certs = #domain_certs;
870
871 if n_certs == 0 then
872 return "No certificates found for "..domain;
873 end
874
875 local function _capitalize_and_colon(byte)
876 return string.upper(byte)..":";
877 end
878 local function pretty_fingerprint(hash)
879 return hash:gsub("..", _capitalize_and_colon):sub(1, -2);
880 end
881
882 for cert_info in values(domain_certs) do
883 local certs = cert_info.certs;
884 local cert = certs[1];
885 print("---")
886 print("Fingerprint (SHA1): "..pretty_fingerprint(cert:digest("sha1")));
887 print("");
888 local n_streams = #cert_info;
889 print("Currently used on "..n_streams.." stream"..(n_streams==1 and "" or "s")..":");
890 for _, stream in ipairs(cert_info) do
891 if stream.direction == "incoming" then
892 print(" "..stream.to.." <- "..stream.from);
893 else
894 print(" "..stream.from.." -> "..stream.to);
895 end
896 end
897 print("");
898 local chain_valid, errors = cert_info.chain_valid, cert_info.chain_errors;
899 local valid_identity = cert_verify_identity(domain, "xmpp-server", cert);
900 if chain_valid then
901 print("Trusted certificate: Yes");
902 else
903 print("Trusted certificate: No");
904 print_errors(print, errors);
905 end
906 print("");
907 print("Issuer: ");
908 print_subject(print, cert:issuer());
909 print("");
910 print("Valid for "..domain..": "..(valid_identity and "Yes" or "No"));
911 print("Subject:");
912 print_subject(print, cert:subject());
913 end
914 print("---");
915 return ("Showing "..n_certs.." certificate"
916 ..(n_certs==1 and "" or "s")
917 .." presented by "..domain..".");
918 end
919
920 function def_env.s2s:close(from, to, text, condition)
921 local print, count = self.session.print, 0;
922 local s2s_sessions = module:shared"/*/s2s/sessions";
923
924 local match_id;
925 if from and not to then
926 match_id, from = from, nil;
927 elseif not to then
928 return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'";
929 elseif from == to then
930 return false, "Both from and to are the same... you can't do that :)";
931 end
932
933 for _, session in pairs(s2s_sessions) do
934 local id = session.id or (session.type..tostring(session):match("[a-f0-9]+$"));
935 if (match_id and match_id == id)
936 or (session.from_host == from and session.to_host == to) then
937 print(("Closing connection from %s to %s [%s]"):format(session.from_host, session.to_host, id));
938 (session.close or s2smanager.destroy_session)(session, build_reason(text, condition));
939 count = count + 1 ;
940 end
941 end
942 return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s");
943 end
944
945 function def_env.s2s:closeall(host, text, condition)
946 local count = 0;
947 local s2s_sessions = module:shared"/*/s2s/sessions";
948 for _,session in pairs(s2s_sessions) do
949 if not host or session.from_host == host or session.to_host == host then
950 session:close(build_reason(text, condition));
951 count = count + 1;
952 end
953 end
954 if count == 0 then return false, "No sessions to close.";
955 else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end
956 end
957
958 def_env.host = {}; def_env.hosts = def_env.host;
959
960 function def_env.host:activate(hostname, config)
961 return hostmanager.activate(hostname, config);
962 end
963 function def_env.host:deactivate(hostname, reason)
964 return hostmanager.deactivate(hostname, reason);
965 end
966
967 function def_env.host:list()
968 local print = self.session.print;
969 local i = 0;
970 local type;
971 for host, host_session in iterators.sorted_pairs(prosody.hosts, _sort_hosts) do
972 i = i + 1;
973 type = host_session.type;
974 if type == "local" then
975 print(host);
976 else
977 type = module:context(host):get_option_string("component_module", type);
978 if type ~= "component" then
979 type = type .. " component";
980 end
981 print(("%s (%s)"):format(host, type));
982 end
983 end
984 return true, i.." hosts";
985 end
986
987 def_env.port = {};
988
989 function def_env.port:list()
990 local print = self.session.print;
991 local services = portmanager.get_active_services().data;
992 local n_services, n_ports = 0, 0;
993 for service, interfaces in iterators.sorted_pairs(services) do
994 n_services = n_services + 1;
995 local ports_list = {};
996 for interface, ports in pairs(interfaces) do
997 for port in pairs(ports) do
998 table.insert(ports_list, "["..interface.."]:"..port);
999 end
1000 end
1001 n_ports = n_ports + #ports_list;
1002 print(service..": "..table.concat(ports_list, ", "));
1003 end
1004 return true, n_services.." services listening on "..n_ports.." ports";
1005 end
1006
1007 function def_env.port:close(close_port, close_interface)
1008 close_port = assert(tonumber(close_port), "Invalid port number");
1009 local n_closed = 0;
1010 local services = portmanager.get_active_services().data;
1011 for service, interfaces in pairs(services) do -- luacheck: ignore 213
1012 for interface, ports in pairs(interfaces) do
1013 if not close_interface or close_interface == interface then
1014 if ports[close_port] then
1015 self.session.print("Closing ["..interface.."]:"..close_port.."...");
1016 local ok, err = portmanager.close(interface, close_port)
1017 if not ok then
1018 self.session.print("Failed to close "..interface.." "..close_port..": "..err);
1019 else
1020 n_closed = n_closed + 1;
1021 end
1022 end
1023 end
1024 end
1025 end
1026 return true, "Closed "..n_closed.." ports";
1027 end
1028
1029 def_env.muc = {};
1030
1031 local console_room_mt = {
1032 __index = function (self, k) return self.room[k]; end;
1033 __tostring = function (self)
1034 return "MUC room <"..self.room.jid..">";
1035 end;
1036 };
1037
1038 local function check_muc(jid)
1039 local room_name, host = jid_split(jid);
1040 if not prosody.hosts[host] then
1041 return nil, "No such host: "..host;
1042 elseif not prosody.hosts[host].modules.muc then
1043 return nil, "Host '"..host.."' is not a MUC service";
1044 end
1045 return room_name, host;
1046 end
1047
1048 function def_env.muc:create(room_jid, config)
1049 local room_name, host = check_muc(room_jid);
1050 if not room_name then
1051 return room_name, host;
1052 end
1053 if not room_name then return nil, host end
1054 if config ~= nil and type(config) ~= "table" then return nil, "Config must be a table"; end
1055 if prosody.hosts[host].modules.muc.get_room_from_jid(room_jid) then return nil, "Room exists already" end
1056 return prosody.hosts[host].modules.muc.create_room(room_jid, config);
1057 end
1058
1059 function def_env.muc:room(room_jid)
1060 local room_name, host = check_muc(room_jid);
1061 if not room_name then
1062 return room_name, host;
1063 end
1064 local room_obj = prosody.hosts[host].modules.muc.get_room_from_jid(room_jid);
1065 if not room_obj then
1066 return nil, "No such room: "..room_jid;
1067 end
1068 return setmetatable({ room = room_obj }, console_room_mt);
1069 end
1070
1071 function def_env.muc:list(host)
1072 local host_session = prosody.hosts[host];
1073 if not host_session or not host_session.modules.muc then
1074 return nil, "Please supply the address of a local MUC component";
1075 end
1076 local print = self.session.print;
1077 local c = 0;
1078 for room in host_session.modules.muc.each_room() do
1079 print(room.jid);
1080 c = c + 1;
1081 end
1082 return true, c.." rooms";
1083 end
1084
1085 local um = require"core.usermanager";
1086
1087 def_env.user = {};
1088 function def_env.user:create(jid, password)
1089 local username, host = jid_split(jid);
1090 if not prosody.hosts[host] then
1091 return nil, "No such host: "..host;
1092 elseif um.user_exists(username, host) then
1093 return nil, "User exists";
1094 end
1095 local ok, err = um.create_user(username, password, host);
1096 if ok then
1097 return true, "User created";
1098 else
1099 return nil, "Could not create user: "..err;
1100 end
1101 end
1102
1103 function def_env.user:delete(jid)
1104 local username, host = jid_split(jid);
1105 if not prosody.hosts[host] then
1106 return nil, "No such host: "..host;
1107 elseif not um.user_exists(username, host) then
1108 return nil, "No such user";
1109 end
1110 local ok, err = um.delete_user(username, host);
1111 if ok then
1112 return true, "User deleted";
1113 else
1114 return nil, "Could not delete user: "..err;
1115 end
1116 end
1117
1118 function def_env.user:password(jid, password)
1119 local username, host = jid_split(jid);
1120 if not prosody.hosts[host] then
1121 return nil, "No such host: "..host;
1122 elseif not um.user_exists(username, host) then
1123 return nil, "No such user";
1124 end
1125 local ok, err = um.set_password(username, password, host, nil);
1126 if ok then
1127 return true, "User password changed";
1128 else
1129 return nil, "Could not change password for user: "..err;
1130 end
1131 end
1132
1133 function def_env.user:list(host, pat)
1134 if not host then
1135 return nil, "No host given";
1136 elseif not prosody.hosts[host] then
1137 return nil, "No such host";
1138 end
1139 local print = self.session.print;
1140 local total, matches = 0, 0;
1141 for user in um.users(host) do
1142 if not pat or user:match(pat) then
1143 print(user.."@"..host);
1144 matches = matches + 1;
1145 end
1146 total = total + 1;
1147 end
1148 return true, "Showing "..(pat and (matches.." of ") or "all " )..total.." users";
1149 end
1150
1151 def_env.xmpp = {};
1152
1153 local new_id = require "util.id".medium;
1154 function def_env.xmpp:ping(localhost, remotehost, timeout)
1155 localhost = select(2, jid_split(localhost));
1156 remotehost = select(2, jid_split(remotehost));
1157 if not localhost then
1158 return nil, "Invalid sender hostname";
1159 elseif not prosody.hosts[localhost] then
1160 return nil, "No such local host";
1161 end
1162 if not remotehost then
1163 return nil, "Invalid destination hostname";
1164 elseif prosody.hosts[remotehost] then
1165 return nil, "Both hosts are local";
1166 end
1167 local iq = st.iq{ from=localhost, to=remotehost, type="get", id=new_id()}
1168 :tag("ping", {xmlns="urn:xmpp:ping"});
1169 local time_start = time.now();
1170 local ret, err = async.wait_for(module:context(localhost):send_iq(iq, nil, timeout));
1171 if ret then
1172 return true, ("pong from %s in %gs"):format(ret.stanza.attr.from, time.now() - time_start);
1173 else
1174 return false, tostring(err);
1175 end
1176 end
1177
1178 def_env.dns = {};
1179 local adns = require"net.adns";
1180
1181 local function get_resolver(session)
1182 local resolver = session.dns_resolver;
1183 if not resolver then
1184 resolver = adns.resolver();
1185 session.dns_resolver = resolver;
1186 end
1187 return resolver;
1188 end
1189
1190 function def_env.dns:lookup(name, typ, class)
1191 local resolver = get_resolver(self.session);
1192 local ret, err = async.wait_for(resolver:lookup_promise(name, typ, class));
1193 if ret then
1194 return true, ret;
1195 elseif err then
1196 return false, err;
1197 end
1198 end
1199
1200 function def_env.dns:addnameserver(...)
1201 local resolver = get_resolver(self.session);
1202 resolver._resolver:addnameserver(...)
1203 return true
1204 end
1205
1206 function def_env.dns:setnameserver(...)
1207 local resolver = get_resolver(self.session);
1208 resolver._resolver:setnameserver(...)
1209 return true
1210 end
1211
1212 function def_env.dns:purge()
1213 local resolver = get_resolver(self.session);
1214 resolver._resolver:purge()
1215 return true
1216 end
1217
1218 function def_env.dns:cache()
1219 local resolver = get_resolver(self.session);
1220 return true, "Cache:\n"..tostring(resolver._resolver.cache)
1221 end
1222
1223 def_env.http = {};
1224
1225 function def_env.http:list(hosts)
1226 local print = self.session.print;
1227
1228 for host in get_hosts_set(hosts) do
1229 local http_apps = modulemanager.get_items("http-provider", host);
1230 if #http_apps > 0 then
1231 local http_host = module:context(host):get_option_string("http_host");
1232 print("HTTP endpoints on "..host..(http_host and (" (using "..http_host.."):") or ":"));
1233 for _, provider in ipairs(http_apps) do
1234 local url = module:context(host):http_url(provider.name, provider.default_path);
1235 print("", url);
1236 end
1237 print("");
1238 end
1239 end
1240
1241 local default_host = module:get_option_string("http_default_host");
1242 if not default_host then
1243 print("HTTP requests to unknown hosts will return 404 Not Found");
1244 else
1245 print("HTTP requests to unknown hosts will be handled by "..default_host);
1246 end
1247 return true;
1248 end
1249
1250 def_env.debug = {};
1251
1252 function def_env.debug:logevents(host)
1253 helpers.log_host_events(host);
1254 return true;
1255 end
1256
1257 function def_env.debug:events(host, event)
1258 local events_obj;
1259 if host and host ~= "*" then
1260 if host == "http" then
1261 events_obj = require "net.http.server"._events;
1262 elseif not prosody.hosts[host] then
1263 return false, "Unknown host: "..host;
1264 else
1265 events_obj = prosody.hosts[host].events;
1266 end
1267 else
1268 events_obj = prosody.events;
1269 end
1270 return true, helpers.show_events(events_obj, event);
1271 end
1272
1273 function def_env.debug:timers()
1274 local print = self.session.print;
1275 local add_task = require"util.timer".add_task;
1276 local h, params = add_task.h, add_task.params;
1277 local function normalize_time(t)
1278 return t;
1279 end
1280 local function format_time(t)
1281 return os.date("%F %T", math.floor(normalize_time(t)));
1282 end
1283 if h then
1284 print("-- util.timer");
1285 elseif server.timer then
1286 print("-- net.server.timer");
1287 h = server.timer.add_task.timers;
1288 normalize_time = server.timer.to_absolute_time or normalize_time;
1289 end
1290 if h then
1291 for i, id in ipairs(h.ids) do
1292 local t, cb = h.priorities[i], h.items[id];
1293 if not params then
1294 local param = cb.param;
1295 if param then
1296 cb = param.callback;
1297 else
1298 cb = cb.timer_callback or cb;
1299 end
1300 elseif params[id] then
1301 cb = params[id].callback or cb;
1302 end
1303 print(format_time(t), cb);
1304 end
1305 end
1306 if server.event_base then
1307 local count = 0;
1308 for _, v in pairs(debug.getregistry()) do
1309 if type(v) == "function" and v.callback and v.callback == add_task._on_timer then
1310 count = count + 1;
1311 end
1312 end
1313 print(count .. " libevent callbacks");
1314 end
1315 if h then
1316 local next_time = h:peek();
1317 if next_time then
1318 return true, ("Next event at %s (in %.6fs)"):format(format_time(next_time), normalize_time(next_time) - time.now());
1319 end
1320 end
1321 return true;
1322 end
1323
1324 -- COMPAT: debug:timers() was timer:info() for some time in trunk
1325 def_env.timer = { info = def_env.debug.timers };
1326
1327 def_env.stats = {};
1328
1329 local short_units = {
1330 seconds = "s",
1331 bytes = "B",
1332 };
1333
1334 local function format_stat(type, unit, value, ref_value)
1335 ref_value = ref_value or value;
1336 --do return tostring(value) end
1337 if not unit then
1338 if type == "duration" then
1339 unit = "seconds"
1340 elseif type == "size" then
1341 unit = "bytes";
1342 elseif type == "rate" then
1343 unit = " events/sec"
1344 if ref_value < 0.9 then
1345 unit = "events/min"
1346 value = value*60;
1347 if ref_value < 0.6/60 then
1348 unit = "events/h"
1349 value = value*60;
1350 end
1351 end
1352 return ("%.3g %s"):format(value, unit);
1353 end
1354 end
1355 return format_number(value, short_units[unit] or unit or "", unit == "bytes" and 'b' or nil);
1356 end
1357
1358 local stats_methods = {};
1359 function stats_methods:bounds(_lower, _upper)
1360 for _, stat_info in ipairs(self) do
1361 local data = stat_info[4];
1362 if data then
1363 local lower = _lower or data.min;
1364 local upper = _upper or data.max;
1365 local new_data = {
1366 min = lower;
1367 max = upper;
1368 samples = {};
1369 sample_count = 0;
1370 count = data.count;
1371 units = data.units;
1372 };
1373 local sum = 0;
1374 for _, v in ipairs(data.samples) do
1375 if v > upper then
1376 break;
1377 elseif v>=lower then
1378 table.insert(new_data.samples, v);
1379 sum = sum + v;
1380 end
1381 end
1382 new_data.sample_count = #new_data.samples;
1383 stat_info[4] = new_data;
1384 stat_info[3] = sum/new_data.sample_count;
1385 end
1386 end
1387 return self;
1388 end
1389
1390 function stats_methods:trim(lower, upper)
1391 upper = upper or (100-lower);
1392 local statistics = require "util.statistics";
1393 for _, stat_info in ipairs(self) do
1394 -- Strip outliers
1395 local data = stat_info[4];
1396 if data then
1397 local new_data = {
1398 min = statistics.get_percentile(data, lower);
1399 max = statistics.get_percentile(data, upper);
1400 samples = {};
1401 sample_count = 0;
1402 count = data.count;
1403 units = data.units;
1404 };
1405 local sum = 0;
1406 for _, v in ipairs(data.samples) do
1407 if v > new_data.max then
1408 break;
1409 elseif v>=new_data.min then
1410 table.insert(new_data.samples, v);
1411 sum = sum + v;
1412 end
1413 end
1414 new_data.sample_count = #new_data.samples;
1415 stat_info[4] = new_data;
1416 stat_info[3] = sum/new_data.sample_count;
1417 end
1418 end
1419 return self;
1420 end
1421
1422 function stats_methods:max(upper)
1423 return self:bounds(nil, upper);
1424 end
1425
1426 function stats_methods:min(lower)
1427 return self:bounds(lower, nil);
1428 end
1429
1430 function stats_methods:summary()
1431 local statistics = require "util.statistics";
1432 for _, stat_info in ipairs(self) do
1433 local type, value, data = stat_info[2], stat_info[3], stat_info[4];
1434 if data and data.samples then
1435 table.insert(stat_info.output, string.format("Count: %d (%d captured)",
1436 data.count,
1437 data.sample_count
1438 ));
1439 table.insert(stat_info.output, string.format("Min: %s Mean: %s Max: %s",
1440 format_stat(type, data.units, data.min),
1441 format_stat(type, data.units, value),
1442 format_stat(type, data.units, data.max)
1443 ));
1444 table.insert(stat_info.output, string.format("Q1: %s Median: %s Q3: %s",
1445 format_stat(type, data.units, statistics.get_percentile(data, 25)),
1446 format_stat(type, data.units, statistics.get_percentile(data, 50)),
1447 format_stat(type, data.units, statistics.get_percentile(data, 75))
1448 ));
1449 end
1450 end
1451 return self;
1452 end
1453
1454 function stats_methods:cfgraph()
1455 for _, stat_info in ipairs(self) do
1456 local name, type, value, data = unpack(stat_info, 1, 4); -- luacheck: ignore 211
1457 local function print(s)
1458 table.insert(stat_info.output, s);
1459 end
1460
1461 if data and data.sample_count and data.sample_count > 0 then
1462 local raw_histogram = require "util.statistics".get_histogram(data);
1463
1464 local graph_width, graph_height = 50, 10;
1465 local eighth_chars = " ▁▂▃▄▅▆▇█";
1466
1467 local range = data.max - data.min;
1468
1469 if range > 0 then
1470 local x_scaling = #raw_histogram/graph_width;
1471 local histogram = {};
1472 for i = 1, graph_width do
1473 histogram[i] = math.max(raw_histogram[i*x_scaling-1] or 0, raw_histogram[i*x_scaling] or 0);
1474 end
1475
1476 print("");
1477 print(("_"):rep(52)..format_stat(type, data.units, data.max));
1478 for row = graph_height, 1, -1 do
1479 local row_chars = {};
1480 local min_eighths, max_eighths = 8, 0;
1481 for i = 1, #histogram do
1482 local char_eighths = math.ceil(math.max(math.min((graph_height/(data.max/histogram[i]))-(row-1), 1), 0)*8);
1483 if char_eighths < min_eighths then
1484 min_eighths = char_eighths;
1485 end
1486 if char_eighths > max_eighths then
1487 max_eighths = char_eighths;
1488 end
1489 if char_eighths == 0 then
1490 row_chars[i] = "-";
1491 else
1492 local char = eighth_chars:sub(char_eighths*3+1, char_eighths*3+3);
1493 row_chars[i] = char;
1494 end
1495 end
1496 print(table.concat(row_chars).."|-"..format_stat(type, data.units, data.max/(graph_height/(row-0.5))));
1497 end
1498 print(("\\ "):rep(11));
1499 local x_labels = {};
1500 for i = 1, 11 do
1501 local s = ("%-4s"):format((i-1)*10);
1502 if #s > 4 then
1503 s = s:sub(1, 3).."…";
1504 end
1505 x_labels[i] = s;
1506 end
1507 print(" "..table.concat(x_labels, " "));
1508 local units = "%";
1509 local margin = math.floor((graph_width-#units)/2);
1510 print((" "):rep(margin)..units);
1511 else
1512 print("[range too small to graph]");
1513 end
1514 print("");
1515 end
1516 end
1517 return self;
1518 end
1519
1520 function stats_methods:histogram()
1521 for _, stat_info in ipairs(self) do
1522 local name, type, value, data = unpack(stat_info, 1, 4); -- luacheck: ignore 211
1523 local function print(s)
1524 table.insert(stat_info.output, s);
1525 end
1526
1527 if not data then
1528 print("[no data]");
1529 return self;
1530 elseif not data.sample_count then
1531 print("[not a sampled metric type]");
1532 return self;
1533 end
1534
1535 local graph_width, graph_height = 50, 10;
1536 local eighth_chars = " ▁▂▃▄▅▆▇█";
1537
1538 local range = data.max - data.min;
1539
1540 if range > 0 then
1541 local n_buckets = graph_width;
1542
1543 local histogram = {};
1544 for i = 1, n_buckets do
1545 histogram[i] = 0;
1546 end
1547 local max_bin_samples = 0;
1548 for _, d in ipairs(data.samples) do
1549 local bucket = math.floor(1+(n_buckets-1)/(range/(d-data.min)));
1550 histogram[bucket] = histogram[bucket] + 1;
1551 if histogram[bucket] > max_bin_samples then
1552 max_bin_samples = histogram[bucket];
1553 end
1554 end
1555
1556 print("");
1557 print(("_"):rep(52)..max_bin_samples);
1558 for row = graph_height, 1, -1 do
1559 local row_chars = {};
1560 local min_eighths, max_eighths = 8, 0;
1561 for i = 1, #histogram do
1562 local char_eighths = math.ceil(math.max(math.min((graph_height/(max_bin_samples/histogram[i]))-(row-1), 1), 0)*8);
1563 if char_eighths < min_eighths then
1564 min_eighths = char_eighths;
1565 end
1566 if char_eighths > max_eighths then
1567 max_eighths = char_eighths;
1568 end
1569 if char_eighths == 0 then
1570 row_chars[i] = "-";
1571 else
1572 local char = eighth_chars:sub(char_eighths*3+1, char_eighths*3+3);
1573 row_chars[i] = char;
1574 end
1575 end
1576 print(table.concat(row_chars).."|-"..math.ceil((max_bin_samples/graph_height)*(row-0.5)));
1577 end
1578 print(("\\ "):rep(11));
1579 local x_labels = {};
1580 for i = 1, 11 do
1581 local s = ("%-4s"):format(format_stat(type, data.units, data.min+range*i/11, data.min):match("^%S+"));
1582 if #s > 4 then
1583 s = s:sub(1, 3).."…";
1584 end
1585 x_labels[i] = s;
1586 end
1587 print(" "..table.concat(x_labels, " "));
1588 local units = format_stat(type, data.units, data.min):match("%s+(.+)$") or data.units or "";
1589 local margin = math.floor((graph_width-#units)/2);
1590 print((" "):rep(margin)..units);
1591 else
1592 print("[range too small to graph]");
1593 end
1594 print("");
1595 end
1596 return self;
1597 end
1598
1599 local function stats_tostring(stats)
1600 local print = stats.session.print;
1601 for _, stat_info in ipairs(stats) do
1602 if #stat_info.output > 0 then
1603 print("\n#"..stat_info[1]);
1604 print("");
1605 for _, v in ipairs(stat_info.output) do
1606 print(v);
1607 end
1608 print("");
1609 else
1610 print(("%-50s %s"):format(stat_info[1], format_stat(stat_info[2], (stat_info[4] or {}).units, stat_info[3])));
1611 end
1612 end
1613 return #stats.." statistics displayed";
1614 end
1615
1616 local stats_mt = {__index = stats_methods, __tostring = stats_tostring }
1617 local function new_stats_context(self)
1618 return setmetatable({ session = self.session, stats = true }, stats_mt);
1619 end
1620
1621 function def_env.stats:show(filter)
1622 -- luacheck: ignore 211/changed
1623 local stats, changed, extra = require "core.statsmanager".get_stats();
1624 local available, displayed = 0, 0;
1625 local displayed_stats = new_stats_context(self);
1626 for name, value in iterators.sorted_pairs(stats) do
1627 available = available + 1;
1628 if not filter or name:match(filter) then
1629 displayed = displayed + 1;
1630 local type = name:match(":(%a+)$");
1631 table.insert(displayed_stats, {
1632 name, type, value, extra[name];
1633 output = {};
1634 });
1635 end
1636 end
1637 return displayed_stats;
1638 end
1639
1640
1641
1642 -------------
1643
1644 function printbanner(session)
1645 local option = module:get_option_string("console_banner", "full");
1646 if option == "full" or option == "graphic" then
1647 session.print [[
1648 ____ \ / _
1649 | _ \ _ __ ___ ___ _-_ __| |_ _
1650 | |_) | '__/ _ \/ __|/ _ \ / _` | | | |
1651 | __/| | | (_) \__ \ |_| | (_| | |_| |
1652 |_| |_| \___/|___/\___/ \__,_|\__, |
1653 A study in simplicity |___/
1654
1655 ]]
1656 end
1657 if option == "short" or option == "full" then
1658 session.print("Welcome to the Prosody administration console. For a list of commands, type: help");
1659 session.print("You may find more help on using this console in our online documentation at ");
1660 session.print("https://prosody.im/doc/console\n");
1661 end
1662 if option ~= "short" and option ~= "full" and option ~= "graphic" then
1663 session.print(option);
1664 end
1665 end