diff options
Diffstat (limited to '')
-rw-r--r-- | src/rspamadm/CMakeLists.txt | 31 | ||||
-rw-r--r-- | src/rspamadm/commands.c | 280 | ||||
-rw-r--r-- | src/rspamadm/configdump.c | 555 | ||||
-rw-r--r-- | src/rspamadm/confighelp.c | 301 | ||||
-rw-r--r-- | src/rspamadm/configtest.c | 190 | ||||
-rw-r--r-- | src/rspamadm/control.c | 258 | ||||
-rw-r--r-- | src/rspamadm/fuzzy_convert.c | 161 | ||||
-rw-r--r-- | src/rspamadm/lua_repl.c | 1026 | ||||
-rw-r--r-- | src/rspamadm/pw.c | 392 | ||||
-rw-r--r-- | src/rspamadm/rspamadm.c | 621 | ||||
-rw-r--r-- | src/rspamadm/rspamadm.h | 92 | ||||
-rw-r--r-- | src/rspamadm/signtool.c | 623 | ||||
-rw-r--r-- | src/rspamadm/stat_convert.c | 262 |
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); +} |