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