1 -- Prosody IM
  2 --
  3 -- This module depends on Prosody's admin_telnet module
  4 -- and some code (redirect_output() and onincoming() from console_listener)
  5 -- is duplicated here...
  6 --
  7 -- Copyright (C) 2008-2010 Matthew Wild
  8 -- Copyright (C) 2008-2010 Waqas Hussain
  9 -- Copyright (C) 2012 Mikael Berthe
 10 --
 11 -- This project is MIT/X11 licensed. Please see the
 12 -- COPYING file in the source package for more information.
 13 --
 14 
 15 local st = require "util.stanza"; -- Import Prosody's stanza API into 'st'
 16 local um_is_admin = require "core.usermanager".is_admin;
 17 
 18 local telnet_def_env = module:shared("/*/admin_telnet/env");
 19 local telnet_commands = module:shared("/*/admin_telnet/commands")
 20 local default_env_mt = { __index = telnet_def_env };
 21 
 22 local host = module.host;
 23 
 24 -- Create our own session.  print() will store the results in a text
 25 -- string.  send(), quit(), disconnect() are no-op.
 26 local function new_session ()
 27         local session = {
 28                         send        = function ()  end;
 29                         quit        = function ()  end;
 30                         disconnect  = function ()  end;
 31                         };
 32 
 33         session.print = function (...)
 34                 local t = {};
 35                 for i=1,select("#", ...) do
 36                         t[i] = tostring(select(i, ...));
 37                 end
 38                 local text = "| "..table.concat(t, "\t");
 39                 if session.fulltext then
 40                     session.fulltext = session.fulltext .. "\n" .. text;
 41                 else
 42                     session.fulltext = text;
 43                 end
 44         end;
 45 
 46         session.env = setmetatable({}, default_env_mt);
 47 
 48         -- Load up environment with helper objects
 49         for name, t in pairs(telnet_def_env) do
 50                 if type(t) == "table" then
 51                         session.env[name] = setmetatable({ session = session },
 52                                                          { __index = t });
 53                 end
 54         end
 55 
 56         return session;
 57 end
 58 
 59 -- This function is 100% duplicated from mod_admin_telnet.
 60 local function redirect_output(_G, session)
 61         local env = setmetatable({ print = session.print },
 62                      { __index = function (t, k) return rawget(_G, k); end });
 63         env.dofile = function(name)
 64                 local f, err = loadfile(name);
 65                 if not f then return f, err; end
 66                 return setfenv(f, env)();
 67         end;
 68         return env;
 69 end
 70 
 71 -- This function is 100% duplicated from mod_admin_telnet.
 72 function onincoming(session, data)
 73 
 74         local commands = telnet_commands;
 75         local partial = session.partial_data;
 76         if partial then
 77                 data = partial..data;
 78         end
 79         -- xxx commands.help(session, "help");
 80 
 81         for line in data:gmatch("[^\n]*[\n\004]") do
 82                 -- Handle data (loop allows us to break to add \0 after response)
 83                 repeat
 84                         local useglobalenv;
 85 
 86                         if line:match("^>") then
 87                                 line = line:gsub("^>", "");
 88                                 useglobalenv = true;
 89                         elseif line == "\004" then
 90                                 commands["bye"](session, line);
 91                                 break;
 92                         else
 93                                 local command = line:lower();
 94                                 command = line:match("^%w+") or line:match("%p");
 95                                 if commands[command] then
 96                                         commands[command](session, line);
 97                                         break;
 98                                 end
 99                         end
100 
101                         session.env._ = line;
102 
103                         local chunkname = "=console";
104                         local chunk, err = loadstring("return "..line, chunkname);
105                         if not chunk then
106                                 chunk, err = loadstring(line, chunkname);
107                                 if not chunk then
108                                         err = err:gsub("^%[string .-%]:%d+: ", "");
109                                         err = err:gsub("^:%d+: ", "");
110                                         err = err:gsub("'<eof>'", "the end of the line");
111                                         session.print("Sorry, I couldn't understand that... "..err);
112                                         break;
113                                 end
114                         end
115 
116                         setfenv(chunk, (useglobalenv and redirect_output(_G, session)) or session.env or nil);
117 
118                         local ranok, taskok, message = pcall(chunk);
119 
120                         if not (ranok or message or useglobalenv) and commands[line:lower()] then
121                                 commands[line:lower()](session, line);
122                                 break;
123                         end
124 
125                         if not ranok then
126                                 session.print("Fatal error while running command, it did not complete");
127                                 session.print("Error: "..taskok);
128                                 break;
129                         end
130 
131                         if not message then
132                                 session.print("Result: "..tostring(taskok));
133                                 break;
134                         elseif (not taskok) and message then
135                                 session.print("Command completed with a problem");
136                                 session.print("Message: "..tostring(message));
137                                 break;
138                         end
139 
140                         session.print("OK: "..tostring(message));
141                 until true
142 
143                 session.send(string.char(0));
144         end
145         session.partial_data = data:match("[^\n]+$");
146 end
147 
148 function on_message(event)
149         -- Check the type of the incoming stanza to avoid loops:
150         if event.stanza.attr.type == "error" then
151                 return; -- We do not want to reply to these, so leave.
152         end
153 
154         local userjid = event.stanza.attr.from;
155         local bodytag = event.stanza:get_child("body");
156         local body = bodytag and bodytag:get_text() or "";
157         if not body or body == "" then
158                 return; -- We do not reply to empty messages (chatstates, etc.)
159         end
160 
161         -- Check the requester is an admin user
162         if not um_is_admin(userjid, module.host) then
163                 module:log("info", "Ignored request from non-admin: %s",
164                            userjid);
165                 return;
166         end
167 
168         -- Create a session in order to use an admin_telnet-like environment
169         local session = new_session();
170 
171         -- Process the message using admin_telnet's onincoming function
172         onincoming(session, body.."\n");
173 
174         -- Strip trailing blank line
175         session.fulltext = tostring(session.fulltext):gsub("\n\|%s*$", "")
176 
177         -- Send the reply stanza
178         local reply_stanza = st.message({ from = host, to = userjid,
179                                         type = "chat" });
180         reply_stanza = reply_stanza:body(session.fulltext);
181         module:send(reply_stanza);
182 
183         -- XXX Prosody still sends a service-unavailable stanza, probably
184         -- related to chat states.
185 end
186 
187 module:hook("message/bare", on_message);
188 module:depends("admin_telnet");
189 
190 -- vim:set noet sts=8 sw=8: