diff --git a/data/Dockerfiles/rspamd/dkim_signing.lua b/data/Dockerfiles/rspamd/dkim_signing.lua index 1d713e3e..b76a73ac 100644 --- a/data/Dockerfiles/rspamd/dkim_signing.lua +++ b/data/Dockerfiles/rspamd/dkim_signing.lua @@ -1,287 +1,286 @@ ---[[ -Copyright (c) 2016, Andrew Lewis -Copyright (c) 2016, Vsevolod Stakhov - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -]]-- - -local rspamd_logger = require "rspamd_logger" -local rspamd_util = require "rspamd_util" - -if confighelp then - return -end - -local settings = { - allow_envfrom_empty = true, - allow_hdrfrom_mismatch = false, - allow_hdrfrom_mismatch_local = false, - allow_hdrfrom_mismatch_sign_networks = false, - allow_hdrfrom_multiple = false, - allow_username_mismatch = false, - auth_only = true, - domain = {}, - path = string.format('%s/%s/%s', rspamd_paths['DBDIR'], 'dkim', '$domain.$selector.key'), - sign_local = true, - selector = 'dkim', - symbol = 'DKIM_SIGNED', - try_fallback = true, - use_domain = 'header', - use_esld = true, - use_redis = false, - key_prefix = 'dkim_keys', -- default hash name -} - -local E = {} -local N = 'dkim_signing' -local redis_params - -local function simple_template(tmpl, keys) - local lpeg = require "lpeg" - - local var_lit = lpeg.P { lpeg.R("az") + lpeg.R("AZ") + lpeg.R("09") + "_" } - local var = lpeg.P { (lpeg.P("$") / "") * ((var_lit^1) / keys) } - local var_braced = lpeg.P { (lpeg.P("${") / "") * ((var_lit^1) / keys) * (lpeg.P("}") / "") } - - local template_grammar = lpeg.Cs((var + var_braced + 1)^0) - - return lpeg.match(template_grammar, tmpl) -end - -local function dkim_signing_cb(task) - local is_local, is_sign_networks - local auser = task:get_user() - local ip = task:get_from_ip() - if ip and ip:is_local() then - is_local = true - end - if settings.auth_only and not auser then - if (settings.sign_networks and settings.sign_networks:get_key(ip)) then - is_sign_networks = true - rspamd_logger.debugm(N, task, 'mail is from address in sign_networks') - elseif settings.sign_local and is_local then - rspamd_logger.debugm(N, task, 'mail is from local address') - else - rspamd_logger.debugm(N, task, 'ignoring unauthenticated mail') - return - end - end - local efrom = task:get_from('smtp') - if not settings.allow_envfrom_empty and - #(((efrom or E)[1] or E).addr or '') == 0 then - rspamd_logger.debugm(N, task, 'empty envelope from not allowed') - return false - end - local hfrom = task:get_from('mime') - if not settings.allow_hdrfrom_multiple and (hfrom or E)[2] then - rspamd_logger.debugm(N, task, 'multiple header from not allowed') - return false - end - local dkim_domain - local hdom = ((hfrom or E)[1] or E).domain - local edom = ((efrom or E)[1] or E).domain - if hdom then - hdom = hdom:lower() - end - if edom then - edom = edom:lower() - end - if settings.use_domain_sign_networks and is_sign_networks then - if settings.use_domain_sign_networks == 'header' then - dkim_domain = hdom - else - dkim_domain = edom - end - elseif settings.use_domain_local and is_local then - if settings.use_domain_local == 'header' then - dkim_domain = hdom - else - dkim_domain = edom - end - else - if settings.use_domain == 'header' then - dkim_domain = hdom - else - dkim_domain = edom - end - end - if not dkim_domain then - rspamd_logger.debugm(N, task, 'could not extract dkim domain') - return false - end - if settings.use_esld then - dkim_domain = rspamd_util.get_tld(dkim_domain) - if settings.use_domain == 'envelope' and hdom then - hdom = rspamd_util.get_tld(hdom) - elseif settings.use_domain == 'header' and edom then - edom = rspamd_util.get_tld(edom) - end - end - if edom and hdom and not settings.allow_hdrfrom_mismatch and hdom ~= edom then - if settings.allow_hdrfrom_mismatch_local and is_local then - rspamd_logger.debugm(N, task, 'domain mismatch allowed for local IP: %1 != %2', hdom, edom) - elseif settings.allow_hdrfrom_mismatch_sign_networks and is_sign_networks then - rspamd_logger.debugm(N, task, 'domain mismatch allowed for sign_networks: %1 != %2', hdom, edom) - else - rspamd_logger.debugm(N, task, 'domain mismatch not allowed: %1 != %2', hdom, edom) - return false - end - end - if auser and not settings.allow_username_mismatch then - local udom = string.match(auser, '.*@(.*)') - if not udom then - rspamd_logger.debugm(N, task, 'couldnt find domain in username') - return false - end - if settings.use_esld then - udom = rspamd_util.get_tld(udom) - end - if udom ~= dkim_domain then - rspamd_logger.debugm(N, task, 'user domain mismatch') - return false - end - end - local p = {} - if settings.domain[dkim_domain] then - p.selector = settings.domain[dkim_domain].selector - p.key = settings.domain[dkim_domain].path - end - if not (p.key and p.selector) and not - (settings.try_fallback or settings.use_redis or settings.selector_map or settings.path_map) then - rspamd_logger.debugm(N, task, 'dkim unconfigured and fallback disabled') - return false - end - if not p.key then - if not settings.use_redis then - p.key = settings.path - end - end - if not p.selector then - p.selector = settings.selector - end - p.domain = dkim_domain - - if settings.selector_map then - local data = settings.selector_map:get_key(dkim_domain) - if data then - p.selector = data - end - end - - if settings.path_map then - local data = settings.path_map:get_key(dkim_domain) - if data then - p.key = data - end - end - - if settings.use_redis then - local function try_redis_key(selector) - p.key = nil - p.selector = selector - local rk = string.format('%s.%s', p.selector, p.domain) - local function redis_key_cb(err, data) - if err or type(data) ~= 'string' then - rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM key for %s: %s", - rk, err) - else - p.rawkey = data - if rspamd_plugins.dkim.sign(task, p) then - task:insert_result(settings.symbol, 1.0) - end - end - end - local ret = rspamd_redis_make_request(task, - redis_params, -- connect params - rk, -- hash key - false, -- is write - redis_key_cb, --callback - 'HGET', -- command - {settings.key_prefix, rk} -- arguments - ) - if not ret then - rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM key for %s", rk) - end - end - if settings.selector_prefix then - rspamd_logger.infox(rspamd_config, "Using selector prefix %s for domain %s", settings.selector_prefix, p.domain); - local function redis_selector_cb(err, data) - if err or type(data) ~= 'string' then - rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM selector for domain %s: %s", p.domain, err) - else - try_redis_key(data) - end - end - local ret = rspamd_redis_make_request(task, - redis_params, -- connect params - p.domain, -- hash key - false, -- is write - redis_selector_cb, --callback - 'HGET', -- command - {settings.selector_prefix, p.domain} -- arguments - ) - if not ret then - rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM selector for %s", p.domain) - end - else - if not p.selector then - rspamd_logger.errx(task, 'No selector specified') - return false - end - try_redis_key(p.selector) - end - else - if (p.key and p.selector) then - p.key = simple_template(p.key, {domain = p.domain, selector = p.selector}) - return rspamd_plugins.dkim.sign(task, p) - else - rspamd_logger.infox(task, 'key path or dkim selector unconfigured; no signing') - return false - end - end -end - -local opts = rspamd_config:get_all_opt('dkim_signing') -if not opts then return end -for k,v in pairs(opts) do - if k == 'sign_networks' then - settings[k] = rspamd_map_add(N, k, 'radix', 'DKIM signing networks') - elseif k == 'path_map' then - settings[k] = rspamd_map_add(N, k, 'map', 'Paths to DKIM signing keys') - elseif k == 'selector_map' then - settings[k] = rspamd_map_add(N, k, 'map', 'DKIM selectors') - else - settings[k] = v - end -end -if not (settings.use_redis or settings.path or settings.domain or settings.path_map or settings.selector_map) then - rspamd_logger.infox(rspamd_config, 'mandatory parameters missing, disable dkim signing') - return -end -if settings.use_redis then - redis_params = rspamd_parse_redis_server('dkim_signing') - - if not redis_params then - rspamd_logger.errx(rspamd_config, 'no servers are specified, but module is configured to load keys from redis, disable dkim signing') - return - end -end -if settings.use_domain ~= 'header' and settings.use_domain ~= 'envelope' then - rspamd_logger.errx(rspamd_config, "Value for 'use_domain' is invalid") - settings.use_domain = 'header' -end - -rspamd_config:register_symbol({ - name = settings['symbol'], - callback = dkim_signing_cb -}) +--[[ +Copyright (c) 2016, Andrew Lewis +Copyright (c) 2016, Vsevolod Stakhov + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]]-- + +local rspamd_logger = require "rspamd_logger" +local rspamd_util = require "rspamd_util" + +if confighelp then + return +end + +local settings = { + allow_envfrom_empty = true, + allow_hdrfrom_mismatch = false, + allow_hdrfrom_mismatch_local = false, + allow_hdrfrom_mismatch_sign_networks = false, + allow_hdrfrom_multiple = false, + allow_username_mismatch = false, + auth_only = true, + domain = {}, + path = string.format('%s/%s/%s', rspamd_paths['DBDIR'], 'dkim', '$domain.$selector.key'), + sign_local = true, + selector = 'dkim', + symbol = 'DKIM_SIGNED', + try_fallback = true, + use_domain = 'header', + use_esld = true, + use_redis = false, + key_prefix = 'dkim_keys', -- default hash name +} + +local E = {} +local N = 'dkim_signing' +local redis_params + +local function simple_template(tmpl, keys) + local lpeg = require "lpeg" + + local var_lit = lpeg.P { lpeg.R("az") + lpeg.R("AZ") + lpeg.R("09") + "_" } + local var = lpeg.P { (lpeg.P("$") / "") * ((var_lit^1) / keys) } + local var_braced = lpeg.P { (lpeg.P("${") / "") * ((var_lit^1) / keys) * (lpeg.P("}") / "") } + + local template_grammar = lpeg.Cs((var + var_braced + 1)^0) + + return lpeg.match(template_grammar, tmpl) +end + +local function dkim_signing_cb(task) + local is_local, is_sign_networks + local auser = task:get_user() + local ip = task:get_from_ip() + if ip and ip:is_local() then + is_local = true + end + if settings.auth_only and not auser then + if (settings.sign_networks and settings.sign_networks:get_key(ip)) then + is_sign_networks = true + rspamd_logger.debugm(N, task, 'mail is from address in sign_networks') + elseif settings.sign_local and is_local then + rspamd_logger.debugm(N, task, 'mail is from local address') + else + rspamd_logger.debugm(N, task, 'ignoring unauthenticated mail') + return + end + end + local efrom = task:get_from('smtp') + if not settings.allow_envfrom_empty and + #(((efrom or E)[1] or E).addr or '') == 0 then + rspamd_logger.debugm(N, task, 'empty envelope from not allowed') + return false + end + local hfrom = task:get_from('mime') + if not settings.allow_hdrfrom_multiple and (hfrom or E)[2] then + rspamd_logger.debugm(N, task, 'multiple header from not allowed') + return false + end + local dkim_domain + local hdom = ((hfrom or E)[1] or E).domain + local edom = ((efrom or E)[1] or E).domain + if hdom then + hdom = hdom:lower() + end + if edom then + edom = edom:lower() + end + if settings.use_domain_sign_networks and is_sign_networks then + if settings.use_domain_sign_networks == 'header' then + dkim_domain = hdom + else + dkim_domain = edom + end + elseif settings.use_domain_local and is_local then + if settings.use_domain_local == 'header' then + dkim_domain = hdom + else + dkim_domain = edom + end + else + if settings.use_domain == 'header' then + dkim_domain = hdom + else + dkim_domain = edom + end + end + if not dkim_domain then + rspamd_logger.debugm(N, task, 'could not extract dkim domain') + return false + end + if settings.use_esld then + dkim_domain = rspamd_util.get_tld(dkim_domain) + if settings.use_domain == 'envelope' and hdom then + hdom = rspamd_util.get_tld(hdom) + elseif settings.use_domain == 'header' and edom then + edom = rspamd_util.get_tld(edom) + end + end + if edom and hdom and not settings.allow_hdrfrom_mismatch and hdom ~= edom then + if settings.allow_hdrfrom_mismatch_local and is_local then + rspamd_logger.debugm(N, task, 'domain mismatch allowed for local IP: %1 != %2', hdom, edom) + elseif settings.allow_hdrfrom_mismatch_sign_networks and is_sign_networks then + rspamd_logger.debugm(N, task, 'domain mismatch allowed for sign_networks: %1 != %2', hdom, edom) + else + rspamd_logger.debugm(N, task, 'domain mismatch not allowed: %1 != %2', hdom, edom) + return false + end + end + if auser and not settings.allow_username_mismatch then + local udom = string.match(auser, '.*@(.*)') + if not udom then + rspamd_logger.debugm(N, task, 'couldnt find domain in username') + return false + end + if settings.use_esld then + udom = rspamd_util.get_tld(udom) + end + if udom ~= dkim_domain then + rspamd_logger.debugm(N, task, 'user domain mismatch') + return false + end + end + local p = {} + if settings.domain[dkim_domain] then + p.selector = settings.domain[dkim_domain].selector + p.key = settings.domain[dkim_domain].path + end + if not (p.key and p.selector) and not + (settings.try_fallback or settings.use_redis or settings.selector_map or settings.path_map) then + rspamd_logger.debugm(N, task, 'dkim unconfigured and fallback disabled') + return false + end + if not p.key then + if not settings.use_redis then + p.key = settings.path + end + end + if not p.selector then + p.selector = settings.selector + end + p.domain = dkim_domain + + if settings.selector_map then + local data = settings.selector_map:get_key(dkim_domain) + if data then + p.selector = data + end + end + if settings.path_map then + local data = settings.path_map:get_key(dkim_domain) + if data then + p.key = data + end + end + + if settings.use_redis then + local function try_redis_key(selector) + p.key = nil + p.selector = selector + local rk = string.format('%s.%s', p.selector, p.domain) + local function redis_key_cb(err, data) + if err or type(data) ~= 'string' then + rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM key for %s: %s", + rk, err) + else + p.rawkey = data + if rspamd_plugins.dkim.sign(task, p) then + task:insert_result(settings.symbol, 1.0) + end + end + end + local ret = rspamd_redis_make_request(task, + redis_params, -- connect params + rk, -- hash key + false, -- is write + redis_key_cb, --callback + 'HGET', -- command + {settings.key_prefix, rk} -- arguments + ) + if not ret then + rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM key for %s", rk) + end + end + if settings.selector_prefix then + rspamd_logger.infox(rspamd_config, "Using selector prefix %s for domain %s", settings.selector_prefix, p.domain); + local function redis_selector_cb(err, data) + if err or type(data) ~= 'string' then + rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM selector for domain %s: %s", p.domain, err) + else + try_redis_key(data) + end + end + local ret = rspamd_redis_make_request(task, + redis_params, -- connect params + p.domain, -- hash key + false, -- is write + redis_selector_cb, --callback + 'HGET', -- command + {settings.selector_prefix, p.domain} -- arguments + ) + if not ret then + rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM selector for %s", p.domain) + end + else + if not p.selector then + rspamd_logger.errx(task, 'No selector specified') + return false + end + try_redis_key(p.selector) + end + else + if (p.key and p.selector) then + p.key = simple_template(p.key, {domain = p.domain, selector = p.selector}) + return rspamd_plugins.dkim.sign(task, p) + else + rspamd_logger.infox(task, 'key path or dkim selector unconfigured; no signing') + return false + end + end +end + +local opts = rspamd_config:get_all_opt('dkim_signing') +if not opts then return end +for k,v in pairs(opts) do + if k == 'sign_networks' then + settings[k] = rspamd_map_add(N, k, 'radix', 'DKIM signing networks') + elseif k == 'path_map' then + settings[k] = rspamd_map_add(N, k, 'map', 'Paths to DKIM signing keys') + elseif k == 'selector_map' then + settings[k] = rspamd_map_add(N, k, 'map', 'DKIM selectors') + else + settings[k] = v + end +end +if not (settings.use_redis or settings.path or settings.domain or settings.path_map or settings.selector_map) then + rspamd_logger.infox(rspamd_config, 'mandatory parameters missing, disable dkim signing') + return +end +if settings.use_redis then + redis_params = rspamd_parse_redis_server('dkim_signing') + + if not redis_params then + rspamd_logger.errx(rspamd_config, 'no servers are specified, but module is configured to load keys from redis, disable dkim signing') + return + end +end +if settings.use_domain ~= 'header' and settings.use_domain ~= 'envelope' then + rspamd_logger.errx(rspamd_config, "Value for 'use_domain' is invalid") + settings.use_domain = 'header' +end + +rspamd_config:register_symbol({ + name = settings['symbol'], + callback = dkim_signing_cb +})