aboutsummaryrefslogtreecommitdiffstats
path: root/src/controller.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/controller.c')
-rw-r--r--src/controller.c259
1 files changed, 225 insertions, 34 deletions
diff --git a/src/controller.c b/src/controller.c
index 386448f93..6e0e4cac1 100644
--- a/src/controller.c
+++ b/src/controller.c
@@ -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.
@@ -53,6 +53,7 @@
#define PATH_HISTORY_RESET "/historyreset"
#define PATH_LEARN_SPAM "/learnspam"
#define PATH_LEARN_HAM "/learnham"
+#define PATH_LEARN_CLASS "/learnclass"
#define PATH_METRICS "/metrics"
#define PATH_READY "/ready"
#define PATH_SAVE_ACTIONS "/saveactions"
@@ -68,6 +69,7 @@
#define PATH_NEIGHBOURS "/neighbours"
#define PATH_PLUGINS "/plugins"
#define PATH_PING "/ping"
+#define PATH_BAYES_CLASSIFIERS "/bayes/classifiers"
#define msg_err_session(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \
session->pool->tag.tagname, session->pool->tag.uid, \
@@ -979,12 +981,6 @@ rspamd_controller_handle_maps(struct rspamd_http_connection_entry *conn_ent,
if (bk->protocol == MAP_PROTO_FILE) {
editable = rspamd_controller_can_edit_map(bk);
-
- if (!editable && access(bk->uri, R_OK) == -1) {
- /* Skip unreadable and non-existing maps */
- continue;
- }
-
obj = ucl_object_typed_new(UCL_OBJECT);
ucl_object_insert_key(obj, ucl_object_fromint(bk->id),
"map", 0, false);
@@ -994,8 +990,34 @@ rspamd_controller_handle_maps(struct rspamd_http_connection_entry *conn_ent,
}
ucl_object_insert_key(obj, ucl_object_fromstring(bk->uri),
"uri", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromstring("file"),
+ "type", 0, false);
ucl_object_insert_key(obj, ucl_object_frombool(editable),
"editable", 0, false);
+ ucl_object_insert_key(obj, ucl_object_frombool(map->shared->loaded),
+ "loaded", 0, false);
+ ucl_object_insert_key(obj, ucl_object_frombool(map->shared->cached),
+ "cached", 0, false);
+ ucl_array_append(top, obj);
+ }
+ else {
+ obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(obj, ucl_object_fromint(bk->id),
+ "map", 0, false);
+ if (map->description) {
+ ucl_object_insert_key(obj, ucl_object_fromstring(map->description),
+ "description", 0, false);
+ }
+ ucl_object_insert_key(obj, ucl_object_fromstring(bk->uri),
+ "uri", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromstring(rspamd_map_fetch_protocol_name(bk->protocol)),
+ "type", 0, false);
+ ucl_object_insert_key(obj, ucl_object_frombool(false),
+ "editable", 0, false);
+ ucl_object_insert_key(obj, ucl_object_frombool(map->shared->loaded),
+ "loaded", 0, false);
+ ucl_object_insert_key(obj, ucl_object_frombool(map->shared->cached),
+ "cached", 0, false);
ucl_array_append(top, obj);
}
}
@@ -1008,6 +1030,21 @@ rspamd_controller_handle_maps(struct rspamd_http_connection_entry *conn_ent,
return 0;
}
+gboolean
+rspamd_controller_map_traverse_callback(gconstpointer key, gconstpointer value, gsize _hits, gpointer ud)
+{
+ rspamd_fstring_t **target = (rspamd_fstring_t **) ud;
+
+ *target = rspamd_fstring_append(*target, key, strlen(key));
+
+ if (value) {
+ *target = rspamd_fstring_append(*target, " ", 1);
+ *target = rspamd_fstring_append(*target, value, strlen(value));
+ }
+ *target = rspamd_fstring_append(*target, "\n", 1);
+
+ return TRUE;
+}
/*
* Get map command handler:
* request: /getmap
@@ -1020,7 +1057,7 @@ rspamd_controller_handle_get_map(struct rspamd_http_connection_entry *conn_ent,
{
struct rspamd_controller_session *session = conn_ent->ud;
GList *cur;
- struct rspamd_map *map;
+ struct rspamd_map *map = NULL;
struct rspamd_map_backend *bk = NULL;
const rspamd_ftok_t *idstr;
struct stat st;
@@ -1054,7 +1091,7 @@ rspamd_controller_handle_get_map(struct rspamd_http_connection_entry *conn_ent,
PTR_ARRAY_FOREACH(map->backends, i, bk)
{
- if (bk->id == id && bk->protocol == MAP_PROTO_FILE) {
+ if (bk->id == id) {
found = TRUE;
break;
}
@@ -1069,32 +1106,53 @@ rspamd_controller_handle_get_map(struct rspamd_http_connection_entry *conn_ent,
return 0;
}
- if (stat(bk->uri, &st) == -1 || (fd = open(bk->uri, O_RDONLY)) == -1) {
+ if (bk->protocol == MAP_PROTO_FILE) {
+ if (stat(bk->uri, &st) == -1 || (fd = open(bk->uri, O_RDONLY)) == -1) {
+ reply = rspamd_http_new_message(HTTP_RESPONSE);
+ reply->date = time(NULL);
+ reply->code = 200;
+ }
+ else {
+
+ reply = rspamd_http_new_message(HTTP_RESPONSE);
+ reply->date = time(NULL);
+ reply->code = 200;
+
+ if (st.st_size > 0) {
+ if (!rspamd_http_message_set_body_from_fd(reply, fd)) {
+ close(fd);
+ rspamd_http_message_unref(reply);
+ msg_err_session("cannot read map %s: %s", bk->uri, strerror(errno));
+ rspamd_controller_send_error(conn_ent, 500, "Map read error");
+ return 0;
+ }
+ }
+ else {
+ rspamd_fstring_t *empty_body = rspamd_fstring_new_init("", 0);
+ rspamd_http_message_set_body_from_fstring_steal(reply, empty_body);
+ }
+
+ close(fd);
+ }
+ }
+ else if (bk->protocol == MAP_PROTO_STATIC) {
+ /* We can just traverse map and form reply */
reply = rspamd_http_new_message(HTTP_RESPONSE);
- reply->date = time(NULL);
reply->code = 200;
+ rspamd_fstring_t *map_body = rspamd_fstring_new();
+ rspamd_map_traverse(bk->map, rspamd_controller_map_traverse_callback, &map_body, FALSE);
+ rspamd_http_message_set_body_from_fstring_steal(reply, map_body);
}
- else {
-
+ else if (map->shared->loaded) {
reply = rspamd_http_new_message(HTTP_RESPONSE);
- reply->date = time(NULL);
reply->code = 200;
-
- if (st.st_size > 0) {
- if (!rspamd_http_message_set_body_from_fd(reply, fd)) {
- close(fd);
- rspamd_http_message_unref(reply);
- msg_err_session("cannot read map %s: %s", bk->uri, strerror(errno));
- rspamd_controller_send_error(conn_ent, 500, "Map read error");
- return 0;
- }
- }
- else {
- rspamd_fstring_t *empty_body = rspamd_fstring_new_init("", 0);
- rspamd_http_message_set_body_from_fstring_steal(reply, empty_body);
- }
-
- close(fd);
+ rspamd_fstring_t *map_body = rspamd_fstring_new();
+ rspamd_map_traverse(bk->map, rspamd_controller_map_traverse_callback, &map_body, FALSE);
+ rspamd_http_message_set_body_from_fstring_steal(reply, map_body);
+ }
+ else {
+ reply = rspamd_http_new_message(HTTP_RESPONSE);
+ reply->code = 404;
}
rspamd_http_connection_reset(conn_ent->conn);
@@ -2069,6 +2127,7 @@ rspamd_controller_handle_learn_common(
struct rspamd_controller_worker_ctx *ctx;
struct rspamd_task *task;
const rspamd_ftok_t *cl_header;
+ const char *class_name;
ctx = session->ctx;
@@ -2110,7 +2169,9 @@ rspamd_controller_handle_learn_common(
goto end;
}
- rspamd_learn_task_spam(task, is_spam, session->classifier, NULL);
+ /* Use unified class-based learning approach */
+ class_name = is_spam ? "spam" : "ham";
+ rspamd_task_set_autolearn_class(task, class_name);
if (!rspamd_task_process(task, RSPAMD_TASK_PROCESS_LEARN)) {
msg_warn_session("<%s> message cannot be processed",
@@ -2155,6 +2216,96 @@ rspamd_controller_handle_learnham(
}
/*
+ * Learn class command handler:
+ * request: /learnclass
+ * headers: Password, Class
+ * input: plaintext data
+ * reply: json {"success":true} or {"error":"error message"}
+ */
+static int
+rspamd_controller_handle_learnclass(
+ struct rspamd_http_connection_entry *conn_ent,
+ struct rspamd_http_message *msg)
+{
+ struct rspamd_controller_session *session = conn_ent->ud;
+ struct rspamd_controller_worker_ctx *ctx;
+ struct rspamd_task *task;
+ const rspamd_ftok_t *cl_header, *class_header;
+ char *class_name = NULL;
+
+ ctx = session->ctx;
+
+ if (!rspamd_controller_check_password(conn_ent, session, msg, TRUE)) {
+ return 0;
+ }
+
+ if (rspamd_http_message_get_body(msg, NULL) == NULL) {
+ msg_err_session("got zero length body, cannot continue");
+ rspamd_controller_send_error(conn_ent,
+ 400,
+ "Empty body is not permitted");
+ return 0;
+ }
+
+ class_header = rspamd_http_message_find_header(msg, "Class");
+ if (!class_header) {
+ msg_err_session("missing Class header for multiclass learning");
+ rspamd_controller_send_error(conn_ent,
+ 400,
+ "Class header is required for multiclass learning");
+ return 0;
+ }
+
+ task = rspamd_task_new(session->ctx->worker, session->cfg, session->pool,
+ session->ctx->lang_det, ctx->event_loop, FALSE);
+
+ task->resolver = ctx->resolver;
+ task->s = rspamd_session_create(session->pool,
+ rspamd_controller_learn_fin_task,
+ NULL,
+ (event_finalizer_t) rspamd_task_free,
+ task);
+ task->fin_arg = conn_ent;
+ task->http_conn = rspamd_http_connection_ref(conn_ent->conn);
+ task->sock = -1;
+ session->task = task;
+
+ cl_header = rspamd_http_message_find_header(msg, "classifier");
+ if (cl_header) {
+ session->classifier = rspamd_mempool_ftokdup(session->pool, cl_header);
+ }
+ else {
+ session->classifier = NULL;
+ }
+
+ if (!rspamd_task_load_message(task, msg, msg->body_buf.begin, msg->body_buf.len)) {
+ goto end;
+ }
+
+ /* Set multiclass learning flag and store class name */
+ class_name = rspamd_mempool_ftokdup(task->task_pool, class_header);
+ rspamd_task_set_autolearn_class(task, class_name);
+
+ if (!rspamd_task_process(task, RSPAMD_TASK_PROCESS_LEARN)) {
+ msg_warn_session("<%s> message cannot be processed",
+ MESSAGE_FIELD_CHECK(task, message_id));
+ goto end;
+ }
+
+end:
+ /* Set session spam flag for logging compatibility */
+ if (class_name) {
+ session->is_spam = (strcmp(class_name, "spam") == 0);
+ }
+ else {
+ session->is_spam = FALSE;
+ }
+ rspamd_session_pending(task->s);
+
+ return 0;
+}
+
+/*
* Scan command handler:
* request: /scan
* headers: Password
@@ -2255,7 +2406,7 @@ rspamd_controller_handle_saveactions(
return 0;
}
- parser = ucl_parser_new(0);
+ parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_chunk(parser, msg->body_buf.begin, msg->body_buf.len)) {
if ((error = ucl_parser_get_error(parser)) != NULL) {
msg_err_session("cannot parse input: %s", error);
@@ -2378,7 +2529,7 @@ rspamd_controller_handle_savesymbols(
return 0;
}
- parser = ucl_parser_new(0);
+ parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_chunk(parser, msg->body_buf.begin, msg->body_buf.len)) {
if ((error = ucl_parser_get_error(parser)) != NULL) {
msg_err_session("cannot parse input: %s", error);
@@ -3235,7 +3386,7 @@ rspamd_controller_handle_unknown(struct rspamd_http_connection_entry *conn_ent,
rspamd_http_message_add_header(rep, "Access-Control-Allow-Methods",
"POST, GET, OPTIONS");
rspamd_http_message_add_header(rep, "Access-Control-Allow-Headers",
- "Content-Type,Password,Map,Weight,Flag");
+ "Classifier,Class,Content-Type,Password,Map,Weight,Flag,Hash");
rspamd_http_connection_reset(conn_ent->conn);
rspamd_http_router_insert_headers(conn_ent->rt, rep);
rspamd_http_connection_write_message(conn_ent->conn,
@@ -3390,6 +3541,40 @@ rspamd_controller_handle_lua_plugin(struct rspamd_http_connection_entry *conn_en
return 0;
}
+/*
+ * Bayes classifier list command handler:
+ * request: /bayes/classifiers
+ * headers: Password
+ * reply: JSON array of Bayes classifier names
+ * Note: list is in reverse of declaration order (GList prepend).
+ */
+static int
+rspamd_controller_handle_bayes_classifiers(struct rspamd_http_connection_entry *conn_ent,
+ struct rspamd_http_message *msg)
+{
+ struct rspamd_controller_session *session = conn_ent->ud;
+ struct rspamd_controller_worker_ctx *ctx = session->ctx;
+ ucl_object_t *arr;
+ struct rspamd_classifier_config *clc;
+ GList *cur;
+
+ if (!rspamd_controller_check_password(conn_ent, session, msg, FALSE)) {
+ return 0;
+ }
+
+ arr = ucl_object_typed_new(UCL_ARRAY);
+ cur = g_list_last(ctx->cfg->classifiers);
+ while (cur) {
+ clc = cur->data;
+ ucl_array_append(arr, ucl_object_fromstring(clc->name));
+ cur = g_list_previous(cur);
+ }
+
+ rspamd_controller_send_ucl(conn_ent, arr);
+ ucl_object_unref(arr);
+ return 0;
+}
+
static void
rspamd_controller_error_handler(struct rspamd_http_connection_entry *conn_ent,
@@ -3958,6 +4143,9 @@ start_controller_worker(struct rspamd_worker *worker)
PATH_LEARN_HAM,
rspamd_controller_handle_learnham);
rspamd_http_router_add_path(ctx->http,
+ PATH_LEARN_CLASS,
+ rspamd_controller_handle_learnclass);
+ rspamd_http_router_add_path(ctx->http,
PATH_METRICS,
rspamd_controller_handle_metrics);
rspamd_http_router_add_path(ctx->http,
@@ -3999,6 +4187,9 @@ start_controller_worker(struct rspamd_worker *worker)
rspamd_http_router_add_path(ctx->http,
PATH_PING,
rspamd_controller_handle_ping);
+ rspamd_http_router_add_path(ctx->http,
+ PATH_BAYES_CLASSIFIERS,
+ rspamd_controller_handle_bayes_classifiers);
rspamd_controller_register_plugins_paths(ctx);
#if 0