mod_console: Allow customisation/suppression of the banner
[prosody.git] / plugins / mod_console.lua
1 -- Prosody IM v0.4
2 -- Copyright (C) 2008-2009 Matthew Wild
3 -- Copyright (C) 2008-2009 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
9 module.host = "*";
10
11 local _G = _G;
12
13 local prosody = _G.prosody;
14 local hosts = prosody.hosts;
15 local connlisteners_register = require "net.connlisteners".register;
16
17 local console_listener = { default_port = 5582; default_mode = "*l"; };
18
19 require "util.iterators";
20 local set, array = require "util.set", require "util.array";
21
22 local commands = {};
23 local def_env = {};
24 local default_env_mt = { __index = def_env };
25
26 local function redirect_output(_G, session)
27         return setmetatable({ print = session.print }, { __index = function (t, k) return rawget(_G, k); end, __newindex = function (t, k, v) rawset(_G, k, v); end });
28 end
29
30 console = {};
31
32 function console:new_session(conn)
33         local w = function(s) conn.write(s:gsub("\n", "\r\n")); end;
34         local session = { conn = conn;
35                         send = function (t) w(tostring(t)); end;
36                         print = function (t) w("| "..tostring(t).."\n"); end;
37                         disconnect = function () conn.close(); end;
38                         };
39         session.env = setmetatable({}, default_env_mt);
40         
41         -- Load up environment with helper objects
42         for name, t in pairs(def_env) do
43                 if type(t) == "table" then
44                         session.env[name] = setmetatable({ session = session }, { __index = t });
45                 end
46         end
47         
48         return session;
49 end
50
51 local sessions = {};
52
53 function console_listener.listener(conn, data)
54         local session = sessions[conn];
55         
56         if not session then
57                 -- Handle new connection
58                 session = console:new_session(conn);
59                 sessions[conn] = session;
60                 printbanner(session);
61         end
62         if data then
63                 -- Handle data
64                 (function(session, data)
65                         local useglobalenv;
66                         
67                         if data:match("[!.]$") then
68                                 local command = data:lower();
69                                 command = data:match("^%w+") or data:match("%p");
70                                 if commands[command] then
71                                         commands[command](session, data);
72                                         return;
73                                 end
74                         end
75
76                         if data:match("^>") then
77                                 data = data:gsub("^>", "");
78                                 useglobalenv = true;
79                         end
80                         
81                         session.env._ = data;
82                         
83                         local chunk, err = loadstring("return "..data);
84                         if not chunk then
85                                 chunk, err = loadstring(data);
86                                 if not chunk then
87                                         err = err:gsub("^%[string .-%]:%d+: ", "");
88                                         err = err:gsub("^:%d+: ", "");
89                                         err = err:gsub("'<eof>'", "the end of the line");
90                                         session.print("Sorry, I couldn't understand that... "..err);
91                                         return;
92                                 end
93                         end
94                         
95                         setfenv(chunk, (useglobalenv and redirect_output(_G, session)) or session.env or nil);
96                         
97                         local ranok, taskok, message = pcall(chunk);
98                         
99                         if not ranok then
100                                 session.print("Fatal error while running command, it did not complete");
101                                 session.print("Error: "..taskok);
102                                 return;
103                         end
104                         
105                         if not message then
106                                 session.print("Result: "..tostring(taskok));
107                                 return;
108                         elseif (not taskok) and message then
109                                 session.print("Command completed with a problem");
110                                 session.print("Message: "..tostring(message));
111                                 return;
112                         end
113                         
114                         session.print("OK: "..tostring(message));
115                 end)(session, data);
116         end
117         session.send(string.char(0));
118 end
119
120 function console_listener.disconnect(conn, err)
121         
122 end
123
124 connlisteners_register('console', console_listener);
125
126 -- Console commands --
127 -- These are simple commands, not valid standalone in Lua
128
129 function commands.bye(session)
130         session.print("See you! :)");
131         session.disconnect();
132 end
133
134 commands["!"] = function (session, data)
135         if data:match("^!!") then
136                 session.print("!> "..session.env._);
137                 return console_listener.listener(session.conn, session.env._);
138         end
139         local old, new = data:match("^!(.-[^\\])!(.-)!$");
140         if old and new then
141                 local ok, res = pcall(string.gsub, session.env._, old, new);
142                 if not ok then
143                         session.print(res)
144                         return;
145                 end
146                 session.print("!> "..res);
147                 return console_listener.listener(session.conn, res);
148         end
149         session.print("Sorry, not sure what you want");
150 end
151
152 -- Session environment --
153 -- Anything in def_env will be accessible within the session as a global variable
154
155 def_env.server = {};
156 function def_env.server:reload()
157         prosody.unlock_globals();
158         dofile "prosody"
159         prosody = _G.prosody;
160         return true, "Server reloaded";
161 end
162
163 def_env.module = {};
164
165 local function get_hosts_set(hosts, module)
166         if type(hosts) == "table" then
167                 if hosts[1] then
168                         return set.new(hosts);
169                 elseif hosts._items then
170                         return hosts;
171                 end
172         elseif type(hosts) == "string" then
173                 return set.new { hosts };
174         elseif hosts == nil then
175                 local mm = require "modulemanager";
176                 return set.new(array.collect(keys(prosody.hosts)))
177                         / function (host) return prosody.hosts[host].type == "local" or module and mm.is_loaded(host, module); end;
178         end
179 end
180
181 function def_env.module:load(name, hosts, config)
182         local mm = require "modulemanager";
183         
184         hosts = get_hosts_set(hosts);
185         
186         -- Load the module for each host
187         local ok, err, count = true, nil, 0;
188         for host in hosts do
189                 if (not mm.is_loaded(host, name)) then
190                         ok, err = mm.load(host, name, config);
191                         if not ok then
192                                 ok = false;
193                                 self.session.print(err or "Unknown error loading module");
194                         else
195                                 count = count + 1;
196                                 self.session.print("Loaded for "..host);
197                         end
198                 end
199         end
200         
201         return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));       
202 end
203
204 function def_env.module:unload(name, hosts)
205         local mm = require "modulemanager";
206
207         hosts = get_hosts_set(hosts, name);
208         
209         -- Unload the module for each host
210         local ok, err, count = true, nil, 0;
211         for host in hosts do
212                 if mm.is_loaded(host, name) then
213                         ok, err = mm.unload(host, name);
214                         if not ok then
215                                 ok = false;
216                                 self.session.print(err or "Unknown error unloading module");
217                         else
218                                 count = count + 1;
219                                 self.session.print("Unloaded from "..host);
220                         end
221                 end
222         end
223         return ok, (ok and "Module unloaded from "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
224 end
225
226 function def_env.module:reload(name, hosts)
227         local mm = require "modulemanager";
228
229         hosts = get_hosts_set(hosts, name);
230         
231         -- Reload the module for each host
232         local ok, err, count = true, nil, 0;
233         for host in hosts do
234                 if mm.is_loaded(host, name) then
235                         ok, err = mm.reload(host, name);
236                         if not ok then
237                                 ok = false;
238                                 self.session.print(err or "Unknown error reloading module");
239                         else
240                                 count = count + 1;
241                                 if ok == nil then
242                                         ok = true;
243                                 end
244                                 self.session.print("Reloaded on "..host);
245                         end
246                 end
247         end
248         return ok, (ok and "Module reloaded on "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
249 end
250
251 def_env.config = {};
252 function def_env.config:load(filename, format)
253         local config_load = require "core.configmanager".load;
254         local ok, err = config_load(filename, format);
255         if not ok then
256                 return false, err or "Unknown error loading config";
257         end
258         return true, "Config loaded";
259 end
260
261 function def_env.config:get(host, section, key)
262         local config_get = require "core.configmanager".get
263         return true, tostring(config_get(host, section, key));
264 end
265
266 def_env.hosts = {};
267 function def_env.hosts:list()
268         for host, host_session in pairs(hosts) do
269                 self.session.print(host);
270         end
271         return true, "Done";
272 end
273
274 function def_env.hosts:add(name)
275 end
276
277 def_env.c2s = {};
278
279 local function show_c2s(callback)
280         for hostname, host in pairs(hosts) do
281                 for username, user in pairs(host.sessions or {}) do
282                         for resource, session in pairs(user.sessions or {}) do
283                                 local jid = username.."@"..hostname.."/"..resource;
284                                 callback(jid, session);
285                         end
286                 end
287         end
288 end
289
290 function def_env.c2s:show(match_jid)
291         local print, count = self.session.print, 0;
292         show_c2s(function (jid)
293                 if (not match_jid) or jid:match(match_jid) then
294                         count = count + 1;
295                         print(jid);
296                 end             
297         end);
298         return true, "Total: "..count.." clients";
299 end
300
301 function def_env.c2s:show_insecure(match_jid)
302         local print, count = self.session.print, 0;
303         show_c2s(function (jid, session)
304                 if ((not match_jid) or jid:match(match_jid)) and not session.secure then
305                         count = count + 1;
306                         print(jid);
307                 end             
308         end);
309         return true, "Total: "..count.." insecure client connections";
310 end
311
312 function def_env.c2s:show_secure(match_jid)
313         local print, count = self.session.print, 0;
314         show_c2s(function (jid, session)
315                 if ((not match_jid) or jid:match(match_jid)) and session.secure then
316                         count = count + 1;
317                         print(jid);
318                 end             
319         end);
320         return true, "Total: "..count.." secure client connections";
321 end
322
323
324 def_env.s2s = {};
325 function def_env.s2s:show(match_jid)
326         local _print = self.session.print;
327         local print = self.session.print;
328         
329         local count_in, count_out = 0,0;
330         
331         for host, host_session in pairs(hosts) do
332                 print = function (...) _print(host); _print(...); print = _print; end
333                 for remotehost, session in pairs(host_session.s2sout) do
334                         if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then
335                                 count_out = count_out + 1;
336                                 print("    "..host.." -> "..remotehost);
337                                 if session.sendq then
338                                         print("        There are "..#session.sendq.." queued outgoing stanzas for this connection");
339                                 end
340                                 if session.type == "s2sout_unauthed" then
341                                         if session.connecting then
342                                                 print("        Connection not yet established");
343                                                 if not session.srv_hosts then
344                                                         if not session.conn then
345                                                                 print("        We do not yet have a DNS answer for this host's SRV records");
346                                                         else
347                                                                 print("        This host has no SRV records, using A record instead");
348                                                         end
349                                                 elseif session.srv_choice then
350                                                         print("        We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts);
351                                                         local srv_choice = session.srv_hosts[session.srv_choice];
352                                                         print("        Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269));
353                                                 end
354                                         elseif session.notopen then
355                                                 print("        The <stream> has not yet been opened");
356                                         elseif not session.dialback_key then
357                                                 print("        Dialback has not been initiated yet");
358                                         elseif session.dialback_key then
359                                                 print("        Dialback has been requested, but no result received");
360                                         end
361                                 end
362                         end
363                 end     
364                 
365                 for session in pairs(incoming_s2s) do
366                         if session.to_host == host and ((not match_jid) or host:match(match_jid) 
367                                 or (session.from_host and session.from_host:match(match_jid))) then
368                                 count_in = count_in + 1;
369                                 print("    "..host.." <- "..(session.from_host or "(unknown)"));
370                                 if session.type == "s2sin_unauthed" then
371                                                 print("        Connection not yet authenticated");
372                                 end
373                                 for name in pairs(session.hosts) do
374                                         if name ~= session.from_host then
375                                                 print("        also hosts "..tostring(name));
376                                         end
377                                 end
378                         end
379                 end
380                 
381                 print = _print;
382         end
383         
384         for session in pairs(incoming_s2s) do
385                 if not session.to_host and ((not match_jid) or session.from_host and session.from_host:match(match_jid)) then
386                         count_in = count_in + 1;
387                         print("Other incoming s2s connections");
388                         print("    (unknown) <- "..(session.from_host or "(unknown)"));                 
389                 end
390         end
391         
392         return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections";
393 end
394
395 function def_env.s2s:close(from, to)
396         local print, count = self.session.print, 0;
397         
398         if not (from and to) then
399                 return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'";
400         elseif from == to then
401                 return false, "Both from and to are the same... you can't do that :)";
402         end
403         
404         if hosts[from] and not hosts[to] then
405                 -- Is an outgoing connection
406                 local session = hosts[from].s2sout[to];
407                 if not session then 
408                         print("No outgoing connection from "..from.." to "..to)
409                 else
410                         s2smanager.destroy_session(session);
411                         count = count + 1;
412                         print("Closed outgoing session from "..from.." to "..to);
413                 end
414         elseif hosts[to] and not hosts[from] then
415                 -- Is an incoming connection
416                 for session in pairs(incoming_s2s) do
417                         if session.to_host == to and session.from_host == from then
418                                 s2smanager.destroy_session(session);
419                                 count = count + 1;
420                         end
421                 end
422                 
423                 if count == 0 then
424                         print("No incoming connections from "..from.." to "..to);
425                 else
426                         print("Closed "..count.." incoming session"..((count == 1 and "") or "s").." from "..from.." to "..to);
427                 end
428         elseif hosts[to] and hosts[from] then
429                 return false, "Both of the hostnames you specified are local, there are no s2s sessions to close";
430         else
431                 return false, "Neither of the hostnames you specified are being used on this server";
432         end
433         
434         return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s");
435 end
436
437 -------------
438
439 function printbanner(session)
440         local option = config.get("*", "core", "console_banner");
441 if option == nil or option == "full" or option == "graphic" then
442 session.print [[
443                    ____                \   /     _       
444                     |  _ \ _ __ ___  ___  _-_   __| |_   _ 
445                     | |_) | '__/ _ \/ __|/ _ \ / _` | | | |
446                     |  __/| | | (_) \__ \ |_| | (_| | |_| |
447                     |_|   |_|  \___/|___/\___/ \__,_|\__, |
448                     A study in simplicity            |___/ 
449
450 ]]
451 end
452 if option == nil or option == "short" or option == "full" then
453 session.print("Welcome to the Prosody administration console. For a list of commands, type: help");
454 session.print("You may find more help on using this console in our online documentation at ");
455 session.print("http://prosody.im/doc/console\n");
456 end
457 if option and option ~= "short" and option ~= "full" and option ~= "graphic" then
458         if type(option) == "string" then
459                 session.print(option)
460         elseif type(option) == "function" then
461                 setfenv(option, redirect_output(_G, session));
462                 pcall(option, session);
463         end
464 end
465 end