diff options
Diffstat (limited to 'src/client/rspamc.cxx')
-rw-r--r-- | src/client/rspamc.cxx | 122 |
1 files changed, 113 insertions, 9 deletions
diff --git a/src/client/rspamc.cxx b/src/client/rspamc.cxx index 31a4aaf24..1dc48faae 100644 --- a/src/client/rspamc.cxx +++ b/src/client/rspamc.cxx @@ -1,5 +1,5 @@ /* - * Copyright 2024 Vsevolod Stakhov + * Copyright 2025 Vsevolod Stakhov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,8 +35,8 @@ #include "frozen/string.h" #include "frozen/unordered_map.h" -#include "fmt/format.h" -#include "fmt/color.h" +#include "contrib/fmt/include/fmt/format.h" +#include "contrib/fmt/include/fmt/color.h" #include "libutil/cxx/file_util.hxx" #include "libutil/cxx/util.hxx" @@ -59,6 +59,7 @@ static const char *user = nullptr; static const char *helo = nullptr; static const char *hostname = nullptr; static const char *classifier = nullptr; +static const char *learn_class_name = nullptr; static const char *local_addr = nullptr; static const char *execute = nullptr; static const char *sort = nullptr; @@ -90,6 +91,9 @@ static gboolean skip_attachments = FALSE; static const char *pubkey = nullptr; static const char *user_agent = "rspamc"; static const char *files_list = nullptr; +static const char *queue_id = nullptr; +static const char *log_tag = nullptr; +static std::string settings; std::vector<GPid> children; static GPatternSpec **exclude_compiled = nullptr; @@ -102,6 +106,11 @@ static gboolean rspamc_password_callback(const char *option_name, gpointer data, GError **error); +static gboolean rspamc_settings_callback(const char *option_name, + const char *value, + gpointer data, + GError **error); + static GOptionEntry entries[] = { {"connect", 'h', 0, G_OPTION_ARG_STRING, &connect_str, @@ -182,6 +191,12 @@ static GOptionEntry entries[] = "Use specific User-Agent instead of \"rspamc\"", nullptr}, {"files-list", '\0', 0, G_OPTION_ARG_FILENAME, &files_list, "Read one or more newline separated filenames to scan from file", nullptr}, + {"queue-id", '\0', 0, G_OPTION_ARG_STRING, &queue_id, + "Set Queue-ID header for the request", nullptr}, + {"log-tag", '\0', 0, G_OPTION_ARG_STRING, &log_tag, + "Set Log-Tag header for the request", nullptr}, + {"settings", '\0', 0, G_OPTION_ARG_CALLBACK, (void *) &rspamc_settings_callback, + "Set Settings header as JSON/UCL for the request", nullptr}, {nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr}}; static void rspamc_symbols_output(FILE *out, ucl_object_t *obj); @@ -198,6 +213,7 @@ enum rspamc_command_type { RSPAMC_COMMAND_SYMBOLS, RSPAMC_COMMAND_LEARN_SPAM, RSPAMC_COMMAND_LEARN_HAM, + RSPAMC_COMMAND_LEARN_CLASS, RSPAMC_COMMAND_FUZZY_ADD, RSPAMC_COMMAND_FUZZY_DEL, RSPAMC_COMMAND_FUZZY_DELHASH, @@ -250,6 +266,15 @@ static const constexpr auto rspamc_commands = rspamd::array_of( .need_input = TRUE, .command_output_func = nullptr}, rspamc_command{ + .cmd = RSPAMC_COMMAND_LEARN_CLASS, + .name = "learn_class", + .path = "learnclass", + .description = "learn message as class", + .is_controller = TRUE, + .is_privileged = TRUE, + .need_input = TRUE, + .command_output_func = nullptr}, + rspamc_command{ .cmd = RSPAMC_COMMAND_FUZZY_ADD, .name = "fuzzy_add", .path = "fuzzyadd", @@ -527,8 +552,7 @@ rspamc_password_callback(const char *option_name, auto *map = (char *) locked_mmap.value().get_map(); value_view = std::string_view{map, locked_mmap->get_size()}; auto right = value_view.end() - 1; - for (; right > value_view.cbegin() && g_ascii_isspace(*right); --right) - ; + for (; right > value_view.cbegin() && g_ascii_isspace(*right); --right); std::string_view str{value_view.begin(), static_cast<size_t>(right - value_view.begin()) + 1}; processed_passwd.assign(std::begin(str), std::end(str)); processed_passwd.push_back('\0'); /* Null-terminate for C part */ @@ -557,6 +581,46 @@ rspamc_password_callback(const char *option_name, return TRUE; } +static gboolean +rspamc_settings_callback(const char *option_name, + const char *value, + gpointer data, + GError **error) +{ + if (value == nullptr) { + g_set_error(error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Settings parameter cannot be empty"); + return FALSE; + } + + // Parse the settings string using UCL to validate it + struct ucl_parser *parser = ucl_parser_new(UCL_PARSER_KEY_LOWERCASE); + if (!ucl_parser_add_string(parser, value, strlen(value))) { + auto *ucl_error = ucl_parser_get_error(parser); + g_set_error(error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Invalid JSON/UCL in settings: %s", ucl_error); + ucl_parser_free(parser); + return FALSE; + } + + // Get the parsed object and validate it + auto *obj = ucl_parser_get_object(parser); + if (obj == nullptr) { + g_set_error(error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Failed to parse settings as JSON/UCL"); + ucl_parser_free(parser); + return FALSE; + } + + // Store the validated settings string + settings = value; + + ucl_object_unref(obj); + ucl_parser_free(parser); + + return TRUE; +} + /* * Parse command line */ @@ -649,6 +713,7 @@ check_rspamc_command(const char *cmd) -> std::optional<rspamc_command> {"report", RSPAMC_COMMAND_SYMBOLS}, {"learn_spam", RSPAMC_COMMAND_LEARN_SPAM}, {"learn_ham", RSPAMC_COMMAND_LEARN_HAM}, + {"learn_class", RSPAMC_COMMAND_LEARN_CLASS}, {"fuzzy_add", RSPAMC_COMMAND_FUZZY_ADD}, {"fuzzy_del", RSPAMC_COMMAND_FUZZY_DEL}, {"fuzzy_delhash", RSPAMC_COMMAND_FUZZY_DELHASH}, @@ -659,10 +724,33 @@ check_rspamc_command(const char *cmd) -> std::optional<rspamc_command> }); std::string cmd_lc = rspamd_string_tolower(cmd); + + // Handle learn_class:classname syntax + if (cmd_lc.find("learn_class:") == 0) { + auto colon_pos = cmd_lc.find(':'); + if (colon_pos != std::string::npos && colon_pos + 1 < cmd_lc.length()) { + auto class_name = cmd_lc.substr(colon_pos + 1); + // Store class name globally for later use + learn_class_name = g_strdup(class_name.c_str()); + // Return the learn_class command + auto elt_it = std::find_if(rspamc_commands.begin(), rspamc_commands.end(), [&](const auto &item) { + return item.cmd == RSPAMC_COMMAND_LEARN_CLASS; + }); + if (elt_it != std::end(rspamc_commands)) { + return *elt_it; + } + } + return std::nullopt; + } + auto ct = rspamd::find_map(str_map, std::string_view{cmd_lc}); + if (!ct.has_value()) { + return std::nullopt; + } + auto elt_it = std::find_if(rspamc_commands.begin(), rspamc_commands.end(), [&](const auto &item) { - return item.cmd == ct; + return item.cmd == ct.value(); }); if (elt_it != std::end(rspamc_commands)) { @@ -799,6 +887,10 @@ add_options(GQueue *opts) add_client_header(opts, "Classifier", classifier); } + if (learn_class_name) { + add_client_header(opts, "Class", learn_class_name); + } + if (weight != 0) { auto nstr = fmt::format("{}", weight); add_client_header(opts, "Weight", nstr.c_str()); @@ -852,6 +944,18 @@ add_options(GQueue *opts) hdr++; } + if (queue_id != nullptr) { + add_client_header(opts, "Queue-Id", queue_id); + } + + if (log_tag != nullptr) { + add_client_header(opts, "Log-Tag", log_tag); + } + + if (!settings.empty()) { + add_client_header(opts, "Settings", settings.c_str()); + } + if (!flagbuf.empty()) { if (flagbuf.back() == ',') { flagbuf.pop_back(); @@ -1918,7 +2022,7 @@ rspamc_client_cb(struct rspamd_client_connection *conn, if (raw_body) { /* We can also output the resulting json */ - rspamc_print(out, "{}\n", std::string_view{raw_body, (std::size_t)(rawlen - bodylen)}); + rspamc_print(out, "{}\n", std::string_view{raw_body, (std::size_t) (rawlen - bodylen)}); } } } @@ -1950,7 +2054,7 @@ rspamc_process_input(struct ev_loop *ev_base, const struct rspamc_command &cmd, p = strrchr(connect_str, ']'); if (p != nullptr) { - hostbuf.assign(connect_str + 1, (std::size_t)(p - connect_str - 1)); + hostbuf.assign(connect_str + 1, (std::size_t) (p - connect_str - 1)); p++; } else { @@ -1965,7 +2069,7 @@ rspamc_process_input(struct ev_loop *ev_base, const struct rspamc_command &cmd, if (hostbuf.empty()) { if (p != nullptr) { - hostbuf.assign(connect_str, (std::size_t)(p - connect_str)); + hostbuf.assign(connect_str, (std::size_t) (p - connect_str)); } else { hostbuf.assign(connect_str); |