util/startup.lua
author Kim Alvefur <zash@zash.se>
Tue, 12 Oct 2021 14:59:18 +0200
changeset 11850 6425dfa3de45
parent 11833 4fad0ca42f66
parent 11848 0f786c4a2cb2
child 11851 2b3ce80ffece
permissions -rw-r--r--
Merge 0.11->trunk

-- Ignore the CFG_* variables
-- luacheck: ignore 113/CFG_CONFIGDIR 113/CFG_SOURCEDIR 113/CFG_DATADIR 113/CFG_PLUGINDIR
local startup = {};

local prosody = { events = require "util.events".new() };
local logger = require "util.logger";
local log = logger.init("startup");
local parse_args = require "util.argparse".parse;

local config = require "core.configmanager";
local config_warnings;

local dependencies = require "util.dependencies";

local original_logging_config;

local default_gc_params = {
	mode = "incremental";
	-- Incremental mode defaults
	threshold = 105, speed = 500;
	-- Generational mode defaults
	minor_threshold = 20, major_threshold = 50;
};

local arg_settigs = {
	prosody = {
		short_params = { D = "daemonize"; F = "no-daemonize" };
		value_params = { config = true };
	};
	prosodyctl = {
		short_params = { v = "verbose" };
		value_params = { config = true };
	};
}

function startup.parse_args()
	local opts, err, where = parse_args(arg, arg_settigs[prosody.process_type]);
	if not opts then
		if err == "param-not-found" then
			print("Unknown command-line option: "..tostring(where));
			print("Perhaps you meant to use prosodyctl instead?");
		elseif err == "missing-value" then
			print("Expected a value to follow command-line option: "..where);
		end
		os.exit(1);
	end
	prosody.opts = opts;
end

function startup.read_config()
	local filenames = {};

	local filename;
	if prosody.opts.config then
		table.insert(filenames, prosody.opts.config);
		if CFG_CONFIGDIR then
			table.insert(filenames, CFG_CONFIGDIR.."/"..prosody.opts.config);
		end
	elseif os.getenv("PROSODY_CONFIG") then -- Passed by prosodyctl
			table.insert(filenames, os.getenv("PROSODY_CONFIG"));
	else
		table.insert(filenames, (CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
	end
	for _,_filename in ipairs(filenames) do
		filename = _filename;
		local file = io.open(filename);
		if file then
			file:close();
			prosody.config_file = filename;
			prosody.paths.config = filename:match("^(.*)[\\/][^\\/]*$");
			CFG_CONFIGDIR = prosody.paths.config; -- luacheck: ignore 111
			break;
		end
	end
	prosody.config_file = filename
	local ok, level, err = config.load(filename);
	if not ok then
		print("\n");
		print("**************************");
		if level == "parser" then
			print("A problem occurred while reading the config file "..filename);
			print("");
			local err_line, err_message = tostring(err):match("%[string .-%]:(%d*): (.*)");
			if err:match("chunk has too many syntax levels$") then
				print("An Include statement in a config file is including an already-included");
				print("file and causing an infinite loop. An Include statement in a config file is...");
			else
				print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err)));
			end
			print("");
		elseif level == "file" then
			print("Prosody was unable to find the configuration file.");
			print("We looked for: "..filename);
			print("A sample config file is included in the Prosody download called prosody.cfg.lua.dist");
			print("Copy or rename it to prosody.cfg.lua and edit as necessary.");
		end
		print("More help on configuring Prosody can be found at https://prosody.im/doc/configure");
		print("Good luck!");
		print("**************************");
		print("");
		os.exit(1);
	elseif err and #err > 0 then
		config_warnings = err;
	end
	prosody.config_loaded = true;
end

function startup.check_dependencies()
	if not dependencies.check_dependencies() then
		os.exit(1);
	end
end

-- luacheck: globals socket server

function startup.load_libraries()
	-- Load socket framework
	-- luacheck: ignore 111/server 111/socket
	socket = require "socket";
	server = require "net.server"
end

