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