#!/usr/bin/env lua
-- * Metronome IM *
--
-- This file is part of the Metronome XMPP server and is released under the
-- ISC License, please see the LICENSE file in this source package for more
-- information about copyright and licensing.
CFG_SOURCEDIR=os.getenv("METRONOME_SRCDIR");
CFG_CONFIGDIR=os.getenv("METRONOME_CFGDIR");
CFG_PLUGINDIR=os.getenv("METRONOME_PLUGINDIR");
CFG_DATADIR=os.getenv("METRONOME_DATADIR");
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
local function is_relative(path)
local path_sep = package.config:sub(1,1);
return ((path_sep == "/" and path:sub(1,1) ~= "/")
or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
end
if CFG_SOURCEDIR then
local function filter_relative_paths(path)
if is_relative(path) then return ""; end
end
local function sanitise_paths(paths)
return (paths:gsub("[^;]+;?", filter_relative_paths):gsub(";;+", ";"));
end
package.path = sanitise_paths(CFG_SOURCEDIR.."/?.lua;"..package.path);
package.cpath = sanitise_paths(CFG_SOURCEDIR.."/?.so;"..package.cpath);
end
if CFG_DATADIR then
if os.getenv("HOME") then
CFG_DATADIR = CFG_DATADIR:gsub("^~", os.getenv("HOME"));
end
end
local metronome = { events = require "util.events".new(); incoming_s2s = {} };
_G.metronome = metronome;
local dependencies = require "util.dependencies";
if not dependencies.check_dependencies() then
os.exit(1);
end
config = require "core.configmanager"
function read_config()
local filenames = {};
local filename;
if arg[1] == "--config" and arg[2] then
table.insert(filenames, arg[2]);
if CFG_CONFIGDIR then
table.insert(filenames, CFG_CONFIGDIR.."/"..arg[2]);
end
else
for _, format in ipairs(config.parsers()) do
table.insert(filenames, (CFG_CONFIGDIR or ".").."/metronome.cfg."..format);
end
end
for _,_filename in ipairs(filenames) do
filename = _filename;
local file = io.open(filename);
if file then
file:close();
CFG_CONFIGDIR = filename:match("^(.*)[\\/][^\\/]*$");
break;
end
end
local ok, level, err = config.load(filename);
if not ok then
print("\n");
print("**************************");
if level == "parser" then
print("A problem occured while reading the config file "..(CFG_CONFIGDIR or ".").."/metronome.cfg.lua"..":");
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("Metronome was unable to find the configuration file.");
print("We looked for: "..(CFG_CONFIGDIR or ".").."/metronome.cfg.lua");
end
print("**************************");
print("");
os.exit(1);
end
end
function files_serialization_format()
metronome.serialization = config.get("*", "flat_files_serialization") or "internal";
end
function preload_libraries()
local libs = config.get("*", "metronome_preload_libraries");
libs = type(libs) ~= "table" and {} or libs;
for _, name in ipairs(libs) do pcall(require, name); end
end
function init_net_server()
server = require "net.server"
end
function init_logging()
require "core.loggingmanager"
end
function log_dependency_warnings()
dependencies.log_warnings();
end
function 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 Metronome to function. Exiting...");
os.exit(1);
end
function sandbox_require()
local _realG = _G;
local _real_require = require;
local getfenv = getfenv or function(f)
local name, env = debug.getupvalue(debug.getinfo(f or 1).func, 1);
if name == "_ENV" then return env; end
end
function require(...)
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)
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 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);
return ("function(%s:%d)"):format(info.short_src:match("[^\\/]*$"), info.linedefined);
end
debug.setmetatable(function() end, mt);
end
function init_global_state()
bare_sessions = {};
full_sessions = {};
hosts = {};
metronome.bare_sessions = bare_sessions;
metronome.full_sessions = full_sessions;
metronome.hosts = hosts;
local data_path = config.get("*", "data_path") or CFG_DATADIR or "data";
local custom_plugin_paths = config.get("*", "plugin_paths");
if custom_plugin_paths then
local path_sep = package.config:sub(3,3);
-- path1;path2;path3;defaultpath...
CFG_PLUGINDIR = table.concat(custom_plugin_paths, path_sep)..path_sep..(CFG_PLUGINDIR or "plugins");
end
metronome.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR or ".",
plugins = CFG_PLUGINDIR or "plugins", data = data_path };
metronome.arg = _G.arg;
metronome.platform = "unknown";
if package.config:sub(1,1) == "/" then metronome.platform = "posix"; end
metronome.installed = nil;
if CFG_SOURCEDIR and CFG_SOURCEDIR:match("^/") then metronome.installed = true; end
if metronome.installed then require "lfs".chdir(data_path); end
function metronome.reload_config()
log("info", "Reloading configuration file");
metronome.events.fire_event("reloading-config");
local ok, level, err = config.load((rawget(_G, "CFG_CONFIGDIR") or ".").."/metronome.cfg.lua");
if not ok then
if level == "parser" then
log("error", "There was an error parsing the configuration file: %s", tostring(err));
elseif level == "file" then
log("error", "Couldn't read the config file when trying to reload: %s", tostring(err));
end
else
metronome.set_gc();
end
return ok, (err and tostring(level)..": "..tostring(err)) or nil;
end
function metronome.reopen_logfiles()
log("info", "Re-opening log files");
metronome.events.fire_event("reopen-log-files");
end
function metronome.shutdown(reason)
log("info", "Shutting down: %s", reason or "unknown reason");
metronome.shutdown_reason = reason;
metronome.events.fire_event("server-stopping", {reason = reason});
server.setquitting(true);
end
function metronome.set_gc()
local settings = config.get("*", "metronome_gc");
settings = type(settings) ~= "table" and {} or settings;
collectgarbage("setpause", settings.setpause or 100);
collectgarbage("setstepmul", settings.setstepmul or 500);
collectgarbage("restart");
end
local certmanager = require "util.certmanager";
local global_ssl_ctx = certmanager.create_context("*", "server");
metronome.global_ssl_ctx = global_ssl_ctx;
metronome.set_gc();
end
read_version = require "util.auxiliary".read_version;
ripairs = require "util.auxiliary".ripairs;
function load_secondary_libraries()
require "util.import";
require "util.xmppstream";
require "core.hostmanager";
require "core.portmanager";
require "core.modulemanager";
require "core.usermanager";
require "core.sessionmanager";
require "net.http";
require "util.array";
require "util.datetime";
require "util.iterators";
require "util.timer";
require "util.helpers";
pcall(require, "util.signal");
require "util.stanza";
require "util.jid";
end
function init_data_store()
require "core.storagemanager";
end
function prepare_to_start()
log("info", "Metronome is using the %s backend for connection handling", server.get_backend());
metronome.events.fire_event("server-starting");
metronome.start_time = os.time();
end
function init_global_protection()
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 metronome.unlock_globals()
setmetatable(_G, nil);
end
function metronome.lock_globals()
setmetatable(_G, locked_globals_mt);
end
metronome.lock_globals();
end
function loop()
local function catch_uncaught_error(err)
if type(err) == "string" and err:match("interrupted!$") then
return "quitting";
end
log("error", "Top-level error, please report:\n%s", tostring(err));
local traceback = debug.traceback("", 2);
if traceback then
log("error", "%s", traceback);
end
metronome.events.fire_event("very-bad-error", {error = err, traceback = traceback});
end
while select(2, xpcall(server.loop, catch_uncaught_error)) ~= "quitting" do
socket.sleep(0.2);
end
end
function cleanup()
log("info", "Shutdown status: Cleaning up");
metronome.events.fire_event("server-cleanup");
server.setquitting(false);
log("info", "Shutdown status: Closing all active sessions");
for hostname, host in pairs(hosts) do
log("debug", "Shutdown status: Closing client connections for %s", hostname)
if host.sessions then
local reason = { condition = "system-shutdown", text = "Server is shutting down" };
if metronome.shutdown_reason then
reason.text = reason.text..": "..metronome.shutdown_reason;
end
for username, user in pairs(host.sessions) do
for resource, session in pairs(user.sessions) do
log("debug", "Closing connection for %s@%s/%s", username, hostname, resource);
session:close(reason);
end
end
end
log("debug", "Shutdown status: Closing outgoing s2s connections from %s", hostname);
if host.s2sout then
for remotehost, session in pairs(host.s2sout) do
if session.close then
session:close("system-shutdown");
else
log("warn", "Unable to close outgoing s2s session to %s, no session:close()?!", remotehost);
end
end
end
end
log("info", "Shutdown status: Closing all server connections");
server.closeall();
server.setquitting(true);
end
read_config();
files_serialization_format();
init_logging();
sanity_check();
sandbox_require();
set_function_metatable();
preload_libraries();
init_net_server();
init_global_state();
read_version();
log("info", "Hello and welcome to Metronome version %s", metronome.version);
log_dependency_warnings();
load_secondary_libraries();
init_data_store();
init_global_protection();
prepare_to_start();
metronome.events.fire_event("server-started");
loop();
log("info", "Shutting down...");
cleanup();
metronome.events.fire_event("server-stopped");
log("info", "Shutdown complete");