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