public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
Merge branch 'master' of https://github.com/mailcow/mailcow-dockerized
853b924f
data/Dockerfiles/rspamd/Dockerfile | 2 -
data/Dockerfiles/rspamd/docker-entrypoint.sh | 2 +-
data/Dockerfiles/rspamd/ratelimit.lua | 843 +++++++++++++--------------
data/Dockerfiles/rspamd/tini | Bin 19968 -> 0 bytes
data/web/admin.php | 2 +-
data/web/css/admin.css | 8 +-
data/web/js/admin.js | 2 +-
data/web/js/mailbox.js | 2 +-
data/web/lang/lang.en.php | 2 +-
docker-compose.yml | 2 +-
10 files changed, 410 insertions(+), 455 deletions(-)
delete mode 100755 data/Dockerfiles/rspamd/tini
Diff
diff --cc data/Dockerfiles/rspamd/Dockerfile
index e5172bf0,899bacb9..ded516eb
--- a/data/Dockerfiles/rspamd/Dockerfile
+++ b/data/Dockerfiles/rspamd/Dockerfile
@@@ -21,10 -21,10 +21,8 @@@ RUN apt-get update && apt-get install -
&& chown _rspamd:_rspamd /run/rspamd
COPY settings.conf /etc/rspamd/modules.d/settings.conf
-#COPY ratelimit.lua /usr/share/rspamd/lua/ratelimit.lua
-#COPY lua_util.lua /usr/share/rspamd/lib/lua_util.lua
+COPY ratelimit.lua /usr/share/rspamd/lua/ratelimit.lua
- #COPY lua_util.lua /usr/share/rspamd/lib/lua_util.lua
COPY docker-entrypoint.sh /docker-entrypoint.sh
--COPY tini /sbin/tini
ENTRYPOINT ["/docker-entrypoint.sh"]
diff --cc data/Dockerfiles/rspamd/docker-entrypoint.sh
index 15ae73da,15ae73da..afb03bb6
--- a/data/Dockerfiles/rspamd/docker-entrypoint.sh
+++ b/data/Dockerfiles/rspamd/docker-entrypoint.sh
@@@ -3,4 -3,4 +3,4 @@@
chown -R _rspamd:_rspamd /var/lib/rspamd
[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Placeholder' > /etc/rspamd/override.d/worker-controller-password.inc
--exec /sbin/tini -- "$@"
++exec "$@"
diff --cc data/Dockerfiles/rspamd/ratelimit.lua
index e25ea42d,e25ea42d..839ec5c6
--- a/data/Dockerfiles/rspamd/ratelimit.lua
+++ b/data/Dockerfiles/rspamd/ratelimit.lua
@@@ -21,165 -21,165 +21,146 @@@ en
-- A plugin that implements ratelimits using redis
--local E, settings = {}, {}
++local E = {}
local N = 'ratelimit'
++local redis_params
-- Senders that are considered as bounce
--local bounce_senders = {'postmaster', 'mailer-daemon', '', 'null', 'fetchmail-daemon', 'mdaemon'}
++local settings = {
++ bounce_senders = { 'postmaster', 'mailer-daemon', '', 'null', 'fetchmail-daemon', 'mdaemon' },
-- Do not check ratelimits for these recipients
--local whitelisted_rcpts = {'postmaster', 'mailer-daemon'}
--local whitelisted_ip
--local whitelisted_user
--local max_rcpt = 5
--local redis_params
--local ratelimit_symbol
---- Do not delay mail after 1 day
--local use_ip_score = false
--local rl_prefix = 'RL'
--local ip_score_lower_bound = 10
--local ip_score_ham_multiplier = 1.1
--local ip_score_spam_divisor = 1.1
--local limits_hash
--
--local message_func = function(_, limit_type)
-- return string.format('Ratelimit "%s" exceeded', limit_type)
--end
--
--local rspamd_logger = require "rspamd_logger"
--local rspamd_util = require "rspamd_util"
--local rspamd_lua_utils = require "lua_util"
--local lua_redis = require "lua_redis"
--local fun = require "fun"
++ whitelisted_rcpts = { 'postmaster', 'mailer-daemon' },
++ prefix = 'RL',
++ ham_factor_rate = 1.01,
++ spam_factor_rate = 0.99,
++ ham_factor_burst = 1.02,
++ spam_factor_burst = 0.98,
++ max_rate_mult = 5,
++ max_bucket_mult = 10,
++ expire = 60 * 60 * 24 * 2, -- 2 days by default
++ limits = {},
++ allow_local = false,
++}
--local user_keywords = {'user'}
--
--local redis_script_sha
--local redis_script = [[local bucket
--local limited = false
--local buckets = {}
--local queue_id = table.remove(ARGV)
--local now = table.remove(ARGV)
--
--local argi = 0
--for i = 1, #KEYS do
-- local key = KEYS[i]
-- local period = tonumber(ARGV[argi+1])
-- local limit = tonumber(ARGV[argi+2])
-- if not buckets[key] then
-- buckets[key] = {
-- max_period = period,
-- limits = { {period, limit} },
-- }
++-- Checks bucket, updating it if needed
++-- KEYS[1] - prefix to update, e.g. RL_<triplet>_<seconds>
++-- KEYS[2] - current time in milliseconds
++-- KEYS[3] - bucket leak rate (messages per millisecond)
++-- KEYS[4] - bucket burst
++-- KEYS[5] - expire for a bucket
++-- return 1 if message should be ratelimited and 0 if not
++-- Redis keys used:
++-- l - last hit
++-- b - current burst
++-- dr - current dynamic rate multiplier (*10000)
++-- db - current dynamic burst multiplier (*10000)
++local bucket_check_script = [[
++ local last = redis.call('HGET', KEYS[1], 'l')
++ local now = tonumber(KEYS[2])
++ local dynr, dynb = 0, 0
++ if not last then
++ -- New bucket
++ redis.call('HSET', KEYS[1], 'l', KEYS[2])
++ redis.call('HSET', KEYS[1], 'b', '0')
++ redis.call('HSET', KEYS[1], 'dr', '10000')
++ redis.call('HSET', KEYS[1], 'db', '10000')
++ redis.call('EXPIRE', KEYS[1], KEYS[5])
++ return {0, 0, 1, 1}
++ end
++
++ last = tonumber(last)
++ local burst = tonumber(redis.call('HGET', KEYS[1], 'b'))
++ -- Perform leak
++ if burst > 0 then
++ if last < tonumber(KEYS[2]) then
++ local rate = tonumber(KEYS[3])
++ dynr = tonumber(redis.call('HGET', KEYS[1], 'dr')) / 10000.0
++ rate = rate * dynr
++ local leaked = ((now - last) * rate)
++ burst = burst - leaked
++ redis.call('HINCRBYFLOAT', KEYS[1], 'b', -(leaked))
++ end
else
-- table.insert(buckets[key].limits, {period, limit})
-- if period > buckets[key].max_period then
-- buckets[key].max_period = period
-- end
++ burst = 0
++ redis.call('HSET', KEYS[1], 'b', '0')
end
-- argi = argi + 2
--end
--for k, v in pairs(buckets) do
-- local maxp = v.max_period
-- redis.call('ZREMRANGEBYSCORE', k, '-inf', now - maxp)
-- for _, lim in ipairs(v.limits) do
-- local period = lim[1]
-- local limit = lim[2]
-- local rate
-- if period == maxp then
-- rate = redis.call('ZCARD', k)
-- else
-- rate = redis.call('ZCOUNT', k, now - period, '+inf')
-- end
-- if rate and rate >= limit then
-- limited = true
-- bucket = k
-- end
++ dynb = tonumber(redis.call('HGET', KEYS[1], 'db')) / 10000.0
++
++ if (burst + 1) * dynb > tonumber(KEYS[4]) then
++ return {1, tostring(burst), tostring(dynr), tostring(dynb)}
end
-- redis.call('EXPIRE', k, maxp)
-- if limited then break end
--end
--if not limited then
-- for k in pairs(buckets) do
-- redis.call('ZADD', k, now, queue_id)
++ return {0, tostring(burst), tostring(dynr), tostring(dynb)}
++]]
++local bucket_check_id
++
++
++-- Updates a bucket
++-- KEYS[1] - prefix to update, e.g. RL_<triplet>_<seconds>
++-- KEYS[2] - current time in milliseconds
++-- KEYS[3] - dynamic rate multiplier
++-- KEYS[4] - dynamic burst multiplier
++-- KEYS[5] - max dyn rate (min: 1/x)
++-- KEYS[6] - max burst rate (min: 1/x)
++-- KEYS[7] - expire for a bucket
++-- Redis keys used:
++-- l - last hit
++-- b - current burst
++-- dr - current dynamic rate multiplier
++-- db - current dynamic burst multiplier
++local bucket_update_script = [[
++ local last = redis.call('HGET', KEYS[1], 'l')
++ local now = tonumber(KEYS[2])
++ if not last then
++ -- New bucket
++ redis.call('HSET', KEYS[1], 'l', KEYS[2])
++ redis.call('HSET', KEYS[1], 'b', '1')
++ redis.call('HSET', KEYS[1], 'dr', '10000')
++ redis.call('HSET', KEYS[1], 'db', '10000')
++ redis.call('EXPIRE', KEYS[1], KEYS[7])
++ return {1, 1, 1}
end
--end
--return {limited, bucket}]]
--
--local redis_script_symbol = [[local limited = false
--local buckets, results = {}, {}
--local queue_id = table.remove(ARGV)
--local now = table.remove(ARGV)
--
--local argi = 0
--for i = 1, #KEYS do
-- local key = KEYS[i]
-- local period = tonumber(ARGV[argi+1])
-- local limit = tonumber(ARGV[argi+2])
-- if not buckets[key] then
-- buckets[key] = {
-- max_period = period,
-- limits = { {period, limit} },
-- }
-- else
-- table.insert(buckets[key].limits, {period, limit})
-- if period > buckets[key].max_period then
-- buckets[key].max_period = period
-- end
++ local burst = tonumber(redis.call('HGET', KEYS[1], 'b'))
++ local db = tonumber(redis.call('HGET', KEYS[1], 'db')) / 10000
++ local dr = tonumber(redis.call('HGET', KEYS[1], 'dr')) / 10000
++
++ if dr < tonumber(KEYS[5]) and dr > 1.0 / tonumber(KEYS[5]) then
++ dr = dr * tonumber(KEYS[3])
++ redis.call('HSET', KEYS[1], 'dr', tostring(math.floor(dr * 10000)))
end
-- argi = argi + 2
--end
--for k, v in pairs(buckets) do
-- local maxp = v.max_period
-- redis.call('ZREMRANGEBYSCORE', k, '-inf', now - maxp)
-- for _, lim in ipairs(v.limits) do
-- local period = lim[1]
-- local limit = lim[2]
-- local rate
-- if period == maxp then
-- rate = redis.call('ZCARD', k)
-- else
-- rate = redis.call('ZCOUNT', k, now - period, '+inf')
-- end
-- if rate then
-- local mult = 2 * math.tanh(rate / (limit * 2))
-- if mult >= 0.5 then
-- table.insert(results, {k, tostring(mult)})
-- end
-- end
++ if db < tonumber(KEYS[6]) and db > 1.0 / tonumber(KEYS[6]) then
++ db = db * tonumber(KEYS[4])
++ redis.call('HSET', KEYS[1], 'db', tostring(math.floor(db * 10000)))
end
-- redis.call('ZADD', k, now, queue_id)
-- redis.call('EXPIRE', k, maxp)
++
++ redis.call('HINCRBYFLOAT', KEYS[1], 'b', 1)
++ redis.call('HSET', KEYS[1], 'l', KEYS[2])
++ redis.call('EXPIRE', KEYS[1], KEYS[7])
++
++ return {tostring(burst), tostring(dr), tostring(db)}
++]]
++local bucket_update_id
++
++-- message_func(task, limit_type, prefix, bucket)
++local message_func = function(_, limit_type, _, _)
++ return string.format('Ratelimit "%s" exceeded', limit_type)
end
--return results]]
++local rspamd_logger = require "rspamd_logger"
++local rspamd_util = require "rspamd_util"
++local rspamd_lua_utils = require "lua_util"
++local lua_redis = require "lua_redis"
++local fun = require "fun"
++local lua_maps = require "lua_maps"
++local lua_util = require "lua_util"
++local rspamd_hash = require "rspamd_cryptobox_hash"
++
local function load_scripts(cfg, ev_base)
-- local function rl_script_cb(err, data)
-- if err then
-- rspamd_logger.errx(cfg, 'Script loading failed: ' .. err)
-- elseif type(data) == 'string' then
-- redis_script_sha = data
-- end
-- end
-- local script
-- if ratelimit_symbol then
-- script = redis_script_symbol
-- else
-- script = redis_script
-- end
-- lua_redis.redis_make_request_taskless(
-- ev_base,
-- cfg,
-- redis_params,
-- nil, -- key
-- true, -- is write
-- rl_script_cb, --callback
-- 'SCRIPT', -- command
-- {'LOAD', script}
-- )
++ bucket_check_id = lua_redis.add_redis_script(bucket_check_script, redis_params)
++ bucket_update_id = lua_redis.add_redis_script(bucket_update_script, redis_params)
end
local limit_parser
@@@ -246,43 -246,43 +227,67 @@@ local function parse_string_limit(lim,
return nil
end
--local function resize_element(x_score, x_total, element)
-- local x_ip_score
-- if not x_total then x_total = 0 end
-- if x_total < ip_score_lower_bound or x_total <= 0 then
-- x_score = 1
-- else
-- x_score = x_score / x_total
-- end
-- if x_score > 0 then
-- x_ip_score = x_score / ip_score_spam_divisor
-- element = element * rspamd_util.tanh(2.718281 * x_ip_score)
-- elseif x_score < 0 then
-- x_ip_score = ((1 + (x_score * -1)) * ip_score_ham_multiplier)
-- element = element * x_ip_score
++local function parse_limit(name, data)
++ local buckets = {}
++ if type(data) == 'table' then
++ -- 3 cases here:
++ -- * old limit in format [burst, rate]
++ -- * vector of strings in Andrew's string format
++ -- * proper bucket table
++ if #data == 2 and tonumber(data[1]) and tonumber(data[2]) then
++ -- Old style ratelimit
++ rspamd_logger.warnx(rspamd_config, 'old style ratelimit for %s', name)
++ if tonumber(data[1]) > 0 and tonumber(data[2]) > 0 then
++ table.insert(buckets, {
++ burst = data[1],
++ rate = data[2]
++ })
++ elseif data[1] ~= 0 then
++ rspamd_logger.warnx(rspamd_config, 'invalid numbers for %s', name)
++ else
++ rspamd_logger.infox(rspamd_config, 'disable limit %s, burst is zero', name)
++ end
++ else
++ -- Recursively map parse_limit and flatten the list
++ fun.each(function(l)
++ -- Flatten list
++ for _,b in ipairs(l) do table.insert(buckets, b) end
++ end, fun.map(function(d) return parse_limit(d, name) end, data))
++ end
++ elseif type(data) == 'string' then
++ local rep_rate, burst = parse_string_limit(data)
++
++ if rep_rate and burst then
++ table.insert(buckets, {
++ burst = burst,
++ rate = 1.0 / rep_rate -- reciprocal
++ })
++ end
end
-- return element
++
++ -- Filter valid
++ return fun.totable(fun.filter(function(val)
++ return type(val.burst) == 'number' and type(val.rate) == 'number'
++ end, buckets))
end
--- Check whether this addr is bounce
local function check_bounce(from)
-- return fun.any(function(b) return b == from end, bounce_senders)
++ return fun.any(function(b) return b == from end, settings.bounce_senders)
end
--local custom_keywords = {}
--
local keywords = {
['ip'] = {
['get_value'] = function(task)
local ip = task:get_ip()
-- if ip and ip:is_valid() then return ip end
++ if ip and ip:is_valid() then return tostring(ip) end
return nil
end,
},
['rip'] = {
['get_value'] = function(task)
local ip = task:get_ip()
-- if ip and ip:is_valid() and not ip:is_local() then return ip end
++ if ip and ip:is_valid() and not ip:is_local() then return tostring(ip) end
return nil
end,
},
@@@ -325,144 -325,144 +330,84 @@@
end,
},
['to'] = {
-- ['get_value'] = function()
-- return '%s' -- 'to' is special
++ ['get_value'] = function(task)
++ return task:get_principal_recipient()
end,
},
}
--local function dynamic_rate_key(task, rtype)
-- local key_t = {rl_prefix, rtype}
-- local key_keywords = rspamd_str_split(rtype, '_')
-- local have_to, have_user = false, false
++local function gen_rate_key(task, rtype, bucket)
++ local key_t = {tostring(lua_util.round(100000.0 / bucket.burst))}
++ local key_keywords = lua_util.str_split(rtype, '_')
++ local have_user = false
++
for _, v in ipairs(key_keywords) do
-- if (custom_keywords[v] and type(custom_keywords[v]['condition']) == 'function') then
-- if not custom_keywords[v]['condition']() then return nil end
-- end
local ret
-- if custom_keywords[v] and type(custom_keywords[v]['get_value']) == 'function' then
-- ret = custom_keywords[v]['get_value'](task)
-- elseif keywords[v] and type(keywords[v]['get_value']) == 'function' then
++
++ if keywords[v] and type(keywords[v]['get_value']) == 'function' then
ret = keywords[v]['get_value'](task)
end
if not ret then return nil end
-- for _, uk in ipairs(user_keywords) do
-- if v == uk then have_user = true end
-- if have_user then break end
-- end
-- if v == 'to' then have_to = true end
++ if v == 'user' then have_user = true end
if type(ret) ~= 'string' then ret = tostring(ret) end
table.insert(key_t, ret)
end
-- if (not have_user) and task:get_user() then
++
++ if have_user and not task:get_user() then
return nil
end
-- if not have_to then
-- return table.concat(key_t, ":")
-- else
-- local rate_keys = {}
-- local rcpts = task:get_recipients(0)
-- if not ((rcpts or E)[1] or E).addr then
-- return nil
-- end
-- local key_s = table.concat(key_t, ":")
-- local total_rcpt = 0
-- for _, r in ipairs(rcpts) do
-- if r['addr'] and total_rcpt < max_rcpt then
-- local key_f = string.format(key_s, string.lower(r['addr']))
-- table.insert(rate_keys, key_f)
-- total_rcpt = total_rcpt + 1
-- end
-- end
-- return rate_keys
-- end
++
++ return table.concat(key_t, ":")
end
--local function process_buckets(task, buckets)
-- if not buckets then return end
-- local function rl_redis_cb(err, data)
-- if err then
-- rspamd_logger.infox(task, 'got error while setting limit: %1', err)
-- end
-- if not data then return end
-- if data[1] == 1 then
-- rspamd_logger.infox(task,
-- 'ratelimit "%s" exceeded',
-- data[2])
-- task:set_pre_result('soft reject',
-- message_func(task, data[2]))
-- end
++local function make_prefix(redis_key, name, bucket)
++ local hash_len = 24
++ if hash_len > #redis_key then hash_len = #redis_key end
++ local hash = settings.prefix ..
++ string.sub(rspamd_hash.create(redis_key):base32(), 1, hash_len)
++ -- Fill defaults
++ if not bucket.spam_factor_rate then
++ bucket.spam_factor_rate = settings.spam_factor_rate
end
-- local function rl_symbol_redis_cb(err, data)
-- if err then
-- rspamd_logger.infox(task, 'got error while setting limit: %1', err)
-- end
-- if not data then return end
-- for i, b in ipairs(data) do
-- task:insert_result(ratelimit_symbol, b[2], string.format('%s:%s:%s', i, b[1], b[2]))
-- end
++ if not bucket.ham_factor_rate then
++ bucket.ham_factor_rate = settings.ham_factor_rate
end
-- local redis_cb = rl_redis_cb
-- if ratelimit_symbol then redis_cb = rl_symbol_redis_cb end
-- local args = {redis_script_sha, #buckets}
-- for _, bucket in ipairs(buckets) do
-- table.insert(args, bucket[2])
-- end
-- for _, bucket in ipairs(buckets) do
-- if use_ip_score then
-- local asn_score,total_asn,
-- country_score,total_country,
-- ipnet_score,total_ipnet,
-- ip_score, total_ip = task:get_mempool():get_variable('ip_score',
-- 'double,double,double,double,double,double,double,double')
-- local key_keywords = rspamd_str_split(bucket[2], '_')
-- local has_asn, has_ip = false, false
-- for _, v in ipairs(key_keywords) do
-- if v == "asn" then has_asn = true end
-- if v == "ip" then has_ip = true end
-- if has_ip and has_asn then break end
-- end
-- if has_asn and not has_ip then
-- bucket[1][2] = resize_element(asn_score, total_asn, bucket[1][2])
-- elseif has_ip then
-- if total_ip and total_ip > ip_score_lower_bound then
-- bucket[1][2] = resize_element(ip_score, total_ip, bucket[1][2])
-- elseif total_ipnet and total_ipnet > ip_score_lower_bound then
-- bucket[1][2] = resize_element(ipnet_score, total_ipnet, bucket[1][2])
-- elseif total_asn and total_asn > ip_score_lower_bound then
-- bucket[1][2] = resize_element(asn_score, total_asn, bucket[1][2])
-- elseif total_country and total_country > ip_score_lower_bound then
-- bucket[1][2] = resize_element(country_score, total_country, bucket[1][2])
-- else
-- bucket[1][2] = resize_element(ip_score, total_ip, bucket[1][2])
-- end
-- end
++ if not bucket.spam_factor_burst then
++ bucket.spam_factor_burst = settings.spam_factor_burst
++ end
++ if not bucket.ham_factor_burst then
++ bucket.ham_factor_burst = settings.ham_factor_burst
++ end
++
++ return {
++ bucket = bucket,
++ name = name,
++ hash = hash
++ }
++end
++
++local function limit_to_prefixes(task, k, v, prefixes)
++ local n = 0
++ for _,bucket in ipairs(v) do
++ local prefix = gen_rate_key(task, k, bucket)
++
++ if prefix then
++ prefixes[prefix] = make_prefix(prefix, k, bucket)
++ n = n + 1
end
-- table.insert(args, bucket[1][1])
-- table.insert(args, bucket[1][2])
-- end
-- table.insert(args, rspamd_util.get_time())
-- table.insert(args, task:get_queue_id() or task:get_uid())
-- local ret = rspamd_redis_make_request(task,
-- redis_params, -- connect params
-- nil, -- hash key
-- true, -- is write
-- redis_cb, --callback
-- 'evalsha', -- command
-- args -- arguments
-- )
-- if not ret then
-- rspamd_logger.errx(task, 'got error connecting to redis')
end
++
++ return n
end
local function ratelimit_cb(task)
-- if rspamd_lua_utils.is_rspamc_or_controller(task) then return end
-- local args = {}
++ if not settings.allow_local and
++ rspamd_lua_utils.is_rspamc_or_controller(task) then return end
++
-- Get initial task data
local ip = task:get_from_ip()
-- if ip and ip:is_valid() and whitelisted_ip then
-- if whitelisted_ip:get_key(ip) then
++ if ip and ip:is_valid() and settings.whitelisted_ip then
++ if settings.whitelisted_ip:get_key(ip) then
-- Do not check whitelisted ip
rspamd_logger.infox(task, 'skip ratelimit for whitelisted IP')
return
@@@ -475,136 -475,136 +420,149 @@@
fun.each(function(r)
fun.each(function(type) table.insert(rcpts_user, r[type]) end, {'user', 'addr'})
end, rcpts)
-- if fun.any(
-- function(r)
-- if fun.any(function(w) return r == w end, whitelisted_rcpts) then return true end
-- end,
-- rcpts_user) then
++ if fun.any(function(r) return settings.whitelisted_rcpts:get_key(r) end, rcpts_user) then
rspamd_logger.infox(task, 'skip ratelimit for whitelisted recipient')
return
end
end
-- Get user (authuser)
-- if whitelisted_user then
++ if settings.whitelisted_user then
local auser = task:get_user()
-- if whitelisted_user:get_key(auser) then
++ if settings.whitelisted_user:get_key(auser) then
rspamd_logger.infox(task, 'skip ratelimit for whitelisted user')
return
end
end
++ -- Now create all ratelimit prefixes
++ local prefixes = {}
++ local nprefixes = 0
-- local redis_keys = {}
-- local redis_keys_rev = {}
-- local function collect_redis_keys()
-- local function collect_cb(err, data)
-- if err then
-- rspamd_logger.errx(task, 'redis error: %1', err)
-- else
-- for i, d in ipairs(data) do
-- if type(d) == 'string' then
-- local plim, size = parse_string_limit(d)
-- if plim then
-- table.insert(args, {{plim, size}, redis_keys_rev[i]})
-- end
-- end
-- end
-- return process_buckets(task, args)
++ for k,v in pairs(settings.limits) do
++ nprefixes = nprefixes + limit_to_prefixes(task, k, v, prefixes)
++ end
++
++ for k, hdl in pairs(settings.custom_keywords or E) do
++ local ret, redis_key, bd = pcall(hdl, task)
++
++ if ret then
++ local bucket = parse_limit(k, bd)
++ if bucket[1] then
++ prefixes[redis_key] = make_prefix(redis_key, k, bucket[1])
end
-- end
-- local params, method
-- if limits_hash then
-- params = {limits_hash, rspamd_lua_utils.unpack(redis_keys)}
-- method = 'HMGET'
++ nprefixes = nprefixes + 1
else
-- method = 'MGET'
-- params = redis_keys
-- end
-- local requested_keys = rspamd_redis_make_request(task,
-- redis_params, -- connect params
-- nil, -- hash key
-- true, -- is write
-- collect_cb, --callback
-- method, -- command
-- params -- arguments
-- )
-- if not requested_keys then
-- rspamd_logger.errx(task, 'got error connecting to redis')
-- return process_buckets(task, args)
++ rspamd_logger.errx(task, 'cannot call handler for %s: %s',
++ k, redis_key)
end
end
-- local rate_key
-- for k in pairs(settings) do
-- rate_key = dynamic_rate_key(task, k)
-- if rate_key then
-- if type(rate_key) == 'table' then
-- for _, rk in ipairs(rate_key) do
-- if type(settings[k]) == 'string' and
-- (custom_keywords[settings[k]] and type(custom_keywords[settings[k]]['get_limit']) == 'function') then
-- local res = custom_keywords[settings[k]]['get_limit'](task)
-- if type(res) == 'string' then res = {res} end
-- for _, r in ipairs(res) do
-- local plim, size = parse_string_limit(r, true)
-- if plim then
-- table.insert(args, {{plim, size}, rk})
-- else
-- local rkey = string.match(settings[k], 'redis:(.*)')
-- if rkey then
-- table.insert(redis_keys, rkey)
-- redis_keys_rev[#redis_keys] = rk
-- else
-- rspamd_logger.infox(task, "Don't know what to do with limit: %1", settings[k])
-- end
-- end
-- end
-- end
-- end
-- else
-- if type(settings[k]) == 'string' and
-- (custom_keywords[settings[k]] and type(custom_keywords[settings[k]]['get_limit']) == 'function') then
-- local res = custom_keywords[settings[k]]['get_limit'](task)
-- if type(res) == 'string' then res = {res} end
-- for _, r in ipairs(res) do
-- local plim, size = parse_string_limit(r, true)
-- if plim then
-- table.insert(args, {{plim, size}, rate_key})
-- else
-- local rkey = string.match(r, 'redis:(.*)')
-- if rkey then
-- table.insert(redis_keys, rkey)
-- redis_keys_rev[#redis_keys] = rate_key
-- else
-- rspamd_logger.infox(task, "Don't know what to do with limit: %1", settings[k])
-- end
-- end
-- end
-- elseif type(settings[k]) == 'table' then
-- for _, rl in ipairs(settings[k]) do
-- table.insert(args, {{rl[1], rl[2]}, rate_key})
-- end
-- elseif type(settings[k]) == 'string' then
-- local rkey = string.match(settings[k], 'redis:(.*)')
-- if rkey then
-- table.insert(redis_keys, rkey)
-- redis_keys_rev[#redis_keys] = rate_key
-- else
-- rspamd_logger.infox(task, "Don't know what to do with limit: %1", settings[k])
-- end
++ local function gen_check_cb(prefix, bucket, lim_name)
++ return function(err, data)
++ if err then
++ rspamd_logger.errx('cannot check limit %s: %s %s', prefix, err, data)
++ elseif type(data) == 'table' and data[1] and data[1] == 1 then
++ -- set symbol only and do NOT soft reject
++ if settings.symbol then
++ task:insert_result(settings.symbol, 0.0, lim_name .. "(" .. prefix .. ")")
++ rspamd_logger.infox(task,
++ 'set_symbol_only: ratelimit "%s(%s)" exceeded, (%s / %s): %s (%s:%s dyn)',
++ lim_name, prefix,
++ bucket.burst, bucket.rate,
++ data[2], data[3], data[4])
++ return
++ -- set INFO symbol and soft reject
++ elseif settings.info_symbol then
++ task:insert_result(settings.info_symbol, 1.0,
++ lim_name .. "(" .. prefix .. ")")
end
++ rspamd_logger.infox(task,
++ 'ratelimit "%s(%s)" exceeded, (%s / %s): %s (%s:%s dyn)',
++ lim_name, prefix,
++ bucket.burst, bucket.rate,
++ data[2], data[3], data[4])
++ task:set_pre_result('soft reject',
++ message_func(task, lim_name, prefix, bucket))
end
end
end
-- if redis_keys[1] then
-- return collect_redis_keys()
-- else
-- return process_buckets(task, args)
++ -- Don't do anything if pre-result has been already set
++ if task:has_pre_result() then return end
++
++ if nprefixes > 0 then
++ -- Save prefixes to the cache to allow update
++ task:cache_set('ratelimit_prefixes', prefixes)
++ local now = rspamd_util.get_time()
++ now = lua_util.round(now * 1000.0) -- Get milliseconds
++ -- Now call check script for all defined prefixes
++
++ for pr,value in pairs(prefixes) do
++ local bucket = value.bucket
++ local rate = (bucket.rate) / 1000.0 -- Leak rate in messages/ms
++ rspamd_logger.debugm(N, task, "check limit %s:%s -> %s (%s/%s)",
++ value.name, pr, value.hash, bucket.burst, bucket.rate)
++ lua_redis.exec_redis_script(bucket_check_id,
++ {key = value.hash, task = task, is_write = true},
++ gen_check_cb(pr, bucket, value.name),
++ {value.hash, tostring(now), tostring(rate), tostring(bucket.burst),
++ tostring(settings.expire)})
++ end
++ end
++end
++
++local function ratelimit_update_cb(task)
++ local prefixes = task:cache_get('ratelimit_prefixes')
++
++ if prefixes then
++ if task:has_pre_result() then
++ -- Already rate limited/greylisted, do nothing
++ rspamd_logger.debugm(N, task, 'pre-action has been set, do not update')
++ return
++ end
++
++ local is_spam = not (task:get_metric_action() == 'no action')
++
++ -- Update each bucket
++ for k, v in pairs(prefixes) do
++ local bucket = v.bucket
++ local function update_bucket_cb(err, data)
++ if err then
++ rspamd_logger.errx(task, 'cannot update rate bucket %s: %s',
++ k, err)
++ else
++ rspamd_logger.debugm(N, task,
++ "updated limit %s:%s -> %s (%s/%s), burst: %s, dyn_rate: %s, dyn_burst: %s",
++ v.name, k, v.hash,
++ bucket.burst, bucket.rate,
++ data[1], data[2], data[3])
++ end
++ end
++ local now = rspamd_util.get_time()
++ now = lua_util.round(now * 1000.0) -- Get milliseconds
++ local mult_burst = bucket.ham_factor_burst or 1.0
++ local mult_rate = bucket.ham_factor_burst or 1.0
++
++ if is_spam then
++ mult_burst = bucket.spam_factor_burst or 1.0
++ mult_rate = bucket.spam_factor_rate or 1.0
++ end
++
++ lua_redis.exec_redis_script(bucket_update_id,
++ {key = v.hash, task = task, is_write = true},
++ update_bucket_cb,
++ {v.hash, tostring(now), tostring(mult_rate), tostring(mult_burst),
++ tostring(settings.max_rate_mult), tostring(settings.max_bucket_mult),
++ tostring(settings.expire)})
++ end
end
end
local opts = rspamd_config:get_all_opt(N)
if opts then
++
++ settings = lua_util.override_defaults(settings, opts)
++
if opts['limit'] then
rspamd_logger.errx(rspamd_config, 'Legacy ratelimit config format no longer supported')
end
@@@ -612,112 -612,112 +570,105 @@@
if opts['rates'] and type(opts['rates']) == 'table' then
-- new way of setting limits
fun.each(function(t, lim)
-- if type(lim) == 'table' then
-- settings[t] = {}
-- fun.each(function(l)
-- local plim, size = parse_string_limit(l)
-- if plim then
-- table.insert(settings[t], {plim, size})
-- end
-- end, lim)
-- elseif type(lim) == 'string' then
-- local plim, size = parse_string_limit(lim)
-- if plim then
-- settings[t] = { {plim, size} }
-- end
-- end
-- end, opts['rates'])
-- end
++ local buckets = parse_limit(t, lim)
-- if opts['dynamic_rates'] and type(opts['dynamic_rates']) == 'table' then
-- fun.each(function(t, lim)
-- if type(lim) == 'string' then
-- settings[t] = lim
++ if buckets and #buckets > 0 then
++ settings.limits[t] = buckets
end
-- end, opts['dynamic_rates'])
++ end, opts['rates'])
end
local enabled_limits = fun.totable(fun.map(function(t)
return t
-- end, settings))
-- rspamd_logger.infox(rspamd_config, 'enabled rate buckets: [%1]', table.concat(enabled_limits, ','))
--
-- if opts['whitelisted_rcpts'] and type(opts['whitelisted_rcpts']) == 'string' then
-- whitelisted_rcpts = rspamd_str_split(opts['whitelisted_rcpts'], ',')
++ end, settings.limits))
++ rspamd_logger.infox(rspamd_config,
++ 'enabled rate buckets: [%1]', table.concat(enabled_limits, ','))
++
++ -- Ret, ret, ret: stupid legacy stuff:
++ -- If we have a string with commas then load it as as static map
++ -- otherwise, apply normal logic of Rspamd maps
++
++ local wrcpts = opts['whitelisted_rcpts']
++ if type(wrcpts) == 'string' then
++ if string.find(wrcpts, ',') then
++ settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(
++ lua_util.rspamd_str_split(wrcpts, ','), 'set', 'Ratelimit whitelisted rcpts')
++ else
++ settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(wrcpts, 'set',
++ 'Ratelimit whitelisted rcpts')
++ end
elseif type(opts['whitelisted_rcpts']) == 'table' then
-- whitelisted_rcpts = opts['whitelisted_rcpts']
++ settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(wrcpts, 'set',
++ 'Ratelimit whitelisted rcpts')
++ else
++ -- Stupid default...
++ settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(
++ settings.whitelisted_rcpts, 'set', 'Ratelimit whitelisted rcpts')
end
if opts['whitelisted_ip'] then
-- whitelisted_ip = rspamd_map_add('ratelimit', 'whitelisted_ip', 'radix',
++ settings.whitelisted_ip = lua_maps.rspamd_map_add('ratelimit', 'whitelisted_ip', 'radix',
'Ratelimit whitelist ip map')
end
if opts['whitelisted_user'] then
-- whitelisted_user = rspamd_map_add('ratelimit', 'whitelisted_user', 'set',
++ settings.whitelisted_user = lua_maps.rspamd_map_add('ratelimit', 'whitelisted_user', 'set',
'Ratelimit whitelist user map')
end
-- if opts['symbol'] then
-- -- We want symbol instead of pre-result
-- ratelimit_symbol = opts['symbol']
-- end
--
-- if opts['max_rcpt'] then
-- max_rcpt = tonumber(opts['max_rcpt'])
-- end
--
-- if opts['use_ip_score'] then
-- use_ip_score = true
-- local ip_score_opts = rspamd_config:get_all_opt('ip_score')
-- if ip_score_opts and ip_score_opts['lower_bound'] then
-- ip_score_lower_bound = ip_score_opts['lower_bound']
-- end
-- end
--
++ settings.custom_keywords = {}
if opts['custom_keywords'] then
-- custom_keywords = dofile(opts['custom_keywords'])
-- end
++ local ret, res_or_err = pcall(loadfile(opts['custom_keywords']))
-- if opts['user_keywords'] then
-- user_keywords = opts['user_keywords']
++ if ret then
++ opts['custom_keywords'] = {}
++ if type(res_or_err) == 'table' then
++ for k,hdl in pairs(res_or_err) do
++ settings['custom_keywords'][k] = hdl
++ end
++ elseif type(res_or_err) == 'function' then
++ settings['custom_keywords']['custom'] = res_or_err
++ end
++ else
++ rspamd_logger.errx(rspamd_config, 'cannot execute %s: %s',
++ opts['custom_keywords'], res_or_err)
++ settings['custom_keywords'] = {}
++ end
end
if opts['message_func'] then
message_func = assert(load(opts['message_func']))()
end
-- if opts['limits_hash'] then
-- limits_hash = opts['limits_hash']
-- end
++ redis_params = lua_redis.parse_redis_server('ratelimit')
-- redis_params = rspamd_parse_redis_server('ratelimit')
if not redis_params then
rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module')
++ lua_util.disable_module(N, "redis")
else
local s = {
type = 'prefilter,nostat',
name = 'RATELIMIT_CHECK',
-- priority = 4,
++ priority = 7,
callback = ratelimit_cb,
++ flags = 'empty',
}
-- if use_ip_score then
-- s.type = 'normal'
-- end
-- if ratelimit_symbol then
-- s.name = ratelimit_symbol
-- end
-- local id = rspamd_config:register_symbol(s)
-- if use_ip_score then
-- rspamd_config:register_dependency(id, 'IP_SCORE')
-- end
-- for _, v in pairs(custom_keywords) do
-- if type(v) == 'table' and type(v['init']) == 'function' then
-- v['init']()
-- end
++
++ if settings.symbol then
++ s.name = settings.symbol
++ elseif settings.info_symbol then
++ s.name = settings.info_symbol
end
++
++ rspamd_config:register_symbol(s)
++ rspamd_config:register_symbol {
++ type = 'idempotent',
++ name = 'RATELIMIT_UPDATE',
++ callback = ratelimit_update_cb,
++ }
end
end
++
rspamd_config:add_on_load(function(cfg, ev_base, worker)
load_scripts(cfg, ev_base)
end)
diff --cc data/Dockerfiles/rspamd/tini
index 6556c966,6556c966..00000000
deleted file mode 100755,100755
Binary files differ
diff --cc docker-compose.yml
index cf747434,50e80f36..856efbc4
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@@ -78,7 -78,7 +78,7 @@@ services
- clamd
rspamd-mailcow:
- image: mailcow/rspamd:1.22
- image: mailcow/rspamd:1.21
++ image: mailcow/rspamd:1.23
build: ./data/Dockerfiles/rspamd
stop_grace_period: 30s
depends_on: