You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

settings.lua 40KB

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