123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437 |
- --[[
- 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.
- ]]--
-
- 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 lua_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, name)
- local cached_name = task:cache_get('settings_name')
- if cached_name then
- local cached_settings = task:cache_get('settings')
- rspamd_logger.warnx(task, "cannot apply settings rule %s (id=%s):" ..
- " settings has been already applied by rule %s (id=%s)",
- name, id, cached_name, cached_settings.id)
- return false
- end
-
- task:set_settings(to_apply)
- task:cache_set('settings', to_apply)
- task:cache_set('settings_name', name or 'unknown')
-
- 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
-
- return true
- 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_text(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)
- lua_util.debugm(N, task, "check settings id for %s", 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
- end
- return elt.apply, cached, cached.priority or 1
- 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 lua_maps.rspamd_maybe_check_map(expected.name, elt.addr) then
- return true
- end
- end
- if expected.user then
- if lua_maps.rspamd_maybe_check_map(expected.user, elt.user) then
- return true
- end
- end
- if expected.domain and elt.domain then
- if lua_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 lua_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 lua_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] then
- return true
- end
- elseif ip:to_string() == expected[1] then
- return true
- end
- end
-
- return false
- end
-
- local function check_map_setting(map, input)
- return map:get_key(input)
- 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 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
-
- local res = rule.expression and rule.expression:process(process_atom) or rule.implicit
-
- 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, id_elt.name)
- 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, 'HTTP query')
- 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
- elseif id_elt and type(id_elt.settings) == 'table' and id_elt.settings.external_map then
- local external_map = id_elt.settings.external_map
- local selector_result = external_map.selector(task)
-
- if selector_result then
- external_map.map:get_key(selector_result, nil, task)
- -- No more selection logic
- return
- else
- rspamd_logger.infox("cannot query selector to make external map request")
- 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 = {}
-
- local result = check_specific_setting(s.rule, matched)
- lua_util.debugm(N, task, "check for settings element %s; result = %s",
- s.name, result)
- -- 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, s.name)
- 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, s.name)
- end
-
- applied = true
- elseif s.rule.external_map then
- local external_map = s.rule.external_map
- local selector_result = external_map.selector(task)
-
- if selector_result then
- external_map.map:get_key(selector_result, nil, task)
- -- No more selection logic
- return
- else
- rspamd_logger.infox("cannot query selector to make external map request")
- end
- 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
-
- local function convert_to_table(chk_elt, out)
- if type(chk_elt) == 'string' then
- return { out }
- end
-
- return out
- end
-
- local function gen_settings_external_cb(name)
- return function(result, err_or_data, code, task)
- if result then
- local parser = ucl.parser()
-
- local res, ucl_err = parser:parse_text(err_or_data)
- if not res then
- rspamd_logger.warnx(task, 'cannot parse settings from the external map %s: %s',
- name, ucl_err)
- else
- local obj = parser:get_object()
- rspamd_logger.infox(task, "<%s> apply settings according to the external map %s",
- name, task:get_message_id())
- apply_settings(task, obj, nil, 'external_map')
- end
- else
- rspamd_logger.infox(task, "<%s> no settings returned from the external map %s: %s (code = %s)",
- task:get_message_id(), name, err_or_data, code)
- end
- end
- end
-
- -- 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:to_string()
- out[2] = 0
- else
- -- It can still be a map
- out[1] = ip
- 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:to_string()
- 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 function get_priority (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
-
- -- 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
- if type(expected) == 'table' then
- ret = fun.any(function(d)
- return check_func(d, value)
- end, expected)
- else
- ret = check_func(expected, value)
- end
- if ret then
- return true
- end
- end
-
- return false
- end
- end
-
- -- Process settings based on their priority
- local function process_settings_table(tbl, allow_ids, mempool, is_static)
-
- -- Check the setting element internal data
- local process_setting_elt = function(name, elt)
-
- lua_util.debugm(N, rspamd_config, 'process settings "%s"', name)
-
- 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.ip_map then
- local ips_map = lua_maps.map_add_from_ucl(elt.ip_map, 'radix',
- 'settings ip map for ' .. name)
-
- if ips_map then
- lua_util.debugm(N, rspamd_config, 'added ip_map condition to "%s"',
- name)
- checks.ip_map = {
- check = gen_check_closure(ips_map, check_map_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.client_ip_map then
- local ips_map = lua_maps.map_add_from_ucl(elt.ip_map, 'radix',
- 'settings client ip map for ' .. name)
-
- if ips_map then
- lua_util.debugm(N, rspamd_config, 'added client ip_map condition to "%s"',
- name)
- checks.client_ip_map = {
- check = gen_check_closure(ips_map, check_map_setting),
- extract = function(task)
- local ip = task:get_client_ip()
- if ip and 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_mime)
- 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[full_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
-
- -- Count checks and create Rspamd expression from a set of rules
- local nchecks = 0
- for k, _ in pairs(checks) do
- if not aliases[k] then
- nchecks = nchecks + 1
- end
- 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, 1, true) 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
- if not elt.disabled and elt.external_map then
- lua_util.debugm(N, rspamd_config, 'registered settings %s with no checks, assume it as implicit',
- name)
- out.implicit = 1
- end
- 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
-
- --[[
- external_map = {
- map = { ... };
- selector = "...";
- }
- --]]
- if type(elt.external_map) == 'table'
- and elt.external_map.map and elt.external_map.selector then
- local maybe_external_map = {}
- maybe_external_map.map = lua_maps.map_add_from_ucl(elt.external_map.map, "",
- string.format("External map for settings element %s", name),
- gen_settings_external_cb(name))
- maybe_external_map.selector = lua_selectors.create_selector_closure_fn(rspamd_config,
- rspamd_config, elt.external_map.selector, ";", lua_selectors.kv_table_from_pairs)
-
- if maybe_external_map.map and maybe_external_map.selector then
- rspamd_logger.infox(rspamd_config, "added external map for user's settings %s", name)
- out.external_map = maybe_external_map
- else
- local incorrect_element
- if not maybe_external_map.map then
- incorrect_element = "map definition"
- else
- incorrect_element = "selector definition"
- end
- rspamd_logger.warnx(rspamd_config, "cannot add external map for user's settings; incorrect element: %s",
- incorrect_element)
- out.external_map = nil
- end
- end
-
- if not elt.external_map then
- 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
- 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 priorities 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 %s 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 = parser:parse_text(map_text)
-
- 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, false)
- else
- process_settings_table(obj, false, settings_map_pool,
- false)
- 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_text(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, 'redis')
- 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 = lua_util.symbols_priorities.top,
- flags = 'empty,nostat',
- augmentations = { string.format("timeout=%f", redis_params.timeout or 0.0) },
- })
- end, redis_key_handlers)
- end
-
- module_sym_id = rspamd_config:register_symbol({
- name = 'SETTINGS_CHECK',
- type = 'prefilter',
- callback = check_settings,
- priority = lua_util.symbols_priorities.top,
- 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.register_symbols then
- for k, v in pairs(elt.register_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
- if type(v) == 'table' then
- for kk, vv in pairs(v) do
- -- Enrich table wih extra values
- rtb[kk] = vv
- end
- end
- end
- rspamd_config:register_symbol(rtb)
- end
- end
- 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)
|