summaryrefslogtreecommitdiffstats
path: root/src/lua/lua_xmlrpc.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/lua/lua_xmlrpc.c796
1 files changed, 796 insertions, 0 deletions
diff --git a/src/lua/lua_xmlrpc.c b/src/lua/lua_xmlrpc.c
new file mode 100644
index 0000000..efb2b22
--- /dev/null
+++ b/src/lua/lua_xmlrpc.c
@@ -0,0 +1,796 @@
+/*-
+ * 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 "lua_common.h"
+
+
+LUA_FUNCTION_DEF(xmlrpc, parse_reply);
+LUA_FUNCTION_DEF(xmlrpc, make_request);
+
+static const struct luaL_reg xmlrpclib_m[] = {
+ LUA_INTERFACE_DEF(xmlrpc, parse_reply),
+ LUA_INTERFACE_DEF(xmlrpc, make_request),
+ {"__tostring", rspamd_lua_class_tostring},
+ {NULL, NULL}};
+
+#define msg_debug_xmlrpc(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_xmlrpc_log_id, "xmlrpc", "", \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+INIT_LOG_MODULE(xmlrpc)
+
+enum lua_xmlrpc_state {
+ read_method_response = 0,
+ read_params = 1,
+ read_param = 2,
+ read_param_value = 3,
+ read_param_element = 4,
+ read_struct = 5,
+ read_struct_member_name = 6,
+ read_struct_member_value = 7,
+ read_struct_element = 8,
+ read_string = 9,
+ read_int = 10,
+ read_double = 11,
+ read_array = 12,
+ read_array_value = 13,
+ read_array_element = 14,
+ error_state = 99,
+ success_state = 100,
+};
+
+enum lua_xmlrpc_stack {
+ st_array = 1,
+ st_struct = 2,
+};
+
+struct lua_xmlrpc_ud {
+ enum lua_xmlrpc_state parser_state;
+ GQueue *st;
+ gint param_count;
+ gboolean got_text;
+ lua_State *L;
+};
+
+static void xmlrpc_start_element(GMarkupParseContext *context,
+ const gchar *name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data,
+ GError **error);
+static void xmlrpc_end_element(GMarkupParseContext *context,
+ const gchar *element_name,
+ gpointer user_data,
+ GError **error);
+static void xmlrpc_error(GMarkupParseContext *context,
+ GError *error,
+ gpointer user_data);
+static void xmlrpc_text(GMarkupParseContext *context,
+ const gchar *text,
+ gsize text_len,
+ gpointer user_data,
+ GError **error);
+
+static GMarkupParser xmlrpc_parser = {
+ .start_element = xmlrpc_start_element,
+ .end_element = xmlrpc_end_element,
+ .passthrough = NULL,
+ .text = xmlrpc_text,
+ .error = xmlrpc_error,
+};
+
+static GQuark
+xmlrpc_error_quark(void)
+{
+ return g_quark_from_static_string("xmlrpc-error-quark");
+}
+
+static void
+xmlrpc_start_element(GMarkupParseContext *context,
+ const gchar *name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data,
+ GError **error)
+{
+ struct lua_xmlrpc_ud *ud = user_data;
+ enum lua_xmlrpc_state last_state;
+
+ last_state = ud->parser_state;
+
+ msg_debug_xmlrpc("got start element %s on state %d", name, last_state);
+
+ switch (ud->parser_state) {
+ case read_method_response:
+ /* Expect tag methodResponse */
+ if (g_ascii_strcasecmp(name, "methodResponse") == 0) {
+ ud->parser_state = read_params;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_params:
+ /* Expect tag params */
+ if (g_ascii_strcasecmp(name, "params") == 0) {
+ ud->parser_state = read_param;
+ /* result -> table of params indexed by int */
+ lua_newtable(ud->L);
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_param:
+ /* Expect tag param */
+ if (g_ascii_strcasecmp(name, "param") == 0) {
+ ud->parser_state = read_param_value;
+ /* Create new param */
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_param_value:
+ /* Expect tag value */
+ if (g_ascii_strcasecmp(name, "value") == 0) {
+ ud->parser_state = read_param_element;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_param_element:
+ /* Expect tag struct */
+ if (g_ascii_strcasecmp(name, "struct") == 0) {
+ ud->parser_state = read_struct;
+ /* Create new param of table type */
+ lua_newtable(ud->L);
+ g_queue_push_head(ud->st, GINT_TO_POINTER(st_struct));
+ msg_debug_xmlrpc("push struct");
+ }
+ else if (g_ascii_strcasecmp(name, "array") == 0) {
+ ud->parser_state = read_array;
+ /* Create new param of table type */
+ lua_newtable(ud->L);
+ g_queue_push_head(ud->st, GINT_TO_POINTER(st_array));
+ msg_debug_xmlrpc("push array");
+ }
+ else if (g_ascii_strcasecmp(name, "string") == 0) {
+ ud->parser_state = read_string;
+ ud->got_text = FALSE;
+ }
+ else if (g_ascii_strcasecmp(name, "int") == 0) {
+ ud->parser_state = read_int;
+ ud->got_text = FALSE;
+ }
+ else if (g_ascii_strcasecmp(name, "double") == 0) {
+ ud->parser_state = read_double;
+ ud->got_text = FALSE;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_struct:
+ /* Parse structure */
+ /* Expect tag member */
+ if (g_ascii_strcasecmp(name, "member") == 0) {
+ ud->parser_state = read_struct_member_name;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_struct_member_name:
+ /* Expect tag name */
+ if (g_ascii_strcasecmp(name, "name") == 0) {
+ ud->parser_state = read_struct_member_value;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_struct_member_value:
+ /* Accept value */
+ if (g_ascii_strcasecmp(name, "value") == 0) {
+ ud->parser_state = read_struct_element;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_struct_element:
+ /* Parse any values */
+ /* Primitives */
+ if (g_ascii_strcasecmp(name, "string") == 0) {
+ ud->parser_state = read_string;
+ ud->got_text = FALSE;
+ }
+ else if (g_ascii_strcasecmp(name, "int") == 0) {
+ ud->parser_state = read_int;
+ ud->got_text = FALSE;
+ }
+ else if (g_ascii_strcasecmp(name, "double") == 0) {
+ ud->parser_state = read_double;
+ ud->got_text = FALSE;
+ }
+ /* Structure */
+ else if (g_ascii_strcasecmp(name, "struct") == 0) {
+ ud->parser_state = read_struct;
+ /* Create new param of table type */
+ lua_newtable(ud->L);
+ g_queue_push_head(ud->st, GINT_TO_POINTER(st_struct));
+ msg_debug_xmlrpc("push struct");
+ }
+ else if (g_ascii_strcasecmp(name, "array") == 0) {
+ ud->parser_state = read_array;
+ /* Create new param of table type */
+ lua_newtable(ud->L);
+ g_queue_push_head(ud->st, GINT_TO_POINTER(st_array));
+ msg_debug_xmlrpc("push array");
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_array:
+ /* Parse array */
+ /* Expect data */
+ if (g_ascii_strcasecmp(name, "data") == 0) {
+ ud->parser_state = read_array_value;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_array_value:
+ /* Accept array value */
+ if (g_ascii_strcasecmp(name, "value") == 0) {
+ ud->parser_state = read_array_element;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_array_element:
+ /* Parse any values */
+ /* Primitives */
+ if (g_ascii_strcasecmp(name, "string") == 0) {
+ ud->parser_state = read_string;
+ ud->got_text = FALSE;
+ }
+ else if (g_ascii_strcasecmp(name, "int") == 0) {
+ ud->parser_state = read_int;
+ ud->got_text = FALSE;
+ }
+ else if (g_ascii_strcasecmp(name, "double") == 0) {
+ ud->parser_state = read_double;
+ ud->got_text = FALSE;
+ }
+ /* Structure */
+ else if (g_ascii_strcasecmp(name, "struct") == 0) {
+ ud->parser_state = read_struct;
+ /* Create new param of table type */
+ lua_newtable(ud->L);
+ g_queue_push_head(ud->st, GINT_TO_POINTER(st_struct));
+ msg_debug_xmlrpc("push struct");
+ }
+ else if (g_ascii_strcasecmp(name, "array") == 0) {
+ ud->parser_state = read_array;
+ /* Create new param of table type */
+ lua_newtable(ud->L);
+ g_queue_push_head(ud->st, GINT_TO_POINTER(st_array));
+ msg_debug_xmlrpc("push array");
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ default:
+ break;
+ }
+
+ msg_debug_xmlrpc("switched state on start tag %d->%d", last_state,
+ ud->parser_state);
+
+ if (ud->parser_state == error_state) {
+ g_set_error(error,
+ xmlrpc_error_quark(), 1, "xmlrpc parse error on state: %d, while parsing start tag: %s",
+ last_state, name);
+ }
+}
+
+static void
+xmlrpc_end_element(GMarkupParseContext *context,
+ const gchar *name,
+ gpointer user_data,
+ GError **error)
+{
+ struct lua_xmlrpc_ud *ud = user_data;
+ enum lua_xmlrpc_state last_state;
+ int last_queued;
+
+ last_state = ud->parser_state;
+
+ msg_debug_xmlrpc("got end element %s on state %d", name, last_state);
+
+ switch (ud->parser_state) {
+ case read_method_response:
+ ud->parser_state = error_state;
+ break;
+ case read_params:
+ /* Got methodResponse */
+ if (g_ascii_strcasecmp(name, "methodResponse") == 0) {
+ /* End processing */
+ ud->parser_state = success_state;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_param:
+ /* Got tag params */
+ if (g_ascii_strcasecmp(name, "params") == 0) {
+ ud->parser_state = read_params;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_param_value:
+ /* Got tag param */
+ if (g_ascii_strcasecmp(name, "param") == 0) {
+ ud->parser_state = read_param;
+ lua_rawseti(ud->L, -2, ++ud->param_count);
+ msg_debug_xmlrpc("set param element idx: %d", ud->param_count);
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_param_element:
+ /* Got tag value */
+ if (g_ascii_strcasecmp(name, "value") == 0) {
+ if (g_queue_get_length(ud->st) == 0) {
+ ud->parser_state = read_param_value;
+ }
+ else {
+ if (GPOINTER_TO_INT(g_queue_peek_head(ud->st)) == st_struct) {
+ ud->parser_state = read_struct_member_name;
+ }
+ else {
+ ud->parser_state = read_array_value;
+ }
+ }
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_struct:
+ /* Got tag struct */
+ if (g_ascii_strcasecmp(name, "struct") == 0) {
+ g_assert(GPOINTER_TO_INT(g_queue_pop_head(ud->st)) == st_struct);
+
+ if (g_queue_get_length(ud->st) == 0) {
+ ud->parser_state = read_param_element;
+ }
+ else {
+ last_queued = GPOINTER_TO_INT(g_queue_peek_head(ud->st));
+ if (last_queued == st_struct) {
+ ud->parser_state = read_struct_element;
+ }
+ else {
+ ud->parser_state = read_array_element;
+ }
+ }
+
+ msg_debug_xmlrpc("pop struct");
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_struct_member_name:
+ /* Got tag member */
+ if (g_ascii_strcasecmp(name, "member") == 0) {
+ ud->parser_state = read_struct;
+ /* Set table */
+ msg_debug_xmlrpc("set struct element idx: %s",
+ lua_tostring(ud->L, -2));
+ lua_settable(ud->L, -3);
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_struct_member_value:
+ /* Got tag name */
+ if (g_ascii_strcasecmp(name, "name") == 0) {
+ ud->parser_state = read_struct_member_value;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_struct_element:
+ /* Got tag value */
+ if (g_ascii_strcasecmp(name, "value") == 0) {
+ ud->parser_state = read_struct_member_name;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_string:
+ case read_int:
+ case read_double:
+ /* Parse any values */
+ /* Handle empty tags */
+ if (!ud->got_text) {
+ lua_pushnil(ud->L);
+ }
+ else {
+ ud->got_text = FALSE;
+ }
+ /* Primitives */
+ if (g_ascii_strcasecmp(name, "string") == 0 ||
+ g_ascii_strcasecmp(name, "int") == 0 ||
+ g_ascii_strcasecmp(name, "double") == 0) {
+ if (GPOINTER_TO_INT(g_queue_peek_head(ud->st)) == st_struct) {
+ ud->parser_state = read_struct_element;
+ }
+ else {
+ ud->parser_state = read_array_element;
+ }
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_array:
+ /* Got tag array */
+ if (g_ascii_strcasecmp(name, "array") == 0) {
+ g_assert(GPOINTER_TO_INT(g_queue_pop_head(ud->st)) == st_array);
+
+ if (g_queue_get_length(ud->st) == 0) {
+ ud->parser_state = read_param_element;
+ }
+ else {
+ last_queued = GPOINTER_TO_INT(g_queue_peek_head(ud->st));
+ if (last_queued == st_struct) {
+ ud->parser_state = read_struct_element;
+ }
+ else {
+ ud->parser_state = read_array_element;
+ }
+ }
+
+ msg_debug_xmlrpc("pop array");
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_array_value:
+ /* Got tag data */
+ if (g_ascii_strcasecmp(name, "data") == 0) {
+ ud->parser_state = read_array;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ case read_array_element:
+ /* Got tag value */
+ if (g_ascii_strcasecmp(name, "value") == 0) {
+ guint tbl_len = rspamd_lua_table_size(ud->L, -2);
+ lua_rawseti(ud->L, -2, tbl_len + 1);
+ msg_debug_xmlrpc("set array element idx: %d", tbl_len + 1);
+ ud->parser_state = read_array_value;
+ }
+ else {
+ /* Error state */
+ ud->parser_state = error_state;
+ }
+ break;
+ default:
+ break;
+ }
+
+ msg_debug_xmlrpc("switched state on end tag %d->%d",
+ last_state, ud->parser_state);
+
+ if (ud->parser_state == error_state) {
+ g_set_error(error,
+ xmlrpc_error_quark(), 1, "xmlrpc parse error on state: %d, while parsing end tag: %s",
+ last_state, name);
+ }
+}
+
+static void
+xmlrpc_text(GMarkupParseContext *context,
+ const gchar *text,
+ gsize text_len,
+ gpointer user_data,
+ GError **error)
+{
+ struct lua_xmlrpc_ud *ud = user_data;
+ gulong num;
+ gdouble dnum;
+
+ /* Strip line */
+ while (text_len > 0 && g_ascii_isspace(*text)) {
+ text++;
+ text_len--;
+ }
+ while (text_len > 0 && g_ascii_isspace(text[text_len - 1])) {
+ text_len--;
+ }
+
+ if (text_len > 0) {
+ msg_debug_xmlrpc("got data on state %d", ud->parser_state);
+ switch (ud->parser_state) {
+ case read_struct_member_value:
+ /* Push key */
+ lua_pushlstring(ud->L, text, text_len);
+ break;
+ case read_string:
+ /* Push string value */
+ lua_pushlstring(ud->L, text, text_len);
+ break;
+ case read_int:
+ /* Push integer value */
+ rspamd_strtoul(text, text_len, &num);
+ lua_pushinteger(ud->L, num);
+ break;
+ case read_double:
+ /* Push integer value */
+ dnum = strtod(text, NULL);
+ lua_pushnumber(ud->L, dnum);
+ break;
+ default:
+ break;
+ }
+ ud->got_text = TRUE;
+ }
+}
+
+static void
+xmlrpc_error(GMarkupParseContext *context, GError *error, gpointer user_data)
+{
+ msg_err("xmlrpc parser error: %s", error->message);
+}
+
+static gint
+lua_xmlrpc_parse_reply(lua_State *L)
+{
+ LUA_TRACE_POINT;
+ const gchar *data;
+ GMarkupParseContext *ctx;
+ GError *err = NULL;
+ struct lua_xmlrpc_ud ud;
+ gsize s;
+ gboolean res;
+
+ data = luaL_checklstring(L, 1, &s);
+
+ if (data != NULL) {
+ ud.L = L;
+ ud.parser_state = read_method_response;
+ ud.param_count = 0;
+ ud.st = g_queue_new();
+
+ ctx = g_markup_parse_context_new(&xmlrpc_parser,
+ G_MARKUP_TREAT_CDATA_AS_TEXT, &ud, NULL);
+ res = g_markup_parse_context_parse(ctx, data, s, &err);
+
+ g_markup_parse_context_free(ctx);
+ if (!res) {
+ lua_pushnil(L);
+ }
+ }
+ else {
+ lua_pushnil(L);
+ }
+
+ /* Return table or nil */
+ return 1;
+}
+
+static gint
+lua_xmlrpc_parse_table(lua_State *L,
+ gint pos,
+ gchar *databuf,
+ gint pr,
+ gsize size)
+{
+ gint r = pr, num;
+ double dnum;
+
+ r += rspamd_snprintf(databuf + r, size - r, "<struct>");
+ lua_pushnil(L); /* first key */
+ while (lua_next(L, pos) != 0) {
+ /* uses 'key' (at index -2) and 'value' (at index -1) */
+ if (lua_type(L, -2) != LUA_TSTRING) {
+ /* Ignore non sting keys */
+ lua_pop(L, 1);
+ continue;
+ }
+ r += rspamd_snprintf(databuf + r,
+ size - r,
+ "<member><name>%s</name><value>",
+ lua_tostring(L, -2));
+ switch (lua_type(L, -1)) {
+ case LUA_TNUMBER:
+ num = lua_tointeger(L, -1);
+ dnum = lua_tonumber(L, -1);
+
+ /* Try to avoid conversion errors */
+ if (dnum != (double) num) {
+ r += rspamd_snprintf(databuf + r,
+ sizeof(databuf) - r,
+ "<double>%f</double>",
+ dnum);
+ }
+ else {
+ r += rspamd_snprintf(databuf + r,
+ sizeof(databuf) - r,
+ "<int>%d</int>",
+ num);
+ }
+ break;
+ case LUA_TBOOLEAN:
+ r += rspamd_snprintf(databuf + r,
+ size - r,
+ "<boolean>%d</boolean>",
+ lua_toboolean(L, -1) ? 1 : 0);
+ break;
+ case LUA_TSTRING:
+ r += rspamd_snprintf(databuf + r, size - r, "<string>%s</string>",
+ lua_tostring(L, -1));
+ break;
+ case LUA_TTABLE:
+ /* Recursive call */
+ r += lua_xmlrpc_parse_table(L, -1, databuf + r, r, size);
+ break;
+ }
+ r += rspamd_snprintf(databuf + r, size - r, "</value></member>");
+ /* removes 'value'; keeps 'key' for next iteration */
+ lua_pop(L, 1);
+ }
+ r += rspamd_snprintf(databuf + r, size - r, "</struct>");
+
+ return r - pr;
+}
+
+/*
+ * Internal limitation: xmlrpc request must NOT be more than
+ * BUFSIZ * 2 (16384 bytes)
+ */
+static gint
+lua_xmlrpc_make_request(lua_State *L)
+{
+ LUA_TRACE_POINT;
+ gchar databuf[BUFSIZ * 2];
+ const gchar *func;
+ gint r, top, i, num;
+ double dnum;
+
+ func = luaL_checkstring(L, 1);
+
+ if (func) {
+ r = rspamd_snprintf(databuf, sizeof(databuf),
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<methodCall><methodName>%s</methodName><params>",
+ func);
+ /* Extract arguments */
+ top = lua_gettop(L);
+ /* Get additional options */
+ for (i = 2; i <= top; i++) {
+ r += rspamd_snprintf(databuf + r,
+ sizeof(databuf) - r,
+ "<param><value>");
+ switch (lua_type(L, i)) {
+ case LUA_TNUMBER:
+ num = lua_tointeger(L, i);
+ dnum = lua_tonumber(L, i);
+
+ /* Try to avoid conversion errors */
+ if (dnum != (double) num) {
+ r += rspamd_snprintf(databuf + r,
+ sizeof(databuf) - r,
+ "<double>%f</double>",
+ dnum);
+ }
+ else {
+ r += rspamd_snprintf(databuf + r,
+ sizeof(databuf) - r,
+ "<int>%d</int>",
+ num);
+ }
+ break;
+ case LUA_TBOOLEAN:
+ r += rspamd_snprintf(databuf + r,
+ sizeof(databuf) - r,
+ "<boolean>%d</boolean>",
+ lua_toboolean(L, i) ? 1 : 0);
+ break;
+ case LUA_TSTRING:
+ r += rspamd_snprintf(databuf + r,
+ sizeof(databuf) - r,
+ "<string>%s</string>",
+ lua_tostring(L, i));
+ break;
+ case LUA_TTABLE:
+ r +=
+ lua_xmlrpc_parse_table(L, i, databuf, r, sizeof(databuf));
+ break;
+ }
+ r += rspamd_snprintf(databuf + r,
+ sizeof(databuf) - r,
+ "</value></param>");
+ }
+
+ r += rspamd_snprintf(databuf + r,
+ sizeof(databuf) - r,
+ "</params></methodCall>");
+ lua_pushlstring(L, databuf, r);
+ }
+ else {
+ lua_pushnil(L);
+ }
+
+ return 1;
+}
+
+static gint
+lua_load_xmlrpc(lua_State *L)
+{
+ lua_newtable(L);
+ luaL_register(L, NULL, xmlrpclib_m);
+
+ return 1;
+}
+
+void luaopen_xmlrpc(lua_State *L)
+{
+ rspamd_lua_add_preload(L, "rspamd_xmlrpc", lua_load_xmlrpc);
+}