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.

lua_util.lua 40KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543
  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. --[[[
  14. -- @module lua_util
  15. -- This module contains utility functions for working with Lua and/or Rspamd
  16. --]]
  17. local exports = {}
  18. local lpeg = require 'lpeg'
  19. local rspamd_util = require "rspamd_util"
  20. local fun = require "fun"
  21. local lupa = require "lupa"
  22. local split_grammar = {}
  23. local spaces_split_grammar
  24. local space = lpeg.S' \t\n\v\f\r'
  25. local nospace = 1 - space
  26. local ptrim = space^0 * lpeg.C((space^0 * nospace^1)^0)
  27. local match = lpeg.match
  28. lupa.configure('{%', '%}', '{=', '=}', '{#', '#}', {
  29. keep_trailing_newline = true,
  30. autoescape = false,
  31. })
  32. lupa.filters.pbkdf = function(s)
  33. local cr = require "rspamd_cryptobox"
  34. return cr.pbkdf(s)
  35. end
  36. local function rspamd_str_split(s, sep)
  37. local gr
  38. if not sep then
  39. if not spaces_split_grammar then
  40. local _sep = space
  41. local elem = lpeg.C((1 - _sep)^0)
  42. local p = lpeg.Ct(elem * (_sep * elem)^0)
  43. spaces_split_grammar = p
  44. end
  45. gr = spaces_split_grammar
  46. else
  47. gr = split_grammar[sep]
  48. if not gr then
  49. local _sep
  50. if type(sep) == 'string' then
  51. _sep = lpeg.S(sep) -- Assume set
  52. else
  53. _sep = sep -- Assume lpeg object
  54. end
  55. local elem = lpeg.C((1 - _sep)^0)
  56. local p = lpeg.Ct(elem * (_sep * elem)^0)
  57. gr = p
  58. split_grammar[sep] = gr
  59. end
  60. end
  61. return gr:match(s)
  62. end
  63. --[[[
  64. -- @function lua_util.str_split(text, delimiter)
  65. -- Splits text into a numeric table by delimiter
  66. -- @param {string} text delimited text
  67. -- @param {string} delimiter the delimiter
  68. -- @return {table} numeric table containing string parts
  69. --]]
  70. exports.rspamd_str_split = rspamd_str_split
  71. exports.str_split = rspamd_str_split
  72. local function rspamd_str_trim(s)
  73. return match(ptrim, s)
  74. end
  75. exports.rspamd_str_trim = rspamd_str_trim
  76. --[[[
  77. -- @function lua_util.str_trim(text)
  78. -- Returns a string with no trailing and leading spaces
  79. -- @param {string} text input text
  80. -- @return {string} string with no trailing and leading spaces
  81. --]]
  82. exports.str_trim = rspamd_str_trim
  83. --[[[
  84. -- @function lua_util.str_startswith(text, prefix)
  85. -- @param {string} text
  86. -- @param {string} prefix
  87. -- @return {boolean} true if text starts with the specified prefix, false otherwise
  88. --]]
  89. exports.str_startswith = function(s, prefix)
  90. return s:sub(1, prefix:len()) == prefix
  91. end
  92. --[[[
  93. -- @function lua_util.str_endswith(text, suffix)
  94. -- @param {string} text
  95. -- @param {string} suffix
  96. -- @return {boolean} true if text ends with the specified suffix, false otherwise
  97. --]]
  98. exports.str_endswith = function(s, suffix)
  99. return s:find(suffix, -suffix:len(), true) ~= nil
  100. end
  101. --[[[
  102. -- @function lua_util.round(number, decimalPlaces)
  103. -- Round number to fixed number of decimal points
  104. -- @param {number} number number to round
  105. -- @param {number} decimalPlaces number of decimal points
  106. -- @return {number} rounded number
  107. --]]
  108. -- modified version from Robert Jay Gould http://lua-users.org/wiki/SimpleRound
  109. exports.round = function(num, numDecimalPlaces)
  110. local mult = 10^(numDecimalPlaces or 0)
  111. if num >= 0 then
  112. return math.floor(num * mult + 0.5) / mult
  113. else
  114. return math.ceil(num * mult - 0.5) / mult
  115. end
  116. end
  117. --[[[
  118. -- @function lua_util.template(text, replacements)
  119. -- Replaces values in a text template
  120. -- Variable names can contain letters, numbers and underscores, are prefixed with `$` and may or not use curly braces.
  121. -- @param {string} text text containing variables
  122. -- @param {table} replacements key/value pairs for replacements
  123. -- @return {string} string containing replaced values
  124. -- @example
  125. -- local goop = lua_util.template("HELLO $FOO ${BAR}!", {['FOO'] = 'LUA', ['BAR'] = 'WORLD'})
  126. -- -- goop contains "HELLO LUA WORLD!"
  127. --]]
  128. exports.template = function(tmpl, keys)
  129. local var_lit = lpeg.P { lpeg.R("az") + lpeg.R("AZ") + lpeg.R("09") + "_" }
  130. local var = lpeg.P { (lpeg.P("$") / "") * ((var_lit^1) / keys) }
  131. local var_braced = lpeg.P { (lpeg.P("${") / "") * ((var_lit^1) / keys) * (lpeg.P("}") / "") }
  132. local template_grammar = lpeg.Cs((var + var_braced + 1)^0)
  133. return lpeg.match(template_grammar, tmpl)
  134. end
  135. local function enrich_template_with_globals(env)
  136. local newenv = exports.shallowcopy(env)
  137. newenv.paths = rspamd_paths
  138. newenv.env = rspamd_env
  139. return newenv
  140. end
  141. --[[[
  142. -- @function lua_util.jinja_template(text, env[, skip_global_env])
  143. -- Replaces values in a text template according to jinja2 syntax
  144. -- @param {string} text text containing variables
  145. -- @param {table} replacements key/value pairs for replacements
  146. -- @param {boolean} skip_global_env don't export Rspamd superglobals
  147. -- @return {string} string containing replaced values
  148. -- @example
  149. -- lua_util.jinja_template("HELLO {{FOO}} {{BAR}}!", {['FOO'] = 'LUA', ['BAR'] = 'WORLD'})
  150. -- "HELLO LUA WORLD!"
  151. --]]
  152. exports.jinja_template = function(text, env, skip_global_env)
  153. if not skip_global_env then
  154. env = enrich_template_with_globals(env)
  155. end
  156. return lupa.expand(text, env)
  157. end
  158. --[[[
  159. -- @function lua_util.jinja_file(filename, env[, skip_global_env])
  160. -- Replaces values in a text template according to jinja2 syntax
  161. -- @param {string} filename name of file to expand
  162. -- @param {table} replacements key/value pairs for replacements
  163. -- @param {boolean} skip_global_env don't export Rspamd superglobals
  164. -- @return {string} string containing replaced values
  165. -- @example
  166. -- lua_util.jinja_template("HELLO {{FOO}} {{BAR}}!", {['FOO'] = 'LUA', ['BAR'] = 'WORLD'})
  167. -- "HELLO LUA WORLD!"
  168. --]]
  169. exports.jinja_template_file = function(filename, env, skip_global_env)
  170. if not skip_global_env then
  171. env = enrich_template_with_globals(env)
  172. end
  173. return lupa.expand_file(filename, env)
  174. end
  175. exports.remove_email_aliases = function(email_addr)
  176. local function check_gmail_user(addr)
  177. -- Remove all points
  178. local no_dots_user = string.gsub(addr.user, '%.', '')
  179. local cap, pluses = string.match(no_dots_user, '^([^%+][^%+]*)(%+.*)$')
  180. if cap then
  181. return cap, rspamd_str_split(pluses, '+'), nil
  182. elseif no_dots_user ~= addr.user then
  183. return no_dots_user,{},nil
  184. end
  185. return nil
  186. end
  187. local function check_address(addr)
  188. if addr.user then
  189. local cap, pluses = string.match(addr.user, '^([^%+][^%+]*)(%+.*)$')
  190. if cap then
  191. return cap, rspamd_str_split(pluses, '+'), nil
  192. end
  193. end
  194. return nil
  195. end
  196. local function set_addr(addr, new_user, new_domain)
  197. if new_user then
  198. addr.user = new_user
  199. end
  200. if new_domain then
  201. addr.domain = new_domain
  202. end
  203. if addr.domain then
  204. addr.addr = string.format('%s@%s', addr.user, addr.domain)
  205. else
  206. addr.addr = string.format('%s@', addr.user)
  207. end
  208. if addr.name and #addr.name > 0 then
  209. addr.raw = string.format('"%s" <%s>', addr.name, addr.addr)
  210. else
  211. addr.raw = string.format('<%s>', addr.addr)
  212. end
  213. end
  214. local function check_gmail(addr)
  215. local nu, tags, nd = check_gmail_user(addr)
  216. if nu then
  217. return nu, tags, nd
  218. end
  219. return nil
  220. end
  221. local function check_googlemail(addr)
  222. local nd = 'gmail.com'
  223. local nu, tags = check_gmail_user(addr)
  224. if nu then
  225. return nu, tags, nd
  226. end
  227. return nil, nil, nd
  228. end
  229. local specific_domains = {
  230. ['gmail.com'] = check_gmail,
  231. ['googlemail.com'] = check_googlemail,
  232. }
  233. if email_addr then
  234. if email_addr.domain and specific_domains[email_addr.domain] then
  235. local nu, tags, nd = specific_domains[email_addr.domain](email_addr)
  236. if nu or nd then
  237. set_addr(email_addr, nu, nd)
  238. return nu, tags
  239. end
  240. else
  241. local nu, tags, nd = check_address(email_addr)
  242. if nu or nd then
  243. set_addr(email_addr, nu, nd)
  244. return nu, tags
  245. end
  246. end
  247. return nil
  248. end
  249. end
  250. exports.is_rspamc_or_controller = function(task)
  251. local ua = task:get_request_header('User-Agent') or ''
  252. local pwd = task:get_request_header('Password')
  253. local is_rspamc = false
  254. if tostring(ua) == 'rspamc' or pwd then is_rspamc = true end
  255. return is_rspamc
  256. end
  257. --[[[
  258. -- @function lua_util.unpack(table)
  259. -- Converts numeric table to varargs
  260. -- This is `unpack` on Lua 5.1/5.2/LuaJIT and `table.unpack` on Lua 5.3
  261. -- @param {table} table numerically indexed table to unpack
  262. -- @return {varargs} unpacked table elements
  263. --]]
  264. local unpack_function = table.unpack or unpack
  265. exports.unpack = function(t)
  266. return unpack_function(t)
  267. end
  268. --[[[
  269. -- @function lua_util.flatten(table)
  270. -- Flatten underlying tables in a single table
  271. -- @param {table} table table of tables
  272. -- @return {table} flattened table
  273. --]]
  274. exports.flatten = function(t)
  275. local res = {}
  276. for _,e in fun.iter(t) do
  277. for _,v in fun.iter(e) do
  278. res[#res + 1] = v
  279. end
  280. end
  281. return res
  282. end
  283. --[[[
  284. -- @function lua_util.spairs(table)
  285. -- Like `pairs` but keys are sorted lexicographically
  286. -- @param {table} table table containing key/value pairs
  287. -- @return {function} generator function returning key/value pairs
  288. --]]
  289. -- Sorted iteration:
  290. -- for k,v in spairs(t) do ... end
  291. --
  292. -- or with custom comparison:
  293. -- for k, v in spairs(t, function(t, a, b) return t[a] < t[b] end)
  294. --
  295. -- optional limit is also available (e.g. return top X elements)
  296. local function spairs(t, order, lim)
  297. -- collect the keys
  298. local keys = {}
  299. for k in pairs(t) do keys[#keys+1] = k end
  300. -- if order function given, sort by it by passing the table and keys a, b,
  301. -- otherwise just sort the keys
  302. if order then
  303. table.sort(keys, function(a,b) return order(t, a, b) end)
  304. else
  305. table.sort(keys)
  306. end
  307. -- return the iterator function
  308. local i = 0
  309. return function()
  310. i = i + 1
  311. if not lim or i <= lim then
  312. if keys[i] then
  313. return keys[i], t[keys[i]]
  314. end
  315. end
  316. end
  317. end
  318. exports.spairs = spairs
  319. --[[[
  320. -- @function lua_util.disable_module(modname, how)
  321. -- Disables a plugin
  322. -- @param {string} modname name of plugin to disable
  323. -- @param {string} how 'redis' to disable redis, 'config' to disable startup
  324. --]]
  325. local function disable_module(modname, how)
  326. if rspamd_plugins_state.enabled[modname] then
  327. rspamd_plugins_state.enabled[modname] = nil
  328. end
  329. if how == 'redis' then
  330. rspamd_plugins_state.disabled_redis[modname] = {}
  331. elseif how == 'config' then
  332. rspamd_plugins_state.disabled_unconfigured[modname] = {}
  333. elseif how == 'experimental' then
  334. rspamd_plugins_state.disabled_experimental[modname] = {}
  335. else
  336. rspamd_plugins_state.disabled_failed[modname] = {}
  337. end
  338. end
  339. exports.disable_module = disable_module
  340. --[[[
  341. -- @function lua_util.disable_module(modname)
  342. -- Checks experimental plugins state and disable if needed
  343. -- @param {string} modname name of plugin to check
  344. -- @return {boolean} true if plugin should be enabled, false otherwise
  345. --]]
  346. local function check_experimental(modname)
  347. if rspamd_config:experimental_enabled() then
  348. return true
  349. else
  350. disable_module(modname, 'experimental')
  351. end
  352. return false
  353. end
  354. exports.check_experimental = check_experimental
  355. --[[[
  356. -- @function lua_util.list_to_hash(list)
  357. -- Converts numerically-indexed table to table indexed by values
  358. -- @param {table} list numerically-indexed table or string, which is treated as a one-element list
  359. -- @return {table} table indexed by values
  360. -- @example
  361. -- local h = lua_util.list_to_hash({"a", "b"})
  362. -- -- h contains {a = true, b = true}
  363. --]]
  364. local function list_to_hash(list)
  365. if type(list) == 'table' then
  366. if list[1] then
  367. local h = {}
  368. for _, e in ipairs(list) do
  369. h[e] = true
  370. end
  371. return h
  372. else
  373. return list
  374. end
  375. elseif type(list) == 'string' then
  376. local h = {}
  377. h[list] = true
  378. return h
  379. end
  380. end
  381. exports.list_to_hash = list_to_hash
  382. --[[[
  383. -- @function lua_util.nkeys(table|gen, param, state)
  384. -- Returns number of keys in a table (i.e. from both the array and hash parts combined)
  385. -- @param {table} list numerically-indexed table or string, which is treated as a one-element list
  386. -- @return {number} number of keys
  387. -- @example
  388. -- print(lua_util.nkeys({})) -- 0
  389. -- print(lua_util.nkeys({ "a", nil, "b" })) -- 2
  390. -- print(lua_util.nkeys({ dog = 3, cat = 4, bird = nil })) -- 2
  391. -- print(lua_util.nkeys({ "a", dog = 3, cat = 4 })) -- 3
  392. --
  393. --]]
  394. local function nkeys(gen, param, state)
  395. local n = 0
  396. if not param then
  397. for _,_ in pairs(gen) do n = n + 1 end
  398. else
  399. for _,_ in fun.iter(gen, param, state) do n = n + 1 end
  400. end
  401. return n
  402. end
  403. exports.nkeys = nkeys
  404. --[[[
  405. -- @function lua_util.parse_time_interval(str)
  406. -- Parses human readable time interval
  407. -- Accepts 's' for seconds, 'm' for minutes, 'h' for hours, 'd' for days,
  408. -- 'w' for weeks, 'y' for years
  409. -- @param {string} str input string
  410. -- @return {number|nil} parsed interval as seconds (might be fractional)
  411. --]]
  412. local function parse_time_interval(str)
  413. local function parse_time_suffix(s)
  414. if s == 's' then
  415. return 1
  416. elseif s == 'm' then
  417. return 60
  418. elseif s == 'h' then
  419. return 3600
  420. elseif s == 'd' then
  421. return 86400
  422. elseif s == 'w' then
  423. return 86400 * 7
  424. elseif s == 'y' then
  425. return 365 * 86400;
  426. end
  427. end
  428. local digit = lpeg.R("09")
  429. local parser = {}
  430. parser.integer =
  431. (lpeg.S("+-") ^ -1) *
  432. (digit ^ 1)
  433. parser.fractional =
  434. (lpeg.P(".") ) *
  435. (digit ^ 1)
  436. parser.number =
  437. (parser.integer *
  438. (parser.fractional ^ -1)) +
  439. (lpeg.S("+-") * parser.fractional)
  440. parser.time = lpeg.Cf(lpeg.Cc(1) *
  441. (parser.number / tonumber) *
  442. ((lpeg.S("smhdwy") / parse_time_suffix) ^ -1),
  443. function (acc, val) return acc * val end)
  444. local t = lpeg.match(parser.time, str)
  445. return t
  446. end
  447. exports.parse_time_interval = parse_time_interval
  448. --[[[
  449. -- @function lua_util.dehumanize_number(str)
  450. -- Parses human readable number
  451. -- Accepts 'k' for thousands, 'm' for millions, 'g' for billions, 'b' suffix for 1024 multiplier,
  452. -- e.g. `10mb` equal to `10 * 1024 * 1024`
  453. -- @param {string} str input string
  454. -- @return {number|nil} parsed number
  455. --]]
  456. local function dehumanize_number(str)
  457. local function parse_suffix(s)
  458. if s == 'k' then
  459. return 1000
  460. elseif s == 'm' then
  461. return 1000000
  462. elseif s == 'g' then
  463. return 1e9
  464. elseif s == 'kb' then
  465. return 1024
  466. elseif s == 'mb' then
  467. return 1024 * 1024
  468. elseif s == 'gb' then
  469. return 1024 * 1024;
  470. end
  471. end
  472. local digit = lpeg.R("09")
  473. local parser = {}
  474. parser.integer =
  475. (lpeg.S("+-") ^ -1) *
  476. (digit ^ 1)
  477. parser.fractional =
  478. (lpeg.P(".") ) *
  479. (digit ^ 1)
  480. parser.number =
  481. (parser.integer *
  482. (parser.fractional ^ -1)) +
  483. (lpeg.S("+-") * parser.fractional)
  484. parser.humanized_number = lpeg.Cf(lpeg.Cc(1) *
  485. (parser.number / tonumber) *
  486. (((lpeg.S("kmg") * (lpeg.P("b") ^ -1)) / parse_suffix) ^ -1),
  487. function (acc, val) return acc * val end)
  488. local t = lpeg.match(parser.humanized_number, str)
  489. return t
  490. end
  491. exports.dehumanize_number = dehumanize_number
  492. --[[[
  493. -- @function lua_util.table_cmp(t1, t2)
  494. -- Compare two tables deeply
  495. --]]
  496. local function table_cmp(table1, table2)
  497. local avoid_loops = {}
  498. local function recurse(t1, t2)
  499. if type(t1) ~= type(t2) then return false end
  500. if type(t1) ~= "table" then return t1 == t2 end
  501. if avoid_loops[t1] then return avoid_loops[t1] == t2 end
  502. avoid_loops[t1] = t2
  503. -- Copy keys from t2
  504. local t2keys = {}
  505. local t2tablekeys = {}
  506. for k, _ in pairs(t2) do
  507. if type(k) == "table" then table.insert(t2tablekeys, k) end
  508. t2keys[k] = true
  509. end
  510. -- Let's iterate keys from t1
  511. for k1, v1 in pairs(t1) do
  512. local v2 = t2[k1]
  513. if type(k1) == "table" then
  514. -- if key is a table, we need to find an equivalent one.
  515. local ok = false
  516. for i, tk in ipairs(t2tablekeys) do
  517. if table_cmp(k1, tk) and recurse(v1, t2[tk]) then
  518. table.remove(t2tablekeys, i)
  519. t2keys[tk] = nil
  520. ok = true
  521. break
  522. end
  523. end
  524. if not ok then return false end
  525. else
  526. -- t1 has a key which t2 doesn't have, fail.
  527. if v2 == nil then return false end
  528. t2keys[k1] = nil
  529. if not recurse(v1, v2) then return false end
  530. end
  531. end
  532. -- if t2 has a key which t1 doesn't have, fail.
  533. if next(t2keys) then return false end
  534. return true
  535. end
  536. return recurse(table1, table2)
  537. end
  538. exports.table_cmp = table_cmp
  539. --[[[
  540. -- @function lua_util.table_cmp(task, name, value, stop_chars)
  541. -- Performs header folding
  542. --]]
  543. exports.fold_header = function(task, name, value, stop_chars)
  544. local how
  545. if task:has_flag("milter") then
  546. how = "lf"
  547. else
  548. how = task:get_newlines_type()
  549. end
  550. return rspamd_util.fold_header(name, value, how, stop_chars)
  551. end
  552. --[[[
  553. -- @function lua_util.override_defaults(defaults, override)
  554. -- Overrides values from defaults with override
  555. --]]
  556. local function override_defaults(def, override)
  557. -- Corner cases
  558. if not override or type(override) ~= 'table' then
  559. return def
  560. end
  561. if not def or type(def) ~= 'table' then
  562. return override
  563. end
  564. local res = {}
  565. for k,v in pairs(override) do
  566. if type(v) == 'table' then
  567. if def[k] and type(def[k]) == 'table' then
  568. -- Recursively override elements
  569. res[k] = override_defaults(def[k], v)
  570. else
  571. res[k] = v
  572. end
  573. else
  574. res[k] = v
  575. end
  576. end
  577. for k,v in pairs(def) do
  578. if type(res[k]) == 'nil' then
  579. res[k] = v
  580. end
  581. end
  582. return res
  583. end
  584. exports.override_defaults = override_defaults
  585. --[[[
  586. -- @function lua_util.filter_specific_urls(urls, params)
  587. -- params: {
  588. - - task - if needed to save in the cache
  589. - - limit <int> (default = 9999)
  590. - - esld_limit <int> (default = 9999) n domains per eSLD (effective second level domain)
  591. works only if number of unique eSLD less than `limit`
  592. - - need_emails <bool> (default = false)
  593. - - filter <callback> (default = nil)
  594. - - prefix <string> cache prefix (default = nil)
  595. -- }
  596. -- Apply heuristic in extracting of urls from `urls` table, this function
  597. -- tries its best to extract specific number of urls from a task based on
  598. -- their characteristics
  599. --]]
  600. exports.filter_specific_urls = function (urls, params)
  601. local cache_key
  602. if params.task and not params.no_cache then
  603. if params.prefix then
  604. cache_key = params.prefix
  605. else
  606. cache_key = string.format('sp_urls_%d%s%s%s', params.limit,
  607. tostring(params.need_emails or false),
  608. tostring(params.need_images or false),
  609. tostring(params.need_content or false))
  610. end
  611. local cached = params.task:cache_get(cache_key)
  612. if cached then
  613. return cached
  614. end
  615. end
  616. if not urls then return {} end
  617. if params.filter then urls = fun.totable(fun.filter(params.filter, urls)) end
  618. -- Filter by tld:
  619. local tlds = {}
  620. local eslds = {}
  621. local ntlds, neslds = 0, 0
  622. local res = {}
  623. local nres = 0
  624. local function insert_url(str, u)
  625. if not res[str] then
  626. res[str] = u
  627. nres = nres + 1
  628. return true
  629. end
  630. return false
  631. end
  632. local function process_single_url(u, default_priority)
  633. local priority = default_priority or 1 -- Normal priority
  634. local flags = u:get_flags()
  635. if params.ignore_ip and flags.numeric then
  636. return
  637. end
  638. if flags.redirected then
  639. local redir = u:get_redirected() -- get the real url
  640. if params.ignore_redirected then
  641. -- Replace `u` with redir
  642. u = redir
  643. priority = 2
  644. else
  645. -- Process both redirected url and the original one
  646. process_single_url(redir, 2)
  647. end
  648. end
  649. if flags.image then
  650. if not params.need_images then
  651. -- Ignore url
  652. return
  653. else
  654. -- Penalise images in urls
  655. priority = 0
  656. end
  657. end
  658. local esld = u:get_tld()
  659. local str_hash = tostring(u)
  660. if esld then
  661. -- Special cases
  662. if (u:get_protocol() ~= 'mailto') and (not flags.html_displayed) then
  663. if flags.obscured then
  664. priority = 3
  665. else
  666. if (flags.has_user or flags.has_port) then
  667. priority = 2
  668. elseif (flags.subject or flags.phished) then
  669. priority = 2
  670. end
  671. end
  672. elseif flags.html_displayed then
  673. priority = 0
  674. end
  675. if not eslds[esld] then
  676. eslds[esld] = {{str_hash, u, priority}}
  677. neslds = neslds + 1
  678. else
  679. if #eslds[esld] < params.esld_limit then
  680. table.insert(eslds[esld], {str_hash, u, priority})
  681. end
  682. end
  683. -- eSLD - 1 part => tld
  684. local parts = rspamd_str_split(esld, '.')
  685. local tld = table.concat(fun.totable(fun.tail(parts)), '.')
  686. if not tlds[tld] then
  687. tlds[tld] = {{str_hash, u, priority}}
  688. ntlds = ntlds + 1
  689. else
  690. table.insert(tlds[tld], {str_hash, u, priority})
  691. end
  692. end
  693. end
  694. for _,u in ipairs(urls) do
  695. process_single_url(u)
  696. end
  697. local limit = params.limit
  698. limit = limit - nres
  699. if limit < 0 then limit = 0 end
  700. if limit == 0 then
  701. res = exports.values(res)
  702. if params.task and not params.no_cache then
  703. params.task:cache_set(cache_key, res)
  704. end
  705. return res
  706. end
  707. -- Sort eSLDs and tlds
  708. local function sort_stuff(tbl)
  709. -- Sort according to max priority
  710. table.sort(tbl, function(e1, e2)
  711. -- Sort by priority so max priority is at the end
  712. table.sort(e1, function(tr1, tr2)
  713. return tr1[3] < tr2[3]
  714. end)
  715. table.sort(e2, function(tr1, tr2)
  716. return tr1[3] < tr2[3]
  717. end)
  718. if e1[#e1][3] ~= e2[#e2][3] then
  719. -- Sort by priority so max priority is at the beginning
  720. return e1[#e1][3] > e2[#e2][3]
  721. else
  722. -- Prefer less urls to more urls per esld
  723. return #e1 < #e2
  724. end
  725. end)
  726. return tbl
  727. end
  728. eslds = sort_stuff(exports.values(eslds))
  729. neslds = #eslds
  730. if neslds <= limit then
  731. -- Number of eslds < limit
  732. repeat
  733. local item_found = false
  734. for _,lurls in ipairs(eslds) do
  735. if #lurls > 0 then
  736. local last = table.remove(lurls)
  737. insert_url(last[1], last[2])
  738. limit = limit - 1
  739. item_found = true
  740. end
  741. end
  742. until limit <= 0 or not item_found
  743. res = exports.values(res)
  744. if params.task and not params.no_cache then
  745. params.task:cache_set(cache_key, res)
  746. end
  747. return res
  748. end
  749. tlds = sort_stuff(exports.values(tlds))
  750. ntlds = #tlds
  751. -- Number of tlds < limit
  752. while limit > 0 do
  753. for _,lurls in ipairs(tlds) do
  754. if #lurls > 0 then
  755. local last = table.remove(lurls)
  756. insert_url(last[1], last[2])
  757. limit = limit - 1
  758. end
  759. if limit == 0 then break end
  760. end
  761. end
  762. res = exports.values(res)
  763. if params.task and not params.no_cache then
  764. params.task:cache_set(cache_key, res)
  765. end
  766. return res
  767. end
  768. --[[[
  769. -- @function lua_util.extract_specific_urls(params)
  770. -- params: {
  771. - - task
  772. - - limit <int> (default = 9999)
  773. - - esld_limit <int> (default = 9999) n domains per eSLD (effective second level domain)
  774. works only if number of unique eSLD less than `limit`
  775. - - need_emails <bool> (default = false)
  776. - - filter <callback> (default = nil)
  777. - - prefix <string> cache prefix (default = nil)
  778. - - ignore_redirected <bool> (default = false)
  779. - - need_images <bool> (default = false)
  780. - - need_content <bool> (default = false)
  781. -- }
  782. -- Apply heuristic in extracting of urls from task, this function
  783. -- tries its best to extract specific number of urls from a task based on
  784. -- their characteristics
  785. --]]
  786. -- exports.extract_specific_urls = function(params_or_task, limit, need_emails, filter, prefix)
  787. exports.extract_specific_urls = function(params_or_task, lim, need_emails, filter, prefix)
  788. local default_params = {
  789. limit = 9999,
  790. esld_limit = 9999,
  791. need_emails = false,
  792. need_images = false,
  793. need_content = false,
  794. filter = nil,
  795. prefix = nil,
  796. ignore_ip = false,
  797. ignore_redirected = false,
  798. no_cache = false,
  799. }
  800. local params
  801. if type(params_or_task) == 'table' and type(lim) == 'nil' then
  802. params = params_or_task
  803. else
  804. -- Deprecated call
  805. params = {
  806. task = params_or_task,
  807. limit = lim,
  808. need_emails = need_emails,
  809. filter = filter,
  810. prefix = prefix
  811. }
  812. end
  813. for k,v in pairs(default_params) do
  814. if type(params[k]) == 'nil' and v ~= nil then params[k] = v end
  815. end
  816. local url_params = {
  817. emails = params.need_emails,
  818. images = params.need_images,
  819. content = params.need_content,
  820. flags = params.flags, -- maybe nil
  821. flags_mode = params.flags_mode, -- maybe nil
  822. }
  823. -- Shortcut for cached stuff
  824. if params.task and not params.no_cache then
  825. local cache_key
  826. if params.prefix then
  827. cache_key = params.prefix
  828. else
  829. local cache_key_suffix
  830. if params.flags then
  831. cache_key_suffix = table.concat(params.flags) .. (params.flags_mode or '')
  832. else
  833. cache_key_suffix = string.format('%s%s%s',
  834. tostring(params.need_emails or false),
  835. tostring(params.need_images or false),
  836. tostring(params.need_content or false))
  837. end
  838. cache_key = string.format('sp_urls_%d%s', params.limit, cache_key_suffix)
  839. end
  840. local cached = params.task:cache_get(cache_key)
  841. if cached then
  842. return cached
  843. end
  844. end
  845. -- No cache version
  846. local urls = params.task:get_urls(url_params)
  847. return exports.filter_specific_urls(urls, params)
  848. end
  849. --[[[
  850. -- @function lua_util.deepcopy(table)
  851. -- params: {
  852. - - table
  853. -- }
  854. -- Performs deep copy of the table. Including metatables
  855. --]]
  856. local function deepcopy(orig)
  857. local orig_type = type(orig)
  858. local copy
  859. if orig_type == 'table' then
  860. copy = {}
  861. for orig_key, orig_value in next, orig, nil do
  862. copy[deepcopy(orig_key)] = deepcopy(orig_value)
  863. end
  864. if getmetatable(orig) then
  865. setmetatable(copy, deepcopy(getmetatable(orig)))
  866. end
  867. else -- number, string, boolean, etc
  868. copy = orig
  869. end
  870. return copy
  871. end
  872. exports.deepcopy = deepcopy
  873. --[[[
  874. -- @function lua_util.deepsort(table)
  875. -- params: {
  876. - - table
  877. -- }
  878. -- Performs recursive in-place sort of a table
  879. --]]
  880. local function default_sort_cmp(e1, e2)
  881. if type(e1) == type(e2) then
  882. return e1 < e2
  883. else
  884. return type(e1) < type(e2)
  885. end
  886. end
  887. local function deepsort(tbl, sort_func)
  888. local orig_type = type(tbl)
  889. if orig_type == 'table' then
  890. table.sort(tbl, sort_func or default_sort_cmp)
  891. for _, orig_value in next, tbl, nil do
  892. deepsort(orig_value)
  893. end
  894. end
  895. end
  896. exports.deepsort = deepsort
  897. --[[[
  898. -- @function lua_util.shallowcopy(tbl)
  899. -- Performs shallow (and fast) copy of a table or another Lua type
  900. --]]
  901. exports.shallowcopy = function(orig)
  902. local orig_type = type(orig)
  903. local copy
  904. if orig_type == 'table' then
  905. copy = {}
  906. for orig_key, orig_value in pairs(orig) do
  907. copy[orig_key] = orig_value
  908. end
  909. else
  910. copy = orig
  911. end
  912. return copy
  913. end
  914. -- Debugging support
  915. local logger = require "rspamd_logger"
  916. local unconditional_debug = logger.log_level() == 'debug'
  917. local debug_modules = {}
  918. local debug_aliases = {}
  919. local log_level = 384 -- debug + forced (1 << 7 | 1 << 8)
  920. exports.init_debug_logging = function(config)
  921. -- Fill debug modules from the config
  922. if not unconditional_debug then
  923. local log_config = config:get_all_opt('logging')
  924. if log_config then
  925. local log_level_str = log_config.level
  926. if log_level_str then
  927. if log_level_str == 'debug' then
  928. unconditional_debug = true
  929. end
  930. end
  931. if log_config.debug_modules then
  932. for _,m in ipairs(log_config.debug_modules) do
  933. debug_modules[m] = true
  934. logger.infox(config, 'enable debug for Lua module %s', m)
  935. end
  936. end
  937. if #debug_aliases > 0 then
  938. for alias,mod in pairs(debug_aliases) do
  939. if debug_modules[mod] then
  940. debug_modules[alias] = true
  941. logger.infox(config, 'enable debug for Lua module %s (%s aliased)',
  942. alias, mod)
  943. end
  944. end
  945. end
  946. end
  947. end
  948. end
  949. exports.enable_debug_logging = function()
  950. unconditional_debug = true
  951. end
  952. exports.enable_debug_modules = function(...)
  953. for _,m in ipairs({...}) do
  954. debug_modules[m] = true
  955. end
  956. end
  957. exports.disable_debug_logging = function()
  958. unconditional_debug = false
  959. end
  960. --[[[
  961. -- @function lua_util.debugm(module, [log_object], format, ...)
  962. -- Performs fast debug log for a specific module
  963. --]]
  964. exports.debugm = function(mod, obj_or_fmt, fmt_or_something, ...)
  965. if unconditional_debug or debug_modules[mod] then
  966. if type(obj_or_fmt) == 'string' then
  967. logger.logx(log_level, mod, '', 2, obj_or_fmt, fmt_or_something, ...)
  968. else
  969. logger.logx(log_level, mod, obj_or_fmt, 2, fmt_or_something, ...)
  970. end
  971. end
  972. end
  973. --[[[
  974. -- @function lua_util.add_debug_alias(mod, alias)
  975. -- Add debugging alias so logging to `alias` will be treated as logging to `mod`
  976. --]]
  977. exports.add_debug_alias = function(mod, alias)
  978. debug_aliases[alias] = mod
  979. if debug_modules[mod] then
  980. debug_modules[alias] = true
  981. logger.infox(rspamd_config, 'enable debug for Lua module %s (%s aliased)',
  982. alias, mod)
  983. end
  984. end
  985. ---[[[
  986. -- @function lua_util.get_task_verdict(task)
  987. -- Returns verdict for a task + score if certain, must be called from idempotent filters only
  988. -- Returns string:
  989. -- * `spam`: if message have over reject threshold and has more than one positive rule
  990. -- * `junk`: if a message has between score between [add_header/rewrite subject] to reject thresholds and has more than two positive rules
  991. -- * `passthrough`: if a message has been passed through some short-circuit rule
  992. -- * `ham`: if a message has overall score below junk level **and** more than three negative rule, or negative total score
  993. -- * `uncertain`: all other cases
  994. --]]
  995. exports.get_task_verdict = function(task)
  996. local lua_verdict = require "lua_verdict"
  997. return lua_verdict.get_default_verdict(task)
  998. end
  999. ---[[[
  1000. -- @function lua_util.maybe_obfuscate_string(subject, settings, prefix)
  1001. -- Obfuscate string if enabled in settings. Also checks utf8 validity - if
  1002. -- string is not valid utf8 then '???' is returned. Empty string returned as is.
  1003. -- Supported settings:
  1004. -- * <prefix>_privacy = false - subject privacy is off
  1005. -- * <prefix>_privacy_alg = 'blake2' - default hash-algorithm to obfuscate subject
  1006. -- * <prefix>_privacy_prefix = 'obf' - prefix to show it's obfuscated
  1007. -- * <prefix>_privacy_length = 16 - cut the length of the hash; if 0 or fasle full hash is returned
  1008. -- @return obfuscated or validated subject
  1009. --]]
  1010. exports.maybe_obfuscate_string = function(subject, settings, prefix)
  1011. local hash = require 'rspamd_cryptobox_hash'
  1012. if not subject or subject == '' then
  1013. return subject
  1014. elseif not rspamd_util.is_valid_utf8(subject) then
  1015. subject = '???'
  1016. elseif settings[prefix .. '_privacy'] then
  1017. local hash_alg = settings[prefix .. '_privacy_alg'] or 'blake2'
  1018. local subject_hash = hash.create_specific(hash_alg, subject)
  1019. local strip_len = settings[prefix .. '_privacy_length']
  1020. if strip_len and strip_len > 0 then
  1021. subject = subject_hash:hex():sub(1, strip_len)
  1022. else
  1023. subject = subject_hash:hex()
  1024. end
  1025. local privacy_prefix = settings[prefix .. '_privacy_prefix']
  1026. if privacy_prefix and #privacy_prefix > 0 then
  1027. subject = privacy_prefix .. ':' .. subject
  1028. end
  1029. end
  1030. return subject
  1031. end
  1032. ---[[[
  1033. -- @function lua_util.callback_from_string(str)
  1034. -- Converts a string like `return function(...) end` to lua function and return true and this function
  1035. -- or returns false + error message
  1036. -- @return status code and function object or an error message
  1037. --]]]
  1038. exports.callback_from_string = function(s)
  1039. local loadstring = loadstring or load
  1040. if not s or #s == 0 then
  1041. return false,'invalid or empty string'
  1042. end
  1043. s = exports.rspamd_str_trim(s)
  1044. local inp
  1045. if s:match('^return%s*function') then
  1046. -- 'return function', can be evaluated directly
  1047. inp = s
  1048. elseif s:match('^function%s*%(') then
  1049. inp = 'return ' .. s
  1050. else
  1051. -- Just a plain sequence
  1052. inp = 'return function(...)\n' .. s .. '; end'
  1053. end
  1054. local ret, res_or_err = pcall(loadstring(inp))
  1055. if not ret or type(res_or_err) ~= 'function' then
  1056. return false,res_or_err
  1057. end
  1058. return ret,res_or_err
  1059. end
  1060. ---[[[
  1061. -- @function lua_util.keys(t)
  1062. -- Returns all keys from a specific table
  1063. -- @param {table} t input table (or iterator triplet)
  1064. -- @return array of keys
  1065. --]]]
  1066. exports.keys = function(gen, param, state)
  1067. local keys = {}
  1068. local i = 1
  1069. if param then
  1070. for k,_ in fun.iter(gen, param, state) do
  1071. rawset(keys, i, k)
  1072. i = i + 1
  1073. end
  1074. else
  1075. for k,_ in pairs(gen) do
  1076. rawset(keys, i, k)
  1077. i = i + 1
  1078. end
  1079. end
  1080. return keys
  1081. end
  1082. ---[[[
  1083. -- @function lua_util.values(t)
  1084. -- Returns all values from a specific table
  1085. -- @param {table} t input table
  1086. -- @return array of values
  1087. --]]]
  1088. exports.values = function(gen, param, state)
  1089. local values = {}
  1090. local i = 1
  1091. if param then
  1092. for _,v in fun.iter(gen, param, state) do
  1093. rawset(values, i, v)
  1094. i = i + 1
  1095. end
  1096. else
  1097. for _,v in pairs(gen) do
  1098. rawset(values, i, v)
  1099. i = i + 1
  1100. end
  1101. end
  1102. return values
  1103. end
  1104. ---[[[
  1105. -- @function lua_util.distance_sorted(t1, t2)
  1106. -- Returns distance between two sorted tables t1 and t2
  1107. -- @param {table} t1 input table
  1108. -- @param {table} t2 input table
  1109. -- @return distance between `t1` and `t2`
  1110. --]]]
  1111. exports.distance_sorted = function(t1, t2)
  1112. local ncomp = #t1
  1113. local ndiff = 0
  1114. local i,j = 1,1
  1115. if ncomp < #t2 then
  1116. ncomp = #t2
  1117. end
  1118. for _=1,ncomp do
  1119. if j > #t2 then
  1120. ndiff = ndiff + ncomp - #t2
  1121. if i > j then
  1122. ndiff = ndiff - (i - j)
  1123. end
  1124. break
  1125. elseif i > #t1 then
  1126. ndiff = ndiff + ncomp - #t1
  1127. if j > i then
  1128. ndiff = ndiff - (j - i)
  1129. end
  1130. break
  1131. end
  1132. if t1[i] == t2[j] then
  1133. i = i + 1
  1134. j = j + 1
  1135. elseif t1[i] < t2[j] then
  1136. i = i + 1
  1137. ndiff = ndiff + 1
  1138. else
  1139. j = j + 1
  1140. ndiff = ndiff + 1
  1141. end
  1142. end
  1143. return ndiff
  1144. end
  1145. ---[[[
  1146. -- @function lua_util.table_digest(t)
  1147. -- Returns hash of all values if t[1] is string or all keys/values otherwise
  1148. -- @param {table} t input array or map
  1149. -- @return {string} base32 representation of blake2b hash of all strings
  1150. --]]]
  1151. local function table_digest(t)
  1152. local cr = require "rspamd_cryptobox_hash"
  1153. local h = cr.create()
  1154. if t[1] then
  1155. for _,e in ipairs(t) do
  1156. if type(e) == 'table' then
  1157. h:update(table_digest(e))
  1158. else
  1159. h:update(tostring(e))
  1160. end
  1161. end
  1162. else
  1163. for k,v in pairs(t) do
  1164. h:update(tostring(k))
  1165. if type(v) == 'string' then
  1166. h:update(v)
  1167. elseif type(v) == 'table' then
  1168. h:update(table_digest(v))
  1169. end
  1170. end
  1171. end
  1172. return h:base32()
  1173. end
  1174. exports.table_digest = table_digest
  1175. ---[[[
  1176. -- @function lua_util.toboolean(v)
  1177. -- Converts a string or a number to boolean
  1178. -- @param {string|number} v
  1179. -- @return {boolean} v converted to boolean
  1180. --]]]
  1181. exports.toboolean = function(v)
  1182. local true_t = {
  1183. ['1'] = true,
  1184. ['true'] = true,
  1185. ['TRUE'] = true,
  1186. ['True'] = true,
  1187. };
  1188. local false_t = {
  1189. ['0'] = false,
  1190. ['false'] = false,
  1191. ['FALSE'] = false,
  1192. ['False'] = false,
  1193. };
  1194. if type(v) == 'string' then
  1195. if true_t[v] == true then
  1196. return true;
  1197. elseif false_t[v] == false then
  1198. return false;
  1199. else
  1200. return false, string.format( 'cannot convert %q to boolean', v);
  1201. end
  1202. elseif type(v) == 'number' then
  1203. return v ~= 0
  1204. else
  1205. return false, string.format( 'cannot convert %q to boolean', v);
  1206. end
  1207. end
  1208. ---[[[
  1209. -- @function lua_util.config_check_local_or_authed(config, modname)
  1210. -- Reads check_local and check_authed from the config as this is used in many modules
  1211. -- @param {rspamd_config} config `rspamd_config` global
  1212. -- @param {name} module name
  1213. -- @return {boolean} v converted to boolean
  1214. --]]]
  1215. exports.config_check_local_or_authed = function(rspamd_config, modname, def_local, def_authed)
  1216. local check_local = def_local or false
  1217. local check_authed = def_authed or false
  1218. local function try_section(where)
  1219. local ret = false
  1220. local opts = rspamd_config:get_all_opt(where)
  1221. if type(opts) == 'table' then
  1222. if type(opts['check_local']) == 'boolean' then
  1223. check_local = opts['check_local']
  1224. ret = true
  1225. end
  1226. if type(opts['check_authed']) == 'boolean' then
  1227. check_authed = opts['check_authed']
  1228. ret = true
  1229. end
  1230. end
  1231. return ret
  1232. end
  1233. if not try_section(modname) then
  1234. try_section('options')
  1235. end
  1236. return {check_local, check_authed}
  1237. end
  1238. ---[[[
  1239. -- @function lua_util.is_skip_local_or_authed(task, conf[, ip])
  1240. -- Returns `true` if local or authenticated task should be skipped for this module
  1241. -- @param {rspamd_task} task
  1242. -- @param {table} conf table returned from `config_check_local_or_authed`
  1243. -- @param {rspamd_ip} ip optional ip address (can be obtained from a task)
  1244. -- @return {boolean} true if check should be skipped
  1245. --]]]
  1246. exports.is_skip_local_or_authed = function(task, conf, ip)
  1247. if not ip then
  1248. ip = task:get_from_ip()
  1249. end
  1250. if not conf then
  1251. conf = {false, false}
  1252. end
  1253. if ((not conf[2] and task:get_user()) or
  1254. (not conf[1] and type(ip) == 'userdata' and ip:is_local())) then
  1255. return true
  1256. end
  1257. return false
  1258. end
  1259. ---[[[
  1260. -- @function lua_util.maybe_smtp_quote_value(str)
  1261. -- Checks string for the forbidden elements (tspecials in RFC and quote string if needed)
  1262. -- @param {string} str input string
  1263. -- @return {string} original or quoted string
  1264. --]]]
  1265. local tspecial = lpeg.S"()<>,;:\\\"/[]?= \t\v"
  1266. local special_match = lpeg.P((1 - tspecial)^0 * tspecial^1)
  1267. exports.maybe_smtp_quote_value = function(str)
  1268. if special_match:match(str) then
  1269. return string.format('"%s"', str:gsub('"', '\\"'))
  1270. end
  1271. return str
  1272. end
  1273. ---[[[
  1274. -- @function lua_util.shuffle(table)
  1275. -- Performs in-place shuffling of a table
  1276. -- @param {table} tbl table to shuffle
  1277. -- @return {table} same table
  1278. --]]]
  1279. exports.shuffle = function(tbl)
  1280. local size = #tbl
  1281. for i = size, 1, -1 do
  1282. local rand = math.random(size)
  1283. tbl[i], tbl[rand] = tbl[rand], tbl[i]
  1284. end
  1285. return tbl
  1286. end
  1287. --
  1288. local hex_table = {}
  1289. for idx = 0, 255 do
  1290. hex_table[("%02X"):format(idx)] = string.char(idx)
  1291. hex_table[("%02x"):format(idx)] = string.char(idx)
  1292. end
  1293. ---[[[
  1294. -- @function lua_util.unhex(str)
  1295. -- Decode hex encoded string
  1296. -- @param {string} str string to decode
  1297. -- @return {string} hex decoded string (valid hex pairs are decoded, everything else is printed as is)
  1298. --]]]
  1299. exports.unhex = function(str) return str:gsub('(..)', hex_table) end
  1300. local http_upstream_lists = {}
  1301. local function http_upstreams_by_url(pool, url)
  1302. local rspamd_url = require "rspamd_url"
  1303. local cached = http_upstream_lists[url]
  1304. if cached then return cached end
  1305. local real_url = rspamd_url.create(pool, url)
  1306. if not real_url then return nil end
  1307. local host = real_url:get_host()
  1308. local proto = real_url:get_protocol() or 'http'
  1309. local port = real_url:get_port() or (proto == 'https' and 443 or 80)
  1310. local upstream_list = require "rspamd_upstream_list"
  1311. local upstreams = upstream_list.create(host, port)
  1312. if upstreams then
  1313. http_upstream_lists[url] = upstreams
  1314. return upstreams
  1315. end
  1316. return nil
  1317. end
  1318. ---[[[
  1319. -- @function lua_util.http_upstreams_by_url(pool, url)
  1320. -- Returns a cached or new upstreams list that corresponds to the specific url
  1321. -- @param {mempool} pool memory pool to use (typically static pool from rspamd_config)
  1322. -- @param {string} url full url
  1323. -- @return {upstreams_list} object to get upstream from an url
  1324. --]]]
  1325. exports.http_upstreams_by_url = http_upstreams_by_url
  1326. ---[[[
  1327. -- @function lua_util.dns_timeout_augmentation(cfg)
  1328. -- Returns an augmentation suitable to define DNS timeout for a module
  1329. -- @return {string} a string in format 'timeout=x' where `x` is a number of seconds for DNS timeout
  1330. --]]]
  1331. local function dns_timeout_augmentation(cfg)
  1332. return string.format('timeout=%f', cfg:get_dns_timeout() or 0.0)
  1333. end
  1334. exports.dns_timeout_augmentation = dns_timeout_augmentation
  1335. -- Defines symbols priorities for common usage in prefilters/postfilters
  1336. exports.symbols_priorities = {
  1337. top = 10, -- Symbols must be executed first (or last), such as settings
  1338. high = 9, -- Example: asn
  1339. medium = 5, -- Everything should use this as default
  1340. low = 0,
  1341. }
  1342. return exports