Software /
code /
prosody
File
util/startup.lua @ 11749:83d6d6a70edf
net.http: fail open if surrounding code does not configure TLS
Previously, if surrounding code was not configuring the TLS context
used default in net.http, it would not validate certificates at all.
This is not a security issue with prosody, because prosody updates the
context with `verify = "peer"` as well as paths to CA certificates in
util.startup.init_http_client.
Nevertheless... Let's not leave this pitfall out there in the open.
author | Jonas Schäfer <jonas@wielicki.name> |
---|---|
date | Sun, 29 Aug 2021 15:04:47 +0200 |
parent | 11560:3bbb1af92514 |
child | 11828:024ac556e907 |
line wrap: on
line source
-- 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 short_params = { D = "daemonize", F = "no-daemonize" }; local value_params = { config = true }; function startup.parse_args() local opts, err, where = parse_args(arg, { short_params = short_params, value_params = value_params, }); 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"); config.set("*", "log", { { levels = { min = os.getenv("PROSODYCTL_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;