summaryrefslogtreecommitdiffstats
path: root/lualib/lua_settings.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lualib/lua_settings.lua')
-rw-r--r--lualib/lua_settings.lua309
1 files changed, 309 insertions, 0 deletions
diff --git a/lualib/lua_settings.lua b/lualib/lua_settings.lua
new file mode 100644
index 0000000..d6d24d6
--- /dev/null
+++ b/lualib/lua_settings.lua
@@ -0,0 +1,309 @@
+--[[
+Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+]]--
+
+--[[[
+-- @module lua_settings
+-- This module contains internal helpers for the settings infrastructure in Rspamd
+-- More details at https://rspamd.com/doc/configuration/settings.html
+--]]
+
+local exports = {}
+local known_ids = {}
+local post_init_added = false
+local post_init_performed = false
+local all_symbols
+local default_symbols
+
+local fun = require "fun"
+local lua_util = require "lua_util"
+local rspamd_logger = require "rspamd_logger"
+
+local function register_settings_cb(from_postload)
+ if not post_init_performed then
+ all_symbols = rspamd_config:get_symbols()
+
+ default_symbols = fun.totable(fun.filter(function(_, v)
+ return not v.allowed_ids or #v.allowed_ids == 0 or v.flags.explicit_disable
+ end, all_symbols))
+
+ local explicit_symbols = lua_util.keys(fun.filter(function(k, v)
+ return v.flags.explicit_disable
+ end, all_symbols))
+
+ local symnames = lua_util.list_to_hash(lua_util.keys(all_symbols))
+
+ for _, set in pairs(known_ids) do
+ local s = set.settings.apply or {}
+ set.symbols = lua_util.shallowcopy(symnames)
+ local enabled_symbols = {}
+ local seen_enabled = false
+ local disabled_symbols = {}
+ local seen_disabled = false
+
+ -- Enabled map
+ if s.symbols_enabled then
+ -- Remove all symbols from set.symbols aside of explicit_disable symbols
+ set.symbols = lua_util.list_to_hash(explicit_symbols)
+ seen_enabled = true
+ for _, sym in ipairs(s.symbols_enabled) do
+ enabled_symbols[sym] = true
+ set.symbols[sym] = true
+ end
+ end
+ if s.groups_enabled then
+ seen_enabled = true
+ for _, gr in ipairs(s.groups_enabled) do
+ local syms = rspamd_config:get_group_symbols(gr)
+
+ if syms then
+ for _, sym in ipairs(syms) do
+ enabled_symbols[sym] = true
+ set.symbols[sym] = true
+ end
+ end
+ end
+ end
+
+ -- Disabled map
+ if s.symbols_disabled then
+ seen_disabled = true
+ for _, sym in ipairs(s.symbols_disabled) do
+ disabled_symbols[sym] = true
+ set.symbols[sym] = false
+ end
+ end
+ if s.groups_disabled then
+ seen_disabled = true
+ for _, gr in ipairs(s.groups_disabled) do
+ local syms = rspamd_config:get_group_symbols(gr)
+
+ if syms then
+ for _, sym in ipairs(syms) do
+ disabled_symbols[sym] = true
+ set.symbols[sym] = false
+ end
+ end
+ end
+ end
+
+ -- Deal with complexity to avoid mess in C
+ if not seen_enabled then
+ enabled_symbols = nil
+ end
+ if not seen_disabled then
+ disabled_symbols = nil
+ end
+
+ if enabled_symbols or disabled_symbols then
+ -- Specify what symbols are really enabled for this settings id
+ set.has_specific_symbols = true
+ end
+
+ rspamd_config:register_settings_id(set.name, enabled_symbols, disabled_symbols)
+
+ -- Remove to avoid clash
+ s.symbols_disabled = nil
+ s.symbols_enabled = nil
+ s.groups_enabled = nil
+ s.groups_disabled = nil
+ end
+
+ -- We now iterate over all symbols and check for allowed_ids/forbidden_ids
+ for k, v in pairs(all_symbols) do
+ if v.allowed_ids and not v.flags.explicit_disable then
+ for _, id in ipairs(v.allowed_ids) do
+ if known_ids[id] then
+ local set = known_ids[id]
+ if not set.has_specific_symbols then
+ set.has_specific_symbols = true
+ end
+ set.symbols[k] = true
+ else
+ rspamd_logger.errx(rspamd_config, 'symbol %s is allowed at unknown settings id %s',
+ k, id)
+ end
+ end
+ end
+ if v.forbidden_ids then
+ for _, id in ipairs(v.forbidden_ids) do
+ if known_ids[id] then
+ local set = known_ids[id]
+ if not set.has_specific_symbols then
+ set.has_specific_symbols = true
+ end
+ set.symbols[k] = false
+ else
+ rspamd_logger.errx(rspamd_config, 'symbol %s is denied at unknown settings id %s',
+ k, id)
+ end
+ end
+ end
+ end
+
+ -- Now we create lists of symbols for each settings and digest
+ for _, set in pairs(known_ids) do
+ set.symbols = lua_util.keys(fun.filter(function(_, v)
+ return v
+ end, set.symbols))
+ table.sort(set.symbols)
+ set.digest = lua_util.table_digest(set.symbols)
+ end
+
+ post_init_performed = true
+ end
+end
+
+-- Returns numeric representation of the settings id
+local function numeric_settings_id(str)
+ local cr = require "rspamd_cryptobox_hash"
+ local util = require "rspamd_util"
+ local ret = util.unpack("I4",
+ cr.create_specific('xxh64'):update(str):bin())
+
+ return ret
+end
+
+exports.numeric_settings_id = numeric_settings_id
+
+-- Used to do the following:
+-- If there is a group of symbols_allowed, it checks if that is an array
+-- If that is a hash table then we transform it to a normal list, probably adding symbols to adjust scores
+local function transform_settings_maybe(settings, name)
+ if settings.apply then
+ local apply = settings.apply
+
+ if apply.symbols_enabled then
+ local senabled = apply.symbols_enabled
+
+ if not senabled[1] then
+ -- Transform map to a list
+ local nlist = {}
+ if not apply.scores then
+ apply.scores = {}
+ end
+ for k, v in pairs(senabled) do
+ if tonumber(v) then
+ -- Move to symbols as well
+ apply.scores[k] = tonumber(v)
+ lua_util.debugm('settings', rspamd_config,
+ 'set symbol %s -> %s for settings %s', k, v, name)
+ end
+ nlist[#nlist + 1] = k
+ end
+ -- Convert
+ apply.symbols_enabled = nlist
+ end
+
+ local symhash = lua_util.list_to_hash(apply.symbols_enabled)
+
+ if apply.symbols then
+ -- Check if added symbols are enabled
+ for k, v in pairs(apply.symbols) do
+ local s
+ -- Check if we have ["sym1", "sym2" ...] or {"sym1": xx, "sym2": yy}
+ if type(k) == 'string' then
+ s = k
+ else
+ s = v
+ end
+ if not symhash[s] then
+ lua_util.debugm('settings', rspamd_config,
+ 'added symbol %s to symbols_enabled for %s', s, name)
+ apply.symbols_enabled[#apply.symbols_enabled + 1] = s
+ end
+ end
+ end
+ end
+ end
+
+ return settings
+end
+
+local function register_settings_id(str, settings, from_postload)
+ local numeric_id = numeric_settings_id(str)
+
+ if known_ids[numeric_id] then
+ -- Might be either rewrite or a collision
+ if known_ids[numeric_id].name ~= str then
+ local logger = require "rspamd_logger"
+
+ logger.errx(rspamd_config, 'settings ID clash! id %s maps to %s and conflicts with %s',
+ numeric_id, known_ids[numeric_id].name, str)
+
+ return nil
+ end
+ else
+ known_ids[numeric_id] = {
+ name = str,
+ id = numeric_id,
+ settings = transform_settings_maybe(settings, str),
+ symbols = {}
+ }
+ end
+
+ if not from_postload and not post_init_added then
+ -- Use high priority to ensure that settings are initialised early but not before all
+ -- plugins are loaded
+ rspamd_config:add_post_init(function()
+ register_settings_cb(true)
+ end, 150)
+ rspamd_config:add_config_unload(function()
+ if post_init_added then
+ known_ids = {}
+ post_init_added = false
+ end
+ post_init_performed = false
+ end)
+
+ post_init_added = true
+ end
+
+ return numeric_id
+end
+
+exports.register_settings_id = register_settings_id
+
+local function settings_by_id(id)
+ if not post_init_performed then
+ register_settings_cb(false)
+ end
+ return known_ids[id]
+end
+
+exports.settings_by_id = settings_by_id
+exports.all_settings = function()
+ if not post_init_performed then
+ register_settings_cb(false)
+ end
+ return known_ids
+end
+exports.all_symbols = function()
+ if not post_init_performed then
+ register_settings_cb(false)
+ end
+ return all_symbols
+end
+-- What is enabled when no settings are there
+exports.default_symbols = function()
+ if not post_init_performed then
+ register_settings_cb(false)
+ end
+ return default_symbols
+end
+
+exports.load_all_settings = register_settings_cb
+
+return exports \ No newline at end of file