function startup.init_logging()
	-- Initialize logging
	local loggingmanager = require "core.loggingmanager"
	loggingmanager.reload_logging();
	prosody.events.add_handler("config-reloaded", function ()
		prosody.events.fire_event("reopen-log-files");
	end);
	prosody.events.add_handler("reopen-log-files", function ()
		loggingmanager.reload_logging();
		prosody.events.fire_event("logging-reloaded");
	end);
end

function startup.log_startup_warnings()
	dependencies.log_warnings();
	if config_warnings then
		for _, warning in ipairs(config_warnings) do
			log("warn", "Configuration warning: %s", warning);
		end
	end
end

function startup.sanity_check()
	for host, host_config in pairs(config.getconfig()) do
		if host ~= "*"
		and host_config.enabled ~= false
		and not host_config.component_module then
			return;
		end
	end
	log("error", "No enabled VirtualHost entries found in the config file.");
	log("error", "At least one active host is required for Prosody to function. Exiting...");
	os.exit(1);
end

function startup.sandbox_require()
	-- Replace require() with one that doesn't pollute _G, required
	-- for neat sandboxing of modules
	-- luacheck: ignore 113/getfenv 111/require
	local _realG = _G;
	local _real_require = require;
	local getfenv = getfenv or function (f)
		-- FIXME: This is a hack to replace getfenv() in Lua 5.2
		local name, env = debug.getupvalue(debug.getinfo(f or 1).func, 1);
		if name == "_ENV" then
			return env;
		end
	end
	function require(...) -- luacheck: ignore 121
		local curr_env = getfenv(2);
		local curr_env_mt = getmetatable(curr_env);
		local _realG_mt = getmetatable(_realG);
		if curr_env_mt and curr_env_mt.__index and not curr_env_mt.__newindex and _realG_mt then
			local old_newindex, old_index;
			old_newindex, _realG_mt.__newindex = _realG_mt.__newindex, curr_env;
			old_index, _realG_mt.__index = _realG_mt.__index, function (_G, k) -- luacheck: ignore 212/_G
				return rawget(curr_env, k);
			end;
			local ret = _real_require(...);
			_realG_mt.__newindex = old_newindex;
			_realG_mt.__index = old_index;
			return ret;
		end
		return _real_require(...);
	end
end

function startup.set_function_metatable()
	local mt = {};
	function mt.__index(f, upvalue)
		local i, name, value = 0;
		repeat
			i = i + 1;
			name, value = debug.getupvalue(f, i);
		until name == upvalue or name == nil;
		return value;
	end
	function mt.__newindex(f, upvalue, value)
		local i, name = 0;
		repeat
			i = i + 1;
			name = debug.getupvalue(f, i);
		until name == upvalue or name == nil;
		if name then
			debug.setupvalue(f, i, value);
		end
	end
	function mt.__tostring(f)
		local info = debug.getinfo(f, "Su");
		local n_params = info.nparams or 0;
		for i = 1, n_params do
			info[i] = debug.getlocal(f, i);
		end
		if info.isvararg then
			info[n_params+1] = "...";
		end
		return ("function<%s:%d>(%s)"):format(info.short_src:match("[^\\/]*$"), info.linedefined, table.concat(info, ", "));
	end
	debug.setmetatable(function() end, mt);
end

function startup.detect_platform()
	prosody.platform = "unknown";
	if os.getenv("WINDIR") then
		prosody.platform = "windows";
	elseif package.config:sub(1,1) == "/" then
		prosody.platform = "posix";
	end
end

function startup.detect_installed()
	prosody.installed = nil;
	if CFG_SOURCEDIR and (prosody.platform == "windows" or CFG_SOURCEDIR:match("^/")) then
		prosody.installed = true;
	end
end

function startup.init_global_state()
	-- luacheck: ignore 121
	prosody.bare_sessions = {};
	prosody.full_sessions = {};
	prosody.hosts = {};

	-- COMPAT: These globals are deprecated
	-- luacheck: ignore 111/bare_sessions 111/full_sessions 111/hosts
	bare_sessions = prosody.bare_sessions;
	full_sessions = prosody.full_sessions;
	hosts = prosody.hosts;

	prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR or ".",
	                  plugins = CFG_PLUGINDIR or "plugins", data = "data" };

	prosody.arg = _G.arg;

	_G.log = logger.init("general");
	prosody.log = logger.init("general");

	startup.detect_platform();
	startup.detect_installed();
	_G.prosody = prosody;
