From: Vsevolod Stakhov Date: Tue, 23 Jun 2026 07:58:48 +0000 (+0100) Subject: [Feature] rspamadm: discover command modules from external directories X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d8a95895b400f89b9521ef7ed510fdc048db40e0;p=thirdparty%2Frspamd.git [Feature] rspamadm: discover command modules from external directories rspamadm discovered Lua sub-commands by globbing only the built-in LUALIBDIR/rspamadm directory, so a third-party or premium package could not ship a rspamadm command without writing into the OSS-owned lualib tree. Scan two additional sources, in order, after the built-in directory: - $CONFDIR/rspamadm.d/*.lua, a drop-in dir consistent with local.d - every directory in the colon-separated RSPAMADM_COMMAND_PATH env var (mirrors how PATH works) Duplicate command names are skipped (first wins), so built-in commands can never be shadowed. Externally loaded modules run with the same globals and lua_path as built-in ones, so they can require premium lualibs and use lua_redis. No behavior change when the env var and the .d directory are absent. Factor the per-file loader and per-directory scan into helpers, and fix a latent out-of-bounds in the basename fallback (it searched .lua in the full path but indexed the basename buffer). Document discovery order and the new env var in the man page. --- diff --git a/doc/rspamadm.1.md b/doc/rspamadm.1.md index 83c6c3e278..3a6cae89a7 100644 --- a/doc/rspamadm.1.md +++ b/doc/rspamadm.1.md @@ -20,7 +20,6 @@ Also for each command you can check list of available **command_options** by run rspamadm help command rspamadm command --help - # OPTIONS @@ -39,6 +38,31 @@ Also for each command you can check list of available **command_options** by run \--var=*value* : Redefine ucl variable in format `VARIABLE=VALUE` +# COMMAND DISCOVERY + +In addition to the built-in commands, `rspamadm` discovers Lua command modules +(`*.lua` files exporting a `handler` function) from the following locations, in +order: + +1. The built-in command directory inside rspamd's `lualib` tree. +2. `$CONFDIR/rspamadm.d/*.lua` (a drop-in directory, consistent with `local.d` + and `modules.local.d`), if it exists. +3. Every directory listed in the colon-separated `RSPAMADM_COMMAND_PATH` + environment variable (mirrors how `PATH` works). + +This lets third-party or premium packages ship `rspamadm` commands without +writing into the rspamd-owned `lualib` tree. Externally loaded commands run with +the same environment as the built-in ones (`rspamd_config`, event base, DNS +resolver) and can `require` Lua libraries reachable through the configured +`lua_path`. If two locations provide a command with the same name, the first one +discovered wins (built-in commands always take precedence). + +# ENVIRONMENT + +RSPAMADM_COMMAND_PATH +: A colon-separated list of extra directories to scan for Lua command modules, + e.g. `RSPAMADM_COMMAND_PATH=/usr/share/rspamd-console/rspamadm`. + # RETURN VALUE On exit `rspamadm` returns `0` if operation was successful and an error code otherwise. diff --git a/src/rspamadm/commands.c b/src/rspamadm/commands.c index 1eff95a864..2884092d35 100644 --- a/src/rspamadm/commands.c +++ b/src/rspamadm/commands.c @@ -170,111 +170,190 @@ rspamadm_lua_command_help(gboolean full_help, return NULL; /* Must be handled in rspamadm itself */ } -void rspamadm_fill_lua_commands(lua_State *L, GPtrArray *dest) +/* + * Load a single rspamadm command module from a .lua file and append it to dest. + * Returns TRUE if a command has been registered. + */ +static gboolean +rspamadm_load_lua_command(lua_State *L, GPtrArray *dest, const char *path) { - int i; - - GPtrArray *lua_paths; - GError *err = NULL; - const char *lualibdir = RSPAMD_LUALIBDIR, *path; struct rspamadm_command *lua_cmd; - char search_dir[PATH_MAX]; + char *cmd_name; - if (g_hash_table_lookup(ucl_vars, "LUALIBDIR")) { - lualibdir = g_hash_table_lookup(ucl_vars, "LUALIBDIR"); + if (luaL_dofile(L, path) != 0) { + msg_err("cannot execute lua script %s: %s", + path, lua_tostring(L, -1)); + lua_settop(L, 0); + + return FALSE; } - rspamd_snprintf(search_dir, sizeof(search_dir), "%s%crspamadm%c", - lualibdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR); + if (lua_type(L, -1) != LUA_TTABLE) { + /* The script did not return a command table */ + lua_settop(L, 0); - 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 FALSE; + } - return; + /* The command must export a `handler` function */ + lua_pushstring(L, "handler"); + lua_gettable(L, -2); + + if (lua_type(L, -1) != LUA_TFUNCTION) { + msg_err("rspamadm script %s does not have 'handler' field with type " + "function", + path); + lua_settop(L, 0); + + return FALSE; } - 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 */ - } + /* Pop handler */ + lua_pop(L, 1); - if (lua_type(L, -1) != LUA_TFUNCTION) { - msg_err("rspamadm script %s does not have 'handler' field with type " - "function", - path); - continue; - } + /* Resolve the command name, falling back to the file basename */ + lua_pushstring(L, "name"); + lua_gettable(L, -2); - /* Pop handler */ - lua_pop(L, 1); - lua_cmd = g_malloc0(sizeof(*lua_cmd)); + if (lua_type(L, -1) == LUA_TSTRING) { + cmd_name = g_strdup(lua_tostring(L, -1)); + } + else { + goffset ext_pos; - lua_pushstring(L, "name"); - lua_gettable(L, -2); + cmd_name = g_path_get_basename(path); + /* Remove .lua from the basename itself (not from the full path) */ + ext_pos = rspamd_substring_search(cmd_name, strlen(cmd_name), ".lua", 4); - if (lua_type(L, -1) == LUA_TSTRING) { - lua_cmd->name = g_strdup(lua_tostring(L, -1)); - } - else { - goffset ext_pos; - char *name; + if (ext_pos != -1) { + cmd_name[ext_pos] = '\0'; + } + } + + lua_pop(L, 1); + + /* + * Skip duplicate command names: built-in commands and directories scanned + * earlier take precedence (PATH-like, first wins). This prevents a drop-in + * package from accidentally shadowing a built-in command. + */ + if (rspamadm_search_command(cmd_name, dest) != NULL) { + msg_info("skip rspamadm command %s from %s: already defined", + cmd_name, path); + g_free(cmd_name); + lua_settop(L, 0); + + return FALSE; + } - name = g_path_get_basename(path); - /* Remove .lua */ - ext_pos = rspamd_substring_search(path, strlen(path), ".lua", 4); + lua_cmd = g_malloc0(sizeof(*lua_cmd)); + lua_cmd->name = cmd_name; - if (ext_pos != -1) { - name[ext_pos] = '\0'; - } + 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); - lua_cmd->name = name; + 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_pop(L, 1); - lua_pushstring(L, "aliases"); - lua_gettable(L, -2); + 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; - if (lua_type(L, -1) == LUA_TTABLE) { - lua_cmd->aliases = g_ptr_array_new_full( - rspamd_lua_table_size(L, -1), - g_free); + g_ptr_array_add(dest, lua_cmd); - 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_settop(L, 0); - lua_pop(L, 1); + return TRUE; +} - 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; +/* + * Glob `dir/ *.lua` and load every matching rspamadm command module. A missing + * directory is not an error (glob simply yields nothing), so this is safe for + * the optional drop-in / extra command paths. + */ +static void +rspamadm_scan_lua_commands_dir(lua_State *L, GPtrArray *dest, const char *dir) +{ + GPtrArray *lua_paths; + GError *err = NULL; + const char *path; + unsigned int i; - g_ptr_array_add(dest, lua_cmd); - } + if ((lua_paths = rspamd_glob_path(dir, "*.lua", FALSE, &err)) == NULL) { + msg_err("cannot glob files in %s/*.lua: %e", dir, err); + g_error_free(err); - lua_settop(L, 0); + return; + } + + PTR_ARRAY_FOREACH(lua_paths, i, path) + { + rspamadm_load_lua_command(L, dest, path); } g_ptr_array_free(lua_paths, TRUE); } + +void rspamadm_fill_lua_commands(lua_State *L, GPtrArray *dest) +{ + const char *lualibdir = RSPAMD_LUALIBDIR, *confdir = RSPAMD_CONFDIR; + const char *extra_path; + char search_dir[PATH_MAX]; + + if (g_hash_table_lookup(ucl_vars, "LUALIBDIR")) { + lualibdir = g_hash_table_lookup(ucl_vars, "LUALIBDIR"); + } + + if (g_hash_table_lookup(ucl_vars, "CONFDIR")) { + confdir = g_hash_table_lookup(ucl_vars, "CONFDIR"); + } + + /* Built-in commands shipped with the OSS lualib tree */ + rspamd_snprintf(search_dir, sizeof(search_dir), "%s%crspamadm", + lualibdir, G_DIR_SEPARATOR); + rspamadm_scan_lua_commands_dir(L, dest, search_dir); + + /* + * Drop-in commands from $CONFDIR/rspamadm.d, consistent with local.d / + * modules.local.d. An absent directory is fine. + */ + rspamd_snprintf(search_dir, sizeof(search_dir), "%s%crspamadm.d", + confdir, G_DIR_SEPARATOR); + rspamadm_scan_lua_commands_dir(L, dest, search_dir); + + /* + * Additional command directories from a colon-separated + * RSPAMADM_COMMAND_PATH (mirrors how PATH works). Lets third-party / + * premium packages ship rspamadm commands without writing into the OSS + * lualib tree. Modules load with the same globals and lua_path as the + * built-in ones, so they can require premium lualibs and use lua_redis. + */ + extra_path = g_getenv("RSPAMADM_COMMAND_PATH"); + + if (extra_path != NULL) { + char **dirs = g_strsplit(extra_path, ":", -1); + + for (char **d = dirs; d != NULL && *d != NULL; d++) { + if (**d != '\0') { + rspamadm_scan_lua_commands_dir(L, dest, *d); + } + } + + g_strfreev(dirs); + } +}