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