end

function startup.setup_datadir()
	prosody.paths.data = config.get("*", "data_path") or CFG_DATADIR or "data";
end

function startup.setup_plugindir()
	local custom_plugin_paths = config.get("*", "plugin_paths");
	local path_sep = package.config:sub(3,3);
	if custom_plugin_paths then
		-- path1;path2;path3;defaultpath...
		-- luacheck: ignore 111
		CFG_PLUGINDIR = table.concat(custom_plugin_paths, path_sep)..path_sep..(CFG_PLUGINDIR or "plugins");
		prosody.paths.plugins = CFG_PLUGINDIR;
	end
end

function startup.setup_plugin_install_path()
	local installer_plugin_path = config.get("*", "installer_plugin_path") or "custom_plugins";
	local path_sep = package.config:sub(3,3);
	installer_plugin_path = config.resolve_relative_path(CFG_DATADIR or "data", installer_plugin_path);
	require"util.paths".complement_lua_path(installer_plugin_path);
	-- luacheck: ignore 111
	CFG_PLUGINDIR = installer_plugin_path..path_sep..(CFG_PLUGINDIR or "plugins");
	prosody.paths.installer = installer_plugin_path;
	prosody.paths.plugins = CFG_PLUGINDIR;
end

function startup.chdir()
	if prosody.installed then
		local lfs = require "lfs";
		-- Ensure paths are absolute, not relative to the working directory which we're about to change
		local cwd = lfs.currentdir();
		prosody.paths.source = config.resolve_relative_path(cwd, prosody.paths.source);
		prosody.paths.config = config.resolve_relative_path(cwd, prosody.paths.config);
		prosody.paths.data = config.resolve_relative_path(cwd, prosody.paths.data);
		-- Change working directory to data path.
		lfs.chdir(prosody.paths.data);
	end
end

function startup.add_global_prosody_functions()
	-- Function to reload the config file
	function prosody.reload_config()
		log("info", "Reloading configuration file");
		prosody.events.fire_event("reloading-config");
		local ok, level, err = config.load(prosody.config_file);
		if not ok then
			if level == "parser" then
				log("error", "There was an error parsing the configuration file: %s", err);
			elseif level == "file" then
				log("error", "Couldn't read the config file when trying to reload: %s", err);
			end
		else
			prosody.events.fire_event("config-reloaded", {
				filename = prosody.config_file,
				config = config.getconfig(),
			});
		end
		return ok, (err and tostring(level)..": "..tostring(err)) or nil;
	end

	-- Function to reopen logfiles
	function prosody.reopen_logfiles()
		log("info", "Re-opening log files");
		prosody.events.fire_event("reopen-log-files");
	end

	-- Function to initiate prosody shutdown
	function prosody.shutdown(reason, code)
		log("info", "Shutting down: %s", reason or "unknown reason");
		prosody.shutdown_reason = reason;
		prosody.shutdown_code = code;
		prosody.events.fire_event("server-stopping", {
			reason = reason;
			code = code;
		});
		server.setquitting(true);
	end
end

function startup.load_secondary_libraries()
	--- Load and initialise core modules
	require "util.import"
	require "util.xmppstream"
	require "core.stanza_router"
	require "core.statsmanager"
	require "core.hostmanager"
	require "core.portmanager"
	require "core.modulemanager"
	require "core.usermanager"
	require "core.rostermanager"
	require "core.sessionmanager"
	package.loaded['core.componentmanager'] = setmetatable({},{__index=function()
		-- COMPAT which version?
		log("warn", "componentmanager is deprecated: %s", debug.traceback():match("\n[^\n]*\n[ \t]*([^\n]*)"));
		return function() end
	end});

	require "util.array"
	require "util.datetime"
	require "util.iterators"
	require "util.timer"
	require "util.helpers"

	pcall(require, "util.signal") -- Not on Windows

	-- Commented to protect us from
	-- the second kind of people
	--[[
	pcall(require, "remdebug.engine");
	if remdebug then remdebug.engine.start() end
	]]

	require "util.stanza"
	require "util.jid"
