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