File Manager
--[[
Copyright (c) 2011-2015, Vsevolod Stakhov <vsevolod@highsecure.ru>
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.
]]--
if confighelp then
return
end
-- This plugin implements user dynamic settings
-- Settings documentation can be found here:
-- https://rspamd.com/doc/configuration/settings.html
local rspamd_logger = require "rspamd_logger"
local rspamd_maps = require "lua_maps"
local lua_util = require "lua_util"
local rspamd_ip = require "rspamd_ip"
local rspamd_regexp = require "rspamd_regexp"
local lua_selectors = require "lua_selectors"
local lua_settings = require "lua_settings"
local ucl = require "ucl"
local fun = require "fun"
local rspamd_mempool = require "rspamd_mempool"
local redis_params
local settings = {}
local N = "settings"
local settings_initialized = false
local max_pri = 0
local module_sym_id -- Main module symbol
local function apply_settings(task, to_apply, id)
task:set_settings(to_apply)
task:cache_set('settings', to_apply)
if id then
task:set_settings_id(id)
end
if to_apply['add_headers'] or to_apply['remove_headers'] then
local rep = {
add_headers = to_apply['add_headers'] or {},
remove_headers = to_apply['remove_headers'] or {},
}
task:set_rmilter_reply(rep)
end
if to_apply.flags and type(to_apply.flags) == 'table' then
for _,fl in ipairs(to_apply.flags) do
task:set_flag(fl)
end
end
if to_apply.symbols then
-- Add symbols, specified in the settings
if #to_apply.symbols > 0 then
-- Array like symbols
for _,val in ipairs(to_apply.symbols) do
task:insert_result(val, 1.0)
end
else
-- Object like symbols
for k,v in pairs(to_apply.symbols) do
if type(v) == 'table' then
task:insert_result(k, v.score or 1.0, v.options or {})
elseif tonumber(v) then
task:insert_result(k, tonumber(v))
end
end
end
end
if to_apply.subject then
task:set_metric_subject(to_apply.subject)
end
-- E.g.
-- messages = { smtp_message = "5.3.1 Go away" }
if to_apply.messages and type(to_apply.messages) == 'table' then
fun.each(function(category, message)
task:append_message(message, category)
end, to_apply.messages)
end
end
-- Checks for overridden settings within query params and returns 3 values:
-- * Apply element
-- * Settings ID element if found
-- * Priority of the settings according to the place where it is found
--
-- If no override has been found, it returns `false`
local function check_query_settings(task)
-- Try 'settings' attribute
local settings_id = task:get_settings_id()
local query_set = task:get_request_header('settings')
if query_set then
local parser = ucl.parser()
local res,err = parser:parse_string(tostring(query_set))
if res then
if settings_id then
rspamd_logger.warnx(task, "both settings-id '%s' and settings headers are presented, ignore settings-id; ",
tostring(settings_id))
end
local settings_obj = parser:get_object()
-- Treat as low priority
return settings_obj,nil,1
else
rspamd_logger.errx(task, 'Parse error: %s', err)
end
end
local query_maxscore = task:get_request_header('maxscore')
local nset
if query_maxscore then
if settings_id then
rspamd_logger.infox(task, "both settings id '%s' and maxscore '%s' headers are presented, merge them; " ..
"settings id has priority",
tostring(settings_id), tostring(query_maxscore))
end
-- We have score limits redefined by request
local ms = tonumber(tostring(query_maxscore))
if ms then
nset = {
actions = {
reject = ms
}
}
local query_softscore = task:get_request_header('softscore')
if query_softscore then
local ss = tonumber(tostring(query_softscore))
nset.actions['add header'] = ss
end
if not settings_id then
rspamd_logger.infox(task, 'apply maxscore = %s', nset.actions)
-- Maxscore is low priority
return nset, nil, 1
end
end
end
if settings_id and settings_initialized then
local cached = lua_settings.settings_by_id(settings_id)
if cached then
local elt = cached.settings
if elt['whitelist'] then
elt['apply'] = {whitelist = true}
end
if elt.apply then
if nset then
elt.apply = lua_util.override_defaults(nset, elt.apply)
end
return elt.apply, cached, cached.priority or 1
end
else
rspamd_logger.warnx(task, 'no settings id "%s" has been found', settings_id)
if nset then
rspamd_logger.infox(task, 'apply maxscore = %s', nset.actions)
return nset, nil, 1
end
end
else
if nset then
rspamd_logger.infox(task, 'apply maxscore = %s', nset.actions)
return nset, nil, 1
end
end
return false
end
local function check_addr_setting(expected, addr)
local function check_specific_addr(elt)
if expected.name then
if rspamd_maps.rspamd_maybe_check_map(expected.name, elt.addr) then
return true
end
end
if expected.user then
if rspamd_maps.rspamd_maybe_check_map(expected.user, elt.user) then
return true
end
end
if expected.domain and elt.domain then
if rspamd_maps.rspamd_maybe_check_map(expected.domain, elt.domain) then
return true
end
end
if expected.regexp then
if expected.regexp:match(elt.addr) then
return true
end
end
return false
end
for _, e in ipairs(addr) do
if check_specific_addr(e) then
return true
end
end
return false
end
local function check_string_setting(expected, str)
if expected.regexp then
if expected.regexp:match(str) then
return true
end
elseif expected.check then
if rspamd_maps.rspamd_maybe_check_map(expected.check, str) then
return true
end
end
return false
end
local function check_ip_setting(expected, ip)
if not expected[2] then
if rspamd_maps.rspamd_maybe_check_map(expected[1], ip:to_string()) then
return true
end
else
if expected[2] ~= 0 then
local nip = ip:apply_mask(expected[2])
if nip and nip:to_string() == expected[1]:to_string() then
return true
end
elseif ip:to_string() == expected[1]:to_string() then
return true
end
end
return false
end
local function priority_to_string(pri)
if pri then
if pri >= 3 then
return "high"
elseif pri >= 2 then
return "medium"
end
end
return "low"
end
-- Check limit for a task
local function check_settings(task)
local function check_specific_setting(rule, matched)
local res = false
local function process_atom(atom)
local elt = rule.checks[atom]
if elt then
local input = elt.extract(task)
if not input then return false end
if elt.check(input) then
matched[#matched + 1] = atom
return 1.0
end
else
rspamd_logger.errx(task, 'error in settings: check %s is not defined!', atom)
end
return 0
end
res = rule.expression and rule.expression:process(process_atom)
if res and res > 0 then
if rule['whitelist'] then
rule['apply'] = {whitelist = true}
end
return rule
end
return nil
end
-- Check if we have override as query argument
local query_apply,id_elt,priority = check_query_settings(task)
local function maybe_apply_query_settings()
if query_apply then
if id_elt then
apply_settings(task, query_apply, id_elt.id)
rspamd_logger.infox(task, "applied settings id %s(%s); priority %s",
id_elt.name, id_elt.id, priority_to_string(priority))
else
apply_settings(task, query_apply, nil)
rspamd_logger.infox(task, "applied settings from query; priority %s",
priority_to_string(priority))
end
end
end
local min_pri = 1
if query_apply then
if priority >= min_pri then
-- Do not check lower or equal priorities
min_pri = priority + 1
end
if priority > max_pri then
-- Our internal priorities are lower then a priority from query, so no need to check
maybe_apply_query_settings()
return
end
end
-- Do not waste resources
if not settings_initialized then
maybe_apply_query_settings()
return
end
-- Match rules according their order
local applied = false
for pri = max_pri,min_pri,-1 do
if not applied and settings[pri] then
for _,s in ipairs(settings[pri]) do
local matched = {}
lua_util.debugm(N, task, "check for settings element %s",
s.name)
local result = check_specific_setting(s.rule, matched)
-- Can use xor here but more complicated for reading
if result then
if s.rule['apply'] then
if s.rule.id then
-- Extract static settings
local cached = lua_settings.settings_by_id(s.rule.id)
if not cached or not cached.settings or not cached.settings.apply then
rspamd_logger.errx(task, 'unregistered settings id found: %s!', s.rule.id)
else
rspamd_logger.infox(task, "<%s> apply static settings %s (id = %s); %s matched; priority %s",
task:get_message_id(),
cached.name, s.rule.id,
table.concat(matched, ','),
priority_to_string(pri))
apply_settings(task, cached.settings.apply, s.rule.id)
end
else
-- Dynamic settings
rspamd_logger.infox(task, "<%s> apply settings according to rule %s (%s matched)",
task:get_message_id(), s.name, table.concat(matched, ','))
apply_settings(task, s.rule.apply, nil)
end
applied = true
end
if s.rule['symbols'] then
-- Add symbols, specified in the settings
fun.each(function(val)
task:insert_result(val, 1.0)
end, s.rule['symbols'])
end
end
end
end
end
if not applied then
maybe_apply_query_settings()
end
end
-- Process settings based on their priority
local function process_settings_table(tbl, allow_ids, mempool, is_static)
local get_priority = function(elt)
local pri_tonum = function(p)
if p then
if type(p) == "number" then
return tonumber(p)
elseif type(p) == "string" then
if p == "high" then
return 3
elseif p == "medium" then
return 2
end
end
end
return 1
end
return pri_tonum(elt['priority'])
end
-- Check the setting element internal data
local process_setting_elt = function(name, elt)
lua_util.debugm(N, rspamd_config, 'process settings "%s"', name)
-- Process IP address: converted to a table {ip, mask}
local function process_ip_condition(ip)
local out = {}
if type(ip) == "table" then
for _,v in ipairs(ip) do
table.insert(out, process_ip_condition(v))
end
elseif type(ip) == "string" then
local slash = string.find(ip, '/')
if not slash then
-- Just a plain IP address
local res = rspamd_ip.from_string(ip)
if res:is_valid() then
out[1] = res
out[2] = 0
else
-- It can still be a map
out[1] = res
end
else
local res = rspamd_ip.from_string(string.sub(ip, 1, slash - 1))
local mask = tonumber(string.sub(ip, slash + 1))
if res:is_valid() then
out[1] = res
out[2] = mask
else
rspamd_logger.errx(rspamd_config, "bad IP address: " .. ip)
return nil
end
end
else
return nil
end
return out
end
-- Process email like condition, converted to a table with fields:
-- name - full email (surprise!)
-- user - user part
-- domain - domain part
-- regexp - full email regexp (yes, it sucks)
local function process_email_condition(addr)
local out = {}
if type(addr) == "table" then
for _,v in ipairs(addr) do
table.insert(out, process_email_condition(v))
end
elseif type(addr) == "string" then
if string.sub(addr, 1, 4) == "map:" then
-- It is map, don't apply any extra logic
out['name'] = addr
else
local start = string.sub(addr, 1, 1)
if start == '/' then
-- It is a regexp
local re = rspamd_regexp.create(addr)
if re then
out['regexp'] = re
else
rspamd_logger.errx(rspamd_config, "bad regexp: " .. addr)
return nil
end
elseif start == '@' then
-- It is a domain if form @domain
out['domain'] = string.sub(addr, 2)
else
-- Check user@domain parts
local at = string.find(addr, '@')
if at then
-- It is full address
out['name'] = addr
else
-- It is a user
out['user'] = addr
end
end
end
else
return nil
end
return out
end
-- Convert a plain string condition to a table:
-- check - string to match
-- regexp - regexp to match
local function process_string_condition(addr)
local out = {}
if type(addr) == "table" then
for _,v in ipairs(addr) do
table.insert(out, process_string_condition(v))
end
elseif type(addr) == "string" then
if string.sub(addr, 1, 4) == "map:" then
-- It is map, don't apply any extra logic
out['check'] = addr
else
local start = string.sub(addr, 1, 1)
if start == '/' then
-- It is a regexp
local re = rspamd_regexp.create(addr)
if re then
out['regexp'] = re
else
rspamd_logger.errx(rspamd_config, "bad regexp: " .. addr)
return nil
end
else
out['check'] = addr
end
end
else
return nil
end
return out
end
local convert_to_table = function(chk_elt, out)
if type(chk_elt) == 'string' then
return {out}
end
return out
end
-- Used to create a checking closure: if value matches expected somehow, return true
local function gen_check_closure(expected, check_func)
return function(value)
if not value then return false end
if type(value) == 'function' then
value = value()
end
if value then
if not check_func then
check_func = function(a, b) return a == b end
end
local ret = fun.any(function(d)
return check_func(d, value)
end, expected)
if ret then
return true
end
end
return false
end
end
local out = {}
local checks = {}
if elt['ip'] then
local ips_table = process_ip_condition(elt['ip'])
if ips_table then
lua_util.debugm(N, rspamd_config, 'added ip condition to "%s": %s',
name, ips_table)
checks.ip = {
check = gen_check_closure(convert_to_table(elt.ip, ips_table), check_ip_setting),
extract = function(task)
local ip = task:get_from_ip()
if ip and ip:is_valid() then return ip end
return nil
end,
}
end
end
if elt['client_ip'] then
local client_ips_table = process_ip_condition(elt['client_ip'])
if client_ips_table then
lua_util.debugm(N, rspamd_config, 'added client_ip condition to "%s": %s',
name, client_ips_table)
checks.client_ip = {
check = gen_check_closure(convert_to_table(elt.client_ip, client_ips_table),
check_ip_setting),
extract = function(task)
local ip = task:get_client_ip()
if ip:is_valid() then return ip end
return nil
end,
}
end
end
if elt['from'] then
local from_condition = process_email_condition(elt['from'])
if from_condition then
lua_util.debugm(N, rspamd_config, 'added from condition to "%s": %s',
name, from_condition)
checks.from = {
check = gen_check_closure(convert_to_table(elt.from, from_condition),
check_addr_setting),
extract = function(task)
return task:get_from(1)
end,
}
end
end
if elt['rcpt'] then
local rcpt_condition = process_email_condition(elt['rcpt'])
if rcpt_condition then
lua_util.debugm(N, rspamd_config, 'added rcpt condition to "%s": %s',
name, rcpt_condition)
checks.rcpt = {
check = gen_check_closure(convert_to_table(elt.rcpt, rcpt_condition),
check_addr_setting),
extract = function(task)
return task:get_recipients(1)
end,
}
end
end
if elt['from_mime'] then
local from_mime_condition = process_email_condition(elt['from_mime'])
if from_mime_condition then
lua_util.debugm(N, rspamd_config, 'added from_mime condition to "%s": %s',
name, from_mime_condition)
checks.from_mime = {
check = gen_check_closure(convert_to_table(elt.from_mime, from_mime_condition),
check_addr_setting),
extract = function(task)
return task:get_from(2)
end,
}
end
end
if elt['rcpt_mime'] then
local rcpt_mime_condition = process_email_condition(elt['rcpt'])
if rcpt_mime_condition then
lua_util.debugm(N, rspamd_config, 'added rcpt mime condition to "%s": %s',
name, rcpt_mime_condition)
checks.rcpt_mime = {
check = gen_check_closure(convert_to_table(elt.rcpt_mime, rcpt_mime_condition),
check_addr_setting),
extract = function(task)
return task:get_recipients(2)
end,
}
end
end
if elt['user'] then
local user_condition = process_email_condition(elt['user'])
if user_condition then
lua_util.debugm(N, rspamd_config, 'added user condition to "%s": %s',
name, user_condition)
checks.user = {
check = gen_check_closure(convert_to_table(elt.user, user_condition),
check_addr_setting),
extract = function(task)
local uname = task:get_user()
local user = {}
if uname then
user[1] = {}
local localpart, domainpart = string.gmatch(uname, "(.+)@(.+)")()
if localpart then
user[1]["user"] = localpart
user[1]["domain"] = domainpart
user[1]["addr"] = uname
else
user[1]["user"] = uname
user[1]["addr"] = uname
end
return user
end
return nil
end,
}
end
end
if elt['hostname'] then
local hostname_condition = process_string_condition(elt['hostname'])
if hostname_condition then
lua_util.debugm(N, rspamd_config, 'added hostname condition to "%s": %s',
name, hostname_condition)
checks.hostname = {
check = gen_check_closure(convert_to_table(elt.hostname, hostname_condition),
check_string_setting),
extract = function(task)
return task:get_hostname() or ''
end,
}
end
end
if elt['authenticated'] then
lua_util.debugm(N, rspamd_config, 'added authenticated condition to "%s"',
name)
checks.authenticated = {
check = function(value) if value then return true end return false end,
extract = function(task)
return task:get_user()
end
}
end
if elt['local'] then
lua_util.debugm(N, rspamd_config, 'added local condition to "%s"',
name)
checks['local'] = {
check = function(value) if value then return true end return false end,
extract = function(task)
local ip = task:get_from_ip()
if not ip or not ip:is_valid() then
return nil
end
if ip:is_local() then
return true
else
return nil
end
end
}
end
local aliases = {}
-- This function is used to convert compound condition with
-- generic type and specific part (e.g. `header`, `Content-Transfer-Encoding`)
-- to a set of usable check elements:
-- `generic:specific` - most common part
-- `generic:<order>` - e.g. `header:1` for the first header
-- `generic:safe` - replace unsafe stuff with safe + lowercase
-- also aliases entry is set to avoid implicit expression
local function process_compound_condition(cond, generic, specific)
local full_key = generic .. ':' .. specific
checks[full_key] = cond
-- Try numeric key
for i=1,1000 do
local num_key = generic .. ':' .. tostring(i)
if not checks[num_key] then
checks[num_key] = cond
aliases[num_key] = true
break
end
end
local safe_key = generic .. ':' ..
specific:gsub('[:%-+&|><]', '_')
:gsub('%(', '[')
:gsub('%)', ']')
:lower()
if not checks[safe_key] then
checks[safe_key] = cond
aliases[safe_key] = true
end
return safe_key
end
-- Headers are tricky:
-- We create an closure with extraction function depending on header name
-- We also inserts it into `checks` table as an atom in form header:<hname>
-- Check function depends on the input:
-- * for something that looks like `header = "/bar/"` we create a regexp
-- * for something that looks like `header = true` we just check the existence
local function process_header_elt(table_element, extractor_func)
if elt[table_element] then
for k, v in pairs(elt[table_element]) do
if type(v) == 'string' then
local re = rspamd_regexp.create(v)
if re then
local cond = {
check = function(values)
return fun.any(function(c) return re:match(c) end, values)
end,
extract = extractor_func(k),
}
local skey = process_compound_condition(cond, table_element,
k)
lua_util.debugm(N, rspamd_config, 'added %s condition to "%s": %s =~ %s',
skey, name, k, v)
end
elseif type(v) == 'boolean' then
local cond = {
check = function(values)
if #values == 0 then return (not v) end
return v
end,
extract = extractor_func(k),
}
local skey = process_compound_condition(cond, table_element,
k)
lua_util.debugm(N, rspamd_config, 'added %s condition to "%s": %s == %s',
skey, name, k, v)
else
rspamd_logger.errx(rspamd_config, 'invalid %s %s = %s', table_element, k, v)
end
end
end
end
process_header_elt('request_header', function(hname)
return function(task)
local rh = task:get_request_header(hname)
if rh then return {rh} end
return {}
end
end)
process_header_elt('header', function(hname)
return function(task)
local rh = task:get_header_full(hname)
if rh then
return fun.totable(fun.map(function(h) return h.decoded end, rh))
end
return {}
end
end)
if elt['selector'] then
local sel = lua_selectors.create_selector_closure(rspamd_config, elt.selector,
elt.delimiter or "")
if sel then
local cond = {
check = function(values)
return fun.any(function(c)
return c
end, values)
end,
extract = sel,
}
local skey = process_compound_condition(cond, 'selector', elt.selector)
lua_util.debugm(N, rspamd_config, 'added selector condition to "%s": %s',
name, skey)
end
end
-- Special, special case!
local inverse = false
if elt['inverse'] then
lua_util.debugm(N, rspamd_config, 'added inverse condition to "%s"',
name)
inverse = true
end
-- Killmeplease
local nchecks = 0
for _,_ in pairs(checks) do nchecks = nchecks + 1 end
if nchecks > 0 then
-- Now we can deal with the expression!
if not elt.expression then
-- Artificial & expression to deal with the legacy parts
-- Here we get all keys and concatenate them with '&&'
local s = ' && '
-- By De Morgan laws
if inverse then s = ' || ' end
-- Exclude aliases and join all checks by key
local expr_str = table.concat(lua_util.keys(fun.filter(
function(k, _) return not aliases[k] end,
checks)), s)
if inverse then
expr_str = string.format('!(%s)', expr_str)
end
elt.expression = expr_str
lua_util.debugm(N, rspamd_config, 'added implicit settings expression for %s: %s',
name, expr_str)
end
-- Parse expression's sanity
local function parse_atom(str)
local atom = table.concat(fun.totable(fun.take_while(function(c)
if string.find(', \t()><+!|&\n', c) then
return false
end
return true
end, fun.iter(str))), '')
if checks[atom] then
return atom
end
rspamd_logger.errx(rspamd_config,
'use of undefined element "%s" when parsing settings expression, known checks: %s',
atom, table.concat(fun.totable(fun.map(function(k, _) return k end, checks)), ','))
return nil
end
local rspamd_expression = require "rspamd_expression"
out.expression = rspamd_expression.create(elt.expression, parse_atom,
mempool)
out.checks = checks
if not out.expression then
rspamd_logger.errx(rspamd_config, 'cannot parse expression %s for %s',
elt.expression, name)
else
lua_util.debugm(N, rspamd_config, 'registered settings %s with %s checks',
name, nchecks)
end
else
lua_util.debugm(N, rspamd_config, 'registered settings %s with no checks',
name)
end
-- Process symbols part/apply part
if elt['symbols'] then
lua_util.debugm(N, rspamd_config, 'added symbols condition to "%s": %s',
name, elt.symbols)
out['symbols'] = elt['symbols']
end
if elt['apply'] then
-- Just insert all metric results to the action key
out['apply'] = elt['apply']
elseif elt['whitelist'] or elt['want_spam'] then
out['whitelist'] = true
else
rspamd_logger.errx(rspamd_config, "no actions in settings: " .. name)
return nil
end
if allow_ids then
if not elt.id then
elt.id = name
end
if elt['id'] then
-- We are here from a postload script
out.id = lua_settings.register_settings_id(elt.id, out, true)
lua_util.debugm(N, rspamd_config,
'added settings id to "%s": %s -> %s',
name, elt.id, out.id)
end
if not is_static then
-- If we apply that from map
-- In fact, it is useless and evil but who cares...
if elt.apply and elt.apply.symbols then
-- Register virtual symbols
for k,v in pairs(elt.apply.symbols) do
local rtb = {
type = 'virtual',
parent = module_sym_id,
}
if type(k) == 'number' and type(v) == 'string' then
rtb.name = v
elseif type(k) == 'string' then
rtb.name = k
end
if out.id then
rtb.allowed_ids = tostring(elt.id)
end
rspamd_config:register_symbol(rtb)
end
end
end
else
if elt['id'] then
rspamd_logger.errx(rspamd_config,
'cannot set static IDs from dynamic settings, please read the docs')
end
end
return out
end
settings_initialized = false
-- filter trash in the input
local ft = fun.filter(
function(_, elt)
if type(elt) == "table" then
return true
end
return false
end, tbl)
-- clear all settings
max_pri = 0
local nrules = 0
for k in pairs(settings) do settings[k]={} end
-- fill new settings by priority
fun.for_each(function(k, v)
local pri = get_priority(v)
if pri > max_pri then max_pri = pri end
if not settings[pri] then
settings[pri] = {}
end
local s = process_setting_elt(k, v)
if s then
table.insert(settings[pri], {name = k, rule = s})
nrules = nrules + 1
end
end, ft)
-- sort settings with equal priopities in alphabetical order
for pri,_ in pairs(settings) do
table.sort(settings[pri], function(a,b) return a.name < b.name end)
end
settings_initialized = true
lua_settings.load_all_settings(true)
rspamd_logger.infox(rspamd_config, 'loaded %1 elements of settings', nrules)
return true
end
-- Parse settings map from the ucl line
local settings_map_pool
local function process_settings_map(map_text)
local parser = ucl.parser()
local res,err
if type(map_text) == 'string' then
res,err = parser:parse_string(map_text)
else
res,err = parser:parse_text(map_text)
end
if not res then
rspamd_logger.warnx(rspamd_config, 'cannot parse settings map: ' .. err)
else
if settings_map_pool then
settings_map_pool:destroy()
end
settings_map_pool = rspamd_mempool.create()
local obj = parser:get_object()
if obj['settings'] then
process_settings_table(obj['settings'], false, settings_map_pool)
else
process_settings_table(obj, false, settings_map_pool)
end
end
return res
end
local function gen_redis_callback(handler, id)
return function(task)
local key = handler(task)
local function redis_settings_cb(err, data)
if not err and type(data) == 'table' then
for _, d in ipairs(data) do
if type(d) == 'string' then
local parser = ucl.parser()
local res,ucl_err = parser:parse_string(d)
if not res then
rspamd_logger.warnx(rspamd_config, 'cannot parse settings from redis: %s',
ucl_err)
else
local obj = parser:get_object()
rspamd_logger.infox(task, "<%1> apply settings according to redis rule %2",
task:get_message_id(), id)
apply_settings(task, obj, nil)
break
end
end
end
elseif err then
rspamd_logger.errx(task, 'Redis error: %1', err)
end
end
if not key then
lua_util.debugm(N, task, 'handler number %s returned nil', id)
return
end
local keys
if type(key) == 'table' then
keys = key
else
keys = {key}
end
key = keys[1]
local ret,_,_ = rspamd_redis_make_request(task,
redis_params, -- connect params
key, -- hash key
false, -- is write
redis_settings_cb, --callback
'MGET', -- command
keys -- arguments
)
if not ret then
rspamd_logger.errx(task, 'Redis MGET failed: %s', ret)
end
end
end
local redis_section = rspamd_config:get_all_opt("settings_redis")
local redis_key_handlers = {}
if redis_section then
redis_params = rspamd_parse_redis_server('settings_redis')
if redis_params then
local handlers = redis_section.handlers
for id,h in pairs(handlers) do
local chunk,err = load(h)
if not chunk then
rspamd_logger.errx(rspamd_config, 'Cannot load handler from string: %s',
tostring(err))
else
local res,func = pcall(chunk)
if not res then
rspamd_logger.errx(rspamd_config, 'Cannot add handler from string: %s',
tostring(func))
else
redis_key_handlers[id] = func
end
end
end
end
fun.each(function(id, h)
rspamd_config:register_symbol({
name = 'REDIS_SETTINGS' .. tostring(id),
type = 'prefilter',
callback = gen_redis_callback(h, id),
priority = 10,
flags = 'empty,nostat',
})
end, redis_key_handlers)
end
module_sym_id = rspamd_config:register_symbol({
name = 'SETTINGS_CHECK',
type = 'prefilter',
callback = check_settings,
priority = 10,
flags = 'empty,nostat,explicit_disable,ignore_passthrough',
})
local set_section = rspamd_config:get_all_opt("settings")
if set_section and set_section[1] and type(set_section[1]) == "string" then
-- Just a map of ucl
local map_attrs = {
url = set_section[1],
description = "settings map",
callback = process_settings_map,
opaque_data = true
}
if not rspamd_config:add_map(map_attrs) then
rspamd_logger.errx(rspamd_config, 'cannot load settings from %1', set_section)
end
elseif set_section and type(set_section) == "table" then
settings_map_pool = rspamd_mempool.create()
-- We need to check this table and register static symbols first
-- Postponed settings init is needed to ensure that all symbols have been
-- registered BEFORE settings plugin. Otherwise, we can have inconsistent settings expressions
fun.each(function(_, elt)
if elt.apply and elt.apply.symbols then
-- Register virtual symbols
for k,v in pairs(elt.apply.symbols) do
local rtb = {
type = 'virtual',
parent = module_sym_id,
}
if type(k) == 'number' and type(v) == 'string' then
rtb.name = v
elseif type(k) == 'string' then
rtb.name = k
end
rspamd_config:register_symbol(rtb)
end
end
end,
-- Include only settings, exclude all maps
fun.filter(
function(_, elt)
if type(elt) == "table" then
return true
end
return false
end, set_section)
)
rspamd_config:add_post_init(function ()
process_settings_table(set_section, true, settings_map_pool, true)
end, 100)
end
rspamd_config:add_config_unload(function()
if settings_map_pool then
settings_map_pool:destroy()
end
end)
File Manager Version 1.0, Coded By Lucas
Email: hehe@yahoo.com