end

function startup.init_http_client()
	local http = require "net.http"
	local config_ssl = config.get("*", "ssl") or {}
	local https_client = config.get("*", "client_https_ssl")
	http.default.options.sslctx = require "core.certmanager".create_context("client_https port 0", "client",
		{ capath = config_ssl.capath, cafile = config_ssl.cafile, verify = "peer", }, https_client);
end

function startup.init_data_store()
	require "core.storagemanager";
end

function startup.prepare_to_start()
	log("info", "Prosody is using the %s backend for connection handling", server.get_backend());
	-- Signal to modules that we are ready to start
	prosody.events.fire_event("server-starting");
	prosody.start_time = os.time();
end

function startup.init_global_protection()
	-- Catch global accesses
	-- luacheck: ignore 212/t
	local locked_globals_mt = {
		__index = function (t, k) log("warn", "%s", debug.traceback("Attempt to read a non-existent global '"..tostring(k).."'", 2)); end;
		__newindex = function (t, k, v) error("Attempt to set a global: "..tostring(k).." = "..tostring(v), 2); end;
	};

	function prosody.unlock_globals()
		setmetatable(_G, nil);
	end

	function prosody.lock_globals()
		setmetatable(_G, locked_globals_mt);
	end

	-- And lock now...
	prosody.lock_globals();
end

function startup.read_version()
	-- Try to determine version
	local version_file = io.open((CFG_SOURCEDIR or ".").."/prosody.version");
	prosody.version = "unknown";
	if version_file then
		prosody.version = version_file:read("*a"):gsub("%s*$", "");
		version_file:close();
		if #prosody.version == 12 and prosody.version:match("^[a-f0-9]+$") then
			prosody.version = "hg:"..prosody.version;
		end
	else
		local hg = require"util.mercurial";
		local hgid = hg.check_id(CFG_SOURCEDIR or ".");
		if hgid then prosody.version = "hg:" .. hgid; end
	end
end

function startup.log_greeting()
	log("info", "Hello and welcome to Prosody version %s", prosody.version);
end

function startup.notify_started()
	prosody.events.fire_event("server-started");
end

-- Override logging config (used by prosodyctl)
function startup.force_console_logging()
	original_logging_config = config.get("*", "log");
	local log_level = os.getenv("PROSODYCTL_LOG_LEVEL");
	if not log_level and prosody.opts.verbose then log_level = "debug"; end
	config.set("*", "log", { { levels = { min = log_level or "info" }, to = "console" } });
end

function startup.switch_user()
	-- Switch away from root and into the prosody user --
	-- NOTE: This function is only used by prosodyctl.
	-- The prosody process is built with the assumption that
	-- it is already started as the appropriate user.

	local want_pposix_version = "0.4.0";
	local have_pposix, pposix = pcall(require, "util.pposix");

	if have_pposix and pposix then
		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));
			os.exit(1);
		end
		prosody.current_uid = pposix.getuid();
		local arg_root = prosody.opts.root;
		if prosody.current_uid == 0 and config.get("*", "run_as_root") ~= true and not arg_root then
			-- We haz root!
			local desired_user = config.get("*", "prosody_user") or "prosody";
			local desired_group = config.get("*", "prosody_group") or desired_user;
			local ok, err = pposix.setgid(desired_group);
			if ok then
				ok, err = pposix.initgroups(desired_user);
			end
			if ok then
				ok, err = pposix.setuid(desired_user);
				if ok then
					-- Yay!
					prosody.switched_user = true;
				end
			end
			if not prosody.switched_user then
				-- Boo!
				print("Warning: Couldn't switch to Prosody user/group '"..tostring(desired_user).."'/'"..tostring(desired_group).."': "..tostring(err));
			else
				-- Make sure the Prosody user can read the config
				local conf, err, errno = io.open(prosody.config_file); --luacheck: ignore 211/errno
				if conf then
					conf:close();
				else
					print("The config file is not readable by the '"..desired_user.."' user.");
					print("Prosody will not be able to read it.");
					print("Error was "..err);
					os.exit(1);
				end
			end
		end

		-- Set our umask to protect data files
		pposix.umask(config.get("*", "umask") or "027");
		pposix.setenv("HOME", prosody.paths.data);
		pposix.setenv("PROSODY_CONFIG", prosody.config_file);
	else
		print("Error: Unable to load pposix module. Check that Prosody is installed correctly.")
		print("For more help send the below error to us through https://prosody.im/discuss");
		print(tostring(pposix))
		os.exit(1);
	end
