summaryrefslogtreecommitdiffstats
path: root/src/rspamadm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/rspamadm/CMakeLists.txt31
-rw-r--r--src/rspamadm/commands.c280
-rw-r--r--src/rspamadm/configdump.c555
-rw-r--r--src/rspamadm/confighelp.c301
-rw-r--r--src/rspamadm/configtest.c190
-rw-r--r--src/rspamadm/control.c258
-rw-r--r--src/rspamadm/fuzzy_convert.c161
-rw-r--r--src/rspamadm/lua_repl.c1026
-rw-r--r--src/rspamadm/pw.c392
-rw-r--r--src/rspamadm/rspamadm.c621
-rw-r--r--src/rspamadm/rspamadm.h92
-rw-r--r--src/rspamadm/signtool.c623
-rw-r--r--src/rspamadm/stat_convert.c262
13 files changed, 4792 insertions, 0 deletions
diff --git a/src/rspamadm/CMakeLists.txt b/src/rspamadm/CMakeLists.txt
new file mode 100644
index 0000000..5e88ec8
--- /dev/null
+++ b/src/rspamadm/CMakeLists.txt
@@ -0,0 +1,31 @@
+SET(RSPAMADMSRC rspamadm.c
+ commands.c
+ pw.c
+ configtest.c
+ fuzzy_convert.c
+ configdump.c
+ control.c
+ confighelp.c
+ stat_convert.c
+ signtool.c
+ lua_repl.c
+ ${CMAKE_BINARY_DIR}/src/workers.c
+ #${CMAKE_BINARY_DIR}/src/modules.c - defined in rspamdserver
+ ${CMAKE_SOURCE_DIR}/src/controller.c
+ ${CMAKE_SOURCE_DIR}/src/fuzzy_storage.c
+ ${CMAKE_SOURCE_DIR}/src/worker.c
+ ${CMAKE_SOURCE_DIR}/src/rspamd_proxy.c)
+INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR})
+IF (ENABLE_HYPERSCAN MATCHES "ON")
+ LIST(APPEND RSPAMADMSRC "${CMAKE_SOURCE_DIR}/src/hs_helper.c")
+ENDIF()
+ADD_EXECUTABLE(rspamadm ${RSPAMADMSRC})
+TARGET_LINK_LIBRARIES(rspamadm rspamd-server)
+
+IF (NOT DEBIAN_BUILD)
+ SET_TARGET_PROPERTIES(rspamadm PROPERTIES VERSION ${RSPAMD_VERSION})
+ENDIF (NOT DEBIAN_BUILD)
+
+SET_TARGET_PROPERTIES(rspamadm PROPERTIES LINKER_LANGUAGE CXX)
+ADD_BACKWARD(rspamadm)
+INSTALL(TARGETS rspamadm RUNTIME DESTINATION bin)
diff --git a/src/rspamadm/commands.c b/src/rspamadm/commands.c
new file mode 100644
index 0000000..d64b172
--- /dev/null
+++ b/src/rspamadm/commands.c
@@ -0,0 +1,280 @@
+/*-
+ * Copyright 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.
+ */
+#include "rspamadm.h"
+#include "libutil/util.h"
+#include "libserver/logger.h"
+#include "lua/lua_common.h"
+#include "lua/lua_thread_pool.h"
+
+extern struct rspamadm_command pw_command;
+extern struct rspamadm_command configtest_command;
+extern struct rspamadm_command configdump_command;
+extern struct rspamadm_command control_command;
+extern struct rspamadm_command confighelp_command;
+extern struct rspamadm_command statconvert_command;
+extern struct rspamadm_command fuzzyconvert_command;
+extern struct rspamadm_command signtool_command;
+extern struct rspamadm_command lua_command;
+
+const struct rspamadm_command *commands[] = {
+ &help_command,
+ &pw_command,
+ &configtest_command,
+ &configdump_command,
+ &control_command,
+ &confighelp_command,
+ &statconvert_command,
+ &fuzzyconvert_command,
+ &signtool_command,
+ &lua_command,
+ NULL};
+
+
+const struct rspamadm_command *
+rspamadm_search_command(const gchar *name, GPtrArray *all_commands)
+{
+ const struct rspamadm_command *ret = NULL, *cmd;
+ const gchar *alias;
+ guint i, j;
+
+ if (name == NULL) {
+ name = "help";
+ }
+
+ PTR_ARRAY_FOREACH(all_commands, i, cmd)
+ {
+ if (strcmp(name, cmd->name) == 0) {
+ ret = cmd;
+ break;
+ }
+
+ PTR_ARRAY_FOREACH(cmd->aliases, j, alias)
+ {
+ if (strcmp(name, alias) == 0) {
+ ret = cmd;
+ break;
+ }
+ }
+ }
+
+ return ret;
+}
+
+void rspamadm_fill_internal_commands(GPtrArray *dest)
+{
+ guint i;
+
+ for (i = 0; i < G_N_ELEMENTS(commands); i++) {
+ if (commands[i]) {
+ g_ptr_array_add(dest, (gpointer) commands[i]);
+ }
+ }
+}
+
+static void
+lua_thread_str_error_cb(struct thread_entry *thread, int ret, const char *msg)
+{
+ msg_err("call to rspamadm lua script failed (%d): %s",
+ ret, msg);
+}
+
+static void
+rspamadm_lua_command_run(gint argc, gchar **argv,
+ const struct rspamadm_command *cmd)
+{
+ struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
+
+ lua_State *L = thread->lua_state;
+
+ gint table_idx = GPOINTER_TO_INT(cmd->command_data);
+ gint i;
+
+ /* Function */
+ lua_rawgeti(L, LUA_REGISTRYINDEX, table_idx);
+ lua_pushstring(L, "handler");
+ lua_gettable(L, -2);
+
+ /* Args */
+ lua_createtable(L, argc + 1, 0);
+
+ for (i = 0; i < argc; i++) {
+ lua_pushstring(L, argv[i]);
+ lua_rawseti(L, -2, i); /* Starting from zero ! */
+ }
+
+ if (lua_repl_thread_call(thread, 1, (void *) cmd, lua_thread_str_error_cb) != 0) {
+ exit(EXIT_FAILURE);
+ }
+
+ lua_settop(L, 0);
+}
+
+static const gchar *
+rspamadm_lua_command_help(gboolean full_help,
+ const struct rspamadm_command *cmd)
+{
+ gint table_idx = GPOINTER_TO_INT(cmd->command_data);
+
+ if (full_help) {
+ struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
+
+ lua_State *L = thread->lua_state;
+ lua_rawgeti(L, LUA_REGISTRYINDEX, table_idx);
+ /* Function */
+ lua_pushstring(L, "handler");
+ lua_gettable(L, -2);
+
+ /* Args */
+ lua_createtable(L, 2, 0);
+ lua_pushstring(L, cmd->name);
+ lua_rawseti(L, -2, 0); /* Starting from zero ! */
+
+ lua_pushstring(L, "--help");
+ lua_rawseti(L, -2, 1);
+
+ if (lua_repl_thread_call(thread, 1, (void *) cmd, lua_thread_str_error_cb) != 0) {
+ exit(EXIT_FAILURE);
+ }
+
+ lua_settop(L, 0);
+ }
+ else {
+ lua_State *L = rspamd_main->cfg->lua_state;
+ lua_rawgeti(L, LUA_REGISTRYINDEX, table_idx);
+ lua_pushstring(L, "description");
+ lua_gettable(L, -2);
+
+ if (lua_isstring(L, -1)) {
+ printf(" %-18s %-60s\n", cmd->name, lua_tostring(L, -1));
+ }
+ else {
+ printf(" %-18s %-60s\n", cmd->name, "no description available");
+ }
+
+ lua_settop(L, 0);
+ }
+
+ return NULL; /* Must be handled in rspamadm itself */
+}
+
+void rspamadm_fill_lua_commands(lua_State *L, GPtrArray *dest)
+{
+ gint i;
+
+ GPtrArray *lua_paths;
+ GError *err = NULL;
+ const gchar *lualibdir = RSPAMD_LUALIBDIR, *path;
+ struct rspamadm_command *lua_cmd;
+ gchar search_dir[PATH_MAX];
+
+ if (g_hash_table_lookup(ucl_vars, "LUALIBDIR")) {
+ lualibdir = g_hash_table_lookup(ucl_vars, "LUALIBDIR");
+ }
+
+ rspamd_snprintf(search_dir, sizeof(search_dir), "%s%crspamadm%c",
+ lualibdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR);
+
+ if ((lua_paths = rspamd_glob_path(search_dir, "*.lua", FALSE, &err)) == NULL) {
+ msg_err("cannot glob files in %s/*.lua: %e", search_dir, err);
+ g_error_free(err);
+
+ return;
+ }
+
+ PTR_ARRAY_FOREACH(lua_paths, i, path)
+ {
+ if (luaL_dofile(L, path) != 0) {
+ msg_err("cannot execute lua script %s: %s",
+ path, lua_tostring(L, -1));
+ lua_settop(L, 0);
+ continue;
+ }
+ else {
+ if (lua_type(L, -1) == LUA_TTABLE) {
+ lua_pushstring(L, "handler");
+ lua_gettable(L, -2);
+ }
+ else {
+ continue; /* Something goes wrong, huh */
+ }
+
+ if (lua_type(L, -1) != LUA_TFUNCTION) {
+ msg_err("rspamadm script %s does not have 'handler' field with type "
+ "function",
+ path);
+ continue;
+ }
+
+ /* Pop handler */
+ lua_pop(L, 1);
+ lua_cmd = g_malloc0(sizeof(*lua_cmd));
+
+ lua_pushstring(L, "name");
+ lua_gettable(L, -2);
+
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ lua_cmd->name = g_strdup(lua_tostring(L, -1));
+ }
+ else {
+ goffset ext_pos;
+ gchar *name;
+
+ name = g_path_get_basename(path);
+ /* Remove .lua */
+ ext_pos = rspamd_substring_search(path, strlen(path), ".lua", 4);
+
+ if (ext_pos != -1) {
+ name[ext_pos] = '\0';
+ }
+
+ lua_cmd->name = name;
+ }
+
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "aliases");
+ lua_gettable(L, -2);
+
+ if (lua_type(L, -1) == LUA_TTABLE) {
+ lua_cmd->aliases = g_ptr_array_new_full(
+ rspamd_lua_table_size(L, -1),
+ g_free);
+
+ for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) {
+ if (lua_isstring(L, -1)) {
+ g_ptr_array_add(lua_cmd->aliases,
+ g_strdup(lua_tostring(L, -1)));
+ }
+ }
+ }
+
+ lua_pop(L, 1);
+
+ lua_pushvalue(L, -1);
+ /* Reference table itself */
+ lua_cmd->command_data = GINT_TO_POINTER(luaL_ref(L, LUA_REGISTRYINDEX));
+ lua_cmd->flags |= RSPAMADM_FLAG_LUA | RSPAMADM_FLAG_DYNAMIC;
+ lua_cmd->run = rspamadm_lua_command_run;
+ lua_cmd->help = rspamadm_lua_command_help;
+
+ g_ptr_array_add(dest, lua_cmd);
+ }
+
+ lua_settop(L, 0);
+ }
+
+ g_ptr_array_free(lua_paths, TRUE);
+}
diff --git a/src/rspamadm/configdump.c b/src/rspamadm/configdump.c
new file mode 100644
index 0000000..dc8b822
--- /dev/null
+++ b/src/rspamadm/configdump.c
@@ -0,0 +1,555 @@
+/*
+ * Copyright 2023 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.
+ */
+#include "config.h"
+#include "rspamadm.h"
+#include "cfg_file.h"
+#include "cfg_rcl.h"
+#include "utlist.h"
+#include "rspamd.h"
+#include "lua/lua_common.h"
+#include "utlist.h"
+
+static gboolean json = FALSE;
+static gboolean compact = FALSE;
+static gboolean show_help = FALSE;
+static gboolean show_comments = FALSE;
+static gboolean modules_state = FALSE;
+static gboolean symbol_groups_only = FALSE;
+static gboolean symbol_full_details = FALSE;
+static gboolean skip_template = FALSE;
+static gchar *config = NULL;
+extern struct rspamd_main *rspamd_main;
+/* Defined in modules.c */
+extern module_t *modules[];
+extern worker_t *workers[];
+
+static void rspamadm_configdump(gint argc, gchar **argv, const struct rspamadm_command *);
+static const char *rspamadm_configdump_help(gboolean full_help, const struct rspamadm_command *);
+
+struct rspamadm_command configdump_command = {
+ .name = "configdump",
+ .flags = 0,
+ .help = rspamadm_configdump_help,
+ .run = rspamadm_configdump,
+ .lua_subrs = NULL,
+};
+
+static GOptionEntry entries[] = {
+ {"json", 'j', 0, G_OPTION_ARG_NONE, &json,
+ "Json output (pretty formatted)", NULL},
+ {"compact", 'C', 0, G_OPTION_ARG_NONE, &compact,
+ "Compacted json output", NULL},
+ {"config", 'c', 0, G_OPTION_ARG_STRING, &config,
+ "Config file to test", NULL},
+ {"show-help", 'h', 0, G_OPTION_ARG_NONE, &show_help,
+ "Show help as comments for each option", NULL},
+ {"show-comments", 's', 0, G_OPTION_ARG_NONE, &show_comments,
+ "Show saved comments from the configuration file", NULL},
+ {"modules-state", 'm', 0, G_OPTION_ARG_NONE, &modules_state,
+ "Show modules state only", NULL},
+ {"groups", 'g', 0, G_OPTION_ARG_NONE, &symbol_groups_only,
+ "Show symbols groups only", NULL},
+ {"symbol-details", 'd', 0, G_OPTION_ARG_NONE, &symbol_full_details,
+ "Show full symbol details only", NULL},
+ {"skip-template", 'T', 0, G_OPTION_ARG_NONE, &skip_template,
+ "Do not apply Jinja templates", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+static const char *
+rspamadm_configdump_help(gboolean full_help, const struct rspamadm_command *cmd)
+{
+ const char *help_str;
+
+ if (full_help) {
+ help_str = "Perform configuration file dump\n\n"
+ "Usage: rspamadm configdump [-c <config_name> [-j --compact -m] [<path1> [<path2> ...]]]\n"
+ "Where options are:\n\n"
+ "-j: output plain json\n"
+ "--compact: output compacted json\n"
+ "-c: config file to test\n"
+ "-m: show state of modules only\n"
+ "-h: show help for dumped options\n"
+ "--help: shows available options and commands";
+ }
+ else {
+ help_str = "Perform configuration file dump";
+ }
+
+ return help_str;
+}
+
+static void
+config_logger(rspamd_mempool_t *pool, gpointer ud)
+{
+}
+
+static void
+rspamadm_add_doc_elt(const ucl_object_t *obj, const ucl_object_t *doc_obj,
+ ucl_object_t *comment_obj)
+{
+ rspamd_fstring_t *comment = rspamd_fstring_new();
+ const ucl_object_t *elt;
+ ucl_object_t *nobj, *cur_comment;
+
+ if (ucl_object_lookup_len(comment_obj, (const char *) &obj,
+ sizeof(void *))) {
+ rspamd_fstring_free(comment);
+ /* Do not rewrite the existing comment */
+ return;
+ }
+
+ if (doc_obj != NULL) {
+ /* Create doc comment */
+ nobj = ucl_object_fromstring_common("/*", 0, 0);
+ }
+ else {
+ rspamd_fstring_free(comment);
+ return;
+ }
+
+ /* We create comments as a list of parts */
+ elt = ucl_object_lookup(doc_obj, "data");
+ if (elt) {
+ rspamd_printf_fstring(&comment, " * %s", ucl_object_tostring(elt));
+ cur_comment = ucl_object_fromstring_common(comment->str, comment->len, 0);
+ rspamd_fstring_erase(comment, 0, comment->len);
+ DL_APPEND(nobj, cur_comment);
+ }
+
+ elt = ucl_object_lookup(doc_obj, "type");
+ if (elt) {
+ rspamd_printf_fstring(&comment, " * Type: %s", ucl_object_tostring(elt));
+ cur_comment = ucl_object_fromstring_common(comment->str, comment->len, 0);
+ rspamd_fstring_erase(comment, 0, comment->len);
+ DL_APPEND(nobj, cur_comment);
+ }
+
+ elt = ucl_object_lookup(doc_obj, "required");
+ if (elt) {
+ rspamd_printf_fstring(&comment, " * Required: %s",
+ ucl_object_toboolean(elt) ? "true" : "false");
+ cur_comment = ucl_object_fromstring_common(comment->str, comment->len, 0);
+ rspamd_fstring_erase(comment, 0, comment->len);
+ DL_APPEND(nobj, cur_comment);
+ }
+
+ cur_comment = ucl_object_fromstring(" */");
+ DL_APPEND(nobj, cur_comment);
+ rspamd_fstring_free(comment);
+
+ ucl_object_insert_key(comment_obj, ucl_object_ref(nobj),
+ (const char *) &obj,
+ sizeof(void *), true);
+
+ ucl_object_unref(nobj);
+}
+
+static void
+rspamadm_gen_comments(const ucl_object_t *obj, const ucl_object_t *doc_obj,
+ ucl_object_t *comments)
+{
+ const ucl_object_t *cur_obj, *cur_doc, *cur_elt;
+ ucl_object_iter_t it = NULL;
+
+ if (obj == NULL || doc_obj == NULL) {
+ return;
+ }
+
+ if (obj->keylen > 0) {
+ rspamadm_add_doc_elt(obj, doc_obj, comments);
+ }
+
+ if (ucl_object_type(obj) == UCL_OBJECT) {
+ while ((cur_obj = ucl_object_iterate(obj, &it, true))) {
+ cur_doc = ucl_object_lookup_len(doc_obj, cur_obj->key,
+ cur_obj->keylen);
+
+ if (cur_doc != NULL) {
+ LL_FOREACH(cur_obj, cur_elt)
+ {
+ if (ucl_object_lookup_len(comments, (const char *) &cur_elt,
+ sizeof(void *)) == NULL) {
+ rspamadm_gen_comments(cur_elt, cur_doc, comments);
+ }
+ }
+ }
+ }
+ }
+}
+
+static void
+rspamadm_dump_section_obj(struct rspamd_config *cfg,
+ const ucl_object_t *obj, const ucl_object_t *doc_obj)
+{
+ rspamd_fstring_t *output;
+ ucl_object_t *comments = NULL;
+
+ output = rspamd_fstring_new();
+
+ if (show_help) {
+ if (show_comments) {
+ comments = cfg->config_comments;
+ }
+ else {
+ comments = ucl_object_typed_new(UCL_OBJECT);
+ }
+
+ rspamadm_gen_comments(obj, doc_obj, comments);
+ }
+ else if (show_comments) {
+ comments = cfg->config_comments;
+ }
+
+ if (json) {
+ rspamd_ucl_emit_fstring_comments(obj, UCL_EMIT_JSON, &output, comments);
+ }
+ else if (compact) {
+ rspamd_ucl_emit_fstring_comments(obj, UCL_EMIT_JSON_COMPACT, &output,
+ comments);
+ }
+ else {
+ rspamd_ucl_emit_fstring_comments(obj, UCL_EMIT_CONFIG, &output,
+ comments);
+ }
+
+ rspamd_printf("%V", output);
+ rspamd_fstring_free(output);
+
+ if (comments != NULL) {
+ ucl_object_unref(comments);
+ }
+}
+
+__attribute__((noreturn)) static void
+rspamadm_configdump(gint argc, gchar **argv, const struct rspamadm_command *cmd)
+{
+ GOptionContext *context;
+ GError *error = NULL;
+ const gchar *confdir;
+ const ucl_object_t *obj = NULL, *cur, *doc_obj;
+ struct rspamd_config *cfg = rspamd_main->cfg;
+ gboolean ret = TRUE;
+ worker_t **pworker;
+ gint i;
+
+ context = g_option_context_new(
+ "configdump - dumps Rspamd configuration");
+ g_option_context_set_summary(context,
+ "Summary:\n Rspamd administration utility version " RVERSION
+ "\n Release id: " RID);
+ g_option_context_add_main_entries(context, entries, NULL);
+
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ fprintf(stderr, "option parsing failed: %s\n", error->message);
+ g_error_free(error);
+ g_option_context_free(context);
+ exit(EXIT_FAILURE);
+ }
+
+ g_option_context_free(context);
+
+ if (config == NULL) {
+ if ((confdir = g_hash_table_lookup(ucl_vars, "CONFDIR")) == NULL) {
+ confdir = RSPAMD_CONFDIR;
+ }
+
+ config = g_strdup_printf("%s%c%s", confdir, G_DIR_SEPARATOR,
+ "rspamd.conf");
+ }
+
+ pworker = &workers[0];
+ while (*pworker) {
+ /* Init string quarks */
+ (void) g_quark_from_static_string((*pworker)->name);
+ pworker++;
+ }
+
+ cfg->compiled_modules = modules;
+ cfg->compiled_workers = workers;
+ cfg->cfg_name = config;
+
+ if (!rspamd_config_read(cfg, cfg->cfg_name, config_logger, rspamd_main,
+ ucl_vars, skip_template, lua_env)) {
+ ret = FALSE;
+ }
+ else {
+ /* Do post-load actions */
+ rspamd_lua_post_load_config(cfg);
+
+ (void) rspamd_init_filters(rspamd_main->cfg, false, false);
+ rspamd_config_post_load(cfg, RSPAMD_CONFIG_INIT_SYMCACHE);
+ }
+
+ if (ret) {
+ if (modules_state) {
+
+ rspamadm_execute_lua_ucl_subr(argc,
+ argv,
+ cfg->cfg_ucl_obj,
+ "plugins_stats",
+ FALSE);
+
+ exit(EXIT_SUCCESS);
+ }
+
+ if (symbol_full_details) {
+ /*
+ * Create object from symbols groups and output it using the
+ * specified format
+ */
+ ucl_object_t *out = ucl_object_typed_new(UCL_OBJECT);
+ GHashTableIter it;
+ gpointer sk, sv;
+
+ g_hash_table_iter_init(&it, cfg->symbols);
+ ucl_object_t *sym_ucl = ucl_object_typed_new(UCL_OBJECT);
+ const ucl_object_t *all_symbols_ucl = ucl_object_lookup(cfg->cfg_ucl_obj, "symbols");
+
+ while (g_hash_table_iter_next(&it, &sk, &sv)) {
+ const gchar *sym_name = (const gchar *) sk;
+ struct rspamd_symbol *s = (struct rspamd_symbol *) sv;
+ ucl_object_t *this_sym_ucl = ucl_object_typed_new(UCL_OBJECT);
+
+ ucl_object_insert_key(this_sym_ucl,
+ ucl_object_fromdouble(s->score),
+ "score", strlen("score"),
+ false);
+
+ ucl_object_insert_key(this_sym_ucl,
+ ucl_object_fromstring(s->description),
+ "description", strlen("description"), false);
+
+ rspamd_symcache_get_symbol_details(cfg->cache, sym_name, this_sym_ucl);
+
+ ucl_object_insert_key(this_sym_ucl,
+ ucl_object_frombool(!!(s->flags & RSPAMD_SYMBOL_FLAG_DISABLED)),
+ "disabled", strlen("disabled"),
+ false);
+
+ if (s->nshots == 1) {
+ ucl_object_insert_key(this_sym_ucl,
+ ucl_object_frombool(true),
+ "one_shot", strlen("one_shot"),
+ false);
+ }
+ else {
+ ucl_object_insert_key(this_sym_ucl,
+ ucl_object_frombool(false),
+ "one_shot", strlen("one_shot"),
+ false);
+ }
+
+ if (s->gr != NULL) {
+ struct rspamd_symbols_group *gr = s->gr;
+ const char *gr_name = gr->name;
+ if (strcmp(gr_name, "ungrouped") != 0) {
+ ucl_object_insert_key(this_sym_ucl,
+ ucl_object_fromstring(gr_name),
+ "group", strlen("group"),
+ false);
+ }
+
+ if (s->groups) {
+ ucl_object_t *add_groups = ucl_object_typed_new(UCL_ARRAY);
+ guint j;
+ struct rspamd_symbols_group *add_gr;
+ bool has_extra_groups = false;
+
+ PTR_ARRAY_FOREACH(s->groups, j, add_gr)
+ {
+ if (add_gr->name && strcmp(add_gr->name, gr_name) != 0) {
+ ucl_array_append(add_groups,
+ ucl_object_fromstring(add_gr->name));
+ has_extra_groups = true;
+ }
+ }
+
+ if (has_extra_groups == true) {
+ ucl_object_insert_key(this_sym_ucl,
+ add_groups,
+ "groups", strlen("groups"),
+ false);
+ }
+ }
+ }
+
+ const ucl_object_t *loaded_symbol_ucl = ucl_object_lookup(all_symbols_ucl, sym_name);
+ if (loaded_symbol_ucl) {
+ ucl_object_iter_t it = NULL;
+ while ((cur = ucl_iterate_object(loaded_symbol_ucl, &it, true)) != NULL) {
+ const char *key = ucl_object_key(cur);
+ /* If this key isn't something we have direct in the symbol item, grab the key/value */
+ if ((strcmp(key, "score") != 0) &&
+ (strcmp(key, "description") != 0) &&
+ (strcmp(key, "disabled") != 0) &&
+ (strcmp(key, "condition") != 0) &&
+ (strcmp(key, "one_shot") != 0) &&
+ (strcmp(key, "any_shot") != 0) &&
+ (strcmp(key, "nshots") != 0) &&
+ (strcmp(key, "one_param") != 0) &&
+ (strcmp(key, "priority") != 0)) {
+ ucl_object_insert_key(this_sym_ucl, (ucl_object_t *) cur, key, strlen(key), false);
+ }
+ }
+ }
+
+ ucl_object_insert_key(sym_ucl, this_sym_ucl, sym_name,
+ strlen(sym_name), true);
+ }
+ ucl_object_insert_key(out, sym_ucl, "symbols",
+ strlen("symbols"), true);
+
+ rspamadm_dump_section_obj(cfg, out, NULL);
+ exit(EXIT_SUCCESS);
+ }
+
+ if (symbol_groups_only) {
+ /*
+ * Create object from symbols groups and output it using the
+ * specified format
+ */
+ ucl_object_t *out = ucl_object_typed_new(UCL_OBJECT);
+ GHashTableIter it;
+ gpointer k, v;
+
+ g_hash_table_iter_init(&it, cfg->groups);
+
+ while (g_hash_table_iter_next(&it, &k, &v)) {
+ const gchar *gr_name = (const gchar *) k;
+ struct rspamd_symbols_group *gr = (struct rspamd_symbols_group *) v;
+ ucl_object_t *gr_ucl = ucl_object_typed_new(UCL_OBJECT);
+
+ ucl_object_insert_key(gr_ucl,
+ ucl_object_frombool(!!(gr->flags & RSPAMD_SYMBOL_GROUP_PUBLIC)),
+ "public", strlen("public"), false);
+ ucl_object_insert_key(gr_ucl,
+ ucl_object_frombool(!!(gr->flags & RSPAMD_SYMBOL_GROUP_DISABLED)),
+ "disabled", strlen("disabled"), false);
+ ucl_object_insert_key(gr_ucl,
+ ucl_object_frombool(!!(gr->flags & RSPAMD_SYMBOL_GROUP_ONE_SHOT)),
+ "one_shot", strlen("one_shot"), false);
+ ucl_object_insert_key(gr_ucl,
+ ucl_object_fromdouble(gr->max_score),
+ "max_score", strlen("max_score"), false);
+ ucl_object_insert_key(gr_ucl,
+ ucl_object_fromstring(gr->description),
+ "description", strlen("description"), false);
+
+ if (gr->symbols) {
+ GHashTableIter sit;
+ gpointer sk, sv;
+
+ g_hash_table_iter_init(&sit, gr->symbols);
+ ucl_object_t *sym_ucl = ucl_object_typed_new(UCL_OBJECT);
+
+ while (g_hash_table_iter_next(&sit, &sk, &sv)) {
+ const gchar *sym_name = (const gchar *) sk;
+ struct rspamd_symbol *s = (struct rspamd_symbol *) sv;
+ ucl_object_t *spec_sym = ucl_object_typed_new(UCL_OBJECT);
+
+ ucl_object_insert_key(spec_sym,
+ ucl_object_fromdouble(s->score),
+ "score", strlen("score"),
+ false);
+ ucl_object_insert_key(spec_sym,
+ ucl_object_fromstring(s->description),
+ "description", strlen("description"), false);
+ ucl_object_insert_key(spec_sym,
+ ucl_object_frombool(!!(s->flags & RSPAMD_SYMBOL_FLAG_DISABLED)),
+ "disabled", strlen("disabled"),
+ false);
+
+ if (s->nshots == 1) {
+ ucl_object_insert_key(spec_sym,
+ ucl_object_frombool(true),
+ "one_shot", strlen("one_shot"),
+ false);
+ }
+ else {
+ ucl_object_insert_key(spec_sym,
+ ucl_object_frombool(false),
+ "one_shot", strlen("one_shot"),
+ false);
+ }
+
+ ucl_object_t *add_groups = ucl_object_typed_new(UCL_ARRAY);
+ guint j;
+ struct rspamd_symbols_group *add_gr;
+
+ PTR_ARRAY_FOREACH(s->groups, j, add_gr)
+ {
+ if (add_gr->name && strcmp(add_gr->name, gr_name) != 0) {
+ ucl_array_append(add_groups,
+ ucl_object_fromstring(add_gr->name));
+ }
+ }
+
+ ucl_object_insert_key(spec_sym,
+ add_groups,
+ "extra_groups", strlen("extra_groups"),
+ false);
+
+ ucl_object_insert_key(sym_ucl, spec_sym, sym_name,
+ strlen(sym_name), true);
+ }
+
+ ucl_object_insert_key(gr_ucl, sym_ucl, "symbols",
+ strlen("symbols"), false);
+ }
+
+ ucl_object_insert_key(out, gr_ucl, gr_name, strlen(gr_name),
+ true);
+ }
+
+ rspamadm_dump_section_obj(cfg, out, NULL);
+
+ exit(EXIT_SUCCESS);
+ }
+
+ /* Output configuration */
+ if (argc == 1) {
+ rspamadm_dump_section_obj(cfg, cfg->cfg_ucl_obj, cfg->doc_strings);
+ }
+ else {
+ for (i = 1; i < argc; i++) {
+ obj = ucl_object_lookup_path(cfg->cfg_ucl_obj, argv[i]);
+ doc_obj = ucl_object_lookup_path(cfg->doc_strings, argv[i]);
+
+ if (!obj) {
+ rspamd_printf("Section %s NOT FOUND\n", argv[i]);
+ }
+ else {
+ LL_FOREACH(obj, cur)
+ {
+ if (!json && !compact) {
+ rspamd_printf("*** Section %s ***\n", argv[i]);
+ }
+ rspamadm_dump_section_obj(cfg, cur, doc_obj);
+
+ if (!json && !compact) {
+ rspamd_printf("\n*** End of section %s ***\n", argv[i]);
+ }
+ else {
+ rspamd_printf("\n");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ exit(ret ? EXIT_SUCCESS : EXIT_FAILURE);
+}
diff --git a/src/rspamadm/confighelp.c b/src/rspamadm/confighelp.c
new file mode 100644
index 0000000..2ad07c0
--- /dev/null
+++ b/src/rspamadm/confighelp.c
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2023 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.
+ */
+#include <ucl.h>
+#include "config.h"
+#include "rspamadm.h"
+#include "cfg_file.h"
+#include "cfg_rcl.h"
+#include "rspamd.h"
+#include "lua/lua_common.h"
+
+static gboolean json = FALSE;
+static gboolean compact = FALSE;
+static gboolean keyword = FALSE;
+static const gchar *plugins_path = RSPAMD_PLUGINSDIR;
+extern struct rspamd_main *rspamd_main;
+/* Defined in modules.c */
+extern module_t *modules[];
+extern worker_t *workers[];
+
+static void rspamadm_confighelp(gint argc, gchar **argv,
+ const struct rspamadm_command *cmd);
+
+static const char *rspamadm_confighelp_help(gboolean full_help,
+ const struct rspamadm_command *cmd);
+
+struct rspamadm_command confighelp_command = {
+ .name = "confighelp",
+ .flags = 0,
+ .help = rspamadm_confighelp_help,
+ .run = rspamadm_confighelp,
+ .lua_subrs = NULL,
+};
+
+static GOptionEntry entries[] = {
+ {"json", 'j', 0, G_OPTION_ARG_NONE, &json,
+ "Output json", NULL},
+ {"compact", 'c', 0, G_OPTION_ARG_NONE, &compact,
+ "Output compacted", NULL},
+ {"keyword", 'k', 0, G_OPTION_ARG_NONE, &keyword,
+ "Search by keyword", NULL},
+ {"plugins", 'P', 0, G_OPTION_ARG_STRING, &plugins_path,
+ "Use the following plugin path (" RSPAMD_PLUGINSDIR ")", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+static const char *
+rspamadm_confighelp_help(gboolean full_help, const struct rspamadm_command *cmd)
+{
+ const char *help_str;
+
+ if (full_help) {
+ help_str = "Shows help for the specified configuration options\n\n"
+ "Usage: rspamadm confighelp [option[, option...]]\n"
+ "Where options are:\n\n"
+ "-c: output compacted JSON\n"
+ "-j: output pretty formatted JSON\n"
+ "-k: search by keyword in doc string\n"
+ "-P: use specific Lua plugins path\n"
+ "--no-color: disable coloured output\n"
+ "--short: show only option names\n"
+ "--no-examples: do not show examples (implied by --short)\n"
+ "--help: shows available options and commands";
+ }
+ else {
+ help_str = "Shows help for configuration options";
+ }
+
+ return help_str;
+}
+
+static void
+rspamadm_confighelp_show(struct rspamd_config *cfg, gint argc, gchar **argv,
+ const char *key, const ucl_object_t *obj)
+{
+ rspamd_fstring_t *out;
+
+ rspamd_lua_set_path(cfg->lua_state, NULL, ucl_vars);
+ out = rspamd_fstring_new();
+
+ if (json) {
+ rspamd_ucl_emit_fstring(obj, UCL_EMIT_JSON, &out);
+ }
+ else if (compact) {
+ rspamd_ucl_emit_fstring(obj, UCL_EMIT_JSON_COMPACT, &out);
+ }
+ else {
+ /* TODO: add lua helper for output */
+ if (key) {
+ rspamd_fprintf(stdout, "Showing help for %s%s:\n",
+ keyword ? "keyword " : "", key);
+ }
+ else {
+ rspamd_fprintf(stdout, "Showing help for all options:\n");
+ }
+
+ rspamadm_execute_lua_ucl_subr(argc,
+ argv,
+ obj,
+ "confighelp",
+ TRUE);
+
+ rspamd_fstring_free(out);
+ return;
+ }
+
+ rspamd_fprintf(stdout, "%V", out);
+ rspamd_fprintf(stdout, "\n");
+
+ rspamd_fstring_free(out);
+}
+
+static void
+rspamadm_confighelp_search_word_step(const ucl_object_t *obj,
+ ucl_object_t *res,
+ const gchar *str,
+ gsize len,
+ GString *path)
+{
+ ucl_object_iter_t it = NULL;
+ const ucl_object_t *cur, *elt;
+ const gchar *dot_pos;
+
+ while ((cur = ucl_object_iterate(obj, &it, true)) != NULL) {
+ if (cur->keylen > 0) {
+ rspamd_printf_gstring(path, ".%*s", (int) cur->keylen, cur->key);
+
+ if (rspamd_substring_search_caseless(cur->key,
+ cur->keylen,
+ str,
+ len) != -1) {
+ ucl_object_insert_key(res, ucl_object_ref(cur),
+ path->str, path->len, true);
+ goto fin;
+ }
+ }
+
+ if (ucl_object_type(cur) == UCL_OBJECT) {
+ elt = ucl_object_lookup(cur, "data");
+
+ if (elt != NULL && ucl_object_type(elt) == UCL_STRING) {
+ if (rspamd_substring_search_caseless(elt->value.sv,
+ elt->len,
+ str,
+ len) != -1) {
+ ucl_object_insert_key(res, ucl_object_ref(cur),
+ path->str, path->len, true);
+ goto fin;
+ }
+ }
+
+ rspamadm_confighelp_search_word_step(cur, res, str, len, path);
+ }
+
+ fin:
+ /* Remove the last component of the path */
+ dot_pos = strrchr(path->str, '.');
+
+ if (dot_pos) {
+ g_string_erase(path, dot_pos - path->str,
+ path->len - (dot_pos - path->str));
+ }
+ }
+}
+
+static ucl_object_t *
+rspamadm_confighelp_search_word(const ucl_object_t *obj, const gchar *str)
+{
+ gsize len = strlen(str);
+ GString *path = g_string_new("");
+ ucl_object_t *res;
+
+
+ res = ucl_object_typed_new(UCL_OBJECT);
+
+ rspamadm_confighelp_search_word_step(obj, res, str, len, path);
+
+ return res;
+}
+
+__attribute__((noreturn)) static void
+rspamadm_confighelp(gint argc, gchar **argv, const struct rspamadm_command *cmd)
+{
+ struct rspamd_config *cfg;
+ ucl_object_t *doc_obj;
+ const ucl_object_t *elt;
+ GOptionContext *context;
+ GError *error = NULL;
+ module_t *mod, **pmod;
+ worker_t **pworker;
+ struct module_ctx *mod_ctx;
+ gint i, ret = 0, processed_args = 0;
+
+ context = g_option_context_new(
+ "confighelp - displays help for the configuration options");
+ g_option_context_set_summary(context,
+ "Summary:\n Rspamd administration utility version " RVERSION
+ "\n Release id: " RID);
+ g_option_context_add_main_entries(context, entries, NULL);
+ g_option_context_set_ignore_unknown_options(context, TRUE);
+
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ rspamd_fprintf(stderr, "option parsing failed: %s\n", error->message);
+ g_error_free(error);
+ g_option_context_free(context);
+ exit(EXIT_FAILURE);
+ }
+
+ g_option_context_free(context);
+ pworker = &workers[0];
+ while (*pworker) {
+ /* Init string quarks */
+ (void) g_quark_from_static_string((*pworker)->name);
+ pworker++;
+ }
+
+ cfg = rspamd_config_new(RSPAMD_CONFIG_INIT_SKIP_LUA);
+ cfg->lua_state = rspamd_main->cfg->lua_state;
+ cfg->compiled_modules = modules;
+ cfg->compiled_workers = workers;
+
+ rspamd_rcl_config_init(cfg, NULL);
+ lua_pushboolean(cfg->lua_state, true);
+ lua_setglobal(cfg->lua_state, "confighelp");
+ rspamd_rcl_add_lua_plugins_path(cfg->rcl_top_section, cfg, plugins_path, FALSE, NULL);
+
+ /* Init modules to get documentation strings */
+ i = 0;
+ for (pmod = cfg->compiled_modules; pmod != NULL && *pmod != NULL; pmod++) {
+ mod = *pmod;
+ mod_ctx = g_malloc0(sizeof(struct module_ctx));
+
+ if (mod->module_init_func(cfg, &mod_ctx) == 0) {
+ g_ptr_array_add(cfg->c_modules, mod_ctx);
+ mod_ctx->mod = mod;
+ mod->ctx_offset = i++;
+ mod_ctx->mod = mod;
+ }
+ }
+ /* Also init all workers */
+ for (pworker = cfg->compiled_workers; *pworker != NULL; pworker++) {
+ (*pworker)->worker_init_func(cfg);
+ }
+
+ /* Init lua modules */
+ rspamd_lua_set_path(cfg->lua_state, cfg->cfg_ucl_obj, ucl_vars);
+ rspamd_init_lua_filters(cfg, true, false);
+
+ if (argc > 1) {
+ for (i = 1; i < argc; i++) {
+ if (argv[i][0] != '-') {
+
+ if (keyword) {
+ doc_obj = rspamadm_confighelp_search_word(cfg->doc_strings,
+ argv[i]);
+ }
+ else {
+ doc_obj = ucl_object_typed_new(UCL_OBJECT);
+ elt = ucl_object_lookup_path(cfg->doc_strings, argv[i]);
+
+ if (elt) {
+ ucl_object_insert_key(doc_obj, ucl_object_ref(elt),
+ argv[i], 0, false);
+ }
+ }
+
+ if (doc_obj != NULL) {
+ rspamadm_confighelp_show(cfg, argc, argv, argv[i], doc_obj);
+ ucl_object_unref(doc_obj);
+ }
+ else {
+ rspamd_fprintf(stderr,
+ "Cannot find help for %s\n",
+ argv[i]);
+ ret = EXIT_FAILURE;
+ }
+ processed_args++;
+ }
+ }
+ }
+
+ if (processed_args == 0) {
+ /* Show all documentation strings */
+ rspamadm_confighelp_show(cfg, argc, argv, NULL, cfg->doc_strings);
+ }
+
+ rspamd_config_free(cfg);
+
+ exit(ret);
+}
diff --git a/src/rspamadm/configtest.c b/src/rspamadm/configtest.c
new file mode 100644
index 0000000..8f1482f
--- /dev/null
+++ b/src/rspamadm/configtest.c
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2023 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.
+ */
+#include "config.h"
+#include "rspamadm.h"
+#include "cfg_file.h"
+#include "cfg_rcl.h"
+#include "rspamd.h"
+#include "lua/lua_common.h"
+
+static gboolean quiet = FALSE;
+static gchar *config = NULL;
+static gboolean strict = FALSE;
+static gboolean skip_template = FALSE;
+extern struct rspamd_main *rspamd_main;
+/* Defined in modules.c */
+extern module_t *modules[];
+extern worker_t *workers[];
+
+static void rspamadm_configtest(gint argc, gchar **argv,
+ const struct rspamadm_command *cmd);
+static const char *rspamadm_configtest_help(gboolean full_help,
+ const struct rspamadm_command *cmd);
+
+struct rspamadm_command configtest_command = {
+ .name = "configtest",
+ .flags = 0,
+ .help = rspamadm_configtest_help,
+ .run = rspamadm_configtest,
+ .lua_subrs = NULL,
+};
+
+static GOptionEntry entries[] = {
+ {"quiet", 'q', 0, G_OPTION_ARG_NONE, &quiet,
+ "Suppress output", NULL},
+ {"config", 'c', 0, G_OPTION_ARG_STRING, &config,
+ "Config file to test", NULL},
+ {"strict", 's', 0, G_OPTION_ARG_NONE, &strict,
+ "Stop on any error in config", NULL},
+ {"skip-template", 'T', 0, G_OPTION_ARG_NONE, &skip_template,
+ "Do not apply Jinja templates", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+static const char *
+rspamadm_configtest_help(gboolean full_help, const struct rspamadm_command *cmd)
+{
+ const char *help_str;
+
+ if (full_help) {
+ help_str = "Perform configuration file test\n\n"
+ "Usage: rspamadm configtest [-q -c <config_name>]\n"
+ "Where options are:\n\n"
+ "-q: quiet output\n"
+ "-c: config file to test\n"
+ "--help: shows available options and commands";
+ }
+ else {
+ help_str = "Perform configuration file test";
+ }
+
+ return help_str;
+}
+
+static void
+config_logger(rspamd_mempool_t *pool, gpointer ud)
+{
+}
+
+static void
+rspamadm_configtest(gint argc, gchar **argv, const struct rspamadm_command *cmd)
+{
+ GOptionContext *context;
+ GError *error = NULL;
+ const gchar *confdir;
+ struct rspamd_config *cfg = rspamd_main->cfg;
+ gboolean ret = TRUE;
+ worker_t **pworker;
+ const guint64 *log_cnt;
+
+ context = g_option_context_new(
+ "configtest - perform configuration file test");
+ g_option_context_set_summary(context,
+ "Summary:\n Rspamd administration utility version " RVERSION
+ "\n Release id: " RID);
+ g_option_context_add_main_entries(context, entries, NULL);
+
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ fprintf(stderr, "option parsing failed: %s\n", error->message);
+ g_error_free(error);
+ g_option_context_free(context);
+ exit(EXIT_FAILURE);
+ }
+
+ g_option_context_free(context);
+
+ if (config == NULL) {
+ static gchar fbuf[PATH_MAX];
+
+ if ((confdir = g_hash_table_lookup(ucl_vars, "CONFDIR")) == NULL) {
+ confdir = RSPAMD_CONFDIR;
+ }
+
+ rspamd_snprintf(fbuf, sizeof(fbuf), "%s%c%s",
+ confdir, G_DIR_SEPARATOR,
+ "rspamd.conf");
+ config = fbuf;
+ }
+
+ pworker = &workers[0];
+ while (*pworker) {
+ /* Init string quarks */
+ (void) g_quark_from_static_string((*pworker)->name);
+ pworker++;
+ }
+
+ cfg->compiled_modules = modules;
+ cfg->compiled_workers = workers;
+ cfg->cfg_name = config;
+
+ if (!rspamd_config_read(cfg, cfg->cfg_name, config_logger, rspamd_main,
+ ucl_vars, skip_template, lua_env)) {
+ ret = FALSE;
+ }
+ else {
+ /* Do post-load actions */
+ rspamd_lua_post_load_config(cfg);
+
+ if (!rspamd_init_filters(rspamd_main->cfg, false, strict)) {
+ ret = FALSE;
+ }
+
+ if (ret) {
+ ret = rspamd_config_post_load(cfg, RSPAMD_CONFIG_INIT_SYMCACHE);
+ }
+
+ if (ret && !rspamd_symcache_validate(cfg->cache,
+ cfg,
+ FALSE)) {
+ ret = FALSE;
+ }
+
+ if (ret) {
+ if (rspamd_lua_require_function(cfg->lua_state, "lua_cfg_utils", "check_configuration_errors")) {
+ GError *err = NULL;
+
+ if (!rspamd_lua_universal_pcall(cfg->lua_state, -1, G_STRLOC, 1, "", &err)) {
+ msg_err_config("call to lua function failed: %s",
+ lua_tostring(cfg->lua_state, -1));
+ lua_pop(cfg->lua_state, 2);
+ ret = FALSE;
+ }
+ else {
+ ret = lua_toboolean(cfg->lua_state, -1);
+ lua_pop(cfg->lua_state, 2);
+ }
+ }
+ }
+ }
+
+ if (strict && ret) {
+ log_cnt = rspamd_log_counters(rspamd_main->logger);
+
+ if (log_cnt && log_cnt[0] > 0) {
+ if (!quiet) {
+ rspamd_printf("%L errors found\n", log_cnt[0]);
+ }
+ ret = FALSE;
+ }
+ }
+
+ if (!quiet) {
+ rspamd_printf("syntax %s\n", ret ? "OK" : "BAD");
+ }
+
+ if (!ret) {
+ exit(EXIT_FAILURE);
+ }
+}
diff --git a/src/rspamadm/control.c b/src/rspamadm/control.c
new file mode 100644
index 0000000..c82d4ac
--- /dev/null
+++ b/src/rspamadm/control.c
@@ -0,0 +1,258 @@
+/*-
+ * Copyright 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.
+ */
+#include "config.h"
+#include "rspamadm.h"
+#include "cryptobox.h"
+#include "printf.h"
+#include "libserver/http/http_connection.h"
+#include "libserver/http/http_private.h"
+#include "addr.h"
+#include "unix-std.h"
+#include "contrib/libev/ev.h"
+#include "libutil/util.h"
+#include "lua/lua_common.h"
+
+static gchar *control_path = RSPAMD_DBDIR "/rspamd.sock";
+static gboolean json = FALSE;
+static gboolean ucl = TRUE;
+static gboolean compact = FALSE;
+static gdouble timeout = 1.0;
+
+static void rspamadm_control(gint argc, gchar **argv,
+ const struct rspamadm_command *cmd);
+static const char *rspamadm_control_help(gboolean full_help,
+ const struct rspamadm_command *cmd);
+
+struct rspamadm_command control_command = {
+ .name = "control",
+ .flags = 0,
+ .help = rspamadm_control_help,
+ .run = rspamadm_control,
+ .lua_subrs = NULL,
+};
+
+struct rspamadm_control_cbdata {
+ const gchar *path;
+ gint argc;
+ gchar **argv;
+};
+
+static GOptionEntry entries[] = {
+ {"json", 'j', 0, G_OPTION_ARG_NONE, &json,
+ "Output json", NULL},
+ {"compact", 'c', 0, G_OPTION_ARG_NONE, &compact,
+ "Output compacted", NULL},
+ {"ucl", 'u', 0, G_OPTION_ARG_NONE, &ucl,
+ "Output ucl (default)", NULL},
+ {"socket", 's', 0, G_OPTION_ARG_STRING, &control_path,
+ "Use the following socket path", NULL},
+ {"timeout", 't', 0, G_OPTION_ARG_DOUBLE, &timeout,
+ "Set IO timeout (1s by default)", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+static const char *
+rspamadm_control_help(gboolean full_help, const struct rspamadm_command *cmd)
+{
+ const char *help_str;
+
+ if (full_help) {
+ help_str = "Manage rspamd main control interface\n\n"
+ "Usage: rspamadm control [-c] [-j] [-u] [-s path] command\n"
+ "Where options are:\n\n"
+ "-c: output compacted json\n"
+ "-j: output linted json\n"
+ "-u: output ucl (default)\n"
+ "-s: use the following socket instead of " RSPAMD_DBDIR "/rspamd.sock\n"
+ "-t: set IO timeout (1.0 seconds default)\n"
+ "--help: shows available options and commands\n\n"
+ "Supported commands:\n"
+ "stat - show statistics\n"
+ "reload - reload workers dynamic data\n"
+ "reresolve - resolve upstreams addresses\n"
+ "recompile - recompile hyperscan regexes\n"
+ "fuzzystat - show fuzzy statistics\n"
+ "fuzzysync - immediately sync fuzzy database to storage\n";
+ }
+ else {
+ help_str = "Manage rspamd main control interface";
+ }
+
+ return help_str;
+}
+
+static void
+rspamd_control_error_handler(struct rspamd_http_connection *conn, GError *err)
+{
+ rspamd_fprintf(stderr, "Cannot make HTTP request: %e\n", err);
+ ev_break(rspamd_main->event_loop, EVBREAK_ALL);
+}
+
+static gint
+rspamd_control_finish_handler(struct rspamd_http_connection *conn,
+ struct rspamd_http_message *msg)
+{
+ struct ucl_parser *parser;
+ ucl_object_t *obj;
+ rspamd_fstring_t *out;
+ const gchar *body;
+ gsize body_len;
+ struct rspamadm_control_cbdata *cbdata = conn->ud;
+
+ body = rspamd_http_message_get_body(msg, &body_len);
+ parser = ucl_parser_new(0);
+
+ if (!body || !ucl_parser_add_chunk(parser, body, body_len)) {
+ rspamd_fprintf(stderr, "cannot parse server's reply: %s\n",
+ ucl_parser_get_error(parser));
+ ucl_parser_free(parser);
+ }
+ else {
+ obj = ucl_parser_get_object(parser);
+ out = rspamd_fstring_new();
+
+ if (json) {
+ rspamd_ucl_emit_fstring(obj, UCL_EMIT_JSON, &out);
+ }
+ else if (compact) {
+ rspamd_ucl_emit_fstring(obj, UCL_EMIT_JSON_COMPACT, &out);
+ }
+ else {
+ if (strcmp(cbdata->path, "/fuzzystat") == 0) {
+ rspamadm_execute_lua_ucl_subr(cbdata->argc - 1,
+ &cbdata->argv[1],
+ obj,
+ "fuzzy_stat",
+ TRUE);
+
+ rspamd_fstring_free(out);
+ ucl_object_unref(obj);
+ ucl_parser_free(parser);
+ goto end;
+ }
+ else {
+ rspamd_ucl_emit_fstring(obj, UCL_EMIT_CONFIG, &out);
+ }
+ }
+
+ rspamd_fprintf(stdout, "%V", out);
+
+ rspamd_fstring_free(out);
+ ucl_object_unref(obj);
+ ucl_parser_free(parser);
+ }
+
+end:
+ ev_break(rspamd_main->event_loop, EVBREAK_ALL);
+
+ return 0;
+}
+
+static void
+rspamadm_control(gint argc, gchar **argv, const struct rspamadm_command *_cmd)
+{
+ GOptionContext *context;
+ GError *error = NULL;
+ const gchar *cmd, *path = NULL;
+ struct rspamd_http_connection *conn;
+ struct rspamd_http_message *msg;
+ rspamd_inet_addr_t *addr;
+ static struct rspamadm_control_cbdata cbdata;
+
+ context = g_option_context_new(
+ "control - manage rspamd main control interface");
+ g_option_context_set_summary(context,
+ "Summary:\n Rspamd administration utility version " RVERSION
+ "\n Release id: " RID);
+ g_option_context_add_main_entries(context, entries, NULL);
+ g_option_context_set_ignore_unknown_options(context, TRUE);
+
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ rspamd_fprintf(stderr, "option parsing failed: %s\n", error->message);
+ g_error_free(error);
+ g_option_context_free(context);
+ exit(EXIT_FAILURE);
+ }
+
+ g_option_context_free(context);
+
+ if (argc <= 1) {
+ rspamd_fprintf(stderr, "command required\n");
+ exit(EXIT_FAILURE);
+ }
+
+ cmd = argv[1];
+
+ if (g_ascii_strcasecmp(cmd, "stat") == 0) {
+ path = "/stat";
+ }
+ else if (g_ascii_strcasecmp(cmd, "reload") == 0) {
+ path = "/reload";
+ }
+ else if (g_ascii_strcasecmp(cmd, "reresolve") == 0) {
+ path = "/reresolve";
+ }
+ else if (g_ascii_strcasecmp(cmd, "recompile") == 0) {
+ path = "/recompile";
+ }
+ else if (g_ascii_strcasecmp(cmd, "fuzzystat") == 0 ||
+ g_ascii_strcasecmp(cmd, "fuzzy_stat") == 0) {
+ path = "/fuzzystat";
+ }
+ else if (g_ascii_strcasecmp(cmd, "fuzzysync") == 0 ||
+ g_ascii_strcasecmp(cmd, "fuzzy_sync") == 0) {
+ path = "/fuzzysync";
+ }
+ else {
+ rspamd_fprintf(stderr, "unknown command: %s\n", cmd);
+ exit(EXIT_FAILURE);
+ }
+
+ if (!rspamd_parse_inet_address(&addr,
+ control_path, strlen(control_path), RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {
+ rspamd_fprintf(stderr, "bad control path: %s\n", control_path);
+ exit(EXIT_FAILURE);
+ }
+
+
+ conn = rspamd_http_connection_new_client(
+ rspamd_main->http_ctx, /* Default context */
+ NULL,
+ rspamd_control_error_handler,
+ rspamd_control_finish_handler,
+ RSPAMD_HTTP_CLIENT_SIMPLE,
+ addr);
+
+ if (!conn) {
+ rspamd_fprintf(stderr, "cannot open connection to %s: %s\n",
+ control_path, strerror(errno));
+ exit(-errno);
+ }
+
+ msg = rspamd_http_new_message(HTTP_REQUEST);
+ msg->url = rspamd_fstring_new_init(path, strlen(path));
+
+ cbdata.argc = argc;
+ cbdata.argv = argv;
+ cbdata.path = path;
+
+ rspamd_http_connection_write_message(conn, msg, NULL, NULL, &cbdata,
+ timeout);
+
+ ev_loop(rspamd_main->event_loop, 0);
+
+ rspamd_http_connection_unref(conn);
+ rspamd_inet_address_free(addr);
+}
diff --git a/src/rspamadm/fuzzy_convert.c b/src/rspamadm/fuzzy_convert.c
new file mode 100644
index 0000000..a4da8bd
--- /dev/null
+++ b/src/rspamadm/fuzzy_convert.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2023 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.
+ */
+
+/*-
+ * Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
+ * Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
+ *
+ * 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.
+ */
+#include "config.h"
+#include "rspamadm.h"
+#include "lua/lua_common.h"
+
+static gchar *source_db = NULL;
+static gchar *redis_host = NULL;
+static gchar *redis_db = NULL;
+static gchar *redis_username = NULL;
+static gchar *redis_password = NULL;
+static int64_t fuzzy_expiry = 0;
+
+static void rspamadm_fuzzyconvert(gint argc, gchar **argv,
+ const struct rspamadm_command *cmd);
+static const char *rspamadm_fuzzyconvert_help(gboolean full_help,
+ const struct rspamadm_command *cmd);
+
+struct rspamadm_command fuzzyconvert_command = {
+ .name = "fuzzyconvert",
+ .flags = 0,
+ .help = rspamadm_fuzzyconvert_help,
+ .run = rspamadm_fuzzyconvert,
+ .lua_subrs = NULL,
+};
+
+static GOptionEntry entries[] = {
+ {"database", 'd', 0, G_OPTION_ARG_FILENAME, &source_db,
+ "Input sqlite", NULL},
+ {"expiry", 'e', 0, G_OPTION_ARG_INT, &fuzzy_expiry,
+ "Time in seconds after which hashes should be expired", NULL},
+ {"host", 'h', 0, G_OPTION_ARG_STRING, &redis_host,
+ "Output redis ip (in format ip:port)", NULL},
+ {"dbname", 'D', 0, G_OPTION_ARG_STRING, &redis_db,
+ "Database in redis (should be numeric)", NULL},
+ {"username", 'u', 0, G_OPTION_ARG_STRING, &redis_username,
+ "Username to connect to redis", NULL},
+ {"password", 'p', 0, G_OPTION_ARG_STRING, &redis_password,
+ "Password to connect to redis", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+
+static const char *
+rspamadm_fuzzyconvert_help(gboolean full_help, const struct rspamadm_command *cmd)
+{
+ const char *help_str;
+
+ if (full_help) {
+ help_str = "Convert fuzzy hashes from sqlite3 to redis\n\n"
+ "Usage: rspamadm fuzzyconvert -d <sqlite_db> -h <redis_ip>\n"
+ "Where options are:\n\n"
+ "-d: input sqlite\n"
+ "-h: output redis ip (in format ip:port)\n"
+ "-D: output redis database\n"
+ "-u: redis username\n"
+ "-p: redis password\n";
+ }
+ else {
+ help_str = "Convert fuzzy hashes from sqlite3 to redis";
+ }
+
+ return help_str;
+}
+
+static void
+rspamadm_fuzzyconvert(gint argc, gchar **argv, const struct rspamadm_command *cmd)
+{
+ GOptionContext *context;
+ GError *error = NULL;
+ ucl_object_t *obj;
+
+ context = g_option_context_new(
+ "fuzzyconvert - converts fuzzy hashes from sqlite3 to redis");
+ g_option_context_set_summary(context,
+ "Summary:\n Rspamd administration utility version " RVERSION
+ "\n Release id: " RID);
+ g_option_context_add_main_entries(context, entries, NULL);
+ g_option_context_set_ignore_unknown_options(context, TRUE);
+
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ rspamd_fprintf(stderr, "option parsing failed: %s\n", error->message);
+ g_error_free(error);
+ g_option_context_free(context);
+ exit(EXIT_FAILURE);
+ }
+
+ g_option_context_free(context);
+
+ if (!source_db) {
+ rspamd_fprintf(stderr, "source db is missing\n");
+ exit(EXIT_FAILURE);
+ }
+ if (!redis_host) {
+ rspamd_fprintf(stderr, "redis host is missing\n");
+ exit(EXIT_FAILURE);
+ }
+ if (!fuzzy_expiry) {
+ rspamd_fprintf(stderr, "expiry is missing\n");
+ exit(EXIT_FAILURE);
+ }
+
+ obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(obj, ucl_object_fromstring(source_db),
+ "source_db", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromstring(redis_host),
+ "redis_host", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromint(fuzzy_expiry),
+ "expiry", 0, false);
+
+ if (redis_username) {
+ ucl_object_insert_key(obj, ucl_object_fromstring(redis_username),
+ "redis_username", 0, false);
+ }
+ if (redis_password) {
+ ucl_object_insert_key(obj, ucl_object_fromstring(redis_password),
+ "redis_password", 0, false);
+ }
+
+ if (redis_db) {
+ ucl_object_insert_key(obj, ucl_object_fromstring(redis_db),
+ "redis_db", 0, false);
+ }
+
+ rspamadm_execute_lua_ucl_subr(argc,
+ argv,
+ obj,
+ "fuzzy_convert",
+ TRUE);
+
+ ucl_object_unref(obj);
+}
diff --git a/src/rspamadm/lua_repl.c b/src/rspamadm/lua_repl.c
new file mode 100644
index 0000000..432c4de
--- /dev/null
+++ b/src/rspamadm/lua_repl.c
@@ -0,0 +1,1026 @@
+/*
+ * Copyright 2023 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.
+ */
+
+#include "config.h"
+#include "rspamadm.h"
+#include "libserver/http/http_connection.h"
+#include "libserver/http/http_private.h"
+#include "libserver/http/http_router.h"
+#include "printf.h"
+#include "lua/lua_common.h"
+#include "lua/lua_thread_pool.h"
+#include "message.h"
+#include "unix-std.h"
+#ifdef WITH_LUA_REPL
+#include "replxx.h"
+#endif
+#include "worker_util.h"
+#ifdef WITH_LUAJIT
+#include <luajit.h>
+#endif
+
+static gchar **paths = NULL;
+static gchar **scripts = NULL;
+static gchar **lua_args = NULL;
+static gchar *histfile = NULL;
+static guint max_history = 2000;
+static gchar *serve = NULL;
+static gchar *exec_line = NULL;
+static gint batch = -1;
+extern struct rspamd_async_session *rspamadm_session;
+
+static const char *default_history_file = ".rspamd_repl.hist";
+
+#ifdef WITH_LUA_REPL
+static Replxx *rx_instance = NULL;
+#endif
+
+#ifdef WITH_LUAJIT
+#define MAIN_PROMPT LUAJIT_VERSION "> "
+#else
+#define MAIN_PROMPT LUA_VERSION "> "
+#endif
+#define MULTILINE_PROMPT "... "
+
+static void rspamadm_lua(gint argc, gchar **argv,
+ const struct rspamadm_command *cmd);
+static const char *rspamadm_lua_help(gboolean full_help,
+ const struct rspamadm_command *cmd);
+
+struct rspamadm_command lua_command = {
+ .name = "lua",
+ .flags = 0,
+ .help = rspamadm_lua_help,
+ .run = rspamadm_lua,
+ .lua_subrs = NULL,
+};
+
+/*
+ * Dot commands
+ */
+typedef void (*rspamadm_lua_dot_handler)(lua_State *L, gint argc, gchar **argv);
+struct rspamadm_lua_dot_command {
+ const gchar *name;
+ const gchar *description;
+ rspamadm_lua_dot_handler handler;
+};
+
+static void rspamadm_lua_help_handler(lua_State *L, gint argc, gchar **argv);
+static void rspamadm_lua_load_handler(lua_State *L, gint argc, gchar **argv);
+static void rspamadm_lua_exec_handler(lua_State *L, gint argc, gchar **argv);
+static void rspamadm_lua_message_handler(lua_State *L, gint argc, gchar **argv);
+
+static void lua_thread_error_cb(struct thread_entry *thread, int ret, const char *msg);
+static void lua_thread_finish_cb(struct thread_entry *thread, int ret);
+
+static struct rspamadm_lua_dot_command cmds[] = {
+ {.name = "help",
+ .description = "shows help for commands",
+ .handler = rspamadm_lua_help_handler},
+ {.name = "load",
+ .description = "load lua file",
+ .handler = rspamadm_lua_load_handler},
+ {.name = "exec",
+ .description = "exec lua file",
+ .handler = rspamadm_lua_exec_handler},
+ {.name = "message",
+ .description = "scans message using specified callback: .message <callback_name> <file>...",
+ .handler = rspamadm_lua_message_handler},
+};
+
+static GHashTable *cmds_hash = NULL;
+
+static GOptionEntry entries[] = {
+ {"script", 's', 0, G_OPTION_ARG_STRING_ARRAY, &scripts,
+ "Load specified scripts", NULL},
+ {"path", 'P', 0, G_OPTION_ARG_STRING_ARRAY, &paths,
+ "Add specified paths to lua paths", NULL},
+ {"history-file", 'H', 0, G_OPTION_ARG_FILENAME, &histfile,
+ "Load history from the specified file", NULL},
+ {"max-history", 'm', 0, G_OPTION_ARG_INT, &max_history,
+ "Store this number of history entries", NULL},
+ {"serve", 'S', 0, G_OPTION_ARG_STRING, &serve,
+ "Serve http lua server", NULL},
+ {"batch", 'b', 0, G_OPTION_ARG_NONE, &batch,
+ "Batch execution mode", NULL},
+ {"exec", 'e', 0, G_OPTION_ARG_STRING, &exec_line,
+ "Execute specified script", NULL},
+ {"args", 'a', 0, G_OPTION_ARG_STRING_ARRAY, &lua_args,
+ "Arguments to pass to Lua", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+static const char *
+rspamadm_lua_help(gboolean full_help, const struct rspamadm_command *cmd)
+{
+ const char *help_str;
+
+ if (full_help) {
+ help_str = "Run lua read/execute/print loop\n\n"
+ "Usage: rspamadm lua [-P paths] [-s scripts]\n"
+ "Where options are:\n\n"
+ "-P: add additional lua paths (may be repeated)\n"
+ "-p: split input to lines and feed each line to the script\n"
+ "-s: load scripts on start from specified files (may be repeated)\n"
+ "-S: listen on a specified address as HTTP server\n"
+ "-a: pass argument to lua (may be repeated)\n"
+ "-e: execute script specified in command line"
+ "--help: shows available options and commands";
+ }
+ else {
+ help_str = "Run LUA interpreter";
+ }
+
+ return help_str;
+}
+
+static void
+rspamadm_lua_add_path(lua_State *L, const gchar *path)
+{
+ const gchar *old_path;
+ gsize len;
+ GString *new_path;
+
+ lua_getglobal(L, "package");
+ lua_getfield(L, -1, "path");
+ old_path = luaL_checklstring(L, -1, &len);
+
+ new_path = g_string_sized_new(len + strlen(path) + sizeof("/?.lua"));
+
+ if (strstr(path, "?.lua") == NULL) {
+ rspamd_printf_gstring(new_path, "%s/?.lua;%s", path, old_path);
+ }
+ else {
+ rspamd_printf_gstring(new_path, "%s;%s", path, old_path);
+ }
+
+ lua_pushlstring(L, new_path->str, new_path->len);
+ lua_setfield(L, -2, "path");
+ lua_settop(L, 0);
+ g_string_free(new_path, TRUE);
+}
+
+
+static void
+lua_thread_finish_cb(struct thread_entry *thread, int ret)
+{
+ struct lua_call_data *cd = thread->cd;
+
+ cd->ret = ret;
+}
+
+static void
+lua_thread_error_cb(struct thread_entry *thread, int ret, const char *msg)
+{
+ struct lua_call_data *cd = thread->cd;
+
+ rspamd_fprintf(stderr, "call failed: %s\n", msg);
+
+ cd->ret = ret;
+}
+
+static void
+lua_thread_str_error_cb(struct thread_entry *thread, int ret, const char *msg)
+{
+ struct lua_call_data *cd = thread->cd;
+ const char *what = cd->ud;
+
+ rspamd_fprintf(stderr, "call to %s failed: %s\n", what, msg);
+
+ cd->ret = ret;
+}
+
+static gboolean
+rspamadm_lua_load_script(lua_State *L, const gchar *path)
+{
+ struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
+ L = thread->lua_state;
+
+ if (luaL_loadfile(L, path) != 0) {
+ rspamd_fprintf(stderr, "cannot load script %s: %s\n",
+ path, lua_tostring(L, -1));
+ lua_settop(L, 0);
+
+ return FALSE;
+ }
+
+ if (lua_repl_thread_call(thread, 0, (void *) path, lua_thread_str_error_cb) != 0) {
+ return FALSE;
+ }
+
+ lua_settop(L, 0);
+
+ return TRUE;
+}
+
+static void
+rspamadm_exec_input(lua_State *L, const gchar *input)
+{
+ GString *tb;
+ gint i, cbref;
+ int top = 0;
+ gchar outbuf[8192];
+ struct lua_logger_trace tr;
+
+ struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
+ L = thread->lua_state;
+
+ /* First try return + input */
+ tb = g_string_sized_new(strlen(input) + sizeof("return "));
+ rspamd_printf_gstring(tb, "return %s", input);
+
+ int r = luaL_loadstring(L, tb->str);
+ if (r != 0) {
+ /* Reset stack */
+ lua_settop(L, 0);
+ /* Try with no return */
+ if (luaL_loadstring(L, input) != 0) {
+ rspamd_fprintf(stderr, "cannot load string %s\n",
+ input);
+ g_string_free(tb, TRUE);
+ lua_settop(L, 0);
+
+ lua_thread_pool_return(rspamd_main->cfg->lua_thread_pool, thread);
+ return;
+ }
+ }
+
+ g_string_free(tb, TRUE);
+
+
+ top = lua_gettop(L);
+
+ if (lua_repl_thread_call(thread, 0, NULL, NULL) == 0) {
+ /* Print output */
+ for (i = top; i <= lua_gettop(L); i++) {
+ if (lua_isfunction(L, i)) {
+ lua_pushvalue(L, i);
+ cbref = luaL_ref(L, LUA_REGISTRYINDEX);
+
+ rspamd_printf("local function: %d\n", cbref);
+ }
+ else {
+ memset(&tr, 0, sizeof(tr));
+ lua_logger_out_type(L, i, outbuf, sizeof(outbuf) - 1, &tr,
+ LUA_ESCAPE_UNPRINTABLE);
+ rspamd_printf("%s\n", outbuf);
+ }
+ }
+ }
+}
+
+static void
+wait_session_events(void)
+{
+ /* XXX: it's probably worth to add timeout here - not to wait forever */
+ while (rspamd_session_events_pending(rspamadm_session) > 0) {
+ ev_loop(rspamd_main->event_loop, EVRUN_ONCE);
+ }
+
+ msg_debug("finished events waiting, terminating session");
+}
+
+gint lua_repl_thread_call(struct thread_entry *thread, gint narg, gpointer ud, lua_thread_error_t error_func)
+{
+ int ret;
+ struct lua_call_data *cd = g_new0(struct lua_call_data, 1);
+ cd->top = lua_gettop(thread->lua_state);
+ cd->ud = ud;
+
+ thread->finish_callback = lua_thread_finish_cb;
+ if (error_func) {
+ thread->error_callback = error_func;
+ }
+ else {
+ thread->error_callback = lua_thread_error_cb;
+ }
+ thread->cd = cd;
+
+ lua_thread_call(thread, narg);
+
+ wait_session_events();
+
+ ret = cd->ret;
+
+ g_free(cd);
+
+ return ret;
+}
+
+static void
+rspamadm_lua_help_handler(lua_State *L, gint argc, gchar **argv)
+{
+ guint i;
+ struct rspamadm_lua_dot_command *cmd;
+
+ if (argv[1] == NULL) {
+ /* Print all commands */
+ for (i = 0; i < G_N_ELEMENTS(cmds); i++) {
+ rspamd_printf("%s: %s\n", cmds[i].name, cmds[i].description);
+ }
+
+ rspamd_printf("{{: start multiline input\n");
+ rspamd_printf("}}: end multiline input\n");
+ }
+ else {
+ for (i = 1; argv[i] != NULL; i++) {
+ cmd = g_hash_table_lookup(cmds_hash, argv[i]);
+
+ if (cmd) {
+ rspamd_printf("%s: %s\n", cmds->name, cmds->description);
+ }
+ else {
+ rspamd_printf("%s: no such command\n", argv[i]);
+ }
+ }
+ }
+}
+
+static void
+rspamadm_lua_load_handler(lua_State *L, gint argc, gchar **argv)
+{
+ guint i;
+ gboolean ret;
+
+ for (i = 1; argv[i] != NULL; i++) {
+ ret = rspamadm_lua_load_script(L, argv[i]);
+ rspamd_printf("%s: %sloaded\n", argv[i], ret ? "" : "NOT ");
+ }
+}
+
+static void
+rspamadm_lua_exec_handler(lua_State *L, gint argc, gchar **argv)
+{
+ gint i;
+
+ struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
+ L = thread->lua_state;
+
+ for (i = 1; argv[i] != NULL; i++) {
+
+ if (luaL_loadfile(L, argv[i]) != 0) {
+ rspamd_fprintf(stderr, "cannot load script %s: %s\n",
+ argv[i], lua_tostring(L, -1));
+ lua_settop(L, 0);
+
+ return;
+ }
+
+ if (lua_repl_thread_call(thread, 0, argv[i], lua_thread_str_error_cb) != 0) {
+ return;
+ }
+ }
+}
+
+static void
+rspamadm_lua_message_handler(lua_State *L, gint argc, gchar **argv)
+{
+ gulong cbref;
+ gint old_top, func_idx, i, j;
+ struct rspamd_task *task, **ptask;
+ gpointer map;
+ gsize len;
+ gchar outbuf[8192];
+ struct lua_logger_trace tr;
+
+ if (argv[1] == NULL) {
+ rspamd_printf("no callback is specified\n");
+ return;
+ }
+
+ for (i = 2; argv[i] != NULL; i++) {
+ struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
+ L = thread->lua_state;
+
+ if (rspamd_strtoul(argv[1], strlen(argv[1]), &cbref)) {
+ lua_rawgeti(L, LUA_REGISTRYINDEX, cbref);
+ }
+ else {
+ lua_getglobal(L, argv[1]);
+ }
+
+ if (lua_type(L, -1) != LUA_TFUNCTION) {
+ rspamd_printf("bad callback type: %s\n", lua_typename(L, lua_type(L, -1)));
+ lua_thread_pool_return(rspamd_main->cfg->lua_thread_pool, thread);
+ return;
+ }
+
+ /* Save index to reuse */
+ func_idx = lua_gettop(L);
+
+ map = rspamd_file_xmap(argv[i], PROT_READ, &len, TRUE);
+
+ if (map == NULL) {
+ rspamd_printf("cannot open %s: %s\n", argv[i], strerror(errno));
+ }
+ else {
+ task = rspamd_task_new(NULL, rspamd_main->cfg, NULL, NULL, NULL, FALSE);
+
+ if (!rspamd_task_load_message(task, NULL, map, len)) {
+ rspamd_printf("cannot load %s\n", argv[i]);
+ rspamd_task_free(task);
+ munmap(map, len);
+ continue;
+ }
+
+ if (!rspamd_message_parse(task)) {
+ rspamd_printf("cannot parse %s: %e\n", argv[i], task->err);
+ rspamd_task_free(task);
+ munmap(map, len);
+ continue;
+ }
+
+ rspamd_message_process(task);
+ old_top = lua_gettop(L);
+
+ lua_pushvalue(L, func_idx);
+ ptask = lua_newuserdata(L, sizeof(*ptask));
+ *ptask = task;
+ rspamd_lua_setclass(L, "rspamd{task}", -1);
+
+
+ if (lua_repl_thread_call(thread, 1, argv[i], lua_thread_str_error_cb) == 0) {
+ rspamd_printf("lua callback for %s returned:\n", argv[i]);
+
+ for (j = old_top + 1; j <= lua_gettop(L); j++) {
+ memset(&tr, 0, sizeof(tr));
+ lua_logger_out_type(L, j, outbuf, sizeof(outbuf), &tr,
+ LUA_ESCAPE_UNPRINTABLE);
+ rspamd_printf("%s\n", outbuf);
+ }
+ }
+
+ rspamd_task_free(task);
+ munmap(map, len);
+ /* Pop all but the original function */
+ lua_settop(L, func_idx);
+ }
+ }
+
+ lua_settop(L, 0);
+}
+
+
+static gboolean
+rspamadm_lua_try_dot_command(lua_State *L, const gchar *input)
+{
+ struct rspamadm_lua_dot_command *cmd;
+ gchar **argv;
+
+ argv = g_strsplit_set(input + 1, " ", -1);
+
+ if (argv == NULL || argv[0] == NULL) {
+ if (argv) {
+ g_strfreev(argv);
+ }
+
+ return FALSE;
+ }
+
+ cmd = g_hash_table_lookup(cmds_hash, argv[0]);
+
+ if (cmd) {
+ cmd->handler(L, g_strv_length(argv), argv);
+ g_strfreev(argv);
+
+ return TRUE;
+ }
+
+ g_strfreev(argv);
+
+ return FALSE;
+}
+
+#ifdef WITH_LUA_REPL
+static gint lex_ref_idx = -1;
+
+static void
+lua_syntax_highlighter(const char *str, ReplxxColor *colours, int size, void *ud)
+{
+ lua_State *L = (lua_State *) ud;
+
+ if (lex_ref_idx == -1) {
+ if (!rspamd_lua_require_function(L, "lua_lexer", "lex_to_table")) {
+ fprintf(stderr, "cannot require lua_lexer!\n");
+
+ exit(EXIT_FAILURE);
+ }
+
+ lex_ref_idx = luaL_ref(L, LUA_REGISTRYINDEX);
+ }
+
+ lua_rawgeti(L, LUA_REGISTRYINDEX, lex_ref_idx);
+ lua_pushstring(L, str);
+
+ if (lua_pcall(L, 1, 1, 0) != 0) {
+ fprintf(stderr, "cannot lex a string!\n");
+ }
+ else {
+ /* Process what we have after lexing */
+ gsize nelts = rspamd_lua_table_size(L, -1);
+
+ for (gsize i = 0; i < nelts; i++) {
+ /*
+ * Indexes in the table:
+ * 1 - type of element (string)
+ * 2 - text (string)
+ * 3 - line num (int), always 1...
+ * 4 - column num (must be less than size)
+ */
+ const gchar *what;
+ gsize column, tlen, cur_top, elt_pos;
+ ReplxxColor elt_color = REPLXX_COLOR_DEFAULT;
+
+ cur_top = lua_gettop(L);
+ lua_rawgeti(L, -1, i + 1);
+ elt_pos = lua_gettop(L);
+ lua_rawgeti(L, elt_pos, 1);
+ what = lua_tostring(L, -1);
+ lua_rawgeti(L, elt_pos, 2);
+ lua_tolstring(L, -1, &tlen);
+ lua_rawgeti(L, elt_pos, 4);
+ column = lua_tointeger(L, -1);
+
+ g_assert(column > 0);
+ column--; /* Start from 0 */
+
+ if (column + tlen > size) {
+ /* Likely utf8 case, too complicated to match */
+ lua_settop(L, cur_top);
+ continue;
+ }
+
+ /* Check what and adjust color */
+ if (strcmp(what, "identifier") == 0) {
+ elt_color = REPLXX_COLOR_NORMAL;
+ }
+ else if (strcmp(what, "number") == 0) {
+ elt_color = REPLXX_COLOR_BLUE;
+ }
+ else if (strcmp(what, "string") == 0) {
+ elt_color = REPLXX_COLOR_GREEN;
+ }
+ else if (strcmp(what, "keyword") == 0) {
+ elt_color = REPLXX_COLOR_WHITE;
+ }
+ else if (strcmp(what, "constant") == 0) {
+ elt_color = REPLXX_COLOR_WHITE;
+ }
+ else if (strcmp(what, "operator") == 0) {
+ elt_color = REPLXX_COLOR_CYAN;
+ }
+ else if (strcmp(what, "comment") == 0) {
+ elt_color = REPLXX_COLOR_BRIGHTGREEN;
+ }
+ else if (strcmp(what, "error") == 0) {
+ elt_color = REPLXX_COLOR_ERROR;
+ }
+
+ for (gsize j = column; j < column + tlen; j++) {
+ colours[j] = elt_color;
+ }
+
+ /* Restore stack */
+ lua_settop(L, cur_top);
+ }
+ }
+
+ lua_settop(L, 0);
+}
+#endif
+
+static void
+rspamadm_lua_run_repl(lua_State *L, bool is_batch)
+{
+ gchar *input;
+#ifdef WITH_LUA_REPL
+ gboolean is_multiline = FALSE;
+ GString *tb = NULL;
+ gsize i;
+#else
+ /* Always set is_batch */
+ is_batch = TRUE;
+#endif
+
+ for (;;) {
+ if (is_batch) {
+ size_t linecap = 0;
+ ssize_t linelen;
+
+ linelen = getline(&input, &linecap, stdin);
+
+ if (linelen > 0) {
+ if (input[linelen - 1] == '\n') {
+ input[linelen - 1] = '\0';
+ linelen--;
+ }
+
+ if (linelen > 0) {
+ if (input[0] == '.') {
+ if (rspamadm_lua_try_dot_command(L, input)) {
+ continue;
+ }
+ }
+
+ rspamadm_exec_input(L, input);
+ }
+ }
+ else {
+ break;
+ }
+
+ lua_settop(L, 0);
+ }
+ else {
+#ifdef WITH_LUA_REPL
+ replxx_set_highlighter_callback(rx_instance, lua_syntax_highlighter,
+ L);
+
+ if (!is_multiline) {
+ input = (gchar *) replxx_input(rx_instance, MAIN_PROMPT);
+
+ if (input == NULL) {
+ return;
+ }
+
+ if (input[0] == '.') {
+ if (rspamadm_lua_try_dot_command(L, input)) {
+ if (!is_batch) {
+ replxx_history_add(rx_instance, input);
+ }
+ continue;
+ }
+ }
+
+ if (strcmp(input, "{{") == 0) {
+ is_multiline = TRUE;
+ tb = g_string_sized_new(8192);
+ continue;
+ }
+
+ rspamadm_exec_input(L, input);
+ if (!is_batch) {
+ replxx_history_add(rx_instance, input);
+ }
+ lua_settop(L, 0);
+ }
+ else {
+ input = (gchar *) replxx_input(rx_instance, MULTILINE_PROMPT);
+
+ if (input == NULL) {
+ g_string_free(tb, TRUE);
+ return;
+ }
+
+ if (strcmp(input, "}}") == 0) {
+ is_multiline = FALSE;
+ rspamadm_exec_input(L, tb->str);
+
+ /* Replace \n with ' ' for sanity */
+ for (i = 0; i < tb->len; i++) {
+ if (tb->str[i] == '\n') {
+ tb->str[i] = ' ';
+ }
+ }
+
+ if (!is_batch) {
+ replxx_history_add(rx_instance, tb->str);
+ }
+ g_string_free(tb, TRUE);
+ }
+ else {
+ g_string_append(tb, input);
+ g_string_append(tb, " \n");
+ }
+ }
+ }
+#endif
+ }
+}
+
+struct rspamadm_lua_repl_context {
+ struct rspamd_http_connection_router *rt;
+ lua_State *L;
+};
+
+struct rspamadm_lua_repl_session {
+ struct rspamd_http_connection_router *rt;
+ rspamd_inet_addr_t *addr;
+ struct rspamadm_lua_repl_context *ctx;
+ gint sock;
+};
+
+static void
+rspamadm_lua_accept_cb(EV_P_ ev_io *w, int revents)
+{
+ struct rspamadm_lua_repl_context *ctx =
+ (struct rspamadm_lua_repl_context *) w->data;
+ rspamd_inet_addr_t *addr = NULL;
+ struct rspamadm_lua_repl_session *session;
+ gint nfd;
+
+ if ((nfd =
+ rspamd_accept_from_socket(w->fd, &addr, NULL, NULL)) == -1) {
+ rspamd_fprintf(stderr, "accept failed: %s", strerror(errno));
+ return;
+ }
+ /* Check for EAGAIN */
+ if (nfd == 0) {
+ rspamd_inet_address_free(addr);
+ return;
+ }
+
+ session = g_malloc0(sizeof(*session));
+ session->rt = ctx->rt;
+ session->ctx = ctx;
+ session->addr = addr;
+ session->sock = nfd;
+
+ rspamd_http_router_handle_socket(ctx->rt, nfd, session);
+}
+
+static void
+rspamadm_lua_error_handler(struct rspamd_http_connection_entry *conn_ent,
+ GError *err)
+{
+ rspamd_fprintf(stderr, "http error occurred: %s\n", err->message);
+}
+
+static void
+rspamadm_lua_finish_handler(struct rspamd_http_connection_entry *conn_ent)
+{
+ struct rspamadm_lua_repl_session *session = conn_ent->ud;
+
+ g_free(session);
+}
+
+static void
+lua_thread_http_error_cb(struct thread_entry *thread, int ret, const char *msg)
+{
+ struct lua_call_data *cd = thread->cd;
+ struct rspamd_http_connection_entry *conn_ent = cd->ud;
+
+ rspamd_controller_send_error(conn_ent, 500, "call failed: %s\n", msg);
+
+ cd->ret = ret;
+}
+
+
+/*
+ * Exec command handler:
+ * request: /exec
+ * body: lua script
+ * reply: json {"status": "ok", "reply": {<lua json object>}}
+ */
+static int
+rspamadm_lua_handle_exec(struct rspamd_http_connection_entry *conn_ent,
+ struct rspamd_http_message *msg)
+{
+ GString *tb;
+ gint err_idx, i;
+ lua_State *L;
+ ucl_object_t *obj, *elt;
+ const gchar *body;
+ gsize body_len;
+ struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
+
+ L = thread->lua_state;
+
+ body = rspamd_http_message_get_body(msg, &body_len);
+
+ if (body == NULL) {
+ rspamd_controller_send_error(conn_ent, 400, "Empty lua script");
+
+ return 0;
+ }
+
+ lua_pushcfunction(L, &rspamd_lua_traceback);
+ err_idx = lua_gettop(L);
+
+ /* First try return + input */
+ tb = g_string_sized_new(body_len + sizeof("return "));
+ rspamd_printf_gstring(tb, "return %*s", (gint) body_len, body);
+
+ if (luaL_loadstring(L, tb->str) != 0) {
+ /* Reset stack */
+ lua_settop(L, 0);
+ lua_pushcfunction(L, &rspamd_lua_traceback);
+ err_idx = lua_gettop(L);
+ /* Try with no return */
+ if (luaL_loadbuffer(L, body, body_len, "http input") != 0) {
+ rspamd_controller_send_error(conn_ent, 400, "Invalid lua script");
+
+ return 0;
+ }
+ }
+
+ g_string_free(tb, TRUE);
+
+ if (lua_repl_thread_call(thread, 0, conn_ent, lua_thread_http_error_cb) != 0) {
+ return 0;
+ }
+
+ obj = ucl_object_typed_new(UCL_ARRAY);
+
+ for (i = err_idx + 1; i <= lua_gettop(L); i++) {
+ if (lua_isfunction(L, i)) {
+ /* XXX: think about API */
+ }
+ else {
+ elt = ucl_object_lua_import(L, i);
+
+ if (elt) {
+ ucl_array_append(obj, elt);
+ }
+ }
+ }
+
+ rspamd_controller_send_ucl(conn_ent, obj);
+ ucl_object_unref(obj);
+ lua_settop(L, 0);
+
+ return 0;
+}
+
+static void
+rspamadm_lua(gint argc, gchar **argv, const struct rspamadm_command *cmd)
+{
+ GOptionContext *context;
+ GError *error = NULL;
+ gchar **elt;
+ guint i;
+ lua_State *L = rspamd_main->cfg->lua_state;
+
+ context = g_option_context_new("lua - run lua interpreter");
+ g_option_context_set_summary(context,
+ "Summary:\n Rspamd administration utility version " RVERSION
+ "\n Release id: " RID);
+ g_option_context_add_main_entries(context, entries, NULL);
+
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ fprintf(stderr, "option parsing failed: %s\n", error->message);
+ g_error_free(error);
+ g_option_context_free(context);
+ exit(EXIT_FAILURE);
+ }
+
+ g_option_context_free(context);
+
+ if (batch == -1) {
+ if (isatty(STDIN_FILENO)) {
+ batch = 0;
+ }
+ else {
+ batch = 1;
+ }
+ }
+
+ if (paths) {
+ for (elt = paths; *elt != NULL; elt++) {
+ rspamadm_lua_add_path(L, *elt);
+ }
+ }
+
+ if (lua_args) {
+ i = 1;
+
+ lua_newtable(L);
+
+ for (elt = lua_args; *elt != NULL; elt++) {
+ lua_pushinteger(L, i);
+ lua_pushstring(L, *elt);
+ lua_settable(L, -3);
+ i++;
+ }
+
+ lua_setglobal(L, "arg");
+ }
+
+ if (scripts) {
+ for (elt = scripts; *elt != NULL; elt++) {
+ if (!rspamadm_lua_load_script(L, *elt)) {
+ exit(EXIT_FAILURE);
+ }
+ }
+ }
+
+ if (exec_line) {
+ rspamadm_exec_input(L, exec_line);
+ }
+
+ if (serve) {
+ /* HTTP Server mode */
+ GPtrArray *addrs = NULL;
+ gchar *name = NULL;
+ struct ev_loop *ev_base;
+ struct rspamd_http_connection_router *http;
+ gint fd;
+ struct rspamadm_lua_repl_context *ctx;
+
+ if (rspamd_parse_host_port_priority(serve, &addrs, NULL, &name,
+ 10000, TRUE, NULL) == RSPAMD_PARSE_ADDR_FAIL) {
+ fprintf(stderr, "cannot listen on %s", serve);
+ exit(EXIT_FAILURE);
+ }
+
+ ev_base = rspamd_main->event_loop;
+ ctx = g_malloc0(sizeof(*ctx));
+ http = rspamd_http_router_new(rspamadm_lua_error_handler,
+ rspamadm_lua_finish_handler,
+ 0.0,
+ NULL,
+ rspamd_main->http_ctx);
+ ctx->L = L;
+ ctx->rt = http;
+ rspamd_http_router_add_path(http,
+ "/exec",
+ rspamadm_lua_handle_exec);
+
+ for (i = 0; i < addrs->len; i++) {
+ rspamd_inet_addr_t *addr = g_ptr_array_index(addrs, i);
+
+ fd = rspamd_inet_address_listen(addr, SOCK_STREAM,
+ RSPAMD_INET_ADDRESS_LISTEN_ASYNC, -1);
+
+ if (fd != -1) {
+ static ev_io ev;
+
+ ev.data = ctx;
+ ev_io_init(&ev, rspamadm_lua_accept_cb, fd, EV_READ);
+ ev_io_start(ev_base, &ev);
+ rspamd_printf("listen on %s\n",
+ rspamd_inet_address_to_string_pretty(addr));
+ }
+ }
+
+ ev_loop(ev_base, 0);
+
+ exit(EXIT_SUCCESS);
+ }
+
+ if (histfile == NULL) {
+ const gchar *homedir;
+ GString *hist_path;
+
+ homedir = getenv("HOME");
+
+ if (homedir) {
+ hist_path = g_string_sized_new(strlen(homedir) +
+ strlen(default_history_file) + 1);
+ rspamd_printf_gstring(hist_path, "%s/%s", homedir,
+ default_history_file);
+ }
+ else {
+ hist_path = g_string_sized_new(strlen(default_history_file) + 2);
+ rspamd_printf_gstring(hist_path, "./%s", default_history_file);
+ }
+
+ histfile = hist_path->str;
+ g_string_free(hist_path, FALSE);
+ }
+
+ if (argc > 1) {
+ for (i = 1; i < argc; i++) {
+ if (!rspamadm_lua_load_script(L, argv[i])) {
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ exit(EXIT_SUCCESS);
+ }
+
+ /* Init dot commands */
+ cmds_hash = g_hash_table_new(rspamd_strcase_hash, rspamd_strcase_equal);
+
+ for (i = 0; i < G_N_ELEMENTS(cmds); i++) {
+ g_hash_table_insert(cmds_hash, (gpointer) cmds[i].name, &cmds[i]);
+ }
+
+ if (!batch) {
+#ifdef WITH_LUA_REPL
+ rx_instance = replxx_init();
+ replxx_set_max_history_size(rx_instance, max_history);
+ replxx_history_load(rx_instance, histfile);
+#endif
+ rspamadm_lua_run_repl(L, false);
+#ifdef WITH_LUA_REPL
+ replxx_history_save(rx_instance, histfile);
+ replxx_end(rx_instance);
+#endif
+ }
+ else {
+ rspamadm_lua_run_repl(L, true);
+ }
+}
diff --git a/src/rspamadm/pw.c b/src/rspamadm/pw.c
new file mode 100644
index 0000000..9fe9cd7
--- /dev/null
+++ b/src/rspamadm/pw.c
@@ -0,0 +1,392 @@
+/*-
+ * Copyright 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.
+ */
+#include "config.h"
+#include "util.h"
+#include "ottery.h"
+#include "cryptobox.h"
+#include "rspamd.h"
+#include "rspamadm.h"
+#include "unix-std.h"
+
+static void rspamadm_pw(gint argc, gchar **argv,
+ const struct rspamadm_command *cmd);
+static const char *rspamadm_pw_help(gboolean full_help,
+ const struct rspamadm_command *cmd);
+static void rspamadm_pw_lua_subrs(gpointer pL);
+
+static gboolean do_encrypt = FALSE;
+static gboolean do_check = FALSE;
+static gboolean quiet = FALSE;
+static gboolean list = FALSE;
+static gchar *type = "catena";
+static gchar *password = NULL;
+
+struct rspamadm_command pw_command = {
+ .name = "pw",
+ .flags = 0,
+ .help = rspamadm_pw_help,
+ .run = rspamadm_pw,
+ .lua_subrs = rspamadm_pw_lua_subrs,
+};
+
+static GOptionEntry entries[] = {
+ {"encrypt", 'e', 0, G_OPTION_ARG_NONE, &do_encrypt,
+ "Encrypt password", NULL},
+ {"check", 'c', 0, G_OPTION_ARG_NONE, &do_check,
+ "Check password", NULL},
+ {"quiet", 'q', 0, G_OPTION_ARG_NONE, &quiet,
+ "Suppress output", NULL},
+ {"password", 'p', 0, G_OPTION_ARG_STRING, &password,
+ "Input password", NULL},
+ {"type", 't', 0, G_OPTION_ARG_STRING, &type,
+ "PBKDF type", NULL},
+ {"list", 'l', 0, G_OPTION_ARG_NONE, &list,
+ "List available algorithms", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+static const char *
+rspamadm_pw_help(gboolean full_help, const struct rspamadm_command *cmd)
+{
+ const char *help_str;
+
+ if (full_help) {
+ help_str = "Manipulate with passwords in Rspamd\n\n"
+ "Usage: rspamadm pw [command]\n"
+ "Where commands are:\n\n"
+ "--encrypt: encrypt password (this is a default command)\n"
+ "--check: check encrypted password using encrypted password\n"
+ "--list: list available pbkdf algorithms\n"
+ "--password: input password\n"
+ "--type: select the specified pbkdf type\n"
+ "--help: shows available options and commands";
+ }
+ else {
+ help_str = "Manage rspamd passwords";
+ }
+
+ return help_str;
+}
+
+static const struct rspamd_controller_pbkdf *
+rspamadm_get_pbkdf(void)
+{
+ const struct rspamd_controller_pbkdf *pbkdf;
+ guint i;
+
+ for (i = 0; i < RSPAMD_PBKDF_ID_MAX - 1; i++) {
+ pbkdf = &pbkdf_list[i];
+
+ if (strcmp(type, pbkdf->alias) == 0) {
+ return pbkdf;
+ }
+ }
+
+ rspamd_fprintf(stderr, "Unknown PKDF type: %s\n", type);
+ exit(EXIT_FAILURE);
+
+ return NULL;
+}
+
+static char *
+rspamadm_pw_encrypt(char *password)
+{
+ const struct rspamd_controller_pbkdf *pbkdf;
+ guchar *salt, *key;
+ gchar *encoded_salt, *encoded_key;
+ GString *result;
+ gsize plen;
+
+ pbkdf = rspamadm_get_pbkdf();
+ g_assert(pbkdf != NULL);
+
+ if (password == NULL) {
+ plen = 8192;
+ password = g_malloc0(plen);
+ plen = rspamd_read_passphrase(password, plen, 0, NULL);
+ }
+ else {
+ plen = strlen(password);
+ }
+
+ if (plen == 0) {
+ fprintf(stderr, "Invalid password\n");
+ exit(EXIT_FAILURE);
+ }
+
+ salt = g_alloca(pbkdf->salt_len);
+ key = g_alloca(pbkdf->key_len);
+ ottery_rand_bytes(salt, pbkdf->salt_len);
+ /* Derive key */
+ rspamd_cryptobox_pbkdf(password, strlen(password),
+ salt, pbkdf->salt_len, key, pbkdf->key_len, pbkdf->complexity,
+ pbkdf->type);
+
+ encoded_salt = rspamd_encode_base32(salt, pbkdf->salt_len, RSPAMD_BASE32_DEFAULT);
+ encoded_key = rspamd_encode_base32(key, pbkdf->key_len, RSPAMD_BASE32_DEFAULT);
+
+ result = g_string_new("");
+ rspamd_printf_gstring(result, "$%d$%s$%s", pbkdf->id, encoded_salt,
+ encoded_key);
+
+ g_free(encoded_salt);
+ g_free(encoded_key);
+ rspamd_explicit_memzero(password, plen);
+ g_free(password);
+ password = result->str;
+ g_string_free(result, FALSE); /* Not freeing memory */
+
+ return password;
+}
+
+static const gchar *
+rspamd_encrypted_password_get_str(const gchar *password, gsize skip,
+ gsize *length)
+{
+ const gchar *str, *start, *end;
+ gsize size;
+
+ start = password + skip;
+ end = start;
+ size = 0;
+
+ while (*end != '\0' && g_ascii_isalnum(*end)) {
+ size++;
+ end++;
+ }
+
+ if (size) {
+ str = start;
+ *length = size;
+ }
+ else {
+ str = NULL;
+ }
+
+ return str;
+}
+
+static void
+rspamadm_pw_check(void)
+{
+ const struct rspamd_controller_pbkdf *pbkdf = NULL;
+ const gchar *salt, *hash;
+ const gchar *start, *end;
+ guchar *salt_decoded, *key_decoded, *local_key;
+ gsize salt_len, key_len, size;
+ gchar test_password[8192], encrypted_password[8192];
+ gsize plen, i;
+ gint id;
+ gboolean ret = FALSE;
+
+ if (password == NULL) {
+ plen = rspamd_read_passphrase_with_prompt("Enter encrypted password: ", encrypted_password,
+ sizeof(encrypted_password), 1, NULL);
+ }
+ else {
+ plen = rspamd_strlcpy(encrypted_password, password, sizeof(encrypted_password));
+ }
+
+ if (encrypted_password[0] == '$') {
+ /* Parse id */
+ start = encrypted_password + 1;
+ end = start;
+ size = 0;
+
+ while (*end != '\0' && g_ascii_isdigit(*end)) {
+ size++;
+ end++;
+ }
+
+ if (size > 0) {
+ gchar *endptr;
+ id = strtoul(start, &endptr, 10);
+
+ if ((endptr == NULL || *endptr == *end)) {
+ for (i = 0; i < RSPAMD_PBKDF_ID_MAX - 1; i++) {
+ pbkdf = &pbkdf_list[i];
+
+ if (pbkdf->id == id) {
+ ret = TRUE;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (!ret) {
+ rspamd_fprintf(stderr, "Invalid password format\n");
+ rspamd_explicit_memzero(encrypted_password, sizeof(encrypted_password));
+ exit(EXIT_FAILURE);
+ }
+
+ if (plen < pbkdf->salt_len + pbkdf->key_len + 3) {
+ msg_err("incorrect salt: password length: %z, must be at least %z characters",
+ plen, pbkdf->salt_len);
+ rspamd_explicit_memzero(encrypted_password, sizeof(encrypted_password));
+ exit(EXIT_FAILURE);
+ }
+
+ /* get salt */
+ salt = rspamd_encrypted_password_get_str(encrypted_password, 3, &salt_len);
+ /* get hash */
+ hash = rspamd_encrypted_password_get_str(encrypted_password,
+ 3 + salt_len + 1,
+ &key_len);
+ if (salt != NULL && hash != NULL) {
+
+ /* decode salt */
+ salt_decoded = rspamd_decode_base32(salt, salt_len, &salt_len, RSPAMD_BASE32_DEFAULT);
+
+ if (salt_decoded == NULL || salt_len != pbkdf->salt_len) {
+ /* We have some unknown salt here */
+ rspamd_explicit_memzero(encrypted_password, sizeof(encrypted_password));
+ msg_err("incorrect salt: %z, while %z expected",
+ salt_len, pbkdf->salt_len);
+ exit(EXIT_FAILURE);
+ }
+
+ key_decoded = rspamd_decode_base32(hash, key_len, &key_len, RSPAMD_BASE32_DEFAULT);
+
+ if (key_decoded == NULL || key_len != pbkdf->key_len) {
+ /* We have some unknown salt here */
+ rspamd_explicit_memzero(encrypted_password, sizeof(encrypted_password));
+ msg_err("incorrect key: %z, while %z expected",
+ key_len, pbkdf->key_len);
+ exit(EXIT_FAILURE);
+ }
+
+ plen = rspamd_read_passphrase(test_password, sizeof(test_password),
+ 0, NULL);
+ if (plen == 0) {
+ rspamd_explicit_memzero(encrypted_password, sizeof(encrypted_password));
+ fprintf(stderr, "Invalid password\n");
+ exit(EXIT_FAILURE);
+ }
+
+ local_key = g_alloca(pbkdf->key_len);
+ rspamd_cryptobox_pbkdf(test_password, plen,
+ salt_decoded, salt_len,
+ local_key, pbkdf->key_len,
+ pbkdf->complexity,
+ pbkdf->type);
+ rspamd_explicit_memzero(test_password, plen);
+ rspamd_explicit_memzero(encrypted_password, sizeof(encrypted_password));
+
+ if (!rspamd_constant_memcmp(key_decoded, local_key, pbkdf->key_len)) {
+ if (!quiet) {
+ rspamd_printf("password incorrect\n");
+ }
+ exit(EXIT_FAILURE);
+ }
+
+ g_free(salt_decoded);
+ g_free(key_decoded);
+ }
+ else {
+ msg_err("bad encrypted password format");
+ rspamd_explicit_memzero(encrypted_password, sizeof(encrypted_password));
+ exit(EXIT_FAILURE);
+ }
+
+ if (!quiet) {
+ rspamd_printf("password correct\n");
+ }
+}
+
+static gint
+rspamadm_pw_lua_encrypt(lua_State *L)
+{
+ const gchar *pw_in = NULL;
+ gchar *ret, *tmp = NULL;
+
+ if (lua_type(L, 1) == LUA_TSTRING) {
+ pw_in = lua_tostring(L, 1);
+ tmp = g_strdup(pw_in);
+ }
+
+ ret = rspamadm_pw_encrypt(tmp);
+
+ lua_pushstring(L, ret);
+ g_free(ret);
+
+ return 1;
+}
+
+
+static void
+rspamadm_pw_lua_subrs(gpointer pL)
+{
+ lua_State *L = pL;
+
+ lua_pushstring(L, "pw_encrypt");
+ lua_pushcfunction(L, rspamadm_pw_lua_encrypt);
+ lua_settable(L, -3);
+}
+
+static void
+rspamadm_alg_list(void)
+{
+ const struct rspamd_controller_pbkdf *pbkdf;
+ guint i;
+
+ for (i = 0; i < RSPAMD_PBKDF_ID_MAX - 1; i++) {
+ pbkdf = &pbkdf_list[i];
+
+ rspamd_printf("%s: %s - %s\n", pbkdf->alias, pbkdf->name,
+ pbkdf->description);
+ }
+}
+
+static void
+rspamadm_pw(gint argc, gchar **argv, const struct rspamadm_command *cmd)
+{
+ GOptionContext *context;
+ GError *error = NULL;
+
+ context = g_option_context_new("pw [--encrypt | --check] - manage rspamd passwords");
+ g_option_context_set_summary(context,
+ "Summary:\n Rspamd administration utility version " RVERSION
+ "\n Release id: " RID);
+ g_option_context_add_main_entries(context, entries, NULL);
+
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ fprintf(stderr, "option parsing failed: %s\n", error->message);
+ g_error_free(error);
+ g_option_context_free(context);
+ exit(EXIT_FAILURE);
+ }
+
+ g_option_context_free(context);
+
+ if (list) {
+ rspamadm_alg_list();
+ exit(EXIT_SUCCESS);
+ }
+
+ if (!do_encrypt && !do_check) {
+ do_encrypt = TRUE;
+ }
+
+ if (do_encrypt) {
+ gchar *encr = rspamadm_pw_encrypt(password);
+ rspamd_printf("%s\n", encr);
+ g_free(encr);
+ }
+ else if (do_check) {
+ rspamadm_pw_check();
+ }
+}
diff --git a/src/rspamadm/rspamadm.c b/src/rspamadm/rspamadm.c
new file mode 100644
index 0000000..0e38dc3
--- /dev/null
+++ b/src/rspamadm/rspamadm.c
@@ -0,0 +1,621 @@
+/*
+ * Copyright 2023 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.
+ */
+#include "config.h"
+#include "rspamadm.h"
+#include "rspamd.h"
+#include "ottery.h"
+#include "lua/lua_common.h"
+#include "lua/lua_thread_pool.h"
+#include "lua_ucl.h"
+#include "unix-std.h"
+#include "contrib/libev/ev.h"
+
+#ifdef HAVE_LIBUTIL_H
+#include <libutil.h>
+#endif
+
+static gboolean verbose = FALSE;
+static gboolean list_commands = FALSE;
+static gboolean show_help = FALSE;
+static gboolean show_version = FALSE;
+GHashTable *ucl_vars = NULL;
+gchar **lua_env = NULL;
+struct rspamd_main *rspamd_main = NULL;
+struct rspamd_async_session *rspamadm_session = NULL;
+lua_State *L = NULL;
+
+/* Defined in modules.c */
+extern module_t *modules[];
+extern worker_t *workers[];
+
+static void rspamadm_help(gint argc, gchar **argv, const struct rspamadm_command *);
+static const char *rspamadm_help_help(gboolean full_help, const struct rspamadm_command *);
+
+struct rspamadm_command help_command = {
+ .name = "help",
+ .flags = RSPAMADM_FLAG_NOHELP,
+ .help = rspamadm_help_help,
+ .run = rspamadm_help};
+
+static gboolean rspamadm_parse_ucl_var(const gchar *option_name,
+ const gchar *value, gpointer data,
+ GError **error);
+
+
+static GOptionEntry entries[] = {
+ {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose,
+ "Enable verbose logging", NULL},
+ {"list-commands", 'l', 0, G_OPTION_ARG_NONE, &list_commands,
+ "List available commands", NULL},
+ {"var", 0, 0, G_OPTION_ARG_CALLBACK, (gpointer) &rspamadm_parse_ucl_var,
+ "Redefine/define environment variable", NULL},
+ {"help", 'h', 0, G_OPTION_ARG_NONE, &show_help,
+ "Show help", NULL},
+ {"version", 'V', 0, G_OPTION_ARG_NONE, &show_version,
+ "Show version", NULL},
+ {"lua-env", '\0', 0, G_OPTION_ARG_FILENAME_ARRAY, &lua_env,
+ "Load lua environment from the specified files", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+GQuark
+rspamadm_error(void)
+{
+ return g_quark_from_static_string("rspamadm");
+}
+
+static void
+rspamadm_version(void)
+{
+ rspamd_printf("Rspamadm %s\n", RVERSION);
+}
+
+static void
+rspamadm_usage(GOptionContext *context)
+{
+ gchar *help_str;
+
+ help_str = g_option_context_get_help(context, TRUE, NULL);
+ rspamd_printf("%s", help_str);
+}
+
+static void
+rspamadm_commands(GPtrArray *all_commands)
+{
+ const struct rspamadm_command *cmd;
+ guint i;
+
+ rspamd_printf("Rspamadm %s\n", RVERSION);
+ rspamd_printf("Usage: rspamadm [global_options] command [command_options]\n");
+ rspamd_printf("\nAvailable commands:\n");
+
+ PTR_ARRAY_FOREACH(all_commands, i, cmd)
+ {
+ if (!(cmd->flags & RSPAMADM_FLAG_NOHELP)) {
+ if (cmd->flags & RSPAMADM_FLAG_LUA) {
+ (void) cmd->help(FALSE, cmd);
+ }
+ else {
+ printf(" %-18s %-60s\n", cmd->name, cmd->help(FALSE, cmd));
+ }
+ }
+ }
+}
+
+static const char *
+rspamadm_help_help(gboolean full_help, const struct rspamadm_command *cmd)
+{
+ const char *help_str;
+
+ if (full_help) {
+ help_str = "Shows help for a specified command\n"
+ "Usage: rspamadm help <command>";
+ }
+ else {
+ help_str = "Shows help for a specified command";
+ }
+
+ return help_str;
+}
+
+static void
+rspamadm_help(gint argc, gchar **argv, const struct rspamadm_command *command)
+{
+ const gchar *cmd_name;
+ const struct rspamadm_command *cmd;
+ GPtrArray *all_commands = (GPtrArray *) command->command_data;
+
+ rspamd_printf("Rspamadm %s\n", RVERSION);
+ rspamd_printf("Usage: rspamadm [global_options] command [command_options]\n\n");
+
+ if (argc <= 1) {
+ cmd_name = "help";
+ }
+ else {
+ cmd_name = argv[1];
+ rspamd_printf("Showing help for %s command\n\n", cmd_name);
+ }
+
+ cmd = rspamadm_search_command(cmd_name, all_commands);
+
+ if (cmd == NULL) {
+ fprintf(stderr, "Invalid command name: %s\n", cmd_name);
+ exit(EXIT_FAILURE);
+ }
+
+ if (strcmp(cmd_name, "help") == 0) {
+ guint i;
+ rspamd_printf("Available commands:\n");
+
+ PTR_ARRAY_FOREACH(all_commands, i, cmd)
+ {
+ if (!(cmd->flags & RSPAMADM_FLAG_NOHELP)) {
+ if (!(cmd->flags & RSPAMADM_FLAG_LUA)) {
+ printf(" %-18s %-60s\n", cmd->name,
+ cmd->help(FALSE, cmd));
+ }
+ else {
+ /* Just call lua subr */
+ (void) cmd->help(FALSE, cmd);
+ }
+ }
+ }
+ }
+ else {
+ if (!(cmd->flags & RSPAMADM_FLAG_LUA)) {
+ rspamd_printf("%s\n", cmd->help(TRUE, cmd));
+ }
+ else {
+ /* Just call lua subr */
+ (void) cmd->help(TRUE, cmd);
+ }
+ }
+}
+
+static gboolean
+rspamadm_parse_ucl_var(const gchar *option_name,
+ const gchar *value, gpointer data,
+ GError **error)
+{
+ gchar *k, *v, *t;
+
+ t = strchr(value, '=');
+
+ if (t != NULL) {
+ k = g_strdup(value);
+ t = k + (t - value);
+ v = g_strdup(t + 1);
+ *t = '\0';
+
+ g_hash_table_insert(ucl_vars, k, v);
+ }
+ else {
+ g_set_error(error, rspamadm_error(), EINVAL,
+ "Bad variable format: %s", value);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+lua_thread_str_error_cb(struct thread_entry *thread, int ret, const char *msg)
+{
+ struct lua_call_data *cd = thread->cd;
+
+ msg_err("call to rspamadm lua script failed (%d): %s", ret, msg);
+
+ cd->ret = ret;
+}
+
+gboolean
+rspamadm_execute_lua_ucl_subr(gint argc, gchar **argv,
+ const ucl_object_t *res,
+ const gchar *script_name,
+ gboolean rspamadm_subcommand)
+{
+ struct thread_entry *thread = lua_thread_pool_get_for_config(rspamd_main->cfg);
+
+ lua_State *L = thread->lua_state;
+
+ gint i;
+ gchar str[PATH_MAX];
+
+ g_assert(script_name != NULL);
+ g_assert(res != NULL);
+ g_assert(L != NULL);
+
+ /* Init internal rspamadm routines */
+
+ if (rspamadm_subcommand) {
+ rspamd_snprintf(str, sizeof(str), "return require \"%s.%s\"", "rspamadm",
+ script_name);
+ }
+ else {
+ rspamd_snprintf(str, sizeof(str), "return require \"%s\"",
+ script_name);
+ }
+
+ if (luaL_dostring(L, str) != 0) {
+ msg_err("cannot execute lua script %s: %s",
+ str, lua_tostring(L, -1));
+ return FALSE;
+ }
+ else {
+ if (lua_type(L, -1) == LUA_TTABLE) {
+ lua_pushstring(L, "handler");
+ lua_gettable(L, -2);
+ }
+
+ if (lua_type(L, -1) != LUA_TFUNCTION) {
+ msg_err("lua script must return "
+ "function and not %s",
+ lua_typename(L, lua_type(L, -1)));
+
+ return FALSE;
+ }
+ }
+
+ /* Push function */
+ lua_pushvalue(L, -1);
+
+ /* Push argv */
+ lua_newtable(L);
+
+ for (i = 1; i < argc; i++) {
+ lua_pushstring(L, argv[i]);
+ lua_rawseti(L, -2, i);
+ }
+
+ /* Push results */
+ ucl_object_push_lua(L, res, TRUE);
+
+ if (lua_repl_thread_call(thread, 2, NULL, lua_thread_str_error_cb) != 0) {
+
+ return FALSE;
+ }
+
+ /* error function */
+ lua_settop(L, 0);
+
+ return TRUE;
+}
+
+static gint
+rspamdadm_commands_sort_func(gconstpointer a, gconstpointer b)
+{
+ const struct rspamadm_command *cmda = *((struct rspamadm_command const **) a),
+ *cmdb = *((struct rspamadm_command const **) b);
+
+ return strcmp(cmda->name, cmdb->name);
+}
+
+static gboolean
+rspamadm_command_maybe_match_name(const gchar *cmd, const gchar *input)
+{
+ gsize clen, inplen;
+
+ clen = strlen(cmd);
+ inplen = strlen(input);
+
+ if (rspamd_strings_levenshtein_distance(cmd, clen,
+ input, inplen, 1) == 1) {
+ return TRUE;
+ }
+ else if ((clen > inplen &&
+ rspamd_substring_search(cmd, clen, input, inplen) != -1) ||
+ (inplen > clen &&
+ rspamd_substring_search(input, inplen, cmd, clen) != -1)) {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+
+static void
+rspamadm_add_lua_globals(struct rspamd_dns_resolver *resolver)
+{
+ struct rspamd_async_session **psession;
+ struct ev_loop **pev_base;
+ struct rspamd_dns_resolver **presolver;
+
+ rspamadm_session = rspamd_session_create(rspamd_main->cfg->cfg_pool, NULL,
+ NULL, (event_finalizer_t) NULL, NULL);
+
+ psession = lua_newuserdata(L, sizeof(struct rspamd_async_session *));
+ rspamd_lua_setclass(L, "rspamd{session}", -1);
+ *psession = rspamadm_session;
+ lua_setglobal(L, "rspamadm_session");
+
+ pev_base = lua_newuserdata(L, sizeof(struct ev_loop *));
+ rspamd_lua_setclass(L, "rspamd{ev_base}", -1);
+ *pev_base = rspamd_main->event_loop;
+ lua_setglobal(L, "rspamadm_ev_base");
+
+ presolver = lua_newuserdata(L, sizeof(struct rspamd_dns_resolver *));
+ rspamd_lua_setclass(L, "rspamd{resolver}", -1);
+ *presolver = resolver;
+ lua_setglobal(L, "rspamadm_dns_resolver");
+}
+
+static void
+rspamadm_cmd_dtor(gpointer p)
+{
+ struct rspamadm_command *cmd = (struct rspamadm_command *) p;
+
+ if (cmd->flags & RSPAMADM_FLAG_DYNAMIC) {
+ if (cmd->aliases) {
+ g_ptr_array_free(cmd->aliases, TRUE);
+ }
+
+ g_free((gpointer) cmd->name);
+ g_free(cmd);
+ }
+}
+
+gint main(gint argc, gchar **argv, gchar **env)
+{
+ GError *error = NULL;
+ GOptionContext *context;
+ GOptionGroup *og;
+ struct rspamd_config *cfg;
+ GQuark process_quark;
+ gchar **nargv, **targv;
+ const gchar *cmd_name;
+ const struct rspamadm_command *cmd;
+ struct rspamd_dns_resolver *resolver;
+ GPtrArray *all_commands = g_ptr_array_new_full(32,
+ rspamadm_cmd_dtor); /* Discovered during check */
+ gint i, nargc, targc;
+ worker_t **pworker;
+ gboolean lua_file = FALSE;
+ gint retcode = 0;
+
+ ucl_vars = g_hash_table_new_full(rspamd_strcase_hash,
+ rspamd_strcase_equal, g_free, g_free);
+ process_quark = g_quark_from_static_string("rspamadm");
+ cfg = rspamd_config_new(RSPAMD_CONFIG_INIT_DEFAULT | RSPAMD_CONFIG_INIT_WIPE_LUA_MEM);
+ cfg->libs_ctx = rspamd_init_libs();
+ rspamd_main = g_malloc0(sizeof(*rspamd_main));
+ rspamd_main->cfg = cfg;
+ rspamd_main->pid = getpid();
+ rspamd_main->type = process_quark;
+ rspamd_main->server_pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "rspamadm", 0);
+
+ rspamadm_fill_internal_commands(all_commands);
+ help_command.command_data = all_commands;
+
+ /* Now read options and store everything till the first non-dash argument */
+ nargv = g_malloc0(sizeof(gchar *) * (argc + 1));
+ nargv[0] = g_strdup(argv[0]);
+
+ for (i = 1, nargc = 1; i < argc; i++) {
+ if (argv[i] && argv[i][0] == '-') {
+ /* Copy to nargv */
+ nargv[nargc] = g_strdup(argv[i]);
+ nargc++;
+ }
+ else {
+ break;
+ }
+ }
+
+ context = g_option_context_new("command - rspamd administration utility");
+ og = g_option_group_new("global", "global options", "global options",
+ NULL, NULL);
+ g_option_context_set_help_enabled(context, FALSE);
+ g_option_group_add_entries(og, entries);
+ g_option_context_set_summary(context,
+ "Summary:\n Rspamd administration utility version " RVERSION
+ "\n Release id: " RID);
+ g_option_context_set_main_group(context, og);
+
+ targv = nargv;
+ targc = nargc;
+
+ if (!g_option_context_parse(context, &targc, &targv, &error)) {
+ fprintf(stderr, "option parsing failed: %s\n", error->message);
+ g_error_free(error);
+ g_option_context_free(context);
+ exit(EXIT_FAILURE);
+ }
+
+
+ /* Setup logger */
+ if (verbose) {
+ rspamd_main->logger = rspamd_log_open_emergency(rspamd_main->server_pool,
+ RSPAMD_LOG_FLAG_USEC | RSPAMD_LOG_FLAG_ENFORCED | RSPAMD_LOG_FLAG_RSPAMADM);
+ rspamd_log_set_log_level(rspamd_main->logger, G_LOG_LEVEL_DEBUG);
+ }
+ else {
+ rspamd_main->logger = rspamd_log_open_emergency(rspamd_main->server_pool,
+ RSPAMD_LOG_FLAG_RSPAMADM);
+ rspamd_log_set_log_level(rspamd_main->logger, G_LOG_LEVEL_MESSAGE);
+ }
+
+ rspamd_main->event_loop = ev_default_loop(rspamd_config_ev_backend_get(cfg));
+
+ resolver = rspamd_dns_resolver_init(rspamd_main->logger,
+ rspamd_main->event_loop,
+ cfg);
+ rspamd_main->http_ctx = rspamd_http_context_create(cfg, rspamd_main->event_loop,
+ NULL);
+
+ g_log_set_default_handler(rspamd_glib_log_function, rspamd_main->logger);
+ g_set_printerr_handler(rspamd_glib_printerr_function);
+ rspamd_config_post_load(cfg,
+ RSPAMD_CONFIG_INIT_LIBS | RSPAMD_CONFIG_INIT_URL | RSPAMD_CONFIG_INIT_NO_TLD);
+
+ pworker = &workers[0];
+ while (*pworker) {
+ /* Init string quarks */
+ (void) g_quark_from_static_string((*pworker)->name);
+ pworker++;
+ }
+
+ cfg->compiled_modules = modules;
+ cfg->compiled_workers = workers;
+
+ rspamd_setproctitle("rspamdadm");
+
+ L = cfg->lua_state;
+ rspamd_lua_set_path(L, NULL, ucl_vars);
+
+ if (!rspamd_lua_set_env(L, ucl_vars, lua_env, &error)) {
+ rspamd_fprintf(stderr, "Cannot load lua environment: %e", error);
+ g_error_free(error);
+
+ goto end;
+ }
+
+ rspamd_lua_set_globals(cfg, L);
+ rspamadm_add_lua_globals(resolver);
+ rspamd_redis_pool_config(cfg->redis_pool, cfg, rspamd_main->event_loop);
+
+ /* Init rspamadm global */
+ lua_newtable(L);
+
+ PTR_ARRAY_FOREACH(all_commands, i, cmd)
+ {
+ if (cmd->lua_subrs != NULL) {
+ cmd->lua_subrs(L);
+ }
+
+ cmd++;
+ }
+
+ lua_setglobal(L, "rspamadm");
+
+ rspamadm_fill_lua_commands(L, all_commands);
+ rspamd_lua_start_gc(cfg);
+ g_ptr_array_sort(all_commands, rspamdadm_commands_sort_func);
+
+ g_strfreev(nargv);
+
+ if (show_version) {
+ rspamadm_version();
+ goto end;
+ }
+ if (show_help) {
+ rspamadm_usage(context);
+ goto end;
+ }
+ if (list_commands) {
+ rspamadm_commands(all_commands);
+ goto end;
+ }
+
+ cmd_name = argv[nargc];
+
+ if (cmd_name == NULL) {
+ cmd_name = "help";
+ }
+
+ gsize cmdlen = strlen(cmd_name);
+
+ if (cmdlen > 4 && memcmp(cmd_name + (cmdlen - 4), ".lua", 4) == 0) {
+ cmd_name = "lua";
+ lua_file = TRUE;
+ }
+
+ cmd = rspamadm_search_command(cmd_name, all_commands);
+
+ if (cmd == NULL) {
+ rspamd_fprintf(stderr, "Invalid command name: %s\n", cmd_name);
+
+ /* Try fuzz search */
+ rspamd_fprintf(stderr, "Suggested commands:\n");
+ PTR_ARRAY_FOREACH(all_commands, i, cmd)
+ {
+ guint j;
+ const gchar *alias;
+
+ if (rspamadm_command_maybe_match_name(cmd->name, cmd_name)) {
+ rspamd_fprintf(stderr, "%s\n", cmd->name);
+ }
+ else {
+ PTR_ARRAY_FOREACH(cmd->aliases, j, alias)
+ {
+ if (rspamadm_command_maybe_match_name(alias, cmd_name)) {
+ rspamd_fprintf(stderr, "%s\n", alias);
+ }
+ }
+ }
+ }
+
+ retcode = EXIT_FAILURE;
+ goto end;
+ }
+
+ if (nargc < argc) {
+
+ if (lua_file) {
+ nargv = g_malloc0(sizeof(gchar *) * (argc - nargc + 2));
+ nargv[1] = g_strdup(argv[nargc]);
+ i = 2;
+ argc++;
+ }
+ else {
+ nargv = g_malloc0(sizeof(gchar *) * (argc - nargc + 1));
+ i = 1;
+ }
+
+ nargv[0] = g_strdup_printf("%s %s", argv[0], cmd_name);
+
+ for (; i < argc - nargc; i++) {
+ if (lua_file) {
+ /*
+ * We append prefix '--arg=' to each argument and shift argv index
+ */
+ gsize arglen = strlen(argv[i + nargc - 1]);
+
+ arglen += sizeof("--args="); /* Including \0 */
+ nargv[i] = g_malloc(arglen);
+ rspamd_snprintf(nargv[i], arglen, "--args=%s", argv[i + nargc - 1]);
+ }
+ else {
+ nargv[i] = g_strdup(argv[i + nargc]);
+ }
+ }
+
+ targc = argc - nargc;
+ targv = nargv;
+ cmd->run(targc, targv, cmd);
+ g_strfreev(nargv);
+ }
+ else {
+ cmd->run(0, NULL, cmd);
+ }
+
+ ev_break(rspamd_main->event_loop, EVBREAK_ALL);
+
+end:
+ rspamd_session_destroy(rspamadm_session);
+ g_option_context_free(context);
+ rspamd_dns_resolver_deinit(resolver);
+ REF_RELEASE(rspamd_main->cfg);
+ rspamd_http_context_free(rspamd_main->http_ctx);
+ rspamd_log_close(rspamd_main->logger);
+ rspamd_url_deinit();
+ g_ptr_array_free(all_commands, TRUE);
+ ev_loop_destroy(rspamd_main->event_loop);
+ g_hash_table_unref(ucl_vars);
+ rspamd_mempool_delete(rspamd_main->server_pool);
+ g_free(rspamd_main);
+
+ return retcode;
+}
diff --git a/src/rspamadm/rspamadm.h b/src/rspamadm/rspamadm.h
new file mode 100644
index 0000000..5fe51c3
--- /dev/null
+++ b/src/rspamadm/rspamadm.h
@@ -0,0 +1,92 @@
+/*-
+ * Copyright 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.
+ */
+#ifndef RSPAMD_RSPAMDADM_H
+#define RSPAMD_RSPAMDADM_H
+
+#include "config.h"
+#include "ucl.h"
+#include <lua.h>
+#include <lauxlib.h>
+#include <lualib.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern GHashTable *ucl_vars;
+extern gchar **lua_env;
+extern struct rspamd_main *rspamd_main;
+
+GQuark rspamadm_error(void);
+
+struct rspamadm_command;
+
+typedef const gchar *(*rspamadm_help_func)(gboolean full_help,
+ const struct rspamadm_command *cmd);
+
+typedef void (*rspamadm_run_func)(gint argc, gchar **argv,
+ const struct rspamadm_command *cmd);
+
+typedef void (*rspamadm_lua_exports_func)(gpointer lua_state);
+
+#define RSPAMADM_FLAG_NOHELP (1u << 0u)
+#define RSPAMADM_FLAG_LUA (1u << 1u)
+#define RSPAMADM_FLAG_DYNAMIC (1u << 2u)
+
+struct rspamadm_command {
+ const gchar *name;
+ guint flags;
+ rspamadm_help_func help;
+ rspamadm_run_func run;
+ rspamadm_lua_exports_func lua_subrs;
+ GPtrArray *aliases;
+ gpointer command_data; /* Opaque data */
+};
+
+extern const struct rspamadm_command *commands[];
+extern struct rspamadm_command help_command;
+
+const struct rspamadm_command *rspamadm_search_command(const gchar *name,
+ GPtrArray *all_commands);
+
+void rspamadm_fill_internal_commands(GPtrArray *dest);
+
+void rspamadm_fill_lua_commands(lua_State *L, GPtrArray *dest);
+
+gboolean rspamadm_execute_lua_ucl_subr(gint argc, gchar **argv,
+ const ucl_object_t *res,
+ const gchar *script_name,
+ gboolean rspamadm_subcommand);
+
+struct thread_entry;
+
+typedef void (*lua_thread_error_t)(struct thread_entry *thread, int ret, const char *msg);
+
+
+struct lua_call_data {
+ gint top;
+ gint ret;
+ gpointer ud;
+};
+
+gint lua_repl_thread_call(struct thread_entry *thread, gint narg,
+ gpointer ud, lua_thread_error_t error_func);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/rspamadm/signtool.c b/src/rspamadm/signtool.c
new file mode 100644
index 0000000..b39b870
--- /dev/null
+++ b/src/rspamadm/signtool.c
@@ -0,0 +1,623 @@
+/*-
+ * Copyright 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.
+ */
+
+#include "config.h"
+#include "rspamadm.h"
+#include "cryptobox.h"
+#include "printf.h"
+#include "ucl.h"
+#include "libcryptobox/keypair.h"
+#include "libutil/str_util.h"
+#include "libutil/util.h"
+#include "unix-std.h"
+#ifdef HAVE_SYS_WAIT_H
+#include <sys/wait.h>
+#endif
+
+static gboolean openssl = FALSE;
+static gboolean verify = FALSE;
+static gboolean quiet = FALSE;
+static gchar *suffix = NULL;
+static gchar *pubkey_file = NULL;
+static gchar *pubkey = NULL;
+static gchar *pubout = NULL;
+static gchar *keypair_file = NULL;
+static gchar *editor = NULL;
+static gboolean edit = FALSE;
+enum rspamd_cryptobox_mode mode = RSPAMD_CRYPTOBOX_MODE_25519;
+
+static void rspamadm_signtool(gint argc, gchar **argv,
+ const struct rspamadm_command *cmd);
+static const char *rspamadm_signtool_help(gboolean full_help,
+ const struct rspamadm_command *cmd);
+
+struct rspamadm_command signtool_command = {
+ .name = "signtool",
+ .flags = 0,
+ .help = rspamadm_signtool_help,
+ .run = rspamadm_signtool,
+ .lua_subrs = NULL,
+};
+
+static GOptionEntry entries[] = {
+ {"openssl", 'o', 0, G_OPTION_ARG_NONE, &openssl,
+ "Generate openssl nistp256 keypair not curve25519 one", NULL},
+ {"verify", 'v', 0, G_OPTION_ARG_NONE, &verify,
+ "Verify signatures and not sign", NULL},
+ {"suffix", 'S', 0, G_OPTION_ARG_STRING, &suffix,
+ "Save signatures in file<suffix> files", NULL},
+ {"pubkey", 'p', 0, G_OPTION_ARG_STRING, &pubkey,
+ "Base32 encoded pubkey to verify", NULL},
+ {"pubout", '\0', 0, G_OPTION_ARG_FILENAME, &pubout,
+ "Output public key to the specified file", NULL},
+ {"pubfile", 'P', 0, G_OPTION_ARG_FILENAME, &pubkey_file,
+ "Load base32 encoded pubkey to verify from the file", NULL},
+ {"keypair", 'k', 0, G_OPTION_ARG_STRING, &keypair_file,
+ "UCL with keypair to load for signing", NULL},
+ {"quiet", 'q', 0, G_OPTION_ARG_NONE, &quiet,
+ "Be quiet", NULL},
+ {"edit", 'e', 0, G_OPTION_ARG_NONE, &edit,
+ "Run editor and sign the edited file", NULL},
+ {"editor", '\0', 0, G_OPTION_ARG_STRING, &editor,
+ "Use the specified editor instead of $EDITOR environment var", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+static const char *
+rspamadm_signtool_help(gboolean full_help,
+ const struct rspamadm_command *cmd)
+{
+ const char *help_str;
+
+ if (full_help) {
+ help_str = "Manage digital signatures\n\n"
+ "Usage: rspamadm signtool [-o] -k <keypair_file> [-v -p <pubkey> | -P <pubkey_file>] [-S <suffix>] file1 ...\n"
+ "Where options are:\n\n"
+ "-v: verify against pubkey instead of \n"
+ "-o: use ECDSA instead of EdDSA\n"
+ "-p: load pubkey as base32 string\n"
+ "-P: load pubkey paced in file\n"
+ "-k: load signing keypair from ucl file\n"
+ "-S: append suffix for signatures and store them in files\n"
+ "-q: be quiet\n"
+ "-e: opens file for editing and sign the result\n"
+ "--editor: use the specified editor instead of $EDITOR environment var\n"
+ "--help: shows available options and commands";
+ }
+ else {
+ help_str = "Sign and verify files tool";
+ }
+
+ return help_str;
+}
+
+static gint
+rspamadm_edit_file(const gchar *fname)
+{
+ gchar tmppath[PATH_MAX], run_cmdline[PATH_MAX];
+ guchar *map;
+ gsize len = 0;
+ gint fd_out, retcode, child_argc;
+ GPid child_pid;
+ gchar *tmpdir, **child_argv = NULL;
+ struct stat st;
+ GError *err = NULL;
+
+ if (editor == NULL) {
+ editor = getenv("EDITOR");
+ }
+
+ if (editor == NULL) {
+ rspamd_fprintf(stderr, "cannot find editor: specify $EDITOR "
+ "environment variable or pass --editor argument\n");
+ exit(EXIT_FAILURE);
+ }
+
+ tmpdir = getenv("TMPDIR");
+ if (tmpdir == NULL) {
+ tmpdir = "/tmp";
+ }
+
+ if (stat(fname, &st) == -1 || st.st_size == 0) {
+ /* The source does not exist, but that shouldn't be a problem */
+ len = 0;
+ map = NULL;
+
+ /* Try to touch source anyway */
+ fd_out = rspamd_file_xopen(fname, O_WRONLY | O_CREAT | O_EXCL, 00644,
+ 0);
+
+ if (fd_out == -1) {
+ rspamd_fprintf(stderr, "cannot open %s: %s\n", fname,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ close(fd_out);
+ }
+ else {
+ map = rspamd_file_xmap(fname, PROT_READ, &len, TRUE);
+
+ if (map == NULL) {
+ rspamd_fprintf(stderr, "cannot open %s: %s\n", fname,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ rspamd_snprintf(tmppath, sizeof(tmppath),
+ "%s/rspamd_sign-XXXXXXXXXX", tmpdir);
+ mode_t cur_umask = umask(S_IRWXO | S_IRWXG);
+ fd_out = mkstemp(tmppath);
+ (void) umask(cur_umask);
+
+ if (fd_out == -1) {
+ rspamd_fprintf(stderr, "cannot open tempfile %s: %s\n", tmppath,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ if (len > 0 && write(fd_out, map, len) == -1) {
+ rspamd_fprintf(stderr, "cannot write to tempfile %s: %s\n", tmppath,
+ strerror(errno));
+ unlink(tmppath);
+ munmap(map, len);
+ close(fd_out);
+ exit(EXIT_FAILURE);
+ }
+
+ if (len > 0) {
+ munmap(map, len);
+ }
+
+ fsync(fd_out);
+ close(fd_out);
+
+ /* Now we spawn editor with the filename as argument */
+ rspamd_snprintf(run_cmdline, sizeof(run_cmdline), "%s %s", editor, tmppath);
+ if (!g_shell_parse_argv(run_cmdline, &child_argc,
+ &child_argv, &err)) {
+ rspamd_fprintf(stderr, "cannot exec %s: %e\n", editor,
+ err);
+ unlink(tmppath);
+ exit(EXIT_FAILURE);
+ }
+
+ if (!g_spawn_async(NULL, child_argv, NULL,
+ G_SPAWN_CHILD_INHERITS_STDIN | G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
+ NULL, NULL, &child_pid, &err)) {
+ rspamd_fprintf(stderr, "cannot exec %s: %e\n", editor,
+ err);
+ unlink(tmppath);
+ exit(EXIT_FAILURE);
+ }
+
+ g_strfreev(child_argv);
+
+ for (;;) {
+ if (waitpid((pid_t) child_pid, &retcode, 0) != -1) {
+ break;
+ }
+
+ if (errno != EINTR) {
+ rspamd_fprintf(stderr, "failed to wait for %s: %s\n", editor,
+ strerror(errno));
+ unlink(tmppath);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+#if GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 34
+#if GLIB_MINOR_VERSION >= 70
+ if (!g_spawn_check_wait_status(retcode, &err)) {
+#else
+ if (!g_spawn_check_exit_status(retcode, &err)) {
+#endif
+ unlink(tmppath);
+ rspamd_fprintf(stderr, "%s returned error code: %d - %e\n", editor,
+ retcode, err);
+ exit(EXIT_FAILURE);
+ }
+#else
+ if (retcode != 0) {
+ unlink(tmppath);
+ rspamd_fprintf(stderr, "%s returned error code: %d\n", editor,
+ retcode);
+ exit(retcode);
+ }
+#endif
+
+ map = rspamd_file_xmap(tmppath, PROT_READ, &len, TRUE);
+
+ if (map == NULL) {
+ rspamd_fprintf(stderr, "cannot map %s: %s\n", tmppath,
+ strerror(errno));
+ unlink(tmppath);
+ exit(EXIT_FAILURE);
+ }
+
+ rspamd_snprintf(run_cmdline, sizeof(run_cmdline), "%s.new", fname);
+ fd_out = rspamd_file_xopen(run_cmdline, O_RDWR | O_CREAT | O_TRUNC, 00600,
+ 0);
+
+ if (fd_out == -1) {
+ rspamd_fprintf(stderr, "cannot open new file %s: %s\n", run_cmdline,
+ strerror(errno));
+ unlink(tmppath);
+ munmap(map, len);
+ exit(EXIT_FAILURE);
+ }
+
+ if (write(fd_out, map, len) == -1) {
+ rspamd_fprintf(stderr, "cannot write new file %s: %s\n", run_cmdline,
+ strerror(errno));
+ unlink(tmppath);
+ unlink(run_cmdline);
+ close(fd_out);
+ munmap(map, len);
+ exit(EXIT_FAILURE);
+ }
+
+ unlink(tmppath);
+ (void) lseek(fd_out, 0, SEEK_SET);
+ munmap(map, len);
+
+ return fd_out;
+}
+
+static bool
+rspamadm_sign_file(const gchar *fname, struct rspamd_cryptobox_keypair *kp)
+{
+ gint fd_sig, fd_input;
+ guchar sig[rspamd_cryptobox_MAX_SIGBYTES], *map;
+ gchar sigpath[PATH_MAX];
+ FILE *pub_fp;
+ struct stat st;
+ const guchar *sk;
+
+ if (suffix == NULL) {
+ suffix = ".sig";
+ }
+
+ if (edit) {
+ /* We need to open editor and then sign the temporary file */
+ fd_input = rspamadm_edit_file(fname);
+ }
+ else {
+ fd_input = rspamd_file_xopen(fname, O_RDONLY, 0, TRUE);
+ }
+
+ if (fd_input == -1) {
+ rspamd_fprintf(stderr, "cannot open %s: %s\n", fname,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ g_assert(fstat(fd_input, &st) != -1);
+
+ rspamd_snprintf(sigpath, sizeof(sigpath), "%s%s", fname, suffix);
+ fd_sig = rspamd_file_xopen(sigpath, O_WRONLY | O_CREAT | O_TRUNC, 00644, 0);
+
+ if (fd_sig == -1) {
+ close(fd_input);
+ rspamd_fprintf(stderr, "cannot open %s: %s\n", sigpath,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd_input, 0);
+ close(fd_input);
+
+ if (map == MAP_FAILED) {
+ close(fd_sig);
+ rspamd_fprintf(stderr, "cannot map %s: %s\n", fname,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ g_assert(rspamd_cryptobox_MAX_SIGBYTES >=
+ rspamd_cryptobox_signature_bytes(mode));
+
+ sk = rspamd_keypair_component(kp, RSPAMD_KEYPAIR_COMPONENT_SK, NULL);
+ rspamd_cryptobox_sign(sig, NULL, map, st.st_size, sk, mode);
+
+ if (edit) {
+ /* We also need to rename .new file */
+ rspamd_snprintf(sigpath, sizeof(sigpath), "%s.new", fname);
+
+ if (rename(sigpath, fname) == -1) {
+ rspamd_fprintf(stderr, "cannot rename %s to %s: %s\n", sigpath, fname,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ unlink(sigpath);
+ }
+
+ rspamd_snprintf(sigpath, sizeof(sigpath), "%s%s", fname, suffix);
+
+ if (write(fd_sig, sig, rspamd_cryptobox_signature_bytes(mode)) == -1) {
+ rspamd_fprintf(stderr, "cannot write signature to %s: %s\n", sigpath,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ close(fd_sig);
+ munmap(map, st.st_size);
+
+ if (!quiet) {
+ rspamd_fprintf(stdout, "signed %s; stored hash in %s\n",
+ fname, sigpath);
+ }
+
+ if (pubout) {
+ GString *b32_pk;
+
+ pub_fp = fopen(pubout, "w");
+
+ if (pub_fp == NULL) {
+ rspamd_fprintf(stderr, "cannot write pubkey to %s: %s",
+ pubout, strerror(errno));
+ }
+ else {
+ b32_pk = rspamd_keypair_print(kp,
+ RSPAMD_KEYPAIR_PUBKEY | RSPAMD_KEYPAIR_BASE32);
+
+ if (b32_pk) {
+ rspamd_fprintf(pub_fp, "%v", b32_pk);
+ g_string_free(b32_pk, TRUE);
+ }
+
+ fclose(pub_fp);
+ }
+ if (!quiet) {
+ rspamd_fprintf(stdout, "stored pubkey in %s\n",
+ pubout);
+ }
+ }
+
+ return true;
+}
+
+static bool
+rspamadm_verify_file(const gchar *fname, const guchar *pk)
+{
+ gint fd_sig, fd_input;
+ guchar *map, *map_sig;
+ gchar sigpath[PATH_MAX];
+ struct stat st, st_sig;
+ bool ret;
+
+ g_assert(rspamd_cryptobox_MAX_SIGBYTES >=
+ rspamd_cryptobox_signature_bytes(mode));
+
+ if (suffix == NULL) {
+ suffix = ".sig";
+ }
+
+ fd_input = rspamd_file_xopen(fname, O_RDONLY, 0, TRUE);
+
+ if (fd_input == -1) {
+ rspamd_fprintf(stderr, "cannot open %s: %s\n", fname,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ g_assert(fstat(fd_input, &st) != -1);
+
+ rspamd_snprintf(sigpath, sizeof(sigpath), "%s%s", fname, suffix);
+ fd_sig = rspamd_file_xopen(sigpath, O_RDONLY, 0, TRUE);
+
+ if (fd_sig == -1) {
+ close(fd_input);
+ rspamd_fprintf(stderr, "cannot open %s: %s\n", sigpath,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd_input, 0);
+ close(fd_input);
+
+ if (map == MAP_FAILED) {
+ close(fd_sig);
+ rspamd_fprintf(stderr, "cannot open %s: %s\n", sigpath,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ g_assert(fstat(fd_sig, &st_sig) != -1);
+
+ if (st_sig.st_size != rspamd_cryptobox_signature_bytes(mode)) {
+ close(fd_sig);
+ rspamd_fprintf(stderr, "invalid signature size %s: %ud\n", fname,
+ (guint) st_sig.st_size);
+ munmap(map, st.st_size);
+ exit(EXIT_FAILURE);
+ }
+
+ map_sig = mmap(NULL, st_sig.st_size, PROT_READ, MAP_SHARED, fd_sig, 0);
+ close(fd_sig);
+
+ if (map_sig == MAP_FAILED) {
+ munmap(map, st.st_size);
+ rspamd_fprintf(stderr, "cannot map %s: %s\n", sigpath,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ ret = rspamd_cryptobox_verify(map_sig, st_sig.st_size,
+ map, st.st_size, pk, mode);
+ munmap(map, st.st_size);
+ munmap(map_sig, st_sig.st_size);
+
+ if (!ret) {
+ rspamd_fprintf(stderr, "cannot verify %s using %s: invalid signature\n",
+ fname, sigpath);
+ }
+ else if (!quiet) {
+ rspamd_fprintf(stdout, "verified %s using %s\n",
+ fname, sigpath);
+ }
+
+ return ret;
+}
+
+
+static void
+rspamadm_signtool(gint argc, gchar **argv, const struct rspamadm_command *cmd)
+{
+ GOptionContext *context;
+ GError *error = NULL;
+ struct ucl_parser *parser;
+ ucl_object_t *top;
+ struct rspamd_cryptobox_pubkey *pk;
+ struct rspamd_cryptobox_keypair *kp;
+ gsize fsize, flen;
+ gint i;
+
+ context = g_option_context_new(
+ "keypair - create encryption keys");
+ g_option_context_set_summary(context,
+ "Summary:\n Rspamd administration utility version " RVERSION
+ "\n Release id: " RID);
+ g_option_context_add_main_entries(context, entries, NULL);
+
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ rspamd_fprintf(stderr, "option parsing failed: %s\n", error->message);
+ g_error_free(error);
+ g_option_context_free(context);
+ exit(EXIT_FAILURE);
+ }
+
+ g_option_context_free(context);
+
+ if (openssl) {
+ mode = RSPAMD_CRYPTOBOX_MODE_NIST;
+ }
+
+ if (verify && (!pubkey && !pubkey_file)) {
+ rspamd_fprintf(stderr, "no pubkey for verification\n");
+ exit(EXIT_FAILURE);
+ }
+ else if (!verify && (!keypair_file)) {
+ rspamd_fprintf(stderr, "no keypair for signing\n");
+ exit(EXIT_FAILURE);
+ }
+
+ if (verify) {
+ g_assert(pubkey || pubkey_file);
+
+ if (pubkey_file) {
+ gint fd;
+ gchar *map;
+ struct stat st;
+
+ fd = open(pubkey_file, O_RDONLY);
+
+ if (fd == -1) {
+ rspamd_fprintf(stderr, "cannot open %s: %s\n", pubkey_file,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ g_assert(fstat(fd, &st) != -1);
+ fsize = st.st_size;
+ flen = fsize;
+ map = mmap(NULL, fsize, PROT_READ, MAP_SHARED, fd, 0);
+ close(fd);
+
+ if (map == MAP_FAILED) {
+ rspamd_fprintf(stderr, "cannot read %s: %s\n", pubkey_file,
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ /* XXX: assume base32 pubkey now */
+ while (flen > 0 && g_ascii_isspace(map[flen - 1])) {
+ flen--;
+ }
+
+ pk = rspamd_pubkey_from_base32(map, flen,
+ RSPAMD_KEYPAIR_SIGN, mode);
+
+ if (pk == NULL) {
+ rspamd_fprintf(stderr, "bad size %s: %ud, %ud expected\n",
+ pubkey_file,
+ (guint) flen,
+ rspamd_cryptobox_pk_sig_bytes(mode));
+ exit(EXIT_FAILURE);
+ }
+
+ munmap(map, fsize);
+ }
+ else {
+ pk = rspamd_pubkey_from_base32(pubkey, strlen(pubkey),
+ RSPAMD_KEYPAIR_SIGN, mode);
+
+ if (pk == NULL) {
+ rspamd_fprintf(stderr, "bad size %s: %ud, %ud expected\n",
+ pubkey_file,
+ (guint) strlen(pubkey),
+ rspamd_cryptobox_pk_sig_bytes(mode));
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ for (i = 1; i < argc; i++) {
+ /* XXX: support cmd line signature */
+ if (!rspamadm_verify_file(argv[i], rspamd_pubkey_get_pk(pk, NULL))) {
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ g_free(pk);
+ }
+ else {
+ g_assert(keypair_file != NULL);
+
+ parser = ucl_parser_new(0);
+
+ if (!ucl_parser_add_file(parser, keypair_file) ||
+ (top = ucl_parser_get_object(parser)) == NULL) {
+ rspamd_fprintf(stderr, "cannot load keypair: %s\n",
+ ucl_parser_get_error(parser));
+ exit(EXIT_FAILURE);
+ }
+
+ ucl_parser_free(parser);
+
+ kp = rspamd_keypair_from_ucl(top);
+
+ if (kp == NULL) {
+ rspamd_fprintf(stderr, "invalid signing key\n");
+ exit(EXIT_FAILURE);
+ }
+
+ if (rspamd_keypair_type(kp) != RSPAMD_KEYPAIR_SIGN) {
+ rspamd_fprintf(stderr, "unsuitable for signing key\n");
+ exit(EXIT_FAILURE);
+ }
+
+ for (i = 1; i < argc; i++) {
+ /* XXX: support cmd line signature */
+ if (!rspamadm_sign_file(argv[i], kp)) {
+ rspamd_keypair_unref(kp);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ rspamd_keypair_unref(kp);
+ }
+}
diff --git a/src/rspamadm/stat_convert.c b/src/rspamadm/stat_convert.c
new file mode 100644
index 0000000..0741279
--- /dev/null
+++ b/src/rspamadm/stat_convert.c
@@ -0,0 +1,262 @@
+/*-
+ * Copyright 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.
+ */
+#include "config.h"
+#include "rspamadm.h"
+#include "lua/lua_common.h"
+
+#include "contrib/uthash/utlist.h"
+
+/* Common */
+static gchar *config_file = NULL;
+static gchar *symbol_ham = NULL;
+static gchar *symbol_spam = NULL;
+
+static gdouble expire = 0.0;
+
+/* Inputs */
+static gchar *spam_db = NULL;
+static gchar *ham_db = NULL;
+static gchar *cache_db = NULL;
+
+/* Outputs */
+static gchar *redis_host = NULL;
+static gchar *redis_db = NULL;
+static gchar *redis_username = NULL;
+static gchar *redis_password = NULL;
+static gboolean reset_previous = FALSE;
+
+static void rspamadm_statconvert(gint argc, gchar **argv,
+ const struct rspamadm_command *cmd);
+static const char *rspamadm_statconvert_help(gboolean full_help,
+ const struct rspamadm_command *cmd);
+
+struct rspamadm_command statconvert_command = {
+ .name = "statconvert",
+ .flags = 0,
+ .help = rspamadm_statconvert_help,
+ .run = rspamadm_statconvert,
+ .lua_subrs = NULL,
+};
+
+static GOptionEntry entries[] = {
+ {"config", 'c', 0, G_OPTION_ARG_FILENAME, &config_file,
+ "Config file to read data from", NULL},
+ {"reset", 'r', 0, G_OPTION_ARG_NONE, &reset_previous,
+ "Reset previous data instead of appending values", NULL},
+ {"expire", 'e', 0, G_OPTION_ARG_DOUBLE, &expire,
+ "Set expiration in seconds (can be fractional)", NULL},
+
+ {"symbol-spam", 0, 0, G_OPTION_ARG_STRING, &symbol_spam,
+ "Symbol for spam (e.g. BAYES_SPAM)", NULL},
+ {"symbol-ham", 0, 0, G_OPTION_ARG_STRING, &symbol_ham,
+ "Symbol for ham (e.g. BAYES_HAM)", NULL},
+ {"spam-db", 0, 0, G_OPTION_ARG_STRING, &spam_db,
+ "Input spam file (sqlite3)", NULL},
+ {"ham-db", 0, 0, G_OPTION_ARG_STRING, &ham_db,
+ "Input ham file (sqlite3)", NULL},
+ {"cache", 0, 0, G_OPTION_ARG_FILENAME, &cache_db,
+ "Input learn cache", NULL},
+ {"redis-host", 'h', 0, G_OPTION_ARG_STRING, &redis_host,
+ "Output redis ip (in format ip:port)", NULL},
+ {"redis-username", 'u', 0, G_OPTION_ARG_STRING, &redis_username,
+ "Username to connect to redis", NULL},
+ {"redis-password", 'p', 0, G_OPTION_ARG_STRING, &redis_password,
+ "Password to connect to redis", NULL},
+ {"redis-db", 'd', 0, G_OPTION_ARG_STRING, &redis_db,
+ "Redis database (should be numeric)", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+
+static const char *
+rspamadm_statconvert_help(gboolean full_help, const struct rspamadm_command *cmd)
+{
+ const char *help_str;
+
+ if (full_help) {
+ help_str = "Convert statistics from sqlite3 to redis\n\n"
+ "Usage: rspamadm statconvert -c /etc/rspamd.conf [-r]\n"
+ "Where options are:\n\n"
+ "-c: config file to read data from\n"
+ "-r: reset previous data instead of increasing values\n"
+ "-e: set expire to that amount of seconds\n"
+ "** Or specify options directly **\n"
+ "--redis-host: output redis ip (in format ip:port)\n"
+ "--redis-db: output redis database\n"
+ "--redis-username: redis username\n"
+ "--redis-password: redis password\n"
+ "--cache: sqlite3 file for learn cache\n"
+ "--spam-db: sqlite3 input file for spam data\n"
+ "--ham-db: sqlite3 input file for ham data\n"
+ "--symbol-spam: symbol in redis for spam (e.g. BAYES_SPAM)\n"
+ "--symbol-ham: symbol in redis for ham (e.g. BAYES_HAM)\n";
+ }
+ else {
+ help_str = "Convert statistics from sqlite3 to redis";
+ }
+
+ return help_str;
+}
+
+static void
+rspamadm_statconvert(gint argc, gchar **argv, const struct rspamadm_command *cmd)
+{
+ GOptionContext *context;
+ GError *error = NULL;
+ ucl_object_t *obj;
+
+ context = g_option_context_new(
+ "statconvert - converts statistics from sqlite3 to redis");
+ g_option_context_set_summary(context,
+ "Summary:\n Rspamd administration utility version " RVERSION
+ "\n Release id: " RID);
+ g_option_context_add_main_entries(context, entries, NULL);
+ g_option_context_set_ignore_unknown_options(context, TRUE);
+
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ rspamd_fprintf(stderr, "option parsing failed: %s\n", error->message);
+ g_error_free(error);
+ g_option_context_free(context);
+ exit(EXIT_FAILURE);
+ }
+
+ g_option_context_free(context);
+
+ if (config_file) {
+ /* Load config file, assuming that it has all information required */
+ struct ucl_parser *parser;
+
+ parser = ucl_parser_new(0);
+ rspamd_ucl_add_conf_variables(parser, ucl_vars);
+
+ if (!ucl_parser_add_file(parser, config_file)) {
+ msg_err("ucl parser error: %s", ucl_parser_get_error(parser));
+ ucl_parser_free(parser);
+
+ exit(EXIT_FAILURE);
+ }
+
+ obj = ucl_parser_get_object(parser);
+ ucl_parser_free(parser);
+ }
+ else {
+ /* We need to get all information from the command line */
+ ucl_object_t *classifier, *statfile_ham, *statfile_spam, *tmp, *redis;
+
+ /* Check arguments sanity */
+ if (spam_db == NULL) {
+ msg_err("No spam-db specified");
+ exit(EXIT_FAILURE);
+ }
+ if (ham_db == NULL) {
+ msg_err("No ham-db specified");
+ exit(EXIT_FAILURE);
+ }
+ if (redis_host == NULL) {
+ msg_err("No redis-host specified");
+ exit(EXIT_FAILURE);
+ }
+ if (symbol_ham == NULL) {
+ msg_err("No symbol-ham specified");
+ exit(EXIT_FAILURE);
+ }
+ if (symbol_spam == NULL) {
+ msg_err("No symbol-spam specified");
+ exit(EXIT_FAILURE);
+ }
+
+ obj = ucl_object_typed_new(UCL_OBJECT);
+
+ classifier = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(obj, classifier, "classifier", 0, false);
+ /* Now we need to create "bayes" key in it */
+ tmp = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(classifier, tmp, "bayes", 0, false);
+ classifier = tmp;
+ ucl_object_insert_key(classifier, ucl_object_fromstring("sqlite3"),
+ "backend", 0, false);
+
+ if (cache_db != NULL) {
+ ucl_object_t *cache;
+
+ cache = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(cache, ucl_object_fromstring("sqlite3"),
+ "type", 0, false);
+ ucl_object_insert_key(cache, ucl_object_fromstring(cache_db),
+ "file", 0, false);
+
+ ucl_object_insert_key(classifier, cache, "cache", 0, false);
+ }
+
+ statfile_ham = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(statfile_ham, ucl_object_fromstring(symbol_ham),
+ "symbol", 0, false);
+ ucl_object_insert_key(statfile_ham, ucl_object_frombool(false),
+ "spam", 0, false);
+ ucl_object_insert_key(statfile_ham, ucl_object_fromstring(ham_db),
+ "db", 0, false);
+
+ statfile_spam = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(statfile_spam, ucl_object_fromstring(symbol_spam),
+ "symbol", 0, false);
+ ucl_object_insert_key(statfile_spam, ucl_object_frombool(true),
+ "spam", 0, false);
+ ucl_object_insert_key(statfile_spam, ucl_object_fromstring(spam_db),
+ "db", 0, false);
+
+ DL_APPEND(statfile_ham, statfile_spam);
+ ucl_object_insert_key(classifier, statfile_ham,
+ "statfile", 0, false);
+
+ /* Deal with redis */
+
+ redis = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(obj, redis, "redis", 0, false);
+
+ ucl_object_insert_key(redis, ucl_object_fromstring(redis_host),
+ "servers", 0, false);
+
+ if (redis_db) {
+ ucl_object_insert_key(redis, ucl_object_fromstring(redis_db),
+ "dbname", 0, false);
+ }
+
+ if (redis_username) {
+ ucl_object_insert_key(redis, ucl_object_fromstring(redis_username),
+ "username", 0, false);
+ }
+
+ if (redis_password) {
+ ucl_object_insert_key(redis, ucl_object_fromstring(redis_password),
+ "password", 0, false);
+ }
+ }
+
+ ucl_object_insert_key(obj, ucl_object_frombool(reset_previous),
+ "reset_previous", 0, false);
+
+ if (expire != 0) {
+ ucl_object_insert_key(obj, ucl_object_fromdouble(expire),
+ "expire", 0, false);
+ }
+
+ rspamadm_execute_lua_ucl_subr(argc,
+ argv,
+ obj,
+ "stat_convert",
+ TRUE);
+
+ ucl_object_unref(obj);
+}