prosodyctl

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