end

function startup.check_unwriteable()
	local function test_writeable(filename)
		local f, err = io.open(filename, "a");
		if not f then
			return false, err;
		end
		f:close();
		return true;
	end

	local unwriteable_files = {};
	if type(original_logging_config) == "string" and original_logging_config:sub(1,1) ~= "*" then
		local ok, err = test_writeable(original_logging_config);
		if not ok then
			table.insert(unwriteable_files, err);
		end
	elseif type(original_logging_config) == "table" then
		for _, rule in ipairs(original_logging_config) do
			if rule.filename then
				local ok, err = test_writeable(rule.filename);
				if not ok then
					table.insert(unwriteable_files, err);
				end
			end
		end
	end

	if #unwriteable_files > 0 then
		print("One of more of the Prosody log files are not");
		print("writeable, please correct the errors and try");
		print("starting prosodyctl again.");
		print("");
		for _, err in ipairs(unwriteable_files) do
			print(err);
		end
		print("");
		os.exit(1);
	end
end

function startup.init_gc()
	-- Apply garbage collector settings from the config file
	local gc = require "util.gc";
	local gc_settings = config.get("*", "gc") or { mode = default_gc_params.mode };

	local ok, err = gc.configure(gc_settings, default_gc_params);
	if not ok then
		log("error", "Failed to apply GC configuration: %s", err);
		return nil, err;
	end
	return true;
end

function startup.init_errors()
	require "util.error".configure(config.get("*", "error_library") or {});
end

function startup.make_host(hostname)
	return {
		type = "local",
		events = prosody.events,
		modules = {},
		sessions = {},
		users = require "core.usermanager".new_null_provider(hostname)
	};
end

function startup.make_dummy_hosts()
	-- When running under prosodyctl, we don't want to
	-- fully initialize the server, so we populate prosody.hosts
	-- with just enough things for most code to work correctly
	-- luacheck: ignore 122/hosts
	prosody.core_post_stanza = function () end; -- TODO: mod_router!

	for hostname in pairs(config.getconfig()) do
		prosody.hosts[hostname] = startup.make_host(hostname);
	end
end

-- prosodyctl only
function startup.prosodyctl()
	prosody.process_type = "prosodyctl";
	startup.parse_args();
	startup.init_global_state();
	startup.read_config();
	startup.force_console_logging();
	startup.init_logging();
	startup.init_gc();
	startup.init_errors();
	startup.setup_plugindir();
	startup.setup_plugin_install_path();
	startup.setup_datadir();
	startup.chdir();
	startup.read_version();
	startup.switch_user();
	startup.check_dependencies();
	startup.log_startup_warnings();
	startup.check_unwriteable();
	startup.load_libraries();
	startup.init_http_client();
	startup.make_dummy_hosts();
end

function startup.prosody()
	-- These actions are in a strict order, as many depend on
	-- previous steps to have already been performed
	prosody.process_type = "prosody";
	startup.parse_args();
	startup.init_global_state();
	startup.read_config();
	startup.init_logging();
	startup.init_gc();
	startup.init_errors();
	startup.sanity_check();
	startup.sandbox_require();
	startup.set_function_metatable();
	startup.check_dependencies();
	startup.load_libraries();
	startup.setup_plugindir();
	startup.setup_plugin_install_path();
	startup.setup_datadir();
	startup.chdir();
	startup.add_global_prosody_functions();
	startup.read_version();
	startup.log_greeting();
	startup.log_startup_warnings();
	startup.load_secondary_libraries();
	startup.init_http_client();
	startup.init_data_store();
	startup.init_global_protection();
	startup.prepare_to_start();
	startup.notify_started();
end

return startup;