1--[[
2Copyright (c) 2011-2015, Vsevolod Stakhov <vsevolod@highsecure.ru>
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15]]--
16
17if confighelp then
18  return
19end
20
21-- This plugin implements user dynamic settings
22-- Settings documentation can be found here:
23-- https://rspamd.com/doc/configuration/settings.html
24
25local rspamd_logger = require "rspamd_logger"
26local lua_maps = require "lua_maps"
27local lua_util = require "lua_util"
28local rspamd_ip = require "rspamd_ip"
29local rspamd_regexp = require "rspamd_regexp"
30local lua_selectors = require "lua_selectors"
31local lua_settings = require "lua_settings"
32local ucl = require "ucl"
33local fun = require "fun"
34local rspamd_mempool = require "rspamd_mempool"
35
36local redis_params
37
38local settings = {}
39local N = "settings"
40local settings_initialized = false
41local max_pri = 0
42local module_sym_id -- Main module symbol
43
44local function apply_settings(task, to_apply, id, name)
45  local cached_name = task:cache_get('settings_name')
46  if cached_name then
47    local cached_settings = task:cache_get('settings')
48    rspamd_logger.warnx(task, "cannot apply settings rule %s (id=%s):" ..
49        " settings has been already applied by rule %s (id=%s)",
50        name, id, cached_name, cached_settings.id)
51    return false
52  end
53
54  task:set_settings(to_apply)
55  task:cache_set('settings', to_apply)
56  task:cache_set('settings_name', name or 'unknown')
57
58  if id then
59    task:set_settings_id(id)
60  end
61
62  if to_apply['add_headers'] or to_apply['remove_headers'] then
63    local rep = {
64      add_headers = to_apply['add_headers'] or {},
65      remove_headers = to_apply['remove_headers'] or {},
66    }
67    task:set_rmilter_reply(rep)
68  end
69
70  if to_apply.flags and type(to_apply.flags) == 'table' then
71    for _,fl in ipairs(to_apply.flags) do
72      task:set_flag(fl)
73    end
74  end
75
76  if to_apply.symbols then
77    -- Add symbols, specified in the settings
78    if #to_apply.symbols > 0 then
79      -- Array like symbols
80      for _,val in ipairs(to_apply.symbols) do
81        task:insert_result(val, 1.0)
82      end
83    else
84      -- Object like symbols
85      for k,v in pairs(to_apply.symbols) do
86        if type(v) == 'table' then
87          task:insert_result(k, v.score or 1.0, v.options or {})
88        elseif tonumber(v) then
89          task:insert_result(k, tonumber(v))
90        end
91      end
92    end
93  end
94
95  if to_apply.subject then
96    task:set_metric_subject(to_apply.subject)
97  end
98
99  -- E.g.
100  -- messages = { smtp_message = "5.3.1 Go away" }
101  if to_apply.messages and type(to_apply.messages) == 'table' then
102    fun.each(function(category, message)
103      task:append_message(message, category)
104    end, to_apply.messages)
105  end
106
107  return true
108end
109
110-- Checks for overridden settings within query params and returns 3 values:
111-- * Apply element
112-- * Settings ID element if found
113-- * Priority of the settings according to the place where it is found
114--
115-- If no override has been found, it returns `false`
116local function check_query_settings(task)
117  -- Try 'settings' attribute
118  local settings_id = task:get_settings_id()
119  local query_set = task:get_request_header('settings')
120  if query_set then
121
122    local parser = ucl.parser()
123    local res,err = parser:parse_string(tostring(query_set))
124    if res then
125      if settings_id then
126        rspamd_logger.warnx(task, "both settings-id '%s' and settings headers are presented, ignore settings-id; ",
127            tostring(settings_id))
128      end
129      local settings_obj = parser:get_object()
130
131      -- Treat as low priority
132      return settings_obj,nil,1
133    else
134      rspamd_logger.errx(task, 'Parse error: %s', err)
135    end
136  end
137
138  local query_maxscore = task:get_request_header('maxscore')
139  local nset
140
141  if query_maxscore then
142    if settings_id then
143      rspamd_logger.infox(task, "both settings id '%s' and maxscore '%s' headers are presented, merge them; " ..
144        "settings id has priority",
145        tostring(settings_id), tostring(query_maxscore))
146    end
147    -- We have score limits redefined by request
148    local ms = tonumber(tostring(query_maxscore))
149    if ms then
150      nset = {
151        actions = {
152          reject = ms
153        }
154      }
155
156      local query_softscore = task:get_request_header('softscore')
157      if query_softscore then
158        local ss = tonumber(tostring(query_softscore))
159        nset.actions['add header'] = ss
160      end
161
162      if not settings_id then
163        rspamd_logger.infox(task, 'apply maxscore = %s', nset.actions)
164        -- Maxscore is low priority
165        return nset, nil, 1
166      end
167    end
168  end
169
170  if settings_id and settings_initialized then
171    local cached = lua_settings.settings_by_id(settings_id)
172
173    if cached then
174      local elt = cached.settings
175      if elt['whitelist'] then
176        elt['apply'] = {whitelist = true}
177      end
178
179      if elt.apply then
180        if nset then
181          elt.apply = lua_util.override_defaults(nset, elt.apply)
182        end
183        return elt.apply, cached, cached.priority or 1
184      end
185    else
186      rspamd_logger.warnx(task, 'no settings id "%s" has been found', settings_id)
187      if nset then
188        rspamd_logger.infox(task, 'apply maxscore = %s', nset.actions)
189        return nset, nil, 1
190      end
191    end
192  else
193    if nset then
194      rspamd_logger.infox(task, 'apply maxscore = %s', nset.actions)
195      return nset, nil, 1
196    end
197  end
198
199  return false
200end
201
202local function check_addr_setting(expected, addr)
203  local function check_specific_addr(elt)
204    if expected.name then
205      if lua_maps.rspamd_maybe_check_map(expected.name, elt.addr) then
206        return true
207      end
208    end
209    if expected.user then
210      if lua_maps.rspamd_maybe_check_map(expected.user, elt.user) then
211        return true
212      end
213    end
214    if expected.domain and elt.domain then
215      if lua_maps.rspamd_maybe_check_map(expected.domain, elt.domain) then
216        return true
217      end
218    end
219    if expected.regexp then
220      if expected.regexp:match(elt.addr) then
221        return true
222      end
223    end
224    return false
225  end
226
227  for _, e in ipairs(addr) do
228    if check_specific_addr(e) then
229      return true
230    end
231  end
232
233  return false
234end
235
236local function check_string_setting(expected, str)
237  if expected.regexp then
238    if expected.regexp:match(str) then
239      return true
240    end
241  elseif expected.check then
242    if lua_maps.rspamd_maybe_check_map(expected.check, str) then
243      return true
244    end
245  end
246  return false
247end
248
249local function check_ip_setting(expected, ip)
250  if not expected[2] then
251    if lua_maps.rspamd_maybe_check_map(expected[1], ip:to_string()) then
252      return true
253    end
254  else
255    if expected[2] ~= 0 then
256      local nip = ip:apply_mask(expected[2])
257      if nip and nip:to_string() == expected[1]:to_string() then
258        return true
259      end
260    elseif ip:to_string() == expected[1]:to_string() then
261      return true
262    end
263  end
264
265  return false
266end
267
268local function check_map_setting(map, input)
269  return map:get_key(input)
270end
271
272local function priority_to_string(pri)
273  if pri then
274    if pri >= 3 then
275      return "high"
276    elseif pri >= 2 then
277      return "medium"
278    end
279  end
280
281  return "low"
282end
283
284-- Check limit for a task
285local function check_settings(task)
286  local function check_specific_setting(rule, matched)
287    local res = false
288
289    local function process_atom(atom)
290      local elt = rule.checks[atom]
291
292      if elt then
293        local input = elt.extract(task)
294        if not input then return false end
295
296        if elt.check(input) then
297          matched[#matched + 1] = atom
298          return 1.0
299        end
300      else
301        rspamd_logger.errx(task, 'error in settings: check %s is not defined!', atom)
302      end
303
304      return 0
305    end
306
307    res = rule.expression and rule.expression:process(process_atom)
308
309    if res and res > 0 then
310      if rule['whitelist'] then
311        rule['apply'] = {whitelist = true}
312      end
313
314      return rule
315    end
316
317    return nil
318  end
319
320  -- Check if we have override as query argument
321  local query_apply,id_elt,priority = check_query_settings(task)
322
323  local function maybe_apply_query_settings()
324    if query_apply then
325      if id_elt then
326        apply_settings(task, query_apply, id_elt.id, id_elt.name)
327        rspamd_logger.infox(task, "applied settings id %s(%s); priority %s",
328            id_elt.name, id_elt.id, priority_to_string(priority))
329      else
330        apply_settings(task, query_apply, nil, 'HTTP query')
331        rspamd_logger.infox(task, "applied settings from query; priority %s",
332            priority_to_string(priority))
333      end
334    end
335  end
336
337  local min_pri = 1
338  if query_apply then
339    if priority >= min_pri then
340      -- Do not check lower or equal priorities
341      min_pri = priority + 1
342    end
343
344    if priority > max_pri then
345      -- Our internal priorities are lower then a priority from query, so no need to check
346      maybe_apply_query_settings()
347
348      return
349    end
350  end
351
352  -- Do not waste resources
353  if not settings_initialized then
354    maybe_apply_query_settings()
355    return
356  end
357
358  -- Match rules according their order
359  local applied = false
360
361  for pri = max_pri,min_pri,-1 do
362    if not applied and settings[pri] then
363      for _,s in ipairs(settings[pri]) do
364        local matched = {}
365
366        lua_util.debugm(N, task, "check for settings element %s",
367            s.name)
368        local result = check_specific_setting(s.rule, matched)
369        -- Can use xor here but more complicated for reading
370        if result then
371          if s.rule['apply'] then
372            if s.rule.id then
373              -- Extract static settings
374              local cached = lua_settings.settings_by_id(s.rule.id)
375
376              if not cached or not cached.settings or not cached.settings.apply then
377                rspamd_logger.errx(task, 'unregistered settings id found: %s!', s.rule.id)
378              else
379                rspamd_logger.infox(task, "<%s> apply static settings %s (id = %s); %s matched; priority %s",
380                    task:get_message_id(),
381                    cached.name, s.rule.id,
382                    table.concat(matched, ','),
383                    priority_to_string(pri))
384                apply_settings(task, cached.settings.apply, s.rule.id, s.name)
385              end
386
387            else
388              -- Dynamic settings
389              rspamd_logger.infox(task, "<%s> apply settings according to rule %s (%s matched)",
390                  task:get_message_id(), s.name, table.concat(matched, ','))
391              apply_settings(task, s.rule.apply, nil, s.name)
392            end
393
394            applied = true
395          end
396          if s.rule['symbols'] then
397            -- Add symbols, specified in the settings
398            fun.each(function(val)
399              task:insert_result(val, 1.0)
400            end, s.rule['symbols'])
401          end
402        end
403      end
404    end
405  end
406
407  if not applied then
408    maybe_apply_query_settings()
409  end
410
411end
412
413local function convert_to_table(chk_elt, out)
414  if type(chk_elt) == 'string' then
415    return {out}
416  end
417
418  return out
419end
420
421-- Process IP address: converted to a table {ip, mask}
422local function process_ip_condition(ip)
423  local out = {}
424
425  if type(ip) == "table" then
426    for _,v in ipairs(ip) do
427      table.insert(out, process_ip_condition(v))
428    end
429  elseif type(ip) == "string" then
430    local slash = string.find(ip, '/')
431
432    if not slash then
433      -- Just a plain IP address
434      local res = rspamd_ip.from_string(ip)
435
436      if res:is_valid() then
437        out[1] = res
438        out[2] = 0
439      else
440        -- It can still be a map
441        out[1] = res
442      end
443    else
444      local res = rspamd_ip.from_string(string.sub(ip, 1, slash - 1))
445      local mask = tonumber(string.sub(ip, slash + 1))
446
447      if res:is_valid() then
448        out[1] = res
449        out[2] = mask
450      else
451        rspamd_logger.errx(rspamd_config, "bad IP address: " .. ip)
452        return nil
453      end
454    end
455  else
456    return nil
457  end
458
459  return out
460end
461
462-- Process email like condition, converted to a table with fields:
463-- name - full email (surprise!)
464-- user - user part
465-- domain - domain part
466-- regexp - full email regexp (yes, it sucks)
467local function process_email_condition(addr)
468  local out = {}
469  if type(addr) == "table" then
470    for _,v in ipairs(addr) do
471      table.insert(out, process_email_condition(v))
472    end
473  elseif type(addr) == "string" then
474    if string.sub(addr, 1, 4) == "map:" then
475      -- It is map, don't apply any extra logic
476      out['name'] = addr
477    else
478      local start = string.sub(addr, 1, 1)
479      if start == '/' then
480        -- It is a regexp
481        local re = rspamd_regexp.create(addr)
482        if re then
483          out['regexp'] = re
484        else
485          rspamd_logger.errx(rspamd_config, "bad regexp: " .. addr)
486          return nil
487        end
488
489      elseif start == '@' then
490        -- It is a domain if form @domain
491        out['domain'] = string.sub(addr, 2)
492      else
493        -- Check user@domain parts
494        local at = string.find(addr, '@')
495        if at then
496          -- It is full address
497          out['name'] = addr
498        else
499          -- It is a user
500          out['user'] = addr
501        end
502      end
503    end
504  else
505    return nil
506  end
507
508  return out
509end
510
511-- Convert a plain string condition to a table:
512-- check - string to match
513-- regexp - regexp to match
514local function process_string_condition(addr)
515  local out = {}
516  if type(addr) == "table" then
517    for _,v in ipairs(addr) do
518      table.insert(out, process_string_condition(v))
519    end
520  elseif type(addr) == "string" then
521    if string.sub(addr, 1, 4) == "map:" then
522      -- It is map, don't apply any extra logic
523      out['check'] = addr
524    else
525      local start = string.sub(addr, 1, 1)
526      if start == '/' then
527        -- It is a regexp
528        local re = rspamd_regexp.create(addr)
529        if re then
530          out['regexp'] = re
531        else
532          rspamd_logger.errx(rspamd_config, "bad regexp: " .. addr)
533          return nil
534        end
535
536      else
537        out['check'] = addr
538      end
539    end
540  else
541    return nil
542  end
543
544  return out
545end
546
547local function get_priority (elt)
548  local pri_tonum = function(p)
549    if p then
550      if type(p) == "number" then
551        return tonumber(p)
552      elseif type(p) == "string" then
553        if p == "high" then
554          return 3
555        elseif p == "medium" then
556          return 2
557        end
558
559      end
560
561    end
562
563    return 1
564  end
565
566  return pri_tonum(elt['priority'])
567end
568
569-- Used to create a checking closure: if value matches expected somehow, return true
570local function gen_check_closure(expected, check_func)
571  return function(value)
572    if not value then return false end
573
574    if type(value) == 'function' then
575      value = value()
576    end
577
578    if value then
579
580      if not check_func then
581        check_func = function(a, b) return a == b end
582      end
583
584      local ret
585      if type(expected) == 'table' then
586        ret = fun.any(function(d)
587          return check_func(d, value)
588        end, expected)
589      else
590        ret = check_func(expected, value)
591      end
592      if ret then
593        return true
594      end
595    end
596
597    return false
598  end
599end
600
601-- Process settings based on their priority
602local function process_settings_table(tbl, allow_ids, mempool, is_static)
603
604  -- Check the setting element internal data
605  local process_setting_elt = function(name, elt)
606
607    lua_util.debugm(N, rspamd_config, 'process settings "%s"', name)
608
609    local out = {}
610
611    local checks = {}
612    if elt.ip then
613      local ips_table = process_ip_condition(elt['ip'])
614
615      if ips_table then
616        lua_util.debugm(N, rspamd_config, 'added ip condition to "%s": %s',
617            name, ips_table)
618        checks.ip = {
619          check = gen_check_closure(convert_to_table(elt.ip, ips_table), check_ip_setting),
620          extract = function(task)
621            local ip = task:get_from_ip()
622            if ip and ip:is_valid() then return ip end
623            return nil
624          end,
625        }
626      end
627    end
628    if elt.ip_map then
629      local ips_map = lua_maps.map_add_from_ucl(elt.ip_map, 'radix',
630          'settings ip map for ' .. name)
631
632      if ips_map then
633        lua_util.debugm(N, rspamd_config, 'added ip_map condition to "%s"',
634            name)
635        checks.ip_map = {
636          check = gen_check_closure(ips_map, check_map_setting),
637          extract = function(task)
638            local ip = task:get_from_ip()
639            if ip and ip:is_valid() then return ip end
640            return nil
641          end,
642        }
643      end
644    end
645
646    if elt.client_ip then
647      local client_ips_table = process_ip_condition(elt.client_ip)
648
649      if client_ips_table then
650        lua_util.debugm(N, rspamd_config, 'added client_ip condition to "%s": %s',
651            name, client_ips_table)
652        checks.client_ip = {
653          check = gen_check_closure(convert_to_table(elt.client_ip, client_ips_table),
654              check_ip_setting),
655          extract = function(task)
656            local ip = task:get_client_ip()
657            if ip:is_valid() then return ip end
658            return nil
659          end,
660        }
661      end
662    end
663    if elt.client_ip_map then
664      local ips_map = lua_maps.map_add_from_ucl(elt.ip_map, 'radix',
665          'settings client ip map for ' .. name)
666
667      if ips_map then
668        lua_util.debugm(N, rspamd_config, 'added client ip_map condition to "%s"',
669            name)
670        checks.client_ip_map = {
671          check = gen_check_closure(ips_map, check_map_setting),
672          extract = function(task)
673            local ip = task:get_client_ip()
674            if ip and ip:is_valid() then return ip end
675            return nil
676          end,
677        }
678      end
679    end
680
681    if elt.from then
682      local from_condition = process_email_condition(elt.from)
683
684      if from_condition then
685        lua_util.debugm(N, rspamd_config, 'added from condition to "%s": %s',
686            name, from_condition)
687        checks.from = {
688          check = gen_check_closure(convert_to_table(elt.from, from_condition),
689              check_addr_setting),
690          extract = function(task)
691            return task:get_from(1)
692          end,
693        }
694      end
695    end
696
697    if elt.rcpt then
698      local rcpt_condition = process_email_condition(elt.rcpt)
699      if rcpt_condition then
700        lua_util.debugm(N, rspamd_config, 'added rcpt condition to "%s": %s',
701            name, rcpt_condition)
702        checks.rcpt = {
703          check = gen_check_closure(convert_to_table(elt.rcpt, rcpt_condition),
704              check_addr_setting),
705          extract = function(task)
706            return task:get_recipients(1)
707          end,
708        }
709      end
710    end
711
712    if elt.from_mime then
713      local from_mime_condition = process_email_condition(elt.from_mime)
714
715      if from_mime_condition then
716        lua_util.debugm(N, rspamd_config, 'added from_mime condition to "%s": %s',
717            name, from_mime_condition)
718        checks.from_mime = {
719          check = gen_check_closure(convert_to_table(elt.from_mime, from_mime_condition),
720              check_addr_setting),
721          extract = function(task)
722            return task:get_from(2)
723          end,
724        }
725      end
726    end
727
728    if elt.rcpt_mime then
729      local rcpt_mime_condition = process_email_condition(elt.rcpt_mime)
730      if rcpt_mime_condition then
731        lua_util.debugm(N, rspamd_config, 'added rcpt mime condition to "%s": %s',
732            name, rcpt_mime_condition)
733        checks.rcpt_mime = {
734          check = gen_check_closure(convert_to_table(elt.rcpt_mime, rcpt_mime_condition),
735              check_addr_setting),
736          extract = function(task)
737            return task:get_recipients(2)
738          end,
739        }
740      end
741    end
742
743    if elt.user then
744      local user_condition = process_email_condition(elt.user)
745      if user_condition then
746        lua_util.debugm(N, rspamd_config, 'added user condition to "%s": %s',
747            name, user_condition)
748        checks.user = {
749          check = gen_check_closure(convert_to_table(elt.user, user_condition),
750              check_addr_setting),
751          extract = function(task)
752            local uname = task:get_user()
753            local user = {}
754            if uname then
755              user[1] = {}
756              local localpart, domainpart = string.gmatch(uname, "(.+)@(.+)")()
757              if localpart then
758                user[1]["user"] = localpart
759                user[1]["domain"] = domainpart
760                user[1]["addr"] = uname
761              else
762                user[1]["user"] = uname
763                user[1]["addr"] = uname
764              end
765
766              return user
767            end
768
769            return nil
770          end,
771        }
772      end
773    end
774
775    if elt.hostname then
776      local hostname_condition = process_string_condition(elt.hostname)
777      if hostname_condition then
778        lua_util.debugm(N, rspamd_config, 'added hostname condition to "%s": %s',
779            name, hostname_condition)
780        checks.hostname = {
781          check = gen_check_closure(convert_to_table(elt.hostname, hostname_condition),
782              check_string_setting),
783          extract = function(task)
784            return task:get_hostname() or ''
785          end,
786        }
787      end
788    end
789
790    if elt.authenticated then
791      lua_util.debugm(N, rspamd_config, 'added authenticated condition to "%s"',
792          name)
793      checks.authenticated = {
794        check = function(value) if value then return true end return false end,
795        extract = function(task)
796          return task:get_user()
797        end
798      }
799    end
800
801    if elt['local'] then
802      lua_util.debugm(N, rspamd_config, 'added local condition to "%s"',
803          name)
804      checks['local'] = {
805        check = function(value) if value then return true end return false end,
806        extract = function(task)
807          local ip = task:get_from_ip()
808          if not ip or not ip:is_valid() then
809            return nil
810          end
811
812          if ip:is_local() then
813            return true
814          else
815            return nil
816          end
817        end
818      }
819    end
820
821    local aliases = {}
822    -- This function is used to convert compound condition with
823    -- generic type and specific part (e.g. `header`, `Content-Transfer-Encoding`)
824    -- to a set of usable check elements:
825    -- `generic:specific` - most common part
826    -- `generic:<order>` - e.g. `header:1` for the first header
827    -- `generic:safe` - replace unsafe stuff with safe + lowercase
828    -- also aliases entry is set to avoid implicit expression
829    local function process_compound_condition(cond, generic, specific)
830      local full_key = generic .. ':' .. specific
831      checks[full_key] = cond
832
833      -- Try numeric key
834      for i=1,1000 do
835        local num_key = generic .. ':' .. tostring(i)
836        if not checks[num_key]  then
837          checks[num_key] = cond
838          aliases[num_key] = true
839          break
840        end
841      end
842
843      local safe_key = generic .. ':' ..
844          specific:gsub('[:%-+&|><]', '_')
845                  :gsub('%(', '[')
846                  :gsub('%)', ']')
847                  :lower()
848
849      if not checks[safe_key] then
850        checks[safe_key] = cond
851        aliases[safe_key] = true
852      end
853
854      return safe_key
855    end
856    -- Headers are tricky:
857    -- We create an closure with extraction function depending on header name
858    -- We also inserts it into `checks` table as an atom in form header:<hname>
859    -- Check function depends on the input:
860    -- * for something that looks like `header = "/bar/"` we create a regexp
861    -- * for something that looks like `header = true` we just check the existence
862    local function process_header_elt(table_element, extractor_func)
863      if elt[table_element] then
864        for k, v in pairs(elt[table_element]) do
865          if type(v) == 'string' then
866            local re = rspamd_regexp.create(v)
867            if re then
868              local cond = {
869                check = function(values)
870                  return fun.any(function(c) return re:match(c) end, values)
871                end,
872                extract = extractor_func(k),
873              }
874              local skey = process_compound_condition(cond, table_element,
875                  k)
876              lua_util.debugm(N, rspamd_config, 'added %s condition to "%s": %s =~ %s',
877                  skey, name, k, v)
878            end
879          elseif type(v) == 'boolean' then
880            local cond = {
881              check = function(values)
882                if #values == 0 then return (not v) end
883                return v
884              end,
885              extract = extractor_func(k),
886            }
887
888            local skey = process_compound_condition(cond, table_element,
889                k)
890            lua_util.debugm(N, rspamd_config, 'added %s condition to "%s": %s == %s',
891                skey, name, k, v)
892          else
893            rspamd_logger.errx(rspamd_config, 'invalid %s %s = %s', table_element, k, v)
894          end
895        end
896      end
897    end
898
899    process_header_elt('request_header', function(hname)
900      return function(task)
901        local rh = task:get_request_header(hname)
902        if rh then return {rh} end
903        return {}
904      end
905    end)
906    process_header_elt('header', function(hname)
907      return function(task)
908        local rh = task:get_header_full(hname)
909        if rh then
910          return fun.totable(fun.map(function(h) return h.decoded end, rh))
911        end
912        return {}
913      end
914    end)
915
916    if elt.selector then
917      local sel = lua_selectors.create_selector_closure(rspamd_config, elt.selector,
918          elt.delimiter or "")
919
920      if sel then
921        local cond = {
922          check = function(values)
923            return fun.any(function(c)
924              return c
925            end, values)
926          end,
927          extract = sel,
928        }
929        local skey = process_compound_condition(cond, 'selector', elt.selector)
930        lua_util.debugm(N, rspamd_config, 'added selector condition to "%s": %s',
931            name, skey)
932      end
933
934    end
935
936    -- Special, special case!
937    local inverse = false
938    if elt.inverse then
939      lua_util.debugm(N, rspamd_config, 'added inverse condition to "%s"',
940          name)
941      inverse = true
942    end
943
944    -- Count checks and create Rspamd expression from a set of rules
945    local nchecks = 0
946    for _,_ in pairs(checks) do nchecks = nchecks + 1 end
947
948    if nchecks > 0 then
949      -- Now we can deal with the expression!
950      if not elt.expression then
951        -- Artificial & expression to deal with the legacy parts
952        -- Here we get all keys and concatenate them with '&&'
953        local s = ' && '
954        -- By De Morgan laws
955        if inverse then s = ' || ' end
956        -- Exclude aliases and join all checks by key
957        local expr_str = table.concat(lua_util.keys(fun.filter(
958            function(k, _) return not aliases[k] end,
959            checks)), s)
960
961        if inverse then
962          expr_str = string.format('!(%s)', expr_str)
963        end
964
965        elt.expression = expr_str
966        lua_util.debugm(N, rspamd_config, 'added implicit settings expression for %s: %s',
967            name, expr_str)
968      end
969
970      -- Parse expression's sanity
971      local function parse_atom(str)
972        local atom = table.concat(fun.totable(fun.take_while(function(c)
973          if string.find(', \t()><+!|&\n', c) then
974            return false
975          end
976          return true
977        end, fun.iter(str))), '')
978
979        if checks[atom] then
980          return atom
981        end
982
983        rspamd_logger.errx(rspamd_config,
984            'use of undefined element "%s" when parsing settings expression, known checks: %s',
985            atom, table.concat(fun.totable(fun.map(function(k, _) return k end, checks)), ','))
986
987        return nil
988      end
989
990      local rspamd_expression = require "rspamd_expression"
991      out.expression = rspamd_expression.create(elt.expression, parse_atom,
992          mempool)
993      out.checks = checks
994
995      if not out.expression then
996        rspamd_logger.errx(rspamd_config, 'cannot parse expression %s for %s',
997            elt.expression, name)
998      else
999        lua_util.debugm(N, rspamd_config, 'registered settings %s with %s checks',
1000            name, nchecks)
1001      end
1002    else
1003      lua_util.debugm(N, rspamd_config, 'registered settings %s with no checks',
1004          name)
1005    end
1006
1007    -- Process symbols part/apply part
1008    if elt['symbols'] then
1009      lua_util.debugm(N, rspamd_config, 'added symbols condition to "%s": %s',
1010          name, elt.symbols)
1011      out['symbols'] = elt['symbols']
1012    end
1013
1014
1015    if elt['apply'] then
1016      -- Just insert all metric results to the action key
1017      out['apply'] = elt['apply']
1018    elseif elt['whitelist'] or elt['want_spam'] then
1019      out['whitelist'] = true
1020    else
1021      rspamd_logger.errx(rspamd_config, "no actions in settings: " .. name)
1022      return nil
1023    end
1024
1025    if allow_ids then
1026      if not elt.id then
1027        elt.id = name
1028      end
1029
1030      if elt['id'] then
1031        -- We are here from a postload script
1032        out.id = lua_settings.register_settings_id(elt.id, out, true)
1033        lua_util.debugm(N, rspamd_config,
1034            'added settings id to "%s": %s -> %s',
1035            name, elt.id, out.id)
1036      end
1037
1038      if not is_static then
1039        -- If we apply that from map
1040        -- In fact, it is useless and evil but who cares...
1041        if elt.apply and elt.apply.symbols then
1042          -- Register virtual symbols
1043          for k,v in pairs(elt.apply.symbols) do
1044            local rtb = {
1045              type = 'virtual',
1046              parent = module_sym_id,
1047            }
1048            if type(k) == 'number' and type(v) == 'string' then
1049              rtb.name = v
1050            elseif type(k) == 'string' then
1051              rtb.name = k
1052            end
1053            if out.id then
1054              rtb.allowed_ids = tostring(elt.id)
1055            end
1056            rspamd_config:register_symbol(rtb)
1057          end
1058        end
1059      end
1060    else
1061      if elt['id'] then
1062        rspamd_logger.errx(rspamd_config,
1063            'cannot set static IDs from dynamic settings, please read the docs')
1064      end
1065    end
1066
1067    return out
1068  end
1069
1070  settings_initialized = false
1071  -- filter trash in the input
1072  local ft = fun.filter(
1073    function(_, elt)
1074      if type(elt) == "table" then
1075        return true
1076      end
1077      return false
1078    end, tbl)
1079
1080  -- clear all settings
1081  max_pri = 0
1082  local nrules = 0
1083  for k in pairs(settings) do settings[k]={} end
1084  -- fill new settings by priority
1085  fun.for_each(function(k, v)
1086    local pri = get_priority(v)
1087    if pri > max_pri then max_pri = pri end
1088    if not settings[pri] then
1089      settings[pri] = {}
1090    end
1091    local s = process_setting_elt(k, v)
1092    if s then
1093      table.insert(settings[pri], {name = k, rule = s})
1094      nrules = nrules + 1
1095    end
1096  end, ft)
1097  -- sort settings with equal priorities in alphabetical order
1098  for pri,_ in pairs(settings) do
1099    table.sort(settings[pri], function(a,b) return a.name < b.name end)
1100  end
1101
1102  settings_initialized = true
1103  lua_settings.load_all_settings(true)
1104  rspamd_logger.infox(rspamd_config, 'loaded %1 elements of settings', nrules)
1105
1106  return true
1107end
1108
1109-- Parse settings map from the ucl line
1110local settings_map_pool
1111local function process_settings_map(map_text)
1112  local parser = ucl.parser()
1113  local res,err
1114
1115  if type(map_text) == 'string' then
1116    res,err = parser:parse_string(map_text)
1117  else
1118    res,err = parser:parse_text(map_text)
1119  end
1120
1121  if not res then
1122    rspamd_logger.warnx(rspamd_config, 'cannot parse settings map: ' .. err)
1123  else
1124    if settings_map_pool then
1125      settings_map_pool:destroy()
1126    end
1127
1128    settings_map_pool = rspamd_mempool.create()
1129    local obj = parser:get_object()
1130    if obj['settings'] then
1131      process_settings_table(obj['settings'], false,
1132          settings_map_pool, false)
1133    else
1134      process_settings_table(obj, false, settings_map_pool,
1135          false)
1136    end
1137  end
1138
1139  return res
1140end
1141
1142local function gen_redis_callback(handler, id)
1143  return function(task)
1144    local key = handler(task)
1145
1146    local function redis_settings_cb(err, data)
1147      if not err and type(data) == 'table' then
1148        for _, d in ipairs(data) do
1149          if type(d) == 'string' then
1150            local parser = ucl.parser()
1151            local res,ucl_err = parser:parse_string(d)
1152            if not res then
1153              rspamd_logger.warnx(rspamd_config, 'cannot parse settings from redis: %s',
1154                ucl_err)
1155            else
1156              local obj = parser:get_object()
1157              rspamd_logger.infox(task, "<%1> apply settings according to redis rule %2",
1158                task:get_message_id(), id)
1159              apply_settings(task, obj, nil, 'redis')
1160              break
1161            end
1162          end
1163        end
1164      elseif err then
1165        rspamd_logger.errx(task, 'Redis error: %1', err)
1166      end
1167    end
1168
1169    if not key then
1170      lua_util.debugm(N, task, 'handler number %s returned nil', id)
1171      return
1172    end
1173
1174    local keys
1175    if type(key) == 'table' then
1176      keys = key
1177    else
1178      keys = {key}
1179    end
1180    key = keys[1]
1181
1182    local ret,_,_ = rspamd_redis_make_request(task,
1183      redis_params, -- connect params
1184      key, -- hash key
1185      false, -- is write
1186      redis_settings_cb, --callback
1187      'MGET', -- command
1188      keys -- arguments
1189    )
1190    if not ret then
1191      rspamd_logger.errx(task, 'Redis MGET failed: %s', ret)
1192    end
1193  end
1194end
1195
1196local redis_section = rspamd_config:get_all_opt("settings_redis")
1197local redis_key_handlers = {}
1198
1199if redis_section then
1200  redis_params = rspamd_parse_redis_server('settings_redis')
1201  if redis_params then
1202    local handlers = redis_section.handlers
1203
1204    for id,h in pairs(handlers) do
1205      local chunk,err = load(h)
1206
1207      if not chunk then
1208        rspamd_logger.errx(rspamd_config, 'Cannot load handler from string: %s',
1209            tostring(err))
1210      else
1211        local res,func = pcall(chunk)
1212        if not res then
1213          rspamd_logger.errx(rspamd_config, 'Cannot add handler from string: %s',
1214            tostring(func))
1215        else
1216          redis_key_handlers[id] = func
1217        end
1218      end
1219    end
1220  end
1221
1222  fun.each(function(id, h)
1223    rspamd_config:register_symbol({
1224      name = 'REDIS_SETTINGS' .. tostring(id),
1225      type = 'prefilter',
1226      callback = gen_redis_callback(h, id),
1227      priority = 10,
1228      flags = 'empty,nostat',
1229    })
1230  end, redis_key_handlers)
1231end
1232
1233module_sym_id = rspamd_config:register_symbol({
1234  name = 'SETTINGS_CHECK',
1235  type = 'prefilter',
1236  callback = check_settings,
1237  priority = 10,
1238  flags = 'empty,nostat,explicit_disable,ignore_passthrough',
1239})
1240
1241local set_section = rspamd_config:get_all_opt("settings")
1242
1243if set_section and set_section[1] and type(set_section[1]) == "string" then
1244  -- Just a map of ucl
1245  local map_attrs = {
1246    url = set_section[1],
1247    description = "settings map",
1248    callback = process_settings_map,
1249    opaque_data = true
1250  }
1251  if not rspamd_config:add_map(map_attrs) then
1252    rspamd_logger.errx(rspamd_config, 'cannot load settings from %1', set_section)
1253  end
1254elseif set_section and type(set_section) == "table" then
1255  settings_map_pool = rspamd_mempool.create()
1256  -- We need to check this table and register static symbols first
1257  -- Postponed settings init is needed to ensure that all symbols have been
1258  -- registered BEFORE settings plugin. Otherwise, we can have inconsistent settings expressions
1259  fun.each(function(_, elt)
1260    if elt.apply and elt.apply.symbols then
1261      -- Register virtual symbols
1262      for k,v in pairs(elt.apply.symbols) do
1263        local rtb = {
1264          type = 'virtual',
1265          parent = module_sym_id,
1266        }
1267        if type(k) == 'number' and type(v) == 'string' then
1268          rtb.name = v
1269        elseif type(k) == 'string' then
1270          rtb.name = k
1271        end
1272        rspamd_config:register_symbol(rtb)
1273      end
1274    end
1275  end,
1276      -- Include only settings, exclude all maps
1277      fun.filter(
1278          function(_, elt)
1279            if type(elt) == "table" then
1280              return true
1281            end
1282            return false
1283          end, set_section)
1284  )
1285  rspamd_config:add_post_init(function ()
1286    process_settings_table(set_section, true, settings_map_pool, true)
1287  end, 100)
1288end
1289
1290rspamd_config:add_config_unload(function()
1291  if settings_map_pool then
1292    settings_map_pool:destroy()
1293  end
1294end)
1295