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