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