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