mirror of
https://github.com/bjc/prosody.git
synced 2025-04-01 20:27:39 +03:00
Delays the string interpolation until the warning is logged, which may slightly lower memory usage. Allows retrieving the filename and line number easily.
412 lines
12 KiB
Lua
412 lines
12 KiB
Lua
-- Prosody IM
|
|
-- Copyright (C) 2008-2010 Matthew Wild
|
|
-- Copyright (C) 2008-2010 Waqas Hussain
|
|
--
|
|
-- This project is MIT/X11 licensed. Please see the
|
|
-- COPYING file in the source package for more information.
|
|
--
|
|
|
|
local _G = _G;
|
|
local setmetatable, rawget, rawset, io, os, error, dofile, type, pairs, ipairs =
|
|
setmetatable, rawget, rawset, io, os, error, dofile, type, pairs, ipairs;
|
|
local format, math_max, t_insert = string.format, math.max, table.insert;
|
|
|
|
local envload = require"prosody.util.envload".envload;
|
|
local deps = require"prosody.util.dependencies";
|
|
local it = require"prosody.util.iterators";
|
|
local resolve_relative_path = require"prosody.util.paths".resolve_relative_path;
|
|
local glob_to_pattern = require"prosody.util.paths".glob_to_pattern;
|
|
local path_sep = package.config:sub(1,1);
|
|
local get_traceback_table = require "prosody.util.debug".get_traceback_table;
|
|
local errors = require "prosody.util.error";
|
|
local log = require "prosody.util.logger".init("config");
|
|
|
|
local encodings = deps.softreq"prosody.util.encodings";
|
|
local nameprep = encodings and encodings.stringprep.nameprep or function (host) return host:lower(); end
|
|
|
|
local _M = {};
|
|
local _ENV = nil;
|
|
-- luacheck: std none
|
|
|
|
_M.resolve_relative_path = resolve_relative_path; -- COMPAT
|
|
|
|
local parser = nil;
|
|
|
|
local config_mt = { __index = function (t, _) return rawget(t, "*"); end};
|
|
local config = setmetatable({ ["*"] = { } }, config_mt);
|
|
local files = {};
|
|
local credentials_directory = nil;
|
|
local credential_fallback_fatal = true;
|
|
|
|
-- When host not found, use global
|
|
local host_mt = { __index = function(_, k) return config["*"][k] end }
|
|
|
|
function _M.getconfig()
|
|
return config;
|
|
end
|
|
|
|
function _M.get(host, key)
|
|
local v = config[host][key];
|
|
if v and errors.is_error(v) then
|
|
log("warn", "%s:%d: %s", v.context.filename, v.context.fileline, v.text);
|
|
return nil;
|
|
end
|
|
return v;
|
|
end
|
|
function _M.rawget(host, key)
|
|
local hostconfig = rawget(config, host);
|
|
if hostconfig then
|
|
return rawget(hostconfig, key);
|
|
end
|
|
end
|
|
|
|
local function set(config_table, host, key, value)
|
|
if host and key then
|
|
local hostconfig = rawget(config_table, host);
|
|
if not hostconfig then
|
|
hostconfig = rawset(config_table, host, setmetatable({}, host_mt))[host];
|
|
end
|
|
hostconfig[key] = value;
|
|
return true;
|
|
end
|
|
return false;
|
|
end
|
|
|
|
local function rawget_option(config_table, host, key)
|
|
if host and key then
|
|
local hostconfig = rawget(config_table, host);
|
|
if not hostconfig then
|
|
return nil;
|
|
end
|
|
return rawget(hostconfig, key);
|
|
end
|
|
end
|
|
|
|
function _M.set(host, key, value)
|
|
return set(config, host, key, value);
|
|
end
|
|
|
|
function _M.load(filename, config_format)
|
|
config_format = config_format or filename:match("%w+$");
|
|
|
|
if config_format == "lua" then
|
|
local f, err = io.open(filename);
|
|
if f then
|
|
local new_config = setmetatable({ ["*"] = { } }, config_mt);
|
|
local ok, err = parser.load(f:read("*a"), filename, new_config);
|
|
f:close();
|
|
if ok then
|
|
config = new_config;
|
|
end
|
|
return ok, "parser", err;
|
|
end
|
|
return f, "file", err;
|
|
end
|
|
|
|
if not config_format then
|
|
return nil, "file", "no parser specified";
|
|
else
|
|
return nil, "file", "no parser for "..(config_format);
|
|
end
|
|
end
|
|
|
|
function _M.files()
|
|
return files;
|
|
end
|
|
|
|
-- Built-in Lua parser
|
|
do
|
|
local pcall = _G.pcall;
|
|
local function get_line_number(config_file)
|
|
local tb = get_traceback_table(nil, 2);
|
|
for i = 1, #tb do
|
|
if tb[i].info.short_src == config_file then
|
|
return tb[i].info.currentline;
|
|
end
|
|
end
|
|
end
|
|
|
|
local config_option_proxy_mt = {
|
|
__index = setmetatable({
|
|
append = function (self, value)
|
|
local original_option = self:value();
|
|
if original_option == nil then
|
|
original_option = {};
|
|
end
|
|
if type(value) ~= "table" then
|
|
error("'append' operation expects a list of values to append to the existing list", 2);
|
|
end
|
|
if value[1] ~= nil then
|
|
for _, v in ipairs(value) do
|
|
t_insert(original_option, v);
|
|
end
|
|
else
|
|
for k, v in pairs(value) do
|
|
original_option[k] = v;
|
|
end
|
|
end
|
|
set(self.config_table, self.host, self.option_name, original_option);
|
|
return self;
|
|
end;
|
|
value = function (self)
|
|
return rawget_option(self.config_table, self.host, self.option_name);
|
|
end;
|
|
values = function (self)
|
|
return it.values(self:value());
|
|
end;
|
|
}, {
|
|
__index = function (t, k) --luacheck: ignore 212/t
|
|
error("Unknown config option operation: '"..k.."'", 2);
|
|
end;
|
|
});
|
|
|
|
__call = function (self, v2)
|
|
local v = self:value() or {};
|
|
if type(v) == "table" and type(v2) == "table" then
|
|
return self:append(v2);
|
|
end
|
|
|
|
error("Invalid syntax - missing '=' perhaps?", 2);
|
|
end;
|
|
};
|
|
|
|
-- For reading config values out of files.
|
|
local function filereader(basepath, defaultmode)
|
|
return function(filename, mode)
|
|
local f, err = io.open(resolve_relative_path(basepath, filename));
|
|
if not f then error(err, 2); end
|
|
local content, err = f:read(mode or defaultmode);
|
|
f:close();
|
|
if not content then error(err, 2); end
|
|
return content;
|
|
end
|
|
end
|
|
|
|
-- Collect lines into an array
|
|
local function linereader(basepath)
|
|
return function(filename)
|
|
local ret = {};
|
|
for line in io.lines(resolve_relative_path(basepath, filename)) do
|
|
t_insert(ret, line);
|
|
end
|
|
return ret;
|
|
end
|
|
end
|
|
|
|
parser = {};
|
|
function parser.load(data, config_file, config_table)
|
|
local set_options = {}; -- set_options[host.."/"..option_name] = true (when the option has been set already in this file)
|
|
local warnings = {};
|
|
local env;
|
|
local config_path = config_file:gsub("[^"..path_sep.."]+$", "");
|
|
|
|
-- The ' = true' are needed so as not to set off __newindex when we assign the functions below
|
|
env = setmetatable({
|
|
Host = true, host = true, VirtualHost = true,
|
|
Component = true, component = true,
|
|
FileContents = true,
|
|
FileLine = true,
|
|
FileLines = true,
|
|
Credential = true,
|
|
Include = true, include = true, RunScript = true }, {
|
|
__index = function (_, k)
|
|
if k:match("^ENV_") then
|
|
return os.getenv(k:sub(5));
|
|
end
|
|
if k == "Lua" then
|
|
return _G;
|
|
end
|
|
local val = rawget_option(config_table, env.__currenthost or "*", k);
|
|
|
|
local g_val = rawget(_G, k);
|
|
|
|
if val ~= nil or g_val == nil then
|
|
if type(val) == "table" then
|
|
return setmetatable({
|
|
config_table = config_table;
|
|
host = env.__currenthost or "*";
|
|
option_name = k;
|
|
}, config_option_proxy_mt);
|
|
end
|
|
return val;
|
|
end
|
|
|
|
if g_val ~= nil then
|
|
t_insert(
|
|
warnings,
|
|
("%s:%d: direct usage of the Lua API is deprecated - replace `%s` with `Lua.%s`"):format(
|
|
config_file,
|
|
get_line_number(config_file),
|
|
k,
|
|
k
|
|
)
|
|
);
|
|
end
|
|
|
|
return g_val;
|
|
end,
|
|
__newindex = function (_, k, v)
|
|
local host = env.__currenthost or "*";
|
|
local option_path = host.."/"..k;
|
|
if set_options[option_path] then
|
|
t_insert(warnings, ("%s:%d: Duplicate option '%s'"):format(config_file, get_line_number(config_file), k));
|
|
end
|
|
set_options[option_path] = true;
|
|
set(config_table, env.__currenthost or "*", k, v);
|
|
end
|
|
});
|
|
|
|
rawset(env, "__currenthost", "*") -- Default is global
|
|
function env.VirtualHost(name)
|
|
if not name then
|
|
error("Host must have a name", 2);
|
|
end
|
|
local prepped_name = nameprep(name);
|
|
if not prepped_name then
|
|
error(format("Name of Host %q contains forbidden characters", name), 0);
|
|
end
|
|
name = prepped_name;
|
|
if rawget(config_table, name) and rawget(config_table[name], "component_module") then
|
|
error(format("Host %q clashes with previously defined %s Component %q, for services use a sub-domain like conference.%s",
|
|
name, config_table[name].component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0);
|
|
end
|
|
rawset(env, "__currenthost", name);
|
|
-- Needs at least one setting to logically exist :)
|
|
set(config_table, name or "*", "defined", true);
|
|
return function (config_options)
|
|
rawset(env, "__currenthost", "*"); -- Return to global scope
|
|
if type(config_options) == "string" then
|
|
error(format("VirtualHost entries do not accept a module name (module '%s' provided for host '%s')", config_options, name), 2);
|
|
elseif type(config_options) ~= "table" then
|
|
error("Invalid syntax following VirtualHost, expected options but received a "..type(config_options), 2);
|
|
end
|
|
for option_name, option_value in pairs(config_options) do
|
|
set(config_table, name or "*", option_name, option_value);
|
|
end
|
|
end;
|
|
end
|
|
env.Host, env.host = env.VirtualHost, env.VirtualHost;
|
|
|
|
function env.Component(name)
|
|
if not name then
|
|
error("Component must have a name", 2);
|
|
end
|
|
local prepped_name = nameprep(name);
|
|
if not prepped_name then
|
|
error(format("Name of Component %q contains forbidden characters", name), 0);
|
|
end
|
|
name = prepped_name;
|
|
if rawget(config_table, name) and rawget(config_table[name], "defined")
|
|
and not rawget(config_table[name], "component_module") then
|
|
error(format("Component %q clashes with previously defined VirtualHost %q, for services use a sub-domain like conference.%s",
|
|
name, name, name), 0);
|
|
end
|
|
set(config_table, name, "component_module", "component");
|
|
-- Don't load the global modules by default
|
|
set(config_table, name, "load_global_modules", false);
|
|
rawset(env, "__currenthost", name);
|
|
local function handle_config_options(config_options)
|
|
rawset(env, "__currenthost", "*"); -- Return to global scope
|
|
for option_name, option_value in pairs(config_options) do
|
|
set(config_table, name or "*", option_name, option_value);
|
|
end
|
|
end
|
|
|
|
return function (module)
|
|
if type(module) == "string" then
|
|
set(config_table, name, "component_module", module);
|
|
return handle_config_options;
|
|
end
|
|
return handle_config_options(module);
|
|
end
|
|
end
|
|
env.component = env.Component;
|
|
|
|
function env.Include(file)
|
|
-- Check whether this is a wildcard Include
|
|
if file:match("[*?]") then
|
|
local lfs = deps.softreq "lfs";
|
|
if not lfs then
|
|
error(format("Error expanding wildcard pattern in Include %q - LuaFileSystem not available", file));
|
|
end
|
|
local path_pos, glob = file:match("()([^"..path_sep.."]+)$");
|
|
local path = file:sub(1, math_max(path_pos-2,0));
|
|
if #path > 0 then
|
|
path = resolve_relative_path(config_path, path);
|
|
else
|
|
path = config_path;
|
|
end
|
|
local patt = glob_to_pattern(glob);
|
|
for f in lfs.dir(path) do
|
|
if f:sub(1,1) ~= "." and f:match(patt) then
|
|
env.Include(path..path_sep..f);
|
|
end
|
|
end
|
|
return;
|
|
end
|
|
-- Not a wildcard, so resolve (potentially) relative path and run through config parser
|
|
file = resolve_relative_path(config_path, file);
|
|
local f, err = io.open(file);
|
|
if f then
|
|
local ret, err = parser.load(f:read("*a"), file, config_table);
|
|
if not ret then error(err:gsub("%[string.-%]", file), 0); end
|
|
if err then
|
|
for _, warning in ipairs(err) do
|
|
t_insert(warnings, warning);
|
|
end
|
|
end
|
|
end
|
|
if not f then error("Error loading included "..file..": "..err, 0); end
|
|
return f, err;
|
|
end
|
|
env.include = env.Include;
|
|
|
|
function env.RunScript(file)
|
|
return dofile(resolve_relative_path(config_path, file));
|
|
end
|
|
|
|
env.FileContents = filereader(config_path, "*a");
|
|
env.FileLine = filereader(config_path, "*l");
|
|
env.FileLines = linereader(config_path);
|
|
|
|
if credentials_directory then
|
|
env.Credential = filereader(credentials_directory, "*a");
|
|
elseif credential_fallback_fatal then
|
|
env.Credential = function() error("Credential() requires the $CREDENTIALS_DIRECTORY environment variable to be set", 2) end
|
|
else
|
|
env.Credential = function()
|
|
return errors.new({
|
|
type = "continue";
|
|
text = "Credential() requires the $CREDENTIALS_DIRECTORY environment variable to be set";
|
|
}, { filename = config_file; fileline = get_line_number(config_file) });
|
|
end
|
|
end
|
|
|
|
local chunk, err = envload(data, "@"..config_file, env);
|
|
|
|
if not chunk then
|
|
return nil, err;
|
|
end
|
|
|
|
local ok, err = pcall(chunk);
|
|
|
|
if not ok then
|
|
return nil, err;
|
|
end
|
|
|
|
t_insert(files, config_file);
|
|
|
|
return true, warnings;
|
|
end
|
|
|
|
end
|
|
|
|
function _M.set_credentials_directory(directory)
|
|
credentials_directory = directory;
|
|
end
|
|
|
|
function _M.set_credential_fallback_mode(mode)
|
|
credential_fallback_fatal = mode == "error";
|
|
end
|
|
|
|
return _M;
|