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