aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorVsevolod Stakhov <vsevolod@rspamd.com>2024-09-17 19:17:14 +0600
committerGitHub <noreply@github.com>2024-09-17 19:17:14 +0600
commit3dda59641af8826d50dd07bc82d67c9ffecef403 (patch)
tree59c47ac8e260cdac03431445879149c6bb472495 /src
parentf7cac80b96de3e6a49109602f9622d00c26b4ec9 (diff)
parent986814257147e9e79df032f5f157d90c0ee430e4 (diff)
downloadrspamd-3dda59641af8826d50dd07bc82d67c9ffecef403.tar.gz
rspamd-3dda59641af8826d50dd07bc82d67c9ffecef403.zip
Merge branch 'master' into vstakhov-utf8-mime
Diffstat (limited to 'src')
-rw-r--r--src/fuzzy_storage.c328
-rw-r--r--src/lua/lua_cryptobox.c20
-rw-r--r--src/lua/lua_rsa.c18
-rw-r--r--src/plugins/fuzzy_check.c110
-rw-r--r--src/plugins/lua/aws_s3.lua2
-rw-r--r--src/plugins/lua/bimi.lua4
-rw-r--r--src/plugins/lua/history_redis.lua2
-rw-r--r--src/plugins/lua/ratelimit.lua143
8 files changed, 380 insertions, 247 deletions
diff --git a/src/fuzzy_storage.c b/src/fuzzy_storage.c
index 5fd3303dc..841d040b2 100644
--- a/src/fuzzy_storage.c
+++ b/src/fuzzy_storage.c
@@ -128,11 +128,17 @@ KHASH_SET_INIT_INT(fuzzy_key_ids_set);
KHASH_INIT(fuzzy_key_flag_stat, int, struct fuzzy_key_stat, 1, kh_int_hash_func,
kh_int_hash_equal);
struct fuzzy_key {
+ char *name;
struct rspamd_cryptobox_keypair *key;
struct rspamd_cryptobox_pubkey *pk;
struct fuzzy_key_stat *stat;
khash_t(fuzzy_key_flag_stat) * flags_stat;
khash_t(fuzzy_key_ids_set) * forbidden_ids;
+ struct rspamd_leaky_bucket_elt *rl_bucket;
+ double burst;
+ double rate;
+ ev_tstamp expire;
+ bool expired;
ref_entry_t ref;
};
@@ -258,7 +264,8 @@ static gboolean rspamd_fuzzy_check_client(struct rspamd_fuzzy_storage_ctx *ctx,
static void rspamd_fuzzy_maybe_call_blacklisted(struct rspamd_fuzzy_storage_ctx *ctx,
rspamd_inet_addr_t *addr,
const char *reason);
-static struct fuzzy_key *fuzzy_add_keypair_from_ucl(const ucl_object_t *obj,
+static struct fuzzy_key *fuzzy_add_keypair_from_ucl(struct rspamd_config *cfg,
+ const ucl_object_t *obj,
khash_t(rspamd_fuzzy_keys_hash) * target);
struct fuzzy_keymap_ucl_buf {
@@ -366,7 +373,7 @@ ucl_keymap_fin_cb(struct map_cb_data *data, void **target)
while ((cur = ucl_object_iterate(top, &it, true)) != NULL) {
struct fuzzy_key *nk;
- nk = fuzzy_add_keypair_from_ucl(cur, jb->ctx->dynamic_keys);
+ nk = fuzzy_add_keypair_from_ucl(cfg, cur, jb->ctx->dynamic_keys);
if (nk == NULL) {
msg_warn_config("cannot add dynamic keypair");
@@ -404,6 +411,78 @@ ucl_keymap_dtor_cb(struct map_cb_data *data)
}
}
+enum rspamd_ratelimit_check_result {
+ ratelimit_pass,
+ ratelimit_new,
+ ratelimit_existing,
+};
+
+enum rspamd_ratelimit_check_policy {
+ ratelimit_policy_permanent,
+ ratelimit_policy_normal,
+};
+
+static enum rspamd_ratelimit_check_result
+rspamd_fuzzy_check_ratelimit_bucket(struct fuzzy_session *session, struct rspamd_leaky_bucket_elt *elt,
+ enum rspamd_ratelimit_check_policy policy, double max_burst, double max_rate)
+{
+ gboolean ratelimited = FALSE, new_ratelimit = FALSE;
+
+ if (isnan(elt->cur)) {
+ /* There is an issue with the previous logic: the TTL is updated each time
+ * we see that new bucket. Hence, we need to check the `last` and act accordingly
+ */
+ if (elt->last < session->timestamp && session->timestamp - elt->last >= session->ctx->leaky_bucket_ttl) {
+ /*
+ * We reset bucket to it's 90% capacity to allow some requests
+ * This should cope with the issue when we block an IP network for some burst and never unblock it
+ */
+ elt->cur = max_burst * 0.9;
+ elt->last = session->timestamp;
+ }
+ else {
+ ratelimited = TRUE;
+ }
+ }
+ else {
+ /* Update bucket: leak some elements */
+ if (elt->last < session->timestamp) {
+ elt->cur -= max_rate * (session->timestamp - elt->last);
+ elt->last = session->timestamp;
+
+ if (elt->cur < 0) {
+ elt->cur = 0;
+ }
+ }
+ else {
+ elt->last = session->timestamp;
+ }
+
+ /* Check the bucket */
+ if (elt->cur >= max_burst) {
+
+ if (policy == ratelimit_policy_permanent) {
+ elt->cur = NAN;
+ }
+ new_ratelimit = TRUE;
+ ratelimited = TRUE;
+ }
+ else {
+ elt->cur++; /* Allow one more request */
+ }
+ }
+
+ if (ratelimited) {
+ rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit");
+ }
+
+ if (new_ratelimit) {
+ return ratelimit_new;
+ }
+
+ return ratelimited ? ratelimit_existing : ratelimit_pass;
+}
+
static gboolean
rspamd_fuzzy_check_ratelimit(struct fuzzy_session *session)
{
@@ -443,59 +522,17 @@ rspamd_fuzzy_check_ratelimit(struct fuzzy_session *session)
(time_t) session->timestamp);
if (elt) {
- gboolean ratelimited = FALSE, new_ratelimit = FALSE;
-
- if (isnan(elt->cur)) {
- /* There is an issue with the previous logic: the TTL is updated each time
- * we see that new bucket. Hence, we need to check the `last` and act accordingly
- */
- if (elt->last < session->timestamp && session->timestamp - elt->last >= session->ctx->leaky_bucket_ttl) {
- /*
- * We reset bucket to it's 90% capacity to allow some requests
- * This should cope with the issue when we block an IP network for some burst and never unblock it
- */
- elt->cur = session->ctx->leaky_bucket_burst * 0.9;
- elt->last = session->timestamp;
- }
- else {
- ratelimited = TRUE;
- }
- }
- else {
- /* Update bucket: leak some elements */
- if (elt->last < session->timestamp) {
- elt->cur -= session->ctx->leaky_bucket_rate * (session->timestamp - elt->last);
- elt->last = session->timestamp;
-
- if (elt->cur < 0) {
- elt->cur = 0;
- }
- }
- else {
- elt->last = session->timestamp;
- }
-
- /* Check the bucket */
- if (elt->cur >= session->ctx->leaky_bucket_burst) {
-
- msg_info("ratelimiting %s (%s), %.1f max elts",
- rspamd_inet_address_to_string(session->addr),
- rspamd_inet_address_to_string(masked),
- session->ctx->leaky_bucket_burst);
- elt->cur = NAN;
- new_ratelimit = TRUE;
- ratelimited = TRUE;
- }
- else {
- elt->cur++; /* Allow one more request */
- }
- }
+ enum rspamd_ratelimit_check_result res = rspamd_fuzzy_check_ratelimit_bucket(session, elt,
+ ratelimit_policy_permanent,
+ session->ctx->leaky_bucket_burst,
+ session->ctx->leaky_bucket_rate);
- if (ratelimited) {
- rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit");
- }
+ if (res == ratelimit_new) {
+ msg_info("ratelimiting %s (%s), %.1f max elts",
+ rspamd_inet_address_to_string(session->addr),
+ rspamd_inet_address_to_string(masked),
+ session->ctx->leaky_bucket_burst);
- if (new_ratelimit) {
struct rspamd_srv_command srv_cmd;
srv_cmd.type = RSPAMD_SRV_FUZZY_BLOCKED;
@@ -514,11 +551,16 @@ rspamd_fuzzy_check_ratelimit(struct fuzzy_session *session)
msg_err("bad address length: %d, expected to be %d", (int) slen, (int) sizeof(srv_cmd.cmd.fuzzy_blocked.addr));
}
}
+
+ rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit");
+ }
+ else if (res == ratelimit_existing) {
+ rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit");
}
rspamd_inet_address_free(masked);
- return !ratelimited;
+ return res == ratelimit_pass;
}
else {
/* New bucket */
@@ -659,6 +701,15 @@ fuzzy_key_dtor(gpointer p)
kh_destroy(fuzzy_key_ids_set, key->forbidden_ids);
}
+ if (key->rl_bucket) {
+ /* TODO: save bucket stats */
+ g_free(key->rl_bucket);
+ }
+
+ if (key->name) {
+ g_free(key->name);
+ }
+
g_free(key);
}
}
@@ -1464,7 +1515,14 @@ rspamd_fuzzy_process_command(struct fuzzy_session *session)
if (session->ctx->encrypted_only && !encrypted) {
/* Do not accept unencrypted commands */
- result.v1.value = 403;
+ result.v1.value = 415;
+ result.v1.prob = 0.0f;
+ rspamd_fuzzy_make_reply(cmd, &result, session, send_flags);
+ return;
+ }
+
+ if (!rspamd_fuzzy_check_client(session->ctx, session->addr)) {
+ result.v1.value = 503;
result.v1.prob = 0.0f;
rspamd_fuzzy_make_reply(cmd, &result, session, send_flags);
return;
@@ -1487,23 +1545,95 @@ rspamd_fuzzy_process_command(struct fuzzy_session *session)
}
if (cmd->cmd == FUZZY_CHECK) {
- bool can_continue = true;
+ bool is_rate_allowed = true;
if (session->ctx->ratelimit_buckets) {
if (session->ctx->ratelimit_log_only) {
(void) rspamd_fuzzy_check_ratelimit(session); /* Check but ignore */
}
else {
- can_continue = rspamd_fuzzy_check_ratelimit(session);
+ is_rate_allowed = rspamd_fuzzy_check_ratelimit(session);
+ }
+ }
+
+ if (session->key && session->key->rl_bucket) {
+ /* Check per-key bucket */
+
+ enum rspamd_ratelimit_check_result res = rspamd_fuzzy_check_ratelimit_bucket(session, session->key->rl_bucket,
+ ratelimit_policy_normal,
+ session->key->burst,
+ session->key->rate);
+
+ if (res == ratelimit_new) {
+ msg_info("ratelimiting key %s %.1f max elts",
+ session->key->name ? session->key->name : "unknown",
+ session->key->burst);
+
+ struct rspamd_srv_command srv_cmd;
+
+ srv_cmd.type = RSPAMD_SRV_FUZZY_BLOCKED;
+ srv_cmd.cmd.fuzzy_blocked.af = rspamd_inet_address_get_af(session->addr);
+
+ if (srv_cmd.cmd.fuzzy_blocked.af == AF_INET || srv_cmd.cmd.fuzzy_blocked.af == AF_INET6) {
+ socklen_t slen;
+ struct sockaddr *sa = rspamd_inet_address_get_sa(session->addr, &slen);
+
+ if (slen <= sizeof(srv_cmd.cmd.fuzzy_blocked.addr)) {
+ memcpy(&srv_cmd.cmd.fuzzy_blocked.addr, sa, slen);
+ msg_debug("propagating blocked address to other workers");
+ rspamd_srv_send_command(session->worker,
+ session->ctx->event_loop,
+ &srv_cmd, -1, NULL, NULL);
+ }
+ else {
+ msg_err("bad address length: %d, expected to be %d",
+ (int) slen, (int) sizeof(srv_cmd.cmd.fuzzy_blocked.addr));
+ }
+ }
+
+ rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit");
+ is_rate_allowed = session->ctx->ratelimit_log_only ? true : false;
+ }
+ else if (res == ratelimit_existing) {
+ rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit");
+ is_rate_allowed = session->ctx->ratelimit_log_only ? true : false;
+ }
+ }
+
+ if (session->key && !isnan(session->key->expire)) {
+ /* Check expire */
+ static ev_tstamp today = NAN;
+
+ /*
+ * Update `today` sometimes
+ */
+ if (isnan(today)) {
+ today = ev_time();
+ }
+ else if (rspamd_random_uint64_fast() > 0xFFFF000000000000ULL) {
+ today = ev_time();
+ }
+
+ if (today > session->key->expire) {
+ if (!session->key->expired) {
+ msg_info("key %s is expired", session->key->name);
+ session->key->expired = true;
+ }
+
+ result.v1.value = 503;
+ result.v1.prob = 0.0f;
+ rspamd_fuzzy_make_reply(cmd, &result, session, send_flags);
+ return;
}
}
- if (can_continue) {
+ if (is_rate_allowed) {
REF_RETAIN(session);
rspamd_fuzzy_backend_check(session->ctx->backend, cmd,
rspamd_fuzzy_check_callback, session);
}
else {
+ /* Should be 429 but we keep compatibility */
result.v1.value = 403;
result.v1.prob = 0.0f;
result.v1.flag = 0;
@@ -1574,7 +1704,7 @@ rspamd_fuzzy_process_command(struct fuzzy_session *session)
result.v1.prob = 1.0f;
}
else {
- result.v1.value = 403;
+ result.v1.value = 503;
result.v1.prob = 0.0f;
}
reply:
@@ -2041,11 +2171,6 @@ accept_fuzzy_socket(EV_P_ ev_io *w, int revents)
if (MSG_FIELD(msg[i], msg_namelen) >= sizeof(struct sockaddr)) {
client_addr = rspamd_inet_address_from_sa(MSG_FIELD(msg[i], msg_name),
MSG_FIELD(msg[i], msg_namelen));
- if (!rspamd_fuzzy_check_client(worker->ctx, client_addr)) {
- /* Disallow forbidden clients silently */
- rspamd_inet_address_free(client_addr);
- continue;
- }
}
else {
client_addr = NULL;
@@ -2761,7 +2886,8 @@ fuzzy_parse_ids(rspamd_mempool_t *pool,
}
static struct fuzzy_key *
-fuzzy_add_keypair_from_ucl(const ucl_object_t *obj, khash_t(rspamd_fuzzy_keys_hash) * target)
+fuzzy_add_keypair_from_ucl(struct rspamd_config *cfg, const ucl_object_t *obj,
+ khash_t(rspamd_fuzzy_keys_hash) * target)
{
struct rspamd_cryptobox_keypair *kp = rspamd_keypair_from_ucl(obj);
@@ -2785,6 +2911,10 @@ fuzzy_add_keypair_from_ucl(const ucl_object_t *obj, khash_t(rspamd_fuzzy_keys_ha
rspamd_inet_address_hash, rspamd_inet_address_equal);
key->stat = keystat;
key->flags_stat = kh_init(fuzzy_key_flag_stat);
+ key->burst = NAN;
+ key->rate = NAN;
+ key->expire = NAN;
+ key->rl_bucket = NULL;
/* Preallocate some space for flags */
kh_resize(fuzzy_key_flag_stat, key->flags_stat, 8);
const unsigned char *pk = rspamd_keypair_component(kp, RSPAMD_KEYPAIR_COMPONENT_PK,
@@ -2816,6 +2946,7 @@ fuzzy_add_keypair_from_ucl(const ucl_object_t *obj, khash_t(rspamd_fuzzy_keys_ha
const ucl_object_t *extensions = rspamd_keypair_get_extensions(kp);
if (extensions) {
+ lua_State *L = RSPAMD_LUA_CFG_STATE(cfg);
const ucl_object_t *forbidden_ids = ucl_object_lookup(extensions, "forbidden_ids");
if (forbidden_ids && ucl_object_type(forbidden_ids) == UCL_ARRAY) {
@@ -2832,9 +2963,72 @@ fuzzy_add_keypair_from_ucl(const ucl_object_t *obj, khash_t(rspamd_fuzzy_keys_ha
}
}
}
+
+ const ucl_object_t *ratelimit = ucl_object_lookup(extensions, "ratelimit");
+
+ static int ratelimit_lua_id = -1;
+
+ if (ratelimit_lua_id == -1) {
+ /* Load ratelimit parsing function */
+ if (!rspamd_lua_require_function(L, "plugins/ratelimit", "parse_limit")) {
+ msg_err_config("cannot load ratelimit parser from ratelimit plugin");
+ }
+ else {
+ ratelimit_lua_id = luaL_ref(L, LUA_REGISTRYINDEX);
+ }
+ }
+
+ if (ratelimit && ratelimit_lua_id != -1) {
+ lua_rawgeti(L, LUA_REGISTRYINDEX, ratelimit_lua_id);
+ lua_pushstring(L, "fuzzy_key_ratelimit");
+ ucl_object_push_lua(L, ratelimit, false);
+
+ if (lua_pcall(L, 2, 1, 0) != 0) {
+ msg_err_config("cannot call ratelimit parser from ratelimit plugin");
+ }
+ else {
+ if (lua_type(L, -1) == LUA_TTABLE) {
+ /*
+ * The returned table is in form { rate = xx, burst = yy }
+ */
+ lua_getfield(L, -1, "rate");
+ key->rate = lua_tonumber(L, -1);
+ lua_pop(L, 1);
+
+ lua_getfield(L, -1, "burst");
+ key->burst = lua_tonumber(L, -1);
+ lua_pop(L, 1);
+
+ key->rl_bucket = g_malloc0(sizeof(*key->rl_bucket));
+ }
+ }
+
+ lua_settop(L, 0);
+ }
+
+ const ucl_object_t *expire = ucl_object_lookup(extensions, "expire");
+ if (expire && ucl_object_type(expire) == UCL_STRING) {
+ struct tm tm;
+
+ /* DD-MM-YYYY */
+ char *end = strptime(ucl_object_tostring(expire), "%d-%m-%Y", &tm);
+
+ if (end != NULL && *end != '\0') {
+ msg_err_config("cannot parse expire date: %s", ucl_object_tostring(expire));
+ }
+ else {
+ key->expire = mktime(&tm);
+ }
+ }
+
+ const ucl_object_t *name = ucl_object_lookup(extensions, "name");
+ if (name && ucl_object_type(name) == UCL_STRING) {
+ key->name = g_strdup(ucl_object_tostring(name));
+ }
}
- msg_debug("loaded keypair %*bs", crypto_box_publickeybytes(), pk);
+ msg_debug("loaded keypair %*bs; expire=%f; rate=%f; burst=%f; name=%s", (int) crypto_box_publickeybytes(), pk,
+ key->expire, key->rate, key->burst, key->name);
return key;
}
@@ -2867,7 +3061,7 @@ fuzzy_parse_keypair(rspamd_mempool_t *pool,
return ret;
}
- key = fuzzy_add_keypair_from_ucl(obj, ctx->keys);
+ key = fuzzy_add_keypair_from_ucl(ctx->cfg, obj, ctx->keys);
if (key == NULL) {
return FALSE;
diff --git a/src/lua/lua_cryptobox.c b/src/lua/lua_cryptobox.c
index fbd44cecd..c9cac1562 100644
--- a/src/lua/lua_cryptobox.c
+++ b/src/lua/lua_cryptobox.c
@@ -998,25 +998,13 @@ rspamd_lua_ssl_hmac_create(struct rspamd_lua_cryptobox_hash *h, const EVP_MD *ht
bool insecure)
{
h->type = LUA_CRYPTOBOX_HASH_HMAC;
- OSSL_PROVIDER *dflt = OSSL_PROVIDER_load(NULL, "default");
-
-#if OPENSSL_VERSION_NUMBER > 0x10100000L
- if (insecure) {
- /* Should never ever be used for crypto/security purposes! */
-#ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW
-#if OPENSSL_VERSION_MAJOR >= 3
- OSSL_PROVIDER *fips = OSSL_PROVIDER_load(NULL, "fips");
-#endif
- }
-#endif
-#endif
#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
(defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x30500000)
h->content.hmac_c = g_malloc0(sizeof(*h->content.hmac_c));
#else
#if OPENSSL_VERSION_MAJOR >= 3
- EVP_MAC* mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
+ EVP_MAC *mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
h->content.hmac_c = EVP_MAC_CTX_new(mac);
EVP_MAC_free(mac);
#else
@@ -1038,7 +1026,7 @@ rspamd_lua_ssl_hmac_create(struct rspamd_lua_cryptobox_hash *h, const EVP_MD *ht
h->out_len = EVP_MD_size(htype);
#if OPENSSL_VERSION_MAJOR >= 3
OSSL_PARAM params[2];
- params[0] = OSSL_PARAM_construct_utf8_string("digest", EVP_MD_get0_name(htype), 0);
+ params[0] = OSSL_PARAM_construct_utf8_string("digest", (char *) EVP_MD_get0_name(htype), 0);
params[1] = OSSL_PARAM_construct_end();
EVP_MAC_init(h->content.hmac_c, key, keylen, params);
@@ -1500,7 +1488,7 @@ lua_cryptobox_hash_finish(struct rspamd_lua_cryptobox_hash *h)
g_assert(ssl_outlen <= sizeof(h->out));
memcpy(h->out, out, ssl_outlen);
break;
- case LUA_CRYPTOBOX_HASH_HMAC:
+ case LUA_CRYPTOBOX_HASH_HMAC: {
#if OPENSSL_VERSION_MAJOR >= 3
size_t ssl_outlen_size_t = ssl_outlen;
EVP_MAC_final(h->content.hmac_c, out, &ssl_outlen_size_t, sizeof(out));
@@ -1512,6 +1500,7 @@ lua_cryptobox_hash_finish(struct rspamd_lua_cryptobox_hash *h)
g_assert(ssl_outlen <= sizeof(h->out));
memcpy(h->out, out, ssl_outlen);
break;
+ }
case LUA_CRYPTOBOX_HASH_XXHASH64:
case LUA_CRYPTOBOX_HASH_XXHASH32:
case LUA_CRYPTOBOX_HASH_XXHASH3:
@@ -2520,7 +2509,6 @@ lua_cryptobox_gen_dkim_keypair(lua_State *L)
if (strcmp(alg_str, "rsa") == 0) {
BIGNUM *e;
- RSA *r;
EVP_PKEY *pk;
e = BN_new();
diff --git a/src/lua/lua_rsa.c b/src/lua/lua_rsa.c
index 0c56b223b..b7be612b0 100644
--- a/src/lua/lua_rsa.c
+++ b/src/lua/lua_rsa.c
@@ -1,11 +1,11 @@
-/*-
- * Copyright 2016 Vsevolod Stakhov
+/*
+ * Copyright 2024 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
+ * 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,
@@ -184,7 +184,14 @@ lua_rsa_privkey_save(lua_State *L)
else {
if (f != stdout) {
/* Set secure permissions for the private key file */
- chmod(filename, S_IRUSR | S_IWUSR);
+ if (fchmod(fileno(f), S_IRUSR | S_IWUSR) == -1) {
+ msg_err("cannot set permissions for private key file: %s, %s",
+ filename,
+ strerror(errno));
+ fclose(f);
+ lua_pushboolean(L, FALSE);
+ return 1;
+ }
}
if (strcmp(type, "der") == 0) {
@@ -463,7 +470,6 @@ lua_rsa_privkey_load_base64(lua_State *L)
rspamd_lua_setclass(L, rspamd_rsa_privkey_classname, -1);
*ppkey = pkey;
}
-
}
else {
msg_err("cannot open EVP private key from data, %s",
@@ -706,7 +712,7 @@ lua_rsa_verify_memory(lua_State *L)
if (pkey != NULL && signature != NULL && data != NULL) {
EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new(pkey, NULL);
- g_assert(pctx != NULL);
+ g_assert(pctx != NULL);
g_assert(EVP_PKEY_verify_init(pctx) == 1);
ret = EVP_PKEY_verify(pctx, signature->str, signature->len, data, sz);
diff --git a/src/plugins/fuzzy_check.c b/src/plugins/fuzzy_check.c
index 91b77c702..ece9a91e0 100644
--- a/src/plugins/fuzzy_check.c
+++ b/src/plugins/fuzzy_check.c
@@ -49,6 +49,9 @@
#include "libutil/libev_helper.h"
#define DEFAULT_SYMBOL "R_FUZZY_HASH"
+#define RSPAMD_FUZZY_SYMBOL_FORBIDDEN "FUZZY_FORBIDDEN"
+#define RSPAMD_FUZZY_SYMBOL_RATELIMITED "FUZZY_RATELIMITED"
+#define RSPAMD_FUZZY_SYMBOL_ENCRYPTION_REQUIRED "FUZZY_ENCRYPTION_REQUIRED"
#define DEFAULT_IO_TIMEOUT 1.0
#define DEFAULT_RETRANSMITS 3
@@ -68,6 +71,12 @@ struct fuzzy_mapping {
double weight;
};
+enum fuzzy_rule_mode {
+ fuzzy_rule_read_only,
+ fuzzy_rule_write_only,
+ fuzzy_rule_read_write
+};
+
struct fuzzy_rule {
struct upstream_list *servers;
const char *symbol;
@@ -84,7 +93,7 @@ struct fuzzy_rule {
struct rspamd_cryptobox_pubkey *peer_key;
double max_score;
double weight_threshold;
- gboolean read_only;
+ enum fuzzy_rule_mode mode;
gboolean skip_unknown;
gboolean no_share;
gboolean no_subject;
@@ -328,7 +337,7 @@ fuzzy_rule_new(const char *default_symbol, rspamd_mempool_t *pool)
rspamd_mempool_add_destructor(pool,
(rspamd_mempool_destruct_t) g_hash_table_unref,
rule->mappings);
- rule->read_only = FALSE;
+ rule->mode = fuzzy_rule_read_write;
rule->weight_threshold = NAN;
return rule;
@@ -458,7 +467,26 @@ fuzzy_parse_rule(struct rspamd_config *cfg, const ucl_object_t *obj,
if ((value = ucl_object_lookup(obj, "read_only")) != NULL) {
- rule->read_only = ucl_obj_toboolean(value);
+ rule->mode = ucl_obj_toboolean(value) ? fuzzy_rule_read_only : fuzzy_rule_read_write;
+ }
+
+ if ((value = ucl_object_lookup(obj, "mode")) != NULL) {
+ const char *mode_str = ucl_object_tostring(value);
+
+ if (g_ascii_strcasecmp(mode_str, "read_only") == 0) {
+ rule->mode = fuzzy_rule_read_only;
+ }
+ else if (g_ascii_strcasecmp(mode_str, "write_only") == 0) {
+ rule->mode = fuzzy_rule_write_only;
+ }
+ else if (g_ascii_strcasecmp(mode_str, "read_write") == 0) {
+ rule->mode = fuzzy_rule_read_write;
+ }
+ else {
+ msg_warn_config("unknown mode: %s, use read_write by default",
+ mode_str);
+ rule->mode = fuzzy_rule_read_write;
+ }
}
if ((value = ucl_object_lookup(obj, "skip_unknown")) != NULL) {
@@ -1153,6 +1181,44 @@ int fuzzy_check_module_config(struct rspamd_config *cfg, bool validate)
1,
1);
+ /* Register meta symbols (blocked, ratelimited, etc) */
+ rspamd_symcache_add_symbol(cfg->cache,
+ RSPAMD_FUZZY_SYMBOL_FORBIDDEN, 0, NULL, NULL,
+ SYMBOL_TYPE_VIRTUAL,
+ cb_id);
+ rspamd_config_add_symbol(cfg,
+ RSPAMD_FUZZY_SYMBOL_FORBIDDEN,
+ 0.0,
+ "Fuzzy access denied",
+ "fuzzy",
+ 0,
+ 1,
+ 1);
+ rspamd_symcache_add_symbol(cfg->cache,
+ RSPAMD_FUZZY_SYMBOL_RATELIMITED, 0, NULL, NULL,
+ SYMBOL_TYPE_VIRTUAL,
+ cb_id);
+ rspamd_config_add_symbol(cfg,
+ RSPAMD_FUZZY_SYMBOL_RATELIMITED,
+ 0.0,
+ "Fuzzy rate limit is reached",
+ "fuzzy",
+ 0,
+ 1,
+ 1);
+ rspamd_symcache_add_symbol(cfg->cache,
+ RSPAMD_FUZZY_SYMBOL_ENCRYPTION_REQUIRED, 0, NULL, NULL,
+ SYMBOL_TYPE_VIRTUAL,
+ cb_id);
+ rspamd_config_add_symbol(cfg,
+ RSPAMD_FUZZY_SYMBOL_ENCRYPTION_REQUIRED,
+ 0.0,
+ "Fuzzy encryption is required by a server",
+ "fuzzy",
+ 0,
+ 1,
+ 1);
+
/*
* Here we can have 2 possibilities:
*
@@ -2486,7 +2552,16 @@ fuzzy_check_try_read(struct fuzzy_client_session *session)
}
}
else if (rep->v1.value == 403) {
- rspamd_task_insert_result(task, "FUZZY_BLOCKED", 0.0,
+ /* In fact, it should be 429, but we preserve compatibility */
+ rspamd_task_insert_result(task, RSPAMD_FUZZY_SYMBOL_RATELIMITED, 1.0,
+ session->rule->name);
+ }
+ else if (rep->v1.value == 503) {
+ rspamd_task_insert_result(task, RSPAMD_FUZZY_SYMBOL_FORBIDDEN, 1.0,
+ session->rule->name);
+ }
+ else if (rep->v1.value == 415) {
+ rspamd_task_insert_result(task, RSPAMD_FUZZY_SYMBOL_ENCRYPTION_REQUIRED, 1.0,
session->rule->name);
}
else if (rep->v1.value == 401) {
@@ -3400,11 +3475,14 @@ fuzzy_symbol_callback(struct rspamd_task *task,
PTR_ARRAY_FOREACH(fuzzy_module_ctx->fuzzy_rules, i, rule)
{
- commands = fuzzy_generate_commands(task, rule, FUZZY_CHECK, 0, 0, 0);
+ if (rule->mode != fuzzy_rule_write_only) {
+ commands = fuzzy_generate_commands(task, rule, FUZZY_CHECK, 0, 0, 0);
- if (commands != NULL) {
- register_fuzzy_client_call(task, rule, commands);
+ if (commands != NULL) {
+ register_fuzzy_client_call(task, rule, commands);
+ }
}
+ /* Skip write only rules from checks */
}
rspamd_symcache_item_async_dec_check(task, item, M);
@@ -3491,9 +3569,9 @@ register_fuzzy_controller_call(struct rspamd_http_connection_entry *entry,
}
static void
-fuzzy_process_handler(struct rspamd_http_connection_entry *conn_ent,
- struct rspamd_http_message *msg, int cmd, int value, int flag,
- struct fuzzy_ctx *ctx, gboolean is_hash, unsigned int flags)
+fuzzy_modify_handler(struct rspamd_http_connection_entry *conn_ent,
+ struct rspamd_http_message *msg, int cmd, int value, int flag,
+ struct fuzzy_ctx *ctx, gboolean is_hash, unsigned int flags)
{
struct fuzzy_rule *rule;
struct rspamd_controller_session *session = conn_ent->ud;
@@ -3541,7 +3619,7 @@ fuzzy_process_handler(struct rspamd_http_connection_entry *conn_ent,
PTR_ARRAY_FOREACH(fuzzy_module_ctx->fuzzy_rules, i, rule)
{
- if (rule->read_only) {
+ if (rule->mode == fuzzy_rule_read_only) {
continue;
}
@@ -3796,8 +3874,8 @@ fuzzy_controller_handler(struct rspamd_http_connection_entry *conn_ent,
send_flags |= FUZZY_CHECK_FLAG_NOTEXT;
}
- fuzzy_process_handler(conn_ent, msg, cmd, value, flag,
- (struct fuzzy_ctx *) ctx, is_hash, send_flags);
+ fuzzy_modify_handler(conn_ent, msg, cmd, value, flag,
+ (struct fuzzy_ctx *) ctx, is_hash, send_flags);
return 0;
}
@@ -3879,7 +3957,7 @@ fuzzy_check_lua_process_learn(struct rspamd_task *task,
if (!res) {
break;
}
- if (rule->read_only) {
+ if (rule->mode == fuzzy_rule_read_only) {
continue;
}
@@ -4181,7 +4259,7 @@ fuzzy_lua_gen_hashes_handler(lua_State *L)
PTR_ARRAY_FOREACH(fuzzy_module_ctx->fuzzy_rules, i, rule)
{
- if (rule->read_only) {
+ if (rule->mode == fuzzy_rule_read_only) {
continue;
}
@@ -4409,7 +4487,7 @@ fuzzy_lua_list_storages(lua_State *L)
{
lua_newtable(L);
- lua_pushboolean(L, rule->read_only);
+ lua_pushboolean(L, rule->mode == fuzzy_rule_read_only);
lua_setfield(L, -2, "read_only");
/* Push servers */
diff --git a/src/plugins/lua/aws_s3.lua b/src/plugins/lua/aws_s3.lua
index 30e88d2cd..ac344d86c 100644
--- a/src/plugins/lua/aws_s3.lua
+++ b/src/plugins/lua/aws_s3.lua
@@ -238,7 +238,7 @@ settings = lua_util.override_defaults(settings, opts)
local res, err = settings_schema:transform(settings)
if not res then
- rspamd_logger.warnx(rspamd_config, 'plugin is misconfigured: %s', err)
+ rspamd_logger.warnx(rspamd_config, 'plugin %s is misconfigured: %s', N, err)
lua_util.disable_module(N, "config")
return
end
diff --git a/src/plugins/lua/bimi.lua b/src/plugins/lua/bimi.lua
index 278359069..78949a5c0 100644
--- a/src/plugins/lua/bimi.lua
+++ b/src/plugins/lua/bimi.lua
@@ -265,7 +265,7 @@ local function check_bimi_vmc(task, domain, record)
end
if redis_params.username then
if redis_params.password then
- password = string.format( '%s:%s@', redis_params.username, redis_params.password)
+ password = string.format('%s:%s@', redis_params.username, redis_params.password)
else
rspamd_logger.warnx(task, "Redis requires a password when username is supplied")
end
@@ -358,7 +358,7 @@ settings = lua_util.override_defaults(settings, opts)
local res, err = settings_schema:transform(settings)
if not res then
- rspamd_logger.warnx(rspamd_config, 'plugin is misconfigured: %s', err)
+ rspamd_logger.warnx(rspamd_config, 'plugin %s is misconfigured: %s', N, err)
local err_msg = string.format("schema error: %s", res)
lua_util.config_utils.push_config_error(N, err_msg)
lua_util.disable_module(N, "failed", err_msg)
diff --git a/src/plugins/lua/history_redis.lua b/src/plugins/lua/history_redis.lua
index fff9f46b3..a3fdb0ec4 100644
--- a/src/plugins/lua/history_redis.lua
+++ b/src/plugins/lua/history_redis.lua
@@ -281,7 +281,7 @@ if opts then
local res, err = settings_schema:transform(settings)
if not res then
- rspamd_logger.warnx(rspamd_config, '%s: plugin is misconfigured: %s', N, err)
+ rspamd_logger.warnx(rspamd_config, 'plugin %s is misconfigured: %s', N, err)
lua_util.disable_module(N, "config")
return
end
diff --git a/src/plugins/lua/ratelimit.lua b/src/plugins/lua/ratelimit.lua
index f3331e850..168d8d63a 100644
--- a/src/plugins/lua/ratelimit.lua
+++ b/src/plugins/lua/ratelimit.lua
@@ -29,8 +29,7 @@ local lua_util = require "lua_util"
local lua_verdict = require "lua_verdict"
local rspamd_hash = require "rspamd_cryptobox_hash"
local lua_selectors = require "lua_selectors"
-local ts = require("tableshape").types
-
+local ratelimit_common = require "plugins/ratelimit"
-- A plugin that implements ratelimits using redis
local E = {}
@@ -76,138 +75,6 @@ local function load_scripts(_, _)
bucket_cleanup_id = lua_redis.load_redis_script_from_file(bucket_cleanup_script, redis_params)
end
-local limit_parser
-local function parse_string_limit(lim, no_error)
- local function parse_time_suffix(s)
- if s == 's' then
- return 1
- elseif s == 'm' then
- return 60
- elseif s == 'h' then
- return 3600
- elseif s == 'd' then
- return 86400
- end
- end
- local function parse_num_suffix(s)
- if s == '' then
- return 1
- elseif s == 'k' then
- return 1000
- elseif s == 'm' then
- return 1000000
- elseif s == 'g' then
- return 1000000000
- end
- end
- local lpeg = require "lpeg"
-
- if not limit_parser then
- local digit = lpeg.R("09")
- limit_parser = {}
- limit_parser.integer = (lpeg.S("+-") ^ -1) *
- (digit ^ 1)
- limit_parser.fractional = (lpeg.P(".")) *
- (digit ^ 1)
- limit_parser.number = (limit_parser.integer *
- (limit_parser.fractional ^ -1)) +
- (lpeg.S("+-") * limit_parser.fractional)
- limit_parser.time = lpeg.Cf(lpeg.Cc(1) *
- (limit_parser.number / tonumber) *
- ((lpeg.S("smhd") / parse_time_suffix) ^ -1),
- function(acc, val)
- return acc * val
- end)
- limit_parser.suffixed_number = lpeg.Cf(lpeg.Cc(1) *
- (limit_parser.number / tonumber) *
- ((lpeg.S("kmg") / parse_num_suffix) ^ -1),
- function(acc, val)
- return acc * val
- end)
- limit_parser.limit = lpeg.Ct(limit_parser.suffixed_number *
- (lpeg.S(" ") ^ 0) * lpeg.S("/") * (lpeg.S(" ") ^ 0) *
- limit_parser.time)
- end
- local t = lpeg.match(limit_parser.limit, lim)
-
- if t and t[1] and t[2] and t[2] ~= 0 then
- return t[2], t[1]
- end
-
- if not no_error then
- rspamd_logger.errx(rspamd_config, 'bad limit: %s', lim)
- end
-
- return nil
-end
-
-local function str_to_rate(str)
- local divider, divisor = parse_string_limit(str, false)
-
- if not divisor then
- rspamd_logger.errx(rspamd_config, 'bad rate string: %s', str)
-
- return nil
- end
-
- return divisor / divider
-end
-
-local bucket_schema = ts.shape {
- burst = ts.number + ts.string / lua_util.dehumanize_number,
- rate = ts.number + ts.string / str_to_rate,
- skip_recipients = ts.boolean:is_optional(),
- symbol = ts.string:is_optional(),
- message = ts.string:is_optional(),
- skip_soft_reject = ts.boolean:is_optional(),
-}
-
-local function parse_limit(name, data)
- if type(data) == 'table' then
- -- 2 cases here:
- -- * old limit in format [burst, rate]
- -- * vector of strings in Andrew's string format (removed from 1.8.2)
- -- * 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
- return {
- 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
-
- return nil
- else
- local parsed_bucket, err = bucket_schema:transform(data)
-
- if not parsed_bucket or err then
- rspamd_logger.errx(rspamd_config, 'cannot parse bucket for %s: %s; original value: %s',
- name, err, data)
- else
- return parsed_bucket
- end
- end
- elseif type(data) == 'string' then
- local rep_rate, burst = parse_string_limit(data)
- rspamd_logger.warnx(rspamd_config, 'old style rate bucket config detected for %s: %s',
- name, data)
- if rep_rate and burst then
- return {
- burst = burst,
- rate = burst / rep_rate -- reciprocal
- }
- end
- end
-
- return nil
-end
-
--- Check whether this addr is bounce
local function check_bounce(from)
return fun.any(function(b)
@@ -490,7 +357,7 @@ local function ratelimit_cb(task)
local ret, redis_key, bd = pcall(hdl, task)
if ret then
- local bucket = parse_limit(k, bd)
+ local bucket = ratelimit_common.parse_limit(k, bd)
if bucket then
prefixes[redis_key] = make_prefix(redis_key, k, bucket)
end
@@ -718,7 +585,7 @@ if opts then
if lim.bucket[1] then
for _, bucket in ipairs(lim.bucket) do
- local b = parse_limit(t, bucket)
+ local b = ratelimit_common.parse_limit(t, bucket)
if not b then
rspamd_logger.errx(rspamd_config, 'bad ratelimit bucket for %s: "%s"',
@@ -729,7 +596,7 @@ if opts then
table.insert(buckets, b)
end
else
- local bucket = parse_limit(t, lim.bucket)
+ local bucket = ratelimit_common.parse_limit(t, lim.bucket)
if not bucket then
rspamd_logger.errx(rspamd_config, 'bad ratelimit bucket for %s: "%s"',
@@ -757,7 +624,7 @@ if opts then
end
else
rspamd_logger.warnx(rspamd_config, 'old syntax for ratelimits: %s', lim)
- buckets = parse_limit(t, lim)
+ buckets = ratelimit_common.parse_limit(t, lim)
if buckets then
settings.limits[t] = {
buckets = { buckets }