* limitations under the License.
*/
#include "config.h"
+/* Must go first to keep the C linkage of the raw lua headers */
+#include "lua/lua_common.h"
+
#include "logger.h"
#include "expression.h"
#include "task.h"
#include <cmath>
#include <vector>
#include <variant>
+#include <optional>
#include "libutil/cxx/util.hxx"
#include "contrib/ankerl/unordered_dense.h"
}
}
+/*
+ * Call a per-symbol Lua condition: f(task, symbol) where symbol is a table
+ * in the task:get_symbol() layout. Must be synchronous (no yields/coroutines).
+ *
+ * Returns std::nullopt when the condition returned `true` (the atom keeps its
+ * score-based weight) or an explicit weight: a number returned by the
+ * condition, or 0.0 for `false`/nil/error (the atom is treated as unmatched).
+ */
+static auto
+check_lua_condition(struct composites_data *cd,
+ std::string_view sym,
+ struct rspamd_symbol_result *ms,
+ int cbref) -> std::optional<double>
+{
+ struct rspamd_task *task = cd->task;
+ auto *L = (lua_State *) task->cfg->lua_state;
+ std::optional<double> ret = 0.0;
+
+ lua_pushcfunction(L, &rspamd_lua_traceback);
+ auto err_idx = lua_gettop(L);
+
+ lua_rawgeti(L, LUA_REGISTRYINDEX, cbref);
+ rspamd_lua_task_push(L, task);
+
+ if (!rspamd_lua_push_symbol_result(L, task, sym.data(), ms,
+ cd->metric_res, FALSE, TRUE)) {
+ lua_pushnil(L);
+ }
+
+ if (lua_pcall(L, 2, 1, err_idx) != 0) {
+ msg_err_task("cannot execute lua condition for symbol %s in composite %s: %s",
+ sym.data(), cd->composite->sym.c_str(), lua_tostring(L, -1));
+ }
+ else {
+ switch (lua_type(L, -1)) {
+ case LUA_TBOOLEAN:
+ if (lua_toboolean(L, -1)) {
+ ret = std::nullopt;
+ }
+ break;
+ case LUA_TNUMBER:
+ ret = lua_tonumber(L, -1);
+ break;
+ case LUA_TNIL:
+ break;
+ default:
+ msg_err_task("lua condition for symbol %s in composite %s returned %s; "
+ "boolean or number expected",
+ sym.data(), cd->composite->sym.c_str(),
+ lua_typename(L, lua_type(L, -1)));
+ break;
+ }
+ }
+
+ lua_settop(L, err_idx - 1);
+
+ return ret;
+}
+
static auto
process_single_symbol(struct composites_data *cd,
std::string_view sym,
}
if (ms) {
- if (ms->score == 0) {
- rc = epsilon * 16.0; /* Distinguish from 0 */
+ /* A Lua condition is ANDed with option filters; a failed
+ * condition is treated exactly as a missing symbol */
+ std::optional<double> cond_weight;
+ auto cbref = cd->composite->find_condition(sym);
+
+ if (cbref != -1) {
+ cond_weight = check_lua_condition(cd, sym, ms, cbref);
+
+ if (cond_weight && *cond_weight == 0) {
+ msg_debug_composites("symbol %s in composite %s failed lua condition",
+ sym.data(), cd->composite->sym.c_str());
+ ms = nullptr;
+ }
}
- else {
- rc = ms->score;
+
+ if (ms) {
+ if (cond_weight) {
+ /* Explicit atom weight returned by the condition */
+ rc = *cond_weight;
+ }
+ else if (ms->score == 0) {
+ rc = epsilon * 16.0; /* Distinguish from 0 */
+ }
+ else {
+ rc = ms->score;
+ }
}
}
}
bool second_pass; /**< true if this composite needs second pass evaluation */
bool has_positive_atoms; /**< true if composite has at least one non-negated atom */
bool disabled; /**< true if composite is a placeholder stub (evaluates to false) */
+
+ /* Per-symbol Lua conditions (registry refs owned by the config UCL tree),
+ * keyed by symbol name as written in the expression (no prefixes/brackets).
+ * A condition is ANDed with option filters of the matching atom. */
+ ankerl::unordered_dense::map<std::string, int,
+ rspamd::smart_str_hash, rspamd::smart_str_equal>
+ conditions;
+ /* Symbols consulted by conditions beyond the expression atoms; feed
+ * first/second pass placement in process_dependencies() */
+ std::vector<std::string> depends_on;
+
+ auto find_condition(std::string_view sym_name) const -> int
+ {
+ if (conditions.empty()) {
+ return -1;
+ }
+
+ auto it = conditions.find(sym_name);
+
+ return it != conditions.end() ? it->second : -1;
+ }
};
/**
* limitations under the License.
*/
+/* Must go first to keep the C linkage of the raw lua headers */
+#include "lua/lua_common.h"
+
#include <memory>
#include <vector>
#include <cmath>
return rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_UNKNOWN;
}// namespace rspamd::composites
+/*
+ * Parse the optional `conditions` object (symbol name -> Lua function, coming
+ * through the config UCL tree as UCL_USERDATA closures) and the optional
+ * `depends_on` string/array. Returns false on a malformed definition: the
+ * whole composite must be rejected rather than silently accepted with a
+ * weaker match, since conditions are used as gates.
+ */
+static auto
+composite_parse_conditions(struct rspamd_config *cfg,
+ std::string_view composite_name,
+ const ucl_object_t *obj,
+ rspamd_composite &composite) -> bool
+{
+ const auto *val = ucl_object_lookup(obj, "conditions");
+
+ if (val != nullptr) {
+ if (ucl_object_type(val) != UCL_OBJECT) {
+ msg_err_config("composite %*s: 'conditions' must be an object keyed by symbol name",
+ (int) composite_name.size(), composite_name.data());
+ return false;
+ }
+
+ auto *it = ucl_object_iterate_new(val);
+ const ucl_object_t *cur;
+
+ while ((cur = ucl_object_iterate_safe(it, true)) != nullptr) {
+ const auto *sym_name = ucl_object_key(cur);
+ const auto *fd = ucl_object_toclosure(cur);
+
+ if (sym_name == nullptr || fd == nullptr) {
+ msg_err_config("composite %*s: condition for symbol '%s' must be a function",
+ (int) composite_name.size(), composite_name.data(),
+ sym_name ? sym_name : "unknown");
+ ucl_object_iterate_free(it);
+ return false;
+ }
+
+ composite.conditions[std::string{sym_name}] = fd->idx;
+ }
+
+ ucl_object_iterate_free(it);
+ }
+
+ val = ucl_object_lookup(obj, "depends_on");
+
+ if (val != nullptr) {
+ if (ucl_object_type(val) == UCL_STRING) {
+ composite.depends_on.emplace_back(ucl_object_tostring(val));
+ }
+ else if (ucl_object_type(val) == UCL_ARRAY) {
+ auto *it = ucl_object_iterate_new(val);
+ const ucl_object_t *cur;
+
+ while ((cur = ucl_object_iterate_safe(it, true)) != nullptr) {
+ const char *dep_sym = nullptr;
+
+ if (!ucl_object_tostring_safe(cur, &dep_sym)) {
+ msg_err_config("composite %*s: 'depends_on' elements must be strings",
+ (int) composite_name.size(), composite_name.data());
+ ucl_object_iterate_free(it);
+ return false;
+ }
+
+ composite.depends_on.emplace_back(dep_sym);
+ }
+
+ ucl_object_iterate_free(it);
+ }
+ else {
+ msg_err_config("composite %*s: 'depends_on' must be a string or an array of strings",
+ (int) composite_name.size(), composite_name.data());
+ return false;
+ }
+ }
+
+ return true;
+}
+
auto composites_manager::add_composite(std::string_view composite_name, const ucl_object_t *obj, bool silent_duplicate) -> rspamd_composite *
{
return nullptr;
}
+ /* Parse conditions/depends_on before the composite is registered anywhere,
+ * so a malformed definition is rejected atomically */
+ rspamd_composite parsed_conditions;
+ if (!composite_parse_conditions(cfg, composite_name, obj, parsed_conditions)) {
+ return nullptr;
+ }
+
const auto &composite = new_composite(composite_name, expr, composite_expression);
+ composite->conditions = std::move(parsed_conditions.conditions);
+ composite->depends_on = std::move(parsed_conditions.depends_on);
auto score = std::isnan(cfg->unknown_weight) ? 0.0 : cfg->unknown_weight;
val = ucl_object_lookup(obj, "score");
composite_dep_callback,
&cbd);
+ if (!cbd.needs_second_pass) {
+ /* Symbols consulted by Lua conditions are invisible to the atom
+ * walk; treat explicit depends_on entries as extra atoms */
+ for (const auto &dep: comp->depends_on) {
+ if (gen.find(dep) != nullptr) {
+ /* Reference to another composite: transitive pass handles it */
+ continue;
+ }
+
+ if (symbol_needs_second_pass(cfg, dep.c_str())) {
+ msg_debug_config("composite '%s' depends on second-pass symbol %s (depends_on)",
+ comp->sym.c_str(), dep.c_str());
+ cbd.needs_second_pass = true;
+ break;
+ }
+ }
+ }
+
if (cbd.needs_second_pass) {
second_pass_set.insert(comp);
msg_debug_config("composite '%s' marked for second pass (direct dependency)",
}
} }, &trans_data);
+ if (!has_second_pass_dep) {
+ /* depends_on may also name other composites */
+ for (const auto &dep: comp->depends_on) {
+ if (auto *dep_comp = gen.find(dep);
+ dep_comp != nullptr &&
+ second_pass_set.contains(const_cast<rspamd_composite *>(dep_comp))) {
+ has_second_pass_dep = true;
+ break;
+ }
+ }
+ }
+
if (has_second_pass_dep) {
second_pass_set.insert(comp);
changed = true;
composite->policy = p;
}
+ /*
+ * Dynamic maps deliver plain UCL text, so `conditions` closures cannot
+ * arrive through this path; the parser will reject them with an error.
+ * `depends_on` is fully supported though.
+ */
+ if (!composite_parse_conditions(cfg, name, obj, *composite)) {
+ return nullptr;
+ }
+
/* Replace any existing entry under this name (came from base_gen
* clone or from an earlier entry in this same map). */
auto sym_key = composite->sym;
*/
void rspamd_lua_task_push(lua_State *L, struct rspamd_task *task);
+/**
+* Push a symbol result as a table in the same layout as task:get_symbol();
+* returns FALSE (nothing is pushed) if the symbol is not found or ignored
+*/
+struct rspamd_symbol_result;
+struct rspamd_scan_result;
+gboolean rspamd_lua_push_symbol_result(lua_State *L,
+ struct rspamd_task *task,
+ const char *symbol,
+ struct rspamd_symbol_result *symbol_result,
+ struct rspamd_scan_result *metric_res,
+ gboolean add_metric,
+ gboolean add_name);
+
/**
* Return lua ip structure at the specified address
*/
/**
* @method rspamd_config:add_composite(name, expression)
* @param {string} name name of composite symbol
- * @param {string} expression symbolic expression of the composite rule
+ * @param {string|table} expression symbolic expression of the composite rule,
+ * or a full definition table: {expression = ..., score = ..., group = ...,
+ * description = ..., policy = ..., conditions = {SYM = function(task, symbol) ... end},
+ * depends_on = {'SYM1', ...}}. A condition is called as f(task, symbol) where
+ * symbol is a table in the task:get_symbol() layout; it must be synchronous and
+ * may return true/false or a number used as the atom weight.
* @return {bool} true if a composite has been added successfully
*/
LUA_FUNCTION_DEF(config, add_composite);
if (cfg) {
name = rspamd_mempool_strdup(cfg->cfg_pool, luaL_checkstring(L, 2));
- expr_str = luaL_checkstring(L, 3);
- if (name && expr_str) {
- composite = rspamd_composites_manager_add_from_string(cfg->composites_manager,
- name, expr_str);
+ if (name && lua_type(L, 3) == LUA_TTABLE) {
+ /* Full definition table, converted to UCL (functions are
+ * preserved as closures with registry refs) */
+ ucl_object_t *obj = ucl_object_lua_import(L, 3);
- if (composite) {
- rspamd_symcache_add_symbol(cfg->cache, name,
- 0, NULL, composite, SYMBOL_TYPE_COMPOSITE, -1);
- ret = TRUE;
+ if (obj != NULL) {
+ /*
+ * The object owns lua registry refs of the conditions, so it
+ * must live as long as the config does
+ */
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) ucl_object_unref, obj);
+ composite = rspamd_composites_manager_add_from_ucl(cfg->composites_manager,
+ name, obj);
+
+ if (composite) {
+ rspamd_symcache_add_symbol(cfg->cache, name,
+ 0, NULL, composite, SYMBOL_TYPE_COMPOSITE, -1);
+ ret = TRUE;
+ }
+ }
+ }
+ else if (name) {
+ expr_str = luaL_checkstring(L, 3);
+
+ if (expr_str) {
+ composite = rspamd_composites_manager_add_from_string(cfg->composites_manager,
+ name, expr_str);
+
+ if (composite) {
+ rspamd_symcache_add_symbol(cfg->cache, name,
+ 0, NULL, composite, SYMBOL_TYPE_COMPOSITE, -1);
+ ret = TRUE;
+ }
}
}
}
return 1;
}
-static inline gboolean
-lua_push_symbol_result(lua_State *L,
- struct rspamd_task *task,
- const char *symbol,
- struct rspamd_symbol_result *symbol_result,
- struct rspamd_scan_result *metric_res,
- gboolean add_metric,
- gboolean add_name)
+gboolean
+rspamd_lua_push_symbol_result(lua_State *L,
+ struct rspamd_task *task,
+ const char *symbol,
+ struct rspamd_symbol_result *symbol_result,
+ struct rspamd_scan_result *metric_res,
+ gboolean add_metric,
+ gboolean add_name)
{
struct rspamd_symbol_result *s = NULL;
struct rspamd_scan_result *sres,
unsigned int *count)
{
- if (lua_push_symbol_result(L, task, name, s, sres, FALSE, FALSE)) {
+ if (rspamd_lua_push_symbol_result(L, task, name, s, sres, FALSE, FALSE)) {
lua_setfield(L, -2, name);
(*count)++;
}
/* Always push as a table for compatibility :( */
lua_createtable(L, 1, 0);
- if ((found = lua_push_symbol_result(L, task, symbol,
- NULL, sres, TRUE, FALSE))) {
+ if ((found = rspamd_lua_push_symbol_result(L, task, symbol,
+ NULL, sres, TRUE, FALSE))) {
lua_rawseti(L, -2, 1);
}
else {
kh_foreach_value(mres->symbols, s, {
if (!(s->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {
- lua_push_symbol_result(L, task, s->name, s, mres, FALSE, TRUE);
+ rspamd_lua_push_symbol_result(L, task, s->name, s, mres, FALSE, TRUE);
lua_rawseti(L, -2, i++);
}
});