net.server_select: Fix oops in last commit
[prosody.git] / prosodyctl
1 #!/usr/bin/env lua
2 -- Prosody IM
3 -- Copyright (C) 2008-2009 Matthew Wild
4 -- Copyright (C) 2008-2009 Waqas Hussain
5 -- 
6 -- This project is MIT/X11 licensed. Please see the
7 -- COPYING file in the source package for more information.
8 --
9
10 -- prosodyctl - command-line controller for Prosody XMPP server
11
12 -- Will be modified by configure script if run --
13
14 CFG_SOURCEDIR=nil;
15 CFG_CONFIGDIR=os.getenv("PROSODY_CFGDIR");
16 CFG_PLUGINDIR=nil;
17 CFG_DATADIR=os.getenv("PROSODY_DATADIR");
18
19 -- -- -- -- -- -- -- ---- -- -- -- -- -- -- -- --
20
21 if CFG_SOURCEDIR then
22         package.path = CFG_SOURCEDIR.."/?.lua;"..package.path
23         package.cpath = CFG_SOURCEDIR.."/?.so;"..package.cpath
24 end
25
26 if CFG_DATADIR then
27         if os.getenv("HOME") then
28                 CFG_DATADIR = CFG_DATADIR:gsub("^~", os.getenv("HOME"));
29         end
30 end
31
32 -- Required to be able to find packages installed with luarocks
33 pcall(require, "luarocks.require")
34
35 config = require "core.configmanager"
36
37 do
38         -- TODO: Check for other formats when we add support for them
39         -- Use lfs? Make a new conf/ dir?
40         local ok, level, err = config.load((CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
41         if not ok then
42                 print("\n");
43                 print("**************************");
44                 if level == "parser" then
45                         print("A problem occured while reading the config file "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
46                         local err_line, err_message = tostring(err):match("%[string .-%]:(%d*): (.*)");
47                         print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err)));
48                         print("");
49                 elseif level == "file" then
50                         print("Prosody was unable to find the configuration file.");
51                         print("We looked for: "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
52                         print("A sample config file is included in the Prosody download called prosody.cfg.lua.dist");
53                         print("Copy or rename it to prosody.cfg.lua and edit as necessary.");
54                 end
55                 print("More help on configuring Prosody can be found at http://prosody.im/doc/configure");
56                 print("Good luck!");
57                 print("**************************");
58                 print("");
59                 os.exit(1);
60         end
61 end
62
63 local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data";
64 require "util.datamanager".set_data_path(data_path);
65
66 -- Switch away from root and into the prosody user --
67 local switched_user, current_uid;
68
69 local want_pposix_version = "0.3.1";
70 local ok, pposix = pcall(require, "util.pposix");
71
72 if ok and pposix then
73         if pposix._VERSION ~= want_pposix_version then print(string.format("Unknown version (%s) of binary pposix module, expected %s", tostring(pposix._VERSION), want_pposix_version)); return; end
74         current_uid = pposix.getuid();
75         if current_uid == 0 then
76                 -- We haz root!
77                 local desired_user = config.get("*", "core", "prosody_user") or "prosody";
78                 local desired_group = config.get("*", "core", "prosody_group") or desired_user;
79                 local ok, err = pposix.setgid(desired_group);
80                 if ok then
81                         ok, err = pposix.setuid(desired_user);
82                         if ok then
83                                 -- Yay!
84                                 switched_user = true;
85                         end
86                 end
87                 if not switched_user then
88                         -- Boo!
89                         print("Warning: Couldn't switch to Prosody user/group '"..tostring(desired_user).."'/'"..tostring(desired_group).."': "..tostring(err));
90                 end
91         end
92 else
93         print("Error: Unable to load pposix module. Check that Prosody is installed correctly.")
94         print("For more help send the below error to us through http://prosody.im/discuss");
95         print(tostring(pposix))
96 end
97
98 local error_messages = setmetatable({ 
99                 ["invalid-username"] = "The given username is invalid in a Jabber ID";
100                 ["invalid-hostname"] = "The given hostname is invalid";
101                 ["no-password"] = "No password was supplied";
102                 ["no-such-user"] = "The given user does not exist on the server";
103                 ["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?";
104                 ["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see http://prosody.im/doc/prosodyctl#pidfile for help";
105                 ["no-such-method"] = "This module has no commands";
106                 ["not-running"] = "Prosody is not running";
107                 }, { __index = function (t,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end });
108
109 local events = require "util.events".new();
110
111 hosts = {};
112 prosody = { hosts = hosts, events = events };
113
114 for hostname, config in pairs(config.getconfig()) do
115         hosts[hostname] = { events = events };
116 end
117         
118 require "core.modulemanager"
119
120 require "util.prosodyctl"
121 require "socket"
122 -----------------------
123
124 function show_message(msg, ...)
125         print(msg:format(...));
126 end
127
128 function show_warning(msg, ...)
129         print(msg:format(...));
130 end
131
132 function show_usage(usage, desc)
133         print("Usage: "..arg[0].." "..usage);
134         if desc then
135                 print(" "..desc);
136         end
137 end
138
139 local function getchar(n)
140         local stty_ret = os.execute("stty raw -echo 2>/dev/null");
141         local ok, char;
142         if stty_ret == 0 then
143                 ok, char = pcall(io.read, n or 1);
144                 os.execute("stty sane");
145         else
146                 ok, char = pcall(io.read, "*l");
147                 if ok then
148                         char = char:sub(1, n or 1);
149                 end
150         end
151         if ok then
152                 return char;
153         end
154 end
155         
156 local function getpass()
157         local stty_ret = os.execute("stty -echo 2>/dev/null");
158         if stty_ret ~= 0 then
159                 io.write("\027[08m"); -- ANSI 'hidden' text attribute
160         end
161         local ok, pass = pcall(io.read, "*l");
162         if stty_ret == 0 then
163                 os.execute("stty sane");
164         else
165                 io.write("\027[00m");
166         end
167         io.write("\n");
168         if ok then
169                 return pass;
170         end
171 end
172
173 function show_yesno(prompt)
174         io.write(prompt, " ");
175         local choice = getchar():lower();
176         io.write("\n");
177         if not choice:match("%a") then
178                 choice = prompt:match("%[.-(%U).-%]$");
179                 if not choice then return nil; end
180         end
181         return (choice == "y");
182 end
183
184 local function read_password()
185         local password;
186         while true do
187                 io.write("Enter new password: ");
188                 password = getpass();
189                 if not password then
190                         show_message("No password - cancelled");
191                         return;
192                 end
193                 io.write("Retype new password: ");
194                 if getpass() ~= password then
195                         if not show_yesno [=[Passwords did not match, try again? [Y/n]]=] then
196                                 return;
197                         end
198                 else
199                         break;
200                 end
201         end
202         return password;
203 end
204
205 local prosodyctl_timeout = (config.get("*", "core", "prosodyctl_timeout") or 5) * 2;
206 -----------------------
207 local commands = {};
208 local command = arg[1];
209
210 function commands.adduser(arg)
211         if not arg[1] or arg[1] == "--help" then
212                 show_usage([[adduser JID]], [[Create the specified user account in Prosody]]);
213                 return 1;
214         end
215         local user, host = arg[1]:match("([^@]+)@(.+)");
216         if not user and host then
217                 show_message [[Failed to understand JID, please supply the JID you want to create]]
218                 show_usage [[adduser user@host]]
219                 return 1;
220         end
221         
222         if not host then
223                 show_message [[Please specify a JID, including a host. e.g. alice@example.com]];
224                 return 1;
225         end
226         
227         if prosodyctl.user_exists{ user = user, host = host } then
228                 show_message [[That user already exists]];
229                 return 1;
230         end
231         
232         if not hosts[host] then
233                 show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
234                 show_warning("The user will not be able to log in until this is changed.");
235         end
236         
237         local password = read_password();
238         if not password then return 1; end
239         
240         local ok, msg = prosodyctl.adduser { user = user, host = host, password = password };
241         
242         if ok then return 0; end
243         
244         show_message(error_messages[msg])
245         return 1;
246 end
247
248 function commands.passwd(arg)
249         if not arg[1] or arg[1] == "--help" then
250                 show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]);
251                 return 1;
252         end
253         local user, host = arg[1]:match("([^@]+)@(.+)");
254         if not user and host then
255                 show_message [[Failed to understand JID, please supply the JID you want to set the password for]]
256                 show_usage [[passwd user@host]]
257                 return 1;
258         end
259         
260         if not host then
261                 show_message [[Please specify a JID, including a host. e.g. alice@example.com]];
262                 return 1;
263         end
264         
265         if not prosodyctl.user_exists { user = user, host = host } then
266                 show_message [[That user does not exist, use prosodyctl adduser to create a new user]]
267                 return 1;
268         end
269         
270         local password = read_password();
271         if not password then return 1; end
272         
273         local ok, msg = prosodyctl.passwd { user = user, host = host, password = password };
274         
275         if ok then return 0; end
276         
277         show_message(error_messages[msg])
278         return 1;
279 end
280
281 function commands.deluser(arg)
282         if not arg[1] or arg[1] == "--help" then
283                 show_usage([[deluser JID]], [[Permanently remove the specified user account from Prosody]]);
284                 return 1;
285         end
286         local user, host = arg[1]:match("([^@]+)@(.+)");
287         if not user and host then
288                 show_message [[Failed to understand JID, please supply the JID you want to set the password for]]
289                 show_usage [[passwd user@host]]
290                 return 1;
291         end
292         
293         if not host then
294                 show_message [[Please specify a JID, including a host. e.g. alice@example.com]];
295                 return 1;
296         end
297         
298         if not prosodyctl.user_exists { user = user, host = host } then
299                 show_message [[That user does not exist on this server]]
300                 return 1;
301         end
302         
303         local ok, msg = prosodyctl.passwd { user = user, host = host };
304         
305         if ok then return 0; end
306         
307         show_message(error_messages[msg])
308         return 1;
309 end
310
311 function commands.start(arg)
312         if arg[1] == "--help" then
313                 show_usage([[start]], [[Start Prosody]]);
314                 return 1;
315         end
316         local ok, ret = prosodyctl.isrunning();
317         if not ok then
318                 show_message(error_messages[ret]);
319                 return 1;
320         end
321         
322         if ret then
323                 local ok, ret = prosodyctl.getpid();
324                 if not ok then
325                         show_message("Couldn't get running Prosody's PID");
326                         show_message(error_messages[ret]);
327                         return 1;
328                 end
329                 show_message("Prosody is already running with PID %s", ret or "(unknown)");
330                 return 1;
331         end
332         
333         local ok, ret = prosodyctl.start();
334         if ok then
335                 local i=1;
336                 while true do
337                         local ok, running = prosodyctl.isrunning();
338                         if ok and running then
339                                 break;
340                         elseif i == 5 then
341                                 show_message("Still waiting...");
342                         elseif i >= prosodyctl_timeout then
343                                 show_message("Prosody is still not running. Please give it some time or check your log files for errors.");
344                                 return 2;
345                         end
346                         socket.sleep(0.5);
347                         i = i + 1;
348                 end
349                 show_message("Started");
350                 return 0;
351         end
352
353         show_message("Failed to start Prosody");
354         show_message(error_messages[ret])       
355         return 1;       
356 end
357
358 function commands.status(arg)
359         if arg[1] == "--help" then
360                 show_usage([[status]], [[Reports the running status of Prosody]]);
361                 return 1;
362         end
363
364         local ok, ret = prosodyctl.isrunning();
365         if not ok then
366                 show_message(error_messages[ret]);
367                 return 1;
368         end
369         
370         if ret then
371                 local ok, ret = prosodyctl.getpid();
372                 if not ok then
373                         show_message("Couldn't get running Prosody's PID");
374                         show_message(error_messages[ret]);
375                         return 1;
376                 end
377                 show_message("Prosody is running with PID %s", ret or "(unknown)");
378                 return 0;
379         else
380                 show_message("Prosody is not running");
381                 if not switched_user and current_uid ~= 0 then
382                         print("\nNote:")
383                         print(" You will also see this if prosodyctl is not running under");
384                         print(" the same user account as Prosody. Try running as root (e.g. ");
385                         print(" with 'sudo' in front) to gain access to Prosody's real status.");
386                 end
387                 return 2
388         end
389         return 1;
390 end
391
392 function commands.stop(arg)
393         if arg[1] == "--help" then
394                 show_usage([[stop]], [[Stop a running Prosody server]]);
395                 return 1;
396         end
397
398         if not prosodyctl.isrunning() then
399                 show_message("Prosody is not running");
400                 return 1;
401         end
402         
403         local ok, ret = prosodyctl.stop();
404         if ok then
405                 local i=1;
406                 while true do
407                         local ok, running = prosodyctl.isrunning();
408                         if ok and not running then
409                                 break;
410                         elseif i == 5 then
411                                 show_message("Still waiting...");
412                         elseif i >= prosodyctl_timeout then
413                                 show_message("Prosody is still running. Please give it some time or check your log files for errors.");
414                                 return 2;
415                         end
416                         socket.sleep(0.5);
417                         i = i + 1;
418                 end
419                 show_message("Stopped");
420                 return 0;
421         end
422
423         show_message(error_messages[ret]);
424         return 1;
425 end
426
427 -- ejabberdctl compatibility
428
429 function commands.register(arg)
430         local user, host, password = unpack(arg);
431         if (not (user and host)) or arg[1] == "--help" then
432                 if user ~= "--help" then
433                         if not user then
434                                 show_message [[No username specified]]
435                         elseif not host then
436                                 show_message [[Please specify which host you want to register the user on]];
437                         end
438                 end
439                 show_usage("register USER HOST [PASSWORD]", "Register a user on the server, with the given password");
440                 return 1;
441         end
442         if not password then
443                 password = read_password();
444                 if not password then
445                         show_message [[Unable to register user with no password]];
446                         return 1;
447                 end
448         end
449         
450         local ok, msg = prosodyctl.adduser { user = user, host = host, password = password };
451         
452         if ok then return 0; end
453         
454         show_message(error_messages[msg])
455         return 1;
456 end
457
458 function commands.unregister(arg)
459         local user, host = unpack(arg);
460         if (not (user and host)) or arg[1] == "--help" then
461                 if user ~= "--help" then
462                         if not user then
463                                 show_message [[No username specified]]
464                         elseif not host then
465                                 show_message [[Please specify which host you want to unregister the user from]];
466                         end
467                 end
468                 show_usage("unregister USER HOST [PASSWORD]", "Permanently remove a user account from the server");
469                 return 1;
470         end
471
472         local ok, msg = prosodyctl.deluser { user = user, host = host };
473         
474         if ok then return 0; end
475         
476         show_message(error_messages[msg])
477         return 1;
478 end
479
480 local http_errors = {
481         [404] = "Plugin not found, did you type the address correctly?"
482         };
483
484 function commands.addplugin(arg)
485         local url = arg[1];
486         if url:match("^http://") then
487                 local http = require "socket.http";
488                 show_message("Fetching...");
489                 local code, err = http.request(url);
490                 if not code or not tostring(err):match("^[23]") then
491                         show_message("Failed: "..(http_errors[err] or ("HTTP error "..err)));
492                         return 1;
493                 end
494                 if url:match("%.lua$") then
495                         local ok, err = datamanager.store(url:match("/mod_([^/]+)$"), "*", "plugins", {code});
496                         if not ok then
497                                 show_message("Failed to save to data store: "..err);
498                                 return 1;
499                         end
500                 end
501                 show_message("Saved. Don't forget to load the module using the config file or admin console!");
502         else
503                 show_message("Sorry, I don't understand how to fetch plugins from there.");
504         end
505 end
506
507 ---------------------
508
509 if command and command:match("^mod_") then -- Is a command in a module
510         local module_name = command:match("^mod_(.+)");
511         local ret, err = modulemanager.load("*", module_name);
512         if not ret then
513                 show_message("Failed to load module '"..module_name.."': "..err);
514                 os.exit(1);
515         end
516         
517         table.remove(arg, 1);
518         
519         local module = modulemanager.get_module("*", module_name);
520         if not module then
521                 show_message("Failed to load module '"..module_name.."': Unknown error");
522                 os.exit(1);
523         end
524         
525         if not modulemanager.module_has_method(module, "command") then
526                 show_message("Fail: mod_"..module_name.." does not support any commands");
527                 os.exit(1);
528         end
529         
530         local ok, ret = modulemanager.call_module_method(module, "command", arg);
531         if ok then
532                 if type(ret) == "number" then
533                         os.exit(ret);
534                 elseif type(ret) == "string" then
535                         show_message(ret);
536                 end
537                 os.exit(0); -- :)
538         else
539                 show_message("Failed to execute command: "..error_messages[ret]);
540                 os.exit(1); -- :(
541         end
542 end
543
544 if not commands[command] then -- Show help for all commands
545         function show_usage(usage, desc)
546                 print(" "..usage);
547                 print("    "..desc);
548         end
549
550         print("prosodyctl - Manage a Prosody server");
551         print("");
552         print("Usage: "..arg[0].." COMMAND [OPTIONS]");
553         print("");
554         print("Where COMMAND may be one of:\n");
555
556         local hidden_commands = require "util.set".new{ "register", "unregister" };
557         local commands_order = { "adduser", "passwd", "deluser" };
558
559         local done = {};
560
561         for _, command_name in ipairs(commands_order) do
562                 local command = commands[command_name];
563                 if command then
564                         command{ "--help" };
565                         print""
566                         done[command_name] = true;
567                 end
568         end
569
570         for command_name, command in pairs(commands) do
571                 if not done[command_name] and not hidden_commands:contains(command_name) then
572                         command{ "--help" };
573                         print""
574                         done[command_name] = true;
575                 end
576         end
577         
578         
579         os.exit(0);
580 end
581
582 os.exit(commands[command]({ select(2, unpack(arg)) }));