1-- Ignore the CFG_* variables
2-- luacheck: ignore 113/CFG_CONFIGDIR 113/CFG_SOURCEDIR 113/CFG_DATADIR 113/CFG_PLUGINDIR
3local startup = {};
4
5local prosody = { events = require "util.events".new() };
6local logger = require "util.logger";
7local log = logger.init("startup");
8
9local config = require "core.configmanager";
10
11local dependencies = require "util.dependencies";
12
13local original_logging_config;
14
15local default_gc_params = {
16	mode = "incremental";
17	-- Incremental mode defaults
18	threshold = 105, speed = 500;
19	-- Generational mode defaults
20	minor_threshold = 20, major_threshold = 50;
21};
22
23local short_params = { D = "daemonize", F = "no-daemonize" };
24local value_params = { config = true };
25
26function startup.parse_args()
27	local parsed_opts = {};
28	prosody.opts = parsed_opts;
29
30	if #arg == 0 then
31		return;
32	end
33	while true do
34		local raw_param = arg[1];
35		if not raw_param then
36			break;
37		end
38
39		local prefix = raw_param:match("^%-%-?");
40		if not prefix then
41			break;
42		elseif prefix == "--" and raw_param == "--" then
43			table.remove(arg, 1);
44			break;
45		end
46		local param = table.remove(arg, 1):sub(#prefix+1);
47		if #param == 1 then
48			param = short_params[param];
49		end
50
51		if not param then
52			print("Unknown command-line option: "..tostring(raw_param));
53			print("Perhaps you meant to use prosodyctl instead?");
54			os.exit(1);
55		end
56
57		local param_k, param_v;
58		if value_params[param] then
59			param_k, param_v = param, table.remove(arg, 1);
60			if not param_v then
61				print("Expected a value to follow command-line option: "..raw_param);
62				os.exit(1);
63			end
64		else
65			param_k, param_v = param:match("^([^=]+)=(.+)$");
66			if not param_k then
67				if param:match("^no%-") then
68					param_k, param_v = param:sub(4), false;
69				else
70					param_k, param_v = param, true;
71				end
72			end
73		end
74		parsed_opts[param_k] = param_v;
75	end
76end
77
78function startup.read_config()
79	local filenames = {};
80
81	local filename;
82	if prosody.opts.config then
83		table.insert(filenames, prosody.opts.config);
84		if CFG_CONFIGDIR then
85			table.insert(filenames, CFG_CONFIGDIR.."/"..prosody.opts.config);
86		end
87	elseif os.getenv("PROSODY_CONFIG") then -- Passed by prosodyctl
88			table.insert(filenames, os.getenv("PROSODY_CONFIG"));
89	else
90		table.insert(filenames, (CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
91	end
92	for _,_filename in ipairs(filenames) do
93		filename = _filename;
94		local file = io.open(filename);
95		if file then
96			file:close();
97			prosody.config_file = filename;
98			prosody.paths.config = filename:match("^(.*)[\\/][^\\/]*$");
99			CFG_CONFIGDIR = prosody.paths.config; -- luacheck: ignore 111
100			break;
101		end
102	end
103	prosody.config_file = filename
104	local ok, level, err = config.load(filename);
105	if not ok then
106		print("\n");
107		print("**************************");
108		if level == "parser" then
109			print("A problem occurred while reading the config file "..filename);
110			print("");
111			local err_line, err_message = tostring(err):match("%[string .-%]:(%d*): (.*)");
112			if err:match("chunk has too many syntax levels$") then
113				print("An Include statement in a config file is including an already-included");
114				print("file and causing an infinite loop. An Include statement in a config file is...");
115			else
116				print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err)));
117			end
118			print("");
119		elseif level == "file" then
120			print("Prosody was unable to find the configuration file.");
121			print("We looked for: "..filename);
122			print("A sample config file is included in the Prosody download called prosody.cfg.lua.dist");
123			print("Copy or rename it to prosody.cfg.lua and edit as necessary.");
124		end
125		print("More help on configuring Prosody can be found at https://prosody.im/doc/configure");
126		print("Good luck!");
127		print("**************************");
128		print("");
129		os.exit(1);
130	end
131	prosody.config_loaded = true;
132end
133
134function startup.check_dependencies()
135	if not dependencies.check_dependencies() then
136		os.exit(1);
137	end
138end
139
140-- luacheck: globals socket server
141
142function startup.load_libraries()
143	-- Load socket framework
144	-- luacheck: ignore 111/server 111/socket
145	socket = require "socket";
146	server = require "net.server"
147end
148
149function startup.init_logging()
150	-- Initialize logging
151	local loggingmanager = require "core.loggingmanager"
152	loggingmanager.reload_logging();
153	prosody.events.add_handler("config-reloaded", function ()
154		prosody.events.fire_event("reopen-log-files");
155	end);
156	prosody.events.add_handler("reopen-log-files", function ()
157		loggingmanager.reload_logging();
158		prosody.events.fire_event("logging-reloaded");
159	end);
160end
161
162function startup.log_dependency_warnings()
163	dependencies.log_warnings();
164end
165
166function startup.sanity_check()
167	for host, host_config in pairs(config.getconfig()) do
168		if host ~= "*"
169		and host_config.enabled ~= false
170		and not host_config.component_module then
171			return;
172		end
173	end
174	log("error", "No enabled VirtualHost entries found in the config file.");
175	log("error", "At least one active host is required for Prosody to function. Exiting...");
176	os.exit(1);
177end
178
179function startup.sandbox_require()
180	-- Replace require() with one that doesn't pollute _G, required
181	-- for neat sandboxing of modules
182	-- luacheck: ignore 113/getfenv 111/require
183	local _realG = _G;
184	local _real_require = require;
185	local getfenv = getfenv or function (f)
186		-- FIXME: This is a hack to replace getfenv() in Lua 5.2
187		local name, env = debug.getupvalue(debug.getinfo(f or 1).func, 1);
188		if name == "_ENV" then
189			return env;
190		end
191	end
192	function require(...) -- luacheck: ignore 121
193		local curr_env = getfenv(2);
194		local curr_env_mt = getmetatable(curr_env);
195		local _realG_mt = getmetatable(_realG);
196		if curr_env_mt and curr_env_mt.__index and not curr_env_mt.__newindex and _realG_mt then
197			local old_newindex, old_index;
198			old_newindex, _realG_mt.__newindex = _realG_mt.__newindex, curr_env;
199			old_index, _realG_mt.__index = _realG_mt.__index, function (_G, k) -- luacheck: ignore 212/_G
200				return rawget(curr_env, k);
201			end;
202			local ret = _real_require(...);
203			_realG_mt.__newindex = old_newindex;
204			_realG_mt.__index = old_index;
205			return ret;
206		end
207		return _real_require(...);
208	end
209end
210
211function startup.set_function_metatable()
212	local mt = {};
213	function mt.__index(f, upvalue)
214		local i, name, value = 0;
215		repeat
216			i = i + 1;
217			name, value = debug.getupvalue(f, i);
218		until name == upvalue or name == nil;
219		return value;
220	end
221	function mt.__newindex(f, upvalue, value)
222		local i, name = 0;
223		repeat
224			i = i + 1;
225			name = debug.getupvalue(f, i);
226		until name == upvalue or name == nil;
227		if name then
228			debug.setupvalue(f, i, value);
229		end
230	end
231	function mt.__tostring(f)
232		local info = debug.getinfo(f);
233		return ("function(%s:%d)"):format(info.short_src:match("[^\\/]*$"), info.linedefined);
234	end
235	debug.setmetatable(function() end, mt);
236end
237
238function startup.detect_platform()
239	prosody.platform = "unknown";
240	if os.getenv("WINDIR") then
241		prosody.platform = "windows";
242	elseif package.config:sub(1,1) == "/" then
243		prosody.platform = "posix";
244	end
245end
246
247function startup.detect_installed()
248	prosody.installed = nil;
249	if CFG_SOURCEDIR and (prosody.platform == "windows" or CFG_SOURCEDIR:match("^/")) then
250		prosody.installed = true;
251	end
252end
253
254function startup.init_global_state()
255	-- luacheck: ignore 121
256	prosody.bare_sessions = {};
257	prosody.full_sessions = {};
258	prosody.hosts = {};
259
260	-- COMPAT: These globals are deprecated
261	-- luacheck: ignore 111/bare_sessions 111/full_sessions 111/hosts
262	bare_sessions = prosody.bare_sessions;
263	full_sessions = prosody.full_sessions;
264	hosts = prosody.hosts;
265
266	prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR or ".",
267	                  plugins = CFG_PLUGINDIR or "plugins", data = "data" };
268
269	prosody.arg = _G.arg;
270
271	_G.log = logger.init("general");
272	prosody.log = logger.init("general");
273
274	startup.detect_platform();
275	startup.detect_installed();
276	_G.prosody = prosody;
277end
278
279function startup.setup_datadir()
280	prosody.paths.data = config.get("*", "data_path") or CFG_DATADIR or "data";
281end
282
283function startup.setup_plugindir()
284	local custom_plugin_paths = config.get("*", "plugin_paths");
285	if custom_plugin_paths then
286		local path_sep = package.config:sub(3,3);
287		-- path1;path2;path3;defaultpath...
288		-- luacheck: ignore 111
289		CFG_PLUGINDIR = table.concat(custom_plugin_paths, path_sep)..path_sep..(CFG_PLUGINDIR or "plugins");
290		prosody.paths.plugins = CFG_PLUGINDIR;
291	end
292end
293
294function startup.chdir()
295	if prosody.installed then
296		local lfs = require "lfs";
297		-- Ensure paths are absolute, not relative to the working directory which we're about to change
298		local cwd = lfs.currentdir();
299		prosody.paths.source = config.resolve_relative_path(cwd, prosody.paths.source);
300		prosody.paths.config = config.resolve_relative_path(cwd, prosody.paths.config);
301		prosody.paths.data = config.resolve_relative_path(cwd, prosody.paths.data);
302		-- Change working directory to data path.
303		lfs.chdir(prosody.paths.data);
304	end
305end
306
307function startup.add_global_prosody_functions()
308	-- Function to reload the config file
309	function prosody.reload_config()
310		log("info", "Reloading configuration file");
311		prosody.events.fire_event("reloading-config");
312		local ok, level, err = config.load(prosody.config_file);
313		if not ok then
314			if level == "parser" then
315				log("error", "There was an error parsing the configuration file: %s", tostring(err));
316			elseif level == "file" then
317				log("error", "Couldn't read the config file when trying to reload: %s", tostring(err));
318			end
319		else
320			prosody.events.fire_event("config-reloaded", {
321				filename = prosody.config_file,
322				config = config.getconfig(),
323			});
324		end
325		return ok, (err and tostring(level)..": "..tostring(err)) or nil;
326	end
327
328	-- Function to reopen logfiles
329	function prosody.reopen_logfiles()
330		log("info", "Re-opening log files");
331		prosody.events.fire_event("reopen-log-files");
332	end
333
334	-- Function to initiate prosody shutdown
335	function prosody.shutdown(reason, code)
336		log("info", "Shutting down: %s", reason or "unknown reason");
337		prosody.shutdown_reason = reason;
338		prosody.shutdown_code = code;
339		prosody.events.fire_event("server-stopping", {
340			reason = reason;
341			code = code;
342		});
343		server.setquitting(true);
344	end
345end
346
347function startup.load_secondary_libraries()
348	--- Load and initialise core modules
349	require "util.import"
350	require "util.xmppstream"
351	require "core.stanza_router"
352	require "core.statsmanager"
353	require "core.hostmanager"
354	require "core.portmanager"
355	require "core.modulemanager"
356	require "core.usermanager"
357	require "core.rostermanager"
358	require "core.sessionmanager"
359	package.loaded['core.componentmanager'] = setmetatable({},{__index=function()
360		-- COMPAT which version?
361		log("warn", "componentmanager is deprecated: %s", debug.traceback():match("\n[^\n]*\n[ \t]*([^\n]*)"));
362		return function() end
363	end});
364
365	require "util.array"
366	require "util.datetime"
367	require "util.iterators"
368	require "util.timer"
369	require "util.helpers"
370
371	pcall(require, "util.signal") -- Not on Windows
372
373	-- Commented to protect us from
374	-- the second kind of people
375	--[[
376	pcall(require, "remdebug.engine");
377	if remdebug then remdebug.engine.start() end
378	]]
379
380	require "util.stanza"
381	require "util.jid"
382end
383
384function startup.init_http_client()
385	local http = require "net.http"
386	local config_ssl = config.get("*", "ssl") or {}
387	local https_client = config.get("*", "client_https_ssl")
388	http.default.options.sslctx = require "core.certmanager".create_context("client_https port 0", "client",
389		{ capath = config_ssl.capath, cafile = config_ssl.cafile, verify = "peer", }, https_client);
390end
391
392function startup.init_data_store()
393	require "core.storagemanager";
394end
395
396function startup.prepare_to_start()
397	log("info", "Prosody is using the %s backend for connection handling", server.get_backend());
398	-- Signal to modules that we are ready to start
399	prosody.events.fire_event("server-starting");
400	prosody.start_time = os.time();
401end
402
403function startup.init_global_protection()
404	-- Catch global accesses
405	-- luacheck: ignore 212/t
406	local locked_globals_mt = {
407		__index = function (t, k) log("warn", "%s", debug.traceback("Attempt to read a non-existent global '"..tostring(k).."'", 2)); end;
408		__newindex = function (t, k, v) error("Attempt to set a global: "..tostring(k).." = "..tostring(v), 2); end;
409	};
410
411	function prosody.unlock_globals()
412		setmetatable(_G, nil);
413	end
414
415	function prosody.lock_globals()
416		setmetatable(_G, locked_globals_mt);
417	end
418
419	-- And lock now...
420	prosody.lock_globals();
421end
422
423function startup.read_version()
424	-- Try to determine version
425	local version_file = io.open((CFG_SOURCEDIR or ".").."/prosody.version");
426	prosody.version = "unknown";
427	if version_file then
428		prosody.version = version_file:read("*a"):gsub("%s*$", "");
429		version_file:close();
430		if #prosody.version == 12 and prosody.version:match("^[a-f0-9]+$") then
431			prosody.version = "hg:"..prosody.version;
432		end
433	else
434		local hg = require"util.mercurial";
435		local hgid = hg.check_id(CFG_SOURCEDIR or ".");
436		if hgid then prosody.version = "hg:" .. hgid; end
437	end
438end
439
440function startup.log_greeting()
441	log("info", "Hello and welcome to Prosody version %s", prosody.version);
442end
443
444function startup.notify_started()
445	prosody.events.fire_event("server-started");
446end
447
448-- Override logging config (used by prosodyctl)
449function startup.force_console_logging()
450	original_logging_config = config.get("*", "log");
451	config.set("*", "log", { { levels = { min = os.getenv("PROSODYCTL_LOG_LEVEL") or "info" }, to = "console" } });
452end
453
454function startup.switch_user()
455	-- Switch away from root and into the prosody user --
456	-- NOTE: This function is only used by prosodyctl.
457	-- The prosody process is built with the assumption that
458	-- it is already started as the appropriate user.
459
460	local want_pposix_version = "0.4.0";
461	local have_pposix, pposix = pcall(require, "util.pposix");
462
463	if have_pposix and pposix then
464		if pposix._VERSION ~= want_pposix_version then
465			print(string.format("Unknown version (%s) of binary pposix module, expected %s",
466				tostring(pposix._VERSION), want_pposix_version));
467			os.exit(1);
468		end
469		prosody.current_uid = pposix.getuid();
470		local arg_root = prosody.opts.root;
471		if prosody.current_uid == 0 and config.get("*", "run_as_root") ~= true and not arg_root then
472			-- We haz root!
473			local desired_user = config.get("*", "prosody_user") or "prosody";
474			local desired_group = config.get("*", "prosody_group") or desired_user;
475			local ok, err = pposix.setgid(desired_group);
476			if ok then
477				ok, err = pposix.initgroups(desired_user);
478			end
479			if ok then
480				ok, err = pposix.setuid(desired_user);
481				if ok then
482					-- Yay!
483					prosody.switched_user = true;
484				end
485			end
486			if not prosody.switched_user then
487				-- Boo!
488				print("Warning: Couldn't switch to Prosody user/group '"..tostring(desired_user).."'/'"..tostring(desired_group).."': "..tostring(err));
489			else
490				-- Make sure the Prosody user can read the config
491				local conf, err, errno = io.open(prosody.config_file);
492				if conf then
493					conf:close();
494				else
495					print("The config file is not readable by the '"..desired_user.."' user.");
496					print("Prosody will not be able to read it.");
497					print("Error was "..err);
498					os.exit(1);
499				end
500			end
501		end
502
503		-- Set our umask to protect data files
504		pposix.umask(config.get("*", "umask") or "027");
505		pposix.setenv("HOME", prosody.paths.data);
506		pposix.setenv("PROSODY_CONFIG", prosody.config_file);
507	else
508		print("Error: Unable to load pposix module. Check that Prosody is installed correctly.")
509		print("For more help send the below error to us through https://prosody.im/discuss");
510		print(tostring(pposix))
511		os.exit(1);
512	end
513end
514
515function startup.check_unwriteable()
516	local function test_writeable(filename)
517		local f, err = io.open(filename, "a");
518		if not f then
519			return false, err;
520		end
521		f:close();
522		return true;
523	end
524
525	local unwriteable_files = {};
526	if type(original_logging_config) == "string" and original_logging_config:sub(1,1) ~= "*" then
527		local ok, err = test_writeable(original_logging_config);
528		if not ok then
529			table.insert(unwriteable_files, err);
530		end
531	elseif type(original_logging_config) == "table" then
532		for _, rule in ipairs(original_logging_config) do
533			if rule.filename then
534				local ok, err = test_writeable(rule.filename);
535				if not ok then
536					table.insert(unwriteable_files, err);
537				end
538			end
539		end
540	end
541
542	if #unwriteable_files > 0 then
543		print("One of more of the Prosody log files are not");
544		print("writeable, please correct the errors and try");
545		print("starting prosodyctl again.");
546		print("");
547		for _, err in ipairs(unwriteable_files) do
548			print(err);
549		end
550		print("");
551		os.exit(1);
552	end
553end
554
555function startup.init_gc()
556	-- Apply garbage collector settings from the config file
557	local gc = require "util.gc";
558	local gc_settings = config.get("*", "gc") or { mode = default_gc_params.mode };
559
560	local ok, err = gc.configure(gc_settings, default_gc_params);
561	if not ok then
562		log("error", "Failed to apply GC configuration: %s", err);
563		return nil, err;
564	end
565	return true;
566end
567
568function startup.make_host(hostname)
569	return {
570		type = "local",
571		events = prosody.events,
572		modules = {},
573		sessions = {},
574		users = require "core.usermanager".new_null_provider(hostname)
575	};
576end
577
578function startup.make_dummy_hosts()
579	-- When running under prosodyctl, we don't want to
580	-- fully initialize the server, so we populate prosody.hosts
581	-- with just enough things for most code to work correctly
582	-- luacheck: ignore 122/hosts
583	prosody.core_post_stanza = function () end; -- TODO: mod_router!
584
585	for hostname in pairs(config.getconfig()) do
586		prosody.hosts[hostname] = startup.make_host(hostname);
587	end
588end
589
590-- prosodyctl only
591function startup.prosodyctl()
592	startup.parse_args();
593	startup.init_global_state();
594	startup.read_config();
595	startup.force_console_logging();
596	startup.init_logging();
597	startup.init_gc();
598	startup.setup_plugindir();
599	startup.setup_datadir();
600	startup.chdir();
601	startup.read_version();
602	startup.switch_user();
603	startup.check_dependencies();
604	startup.log_dependency_warnings();
605	startup.check_unwriteable();
606	startup.load_libraries();
607	startup.init_http_client();
608	startup.make_dummy_hosts();
609end
610
611function startup.prosody()
612	-- These actions are in a strict order, as many depend on
613	-- previous steps to have already been performed
614	startup.parse_args();
615	startup.init_global_state();
616	startup.read_config();
617	startup.init_logging();
618	startup.init_gc();
619	startup.sanity_check();
620	startup.sandbox_require();
621	startup.set_function_metatable();
622	startup.check_dependencies();
623	startup.load_libraries();
624	startup.setup_plugindir();
625	startup.setup_datadir();
626	startup.chdir();
627	startup.add_global_prosody_functions();
628	startup.read_version();
629	startup.log_greeting();
630	startup.log_dependency_warnings();
631	startup.load_secondary_libraries();
632	startup.init_http_client();
633	startup.init_data_store();
634	startup.init_global_protection();
635	startup.prepare_to_start();
636	startup.notify_started();
637end
638
639return startup;